Skip to content

Commit a33688b

Browse files
authored
[ES|QL] Add subqueries support for walker and visitor (#241451)
## Summary Part of #237401 Parens support for walker and visitor
1 parent 3a4e03e commit a33688b

File tree

7 files changed

+168
-2
lines changed

7 files changed

+168
-2
lines changed

src/platform/packages/shared/kbn-esql-ast/src/visitor/__tests__/commands.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,39 @@ test('can visit COMPLETION command', () => {
230230

231231
expect(list).toEqual(['COMPLETION']);
232232
});
233+
234+
test('can visit FROM command with complex subqueries', () => {
235+
const { ast } = EsqlQuery.fromSrc(`
236+
FROM index1,
237+
(FROM index2
238+
| WHERE a > 10
239+
| EVAL b = a * 2
240+
| STATS cnt = COUNT(*) BY c
241+
| SORT cnt desc
242+
| LIMIT 10),
243+
index3,
244+
(FROM index4 | STATS count(*))
245+
| WHERE d > 10
246+
| STATS max = max(*) BY e
247+
| SORT max desc
248+
`);
249+
const visitor = new Visitor()
250+
.on('visitParensExpression', () => {
251+
return 'SUBQUERY';
252+
})
253+
.on('visitExpression', () => {
254+
return null;
255+
})
256+
.on('visitFromCommand', (ctx) => {
257+
return [...ctx.visitArguments()];
258+
})
259+
.on('visitCommand', (ctx) => {
260+
return null;
261+
})
262+
.on('visitQuery', (ctx) => {
263+
return [...ctx.visitCommands()].flat();
264+
});
265+
const list = visitor.visitQuery(ast).flat().filter(Boolean);
266+
267+
expect(list).toEqual(['SUBQUERY', 'SUBQUERY']);
268+
});

src/platform/packages/shared/kbn-esql-ast/src/visitor/__tests__/expressions.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,40 @@ test('"visitExpression" does visit identifier nodes', () => {
263263

264264
expect(expressions.sort()).toEqual(['a', 'index']);
265265
});
266+
267+
test('"visitParensExpression" can traverse complex subqueries with processing', () => {
268+
const { ast } = parse(`
269+
FROM index1,
270+
(FROM index2
271+
| WHERE a > 10
272+
| EVAL b = a * 2
273+
| STATS cnt = COUNT(*) BY c
274+
| SORT cnt desc
275+
| LIMIT 10),
276+
index3,
277+
(FROM index4 | STATS count(*))
278+
| WHERE d > 10
279+
| STATS max = max(*) BY e
280+
| SORT max desc
281+
`);
282+
const visitor = new Visitor()
283+
.on('visitParensExpression', (ctx) => {
284+
const child = ctx.visitChild(undefined);
285+
return `PARENS(${child})`;
286+
})
287+
.on('visitExpression', (ctx) => {
288+
return 'E';
289+
})
290+
.on('visitCommand', (ctx) => {
291+
const args = [...ctx.visitArguments()].join(', ');
292+
return `${ctx.name()}${args ? ` ${args}` : ''}`;
293+
})
294+
.on('visitQuery', (ctx) => {
295+
return [...ctx.visitCommands()].join(' | ');
296+
});
297+
const text = visitor.visitQuery(ast);
298+
299+
expect(text).toBe(
300+
'FROM E, PARENS(FROM E | WHERE E | EVAL E | STATS E | SORT E | LIMIT E), E, PARENS(FROM E | STATS E) | WHERE E | STATS E | SORT E'
301+
);
302+
});

src/platform/packages/shared/kbn-esql-ast/src/visitor/contexts.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import type {
3535
ESQLMap,
3636
ESQLMapEntry,
3737
ESQLOrderExpression,
38+
ESQLParens,
3839
ESQLSource,
3940
ESQLStringLiteral,
4041
} from '../types';
@@ -725,3 +726,20 @@ export class MapEntryExpressionVisitorContext<
725726
return this.visitExpression(this.value(), input as any);
726727
}
727728
}
729+
730+
export class ParensExpressionVisitorContext<
731+
Methods extends VisitorMethods = VisitorMethods,
732+
Data extends SharedData = SharedData
733+
> extends VisitorContext<Methods, Data, ESQLParens> {
734+
public child(): ESQLAstExpression {
735+
return this.node.child;
736+
}
737+
738+
public visitChild(
739+
input: VisitorInput<Methods, 'visitExpression'>
740+
): VisitorOutput<Methods, 'visitExpression'> {
741+
this.ctx.assertMethodExists('visitExpression');
742+
743+
return this.visitExpression(this.child(), input as any);
744+
}
745+
}

src/platform/packages/shared/kbn-esql-ast/src/visitor/global_visitor_context.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
ESQLMap,
2626
ESQLMapEntry,
2727
ESQLOrderExpression,
28+
ESQLParens,
2829
ESQLSource,
2930
} from '../types';
3031
import type * as types from './types';
@@ -543,6 +544,10 @@ export class GlobalVisitorContext<
543544
if (!this.methods.visitQuery || expressionNode.type !== 'query') break;
544545
return this.visitQuery(parent, expressionNode, input as any);
545546
}
547+
case 'parens': {
548+
if (!this.methods.visitParensExpression) break;
549+
return this.visitParensExpression(parent, expressionNode, input as any);
550+
}
546551
}
547552
return this.visitExpressionGeneric(parent, expressionNode, input as any);
548553
}
@@ -645,6 +650,15 @@ export class GlobalVisitorContext<
645650
const context = new contexts.MapEntryExpressionVisitorContext(this, node, parent);
646651
return this.visitWithSpecificContext('visitMapEntryExpression', context, input);
647652
}
653+
654+
public visitParensExpression(
655+
parent: contexts.VisitorContext | null,
656+
node: ESQLParens,
657+
input: types.VisitorInput<Methods, 'visitParensExpression'>
658+
): types.VisitorOutput<Methods, 'visitParensExpression'> {
659+
const context = new contexts.ParensExpressionVisitorContext(this, node, parent);
660+
return this.visitWithSpecificContext('visitParensExpression', context, input);
661+
}
648662
}
649663

650664
// #endregion

src/platform/packages/shared/kbn-esql-ast/src/visitor/types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ export type ExpressionVisitorInput<Methods extends VisitorMethods> = AnyToVoid<
6363
VisitorInput<Methods, 'visitOrderExpression'> &
6464
VisitorInput<Methods, 'visitIdentifierExpression'> &
6565
VisitorInput<Methods, 'visitMapExpression'> &
66-
VisitorInput<Methods, 'visitMapEntryExpression'>
66+
VisitorInput<Methods, 'visitMapEntryExpression'> &
67+
VisitorInput<Methods, 'visitParensExpression'>
6768
>;
6869

6970
/**
@@ -81,7 +82,8 @@ export type ExpressionVisitorOutput<Methods extends VisitorMethods> =
8182
| VisitorOutput<Methods, 'visitOrderExpression'>
8283
| VisitorOutput<Methods, 'visitIdentifierExpression'>
8384
| VisitorOutput<Methods, 'visitMapExpression'>
84-
| VisitorOutput<Methods, 'visitMapEntryExpression'>;
85+
| VisitorOutput<Methods, 'visitMapEntryExpression'>
86+
| VisitorOutput<Methods, 'visitParensExpression'>;
8587

8688
/**
8789
* Input that satisfies any command visitor input constraints.
@@ -237,6 +239,11 @@ export interface VisitorMethods<
237239
any,
238240
any
239241
>;
242+
visitParensExpression?: Visitor<
243+
contexts.ParensExpressionVisitorContext<Visitors, Data>,
244+
any,
245+
any
246+
>;
240247
}
241248

242249
/**
@@ -268,6 +275,8 @@ export type AstNodeToVisitorName<Node extends VisitorAstNode> = Node extends ESQ
268275
? 'visitMapExpression'
269276
: Node extends ast.ESQLMapEntry
270277
? 'visitMapEntryExpression'
278+
: Node extends ast.ESQLParens
279+
? 'visitParensExpression'
271280
: never;
272281

273282
/**

src/platform/packages/shared/kbn-esql-ast/src/walker/__tests__/walker.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,4 +1246,38 @@ describe('header commands', () => {
12461246
expect(regularCommands.map((cmd) => cmd.name)).toStrictEqual(['from', 'limit']);
12471247
});
12481248
});
1249+
1250+
describe('parens (subquery)', () => {
1251+
test('can visit complex subqueries with processing', () => {
1252+
const src = `
1253+
FROM index1,
1254+
(FROM index2
1255+
| WHERE a > 10
1256+
| EVAL b = a * 2
1257+
| STATS cnt = COUNT(*) BY c
1258+
| SORT cnt desc
1259+
| LIMIT 10),
1260+
index3,
1261+
(FROM index4 | STATS count(*))
1262+
| WHERE d > 10
1263+
| STATS max = max(*) BY e
1264+
| SORT max desc
1265+
`;
1266+
const { ast } = parse(src);
1267+
let parensCount = 0;
1268+
const sources: string[] = [];
1269+
1270+
walk(ast, {
1271+
visitParens: (node) => {
1272+
parensCount++;
1273+
},
1274+
visitSource: (node) => {
1275+
sources.push(node.name);
1276+
},
1277+
});
1278+
1279+
expect(parensCount).toBe(2);
1280+
expect(sources).toEqual(['index1', 'index2', 'index3', 'index4']);
1281+
});
1282+
});
12491283
});

src/platform/packages/shared/kbn-esql-ast/src/walker/walker.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ export interface WalkerOptions {
9090
parent: types.ESQLProperNode | undefined,
9191
walker: WalkerVisitorApi
9292
) => void;
93+
visitParens?: (
94+
node: types.ESQLParens,
95+
parent: types.ESQLProperNode | undefined,
96+
walker: WalkerVisitorApi
97+
) => void;
9398

9499
/**
95100
* Called on every expression node.
@@ -703,6 +708,15 @@ export class Walker {
703708
}
704709
}
705710

711+
public walkParens(node: types.ESQLParens, parent: types.ESQLProperNode | undefined): void {
712+
const { options } = this;
713+
(options.visitParens ?? options.visitAny)?.(node, parent, this);
714+
715+
if (node.child) {
716+
this.walkSingleAstItem(node.child, node);
717+
}
718+
}
719+
706720
public walkQuery(
707721
node: types.ESQLAstQueryExpression,
708722
parent: types.ESQLProperNode | undefined
@@ -770,6 +784,10 @@ export class Walker {
770784
this.walkInlineCast(node, parent);
771785
break;
772786
}
787+
case 'parens': {
788+
this.walkParens(node as types.ESQLParens, parent);
789+
break;
790+
}
773791
case 'identifier': {
774792
(options.visitIdentifier ?? options.visitAny)?.(node, parent, this);
775793
break;

0 commit comments

Comments
 (0)