Skip to content

Commit 2b1e028

Browse files
authored
[ES|QL] Walker and Visitor support for SET instructions (elastic#237659)
## Summary Partially addresses elastic#233942 Implements parsing for `SET` pseudo-command traversal in `Walker` and `Visitor`. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
1 parent b20f3cb commit 2b1e028

File tree

7 files changed

+590
-0
lines changed

7 files changed

+590
-0
lines changed

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

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,194 @@ test('specific commands receive specific visitor contexts', () => {
134134
.on('visitQuery', (ctx) => [...ctx.visitCommands()])
135135
.visitQuery(root);
136136
});
137+
138+
describe('header commands', () => {
139+
test('can visit header commands', () => {
140+
const { root } = parse('SET timeout = "30s"; FROM index | LIMIT 10');
141+
const headerNames: string[] = [];
142+
143+
new Visitor()
144+
.on('visitHeaderCommand', (ctx) => {
145+
headerNames.push(ctx.node.name);
146+
})
147+
.on('visitQuery', (ctx) => {
148+
for (const _headerResult of ctx.visitHeaderCommands()) {
149+
// Visit header commands
150+
}
151+
})
152+
.visitQuery(root);
153+
154+
expect(headerNames).toEqual(['set']);
155+
});
156+
157+
test('can visit multiple header commands', () => {
158+
const { root } = parse('SET a = 1; SET b = 2; SET c = 3; FROM index');
159+
const headerNames: string[] = [];
160+
161+
new Visitor()
162+
.on('visitHeaderCommand', (ctx) => {
163+
headerNames.push(ctx.name());
164+
})
165+
.on('visitQuery', (ctx) => {
166+
for (const _headerResult of ctx.visitHeaderCommands()) {
167+
// Visit header commands
168+
}
169+
})
170+
.visitQuery(root);
171+
172+
expect(headerNames).toEqual(['SET', 'SET', 'SET']);
173+
});
174+
175+
test('can visit header command arguments', () => {
176+
const { root } = parse('SET timeout = "30s"; FROM index');
177+
const identifiers: string[] = [];
178+
const literals: string[] = [];
179+
180+
new Visitor()
181+
.on('visitExpression', (ctx) => {
182+
// Default expression visitor
183+
})
184+
.on('visitFunctionCallExpression', (ctx) => {
185+
// Visit function arguments recursively
186+
for (const _arg of ctx.visitArguments(undefined)) {
187+
// Process arguments
188+
}
189+
})
190+
.on('visitHeaderCommand', (ctx) => {
191+
for (const _argResult of ctx.visitArgs(undefined)) {
192+
// Visit arguments
193+
}
194+
})
195+
.on('visitIdentifierExpression', (ctx) => {
196+
if (ctx.node.name !== '=') {
197+
identifiers.push(ctx.node.name);
198+
}
199+
})
200+
.on('visitLiteralExpression', (ctx) => {
201+
if (ctx.node.literalType === 'keyword') {
202+
literals.push(ctx.node.valueUnquoted || '');
203+
}
204+
})
205+
.on('visitQuery', (ctx) => {
206+
for (const _headerResult of ctx.visitHeaderCommands()) {
207+
// Visit header commands
208+
}
209+
})
210+
.visitQuery(root);
211+
212+
expect(identifiers).toEqual(['timeout']);
213+
expect(literals).toEqual(['30s']);
214+
});
215+
216+
test('header commands are visited before regular commands', () => {
217+
const { root } = parse('SET a = 1; FROM index | LIMIT 10');
218+
const visitOrder: string[] = [];
219+
220+
new Visitor()
221+
.on('visitHeaderCommand', (ctx) => {
222+
visitOrder.push(`header:${ctx.node.name}`);
223+
})
224+
.on('visitCommand', (ctx) => {
225+
visitOrder.push(`command:${ctx.node.name}`);
226+
})
227+
.on('visitQuery', (ctx) => {
228+
for (const _headerResult of ctx.visitHeaderCommands()) {
229+
// Visit header commands first
230+
}
231+
for (const _cmdResult of ctx.visitCommands()) {
232+
// Then visit regular commands
233+
}
234+
})
235+
.visitQuery(root);
236+
237+
expect(visitOrder).toEqual(['header:set', 'command:from', 'command:limit']);
238+
});
239+
240+
test('can iterate through header commands', () => {
241+
const { root } = parse('SET a = 1; SET b = 2; FROM index');
242+
const headerCommandCount = [
243+
...new Visitor().on('visitQuery', (ctx) => ctx.headerCommands()).visitQuery(root),
244+
].length;
245+
246+
expect(headerCommandCount).toBe(2);
247+
});
248+
249+
test('header command context has correct parent', () => {
250+
const { root } = parse('SET timeout = "30s"; FROM index');
251+
252+
new Visitor()
253+
.on('visitHeaderCommand', (ctx) => {
254+
if (ctx.parent!.node !== root) {
255+
throw new Error('Expected parent to be query node');
256+
}
257+
})
258+
.on('visitQuery', (ctx) => {
259+
for (const _headerResult of ctx.visitHeaderCommands()) {
260+
// Visit header commands
261+
}
262+
})
263+
.visitQuery(root);
264+
});
265+
266+
test('can visit header command directly', () => {
267+
const { root } = parse('SET timeout = "30s"; FROM index');
268+
const headerCommand = root.header![0];
269+
270+
const result = new Visitor()
271+
.on('visitHeaderCommand', (ctx) => {
272+
return `visited:${ctx.node.name}`;
273+
})
274+
.visitHeaderCommand(headerCommand);
275+
276+
expect(result).toBe('visited:set');
277+
});
278+
279+
test('header commands with various value types', () => {
280+
const { root } = parse('SET a = 1; SET b = "value"; SET c = true; FROM index');
281+
const literals: any[] = [];
282+
283+
new Visitor()
284+
.on('visitExpression', (ctx) => {
285+
// Default expression visitor
286+
})
287+
.on('visitFunctionCallExpression', (ctx) => {
288+
// Visit function arguments recursively
289+
for (const _arg of ctx.visitArguments(undefined)) {
290+
// Process arguments
291+
}
292+
})
293+
.on('visitHeaderCommand', (ctx) => {
294+
for (const _argResult of ctx.visitArgs(undefined)) {
295+
// Visit arguments
296+
}
297+
})
298+
.on('visitLiteralExpression', (ctx) => {
299+
literals.push(ctx.node.value);
300+
})
301+
.on('visitQuery', (ctx) => {
302+
for (const _headerResult of ctx.visitHeaderCommands()) {
303+
// Visit header commands
304+
}
305+
})
306+
.visitQuery(root);
307+
308+
expect(literals).toContain(1);
309+
expect(literals).toContain('"value"');
310+
expect(literals).toContain('true');
311+
});
312+
313+
test('can return values from header command visitor', () => {
314+
const { root } = parse('SET a = 1; SET b = 2; FROM index');
315+
316+
const results = new Visitor()
317+
.on('visitHeaderCommand', (ctx) => {
318+
return `header-${ctx.node.name}`;
319+
})
320+
.on('visitQuery', (ctx) => {
321+
return [...ctx.visitHeaderCommands()];
322+
})
323+
.visitQuery(root);
324+
325+
expect(results).toEqual(['header-set', 'header-set']);
326+
});
327+
});

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
ESQLAstChangePointCommand,
1919
ESQLAstCommand,
2020
ESQLAstExpression,
21+
ESQLAstHeaderCommand,
2122
ESQLAstItem,
2223
ESQLAstJoinCommand,
2324
ESQLAstQueryExpression,
@@ -147,12 +148,37 @@ export class VisitorContext<
147148
): ExpressionVisitorOutput<Methods> {
148149
return this.ctx.visitCommand(this, commandNode, input);
149150
}
151+
152+
public visitHeaderCommand(
153+
headerCommandNode: ESQLAstHeaderCommand,
154+
input: UndefinedToVoid<Parameters<NonNullable<Methods['visitHeaderCommand']>>[1]>
155+
): ReturnType<NonNullable<Methods['visitHeaderCommand']>> {
156+
return this.ctx.visitHeaderCommand(this, headerCommandNode, input);
157+
}
150158
}
151159

152160
export class QueryVisitorContext<
153161
Methods extends VisitorMethods = VisitorMethods,
154162
Data extends SharedData = SharedData
155163
> extends VisitorContext<Methods, Data, ESQLAstQueryNode> {
164+
public *headerCommands(): Iterable<ESQLAstHeaderCommand> {
165+
if (this.node.header) {
166+
yield* this.node.header;
167+
}
168+
}
169+
170+
public *visitHeaderCommands(
171+
input: UndefinedToVoid<Parameters<NonNullable<Methods['visitHeaderCommand']>>[1]>
172+
): Iterable<ReturnType<NonNullable<Methods['visitHeaderCommand']>>> {
173+
this.ctx.assertMethodExists('visitHeaderCommand');
174+
175+
if (this.node.header) {
176+
for (const headerCmd of this.node.header) {
177+
yield this.visitHeaderCommand(headerCmd, input as any);
178+
}
179+
}
180+
}
181+
156182
public *commands(): Iterable<ESQLAstCommand> {
157183
yield* this.node.commands;
158184
}
@@ -286,6 +312,37 @@ export class CommandVisitorContext<
286312
}
287313
}
288314

315+
export class HeaderCommandVisitorContext<
316+
Methods extends VisitorMethods = VisitorMethods,
317+
Data extends SharedData = SharedData,
318+
Node extends ESQLAstHeaderCommand = ESQLAstHeaderCommand
319+
> extends VisitorContext<Methods, Data, Node> {
320+
public name(): string {
321+
return this.node.name.toUpperCase();
322+
}
323+
324+
public *args(): Iterable<ESQLAstExpression> {
325+
yield* this.node.args;
326+
}
327+
328+
public *visitArgs(
329+
input:
330+
| VisitorInput<Methods, 'visitExpression'>
331+
| (() => VisitorInput<Methods, 'visitExpression'>)
332+
): Iterable<ExpressionVisitorOutput<Methods>> {
333+
this.ctx.assertMethodExists('visitExpression');
334+
335+
for (const arg of this.args()) {
336+
yield this.visitExpression(
337+
arg,
338+
typeof input === 'function'
339+
? (input as () => VisitorInput<Methods, 'visitExpression'>)()
340+
: (input as VisitorInput<Methods, 'visitExpression'>)
341+
);
342+
}
343+
}
344+
}
345+
289346
export class CommandOptionVisitorContext<
290347
Methods extends VisitorMethods = VisitorMethods,
291348
Data extends SharedData = SharedData

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
@@ -12,6 +12,7 @@ import type {
1212
ESQLAstChangePointCommand,
1313
ESQLAstCommand,
1414
ESQLAstCompletionCommand,
15+
ESQLAstHeaderCommand,
1516
ESQLAstJoinCommand,
1617
ESQLAstQueryExpression,
1718
ESQLAstRerankCommand,
@@ -216,6 +217,19 @@ export class GlobalVisitorContext<
216217
return this.visitCommandGeneric(parent, commandNode, input as any);
217218
}
218219

220+
public visitHeaderCommand(
221+
parent: contexts.VisitorContext | null,
222+
node: ESQLAstHeaderCommand,
223+
input: types.VisitorInput<Methods, 'visitHeaderCommand'>
224+
): types.VisitorOutput<Methods, 'visitHeaderCommand'> {
225+
this.assertMethodExists('visitHeaderCommand');
226+
227+
const context = new contexts.HeaderCommandVisitorContext(this, node, parent);
228+
const output = this.methods.visitHeaderCommand!(context, input);
229+
230+
return output;
231+
}
232+
219233
public visitFromCommand(
220234
parent: contexts.VisitorContext | null,
221235
node: ESQLAstCommand,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export interface VisitorMethods<
149149
> {
150150
visitQuery?: Visitor<contexts.QueryVisitorContext<Visitors, Data>, any, any>;
151151
visitCommand?: Visitor<contexts.CommandVisitorContext<Visitors, Data>, any, any>;
152+
visitHeaderCommand?: Visitor<contexts.HeaderCommandVisitorContext<Visitors, Data>, any, any>;
152153
visitFromCommand?: Visitor<contexts.FromCommandVisitorContext<Visitors, Data>, any, any>;
153154
visitLimitCommand?: Visitor<contexts.LimitCommandVisitorContext<Visitors, Data>, any, any>;
154155
visitExplainCommand?: Visitor<contexts.ExplainCommandVisitorContext<Visitors, Data>, any, any>;
@@ -243,6 +244,8 @@ export interface VisitorMethods<
243244
*/
244245
export type AstNodeToVisitorName<Node extends VisitorAstNode> = Node extends ESQLAstQueryNode
245246
? 'visitQuery'
247+
: Node extends ast.ESQLAstHeaderCommand
248+
? 'visitHeaderCommand'
246249
: Node extends ast.ESQLCommand
247250
? 'visitCommand'
248251
: Node extends ast.ESQLCommandOption

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,12 @@ export class Visitor<
225225
NonNullable<Methods['visitQuery']>
226226
>;
227227
}
228+
case 'header-command': {
229+
this.ctx.assertMethodExists('visitHeaderCommand');
230+
return this.ctx.methods.visitHeaderCommand!(ctx as any, input) as ReturnType<
231+
NonNullable<Methods['visitHeaderCommand']>
232+
>;
233+
}
228234
case 'command': {
229235
this.ctx.assertMethodExists('visitCommand');
230236
return this.ctx.methods.visitCommand!(ctx as any, input) as ReturnType<
@@ -270,6 +276,21 @@ export class Visitor<
270276
return this.ctx.visitCommand(null, node, input);
271277
}
272278

279+
/**
280+
* Traverse starting from known header command node with default context.
281+
*
282+
* @param node Header command node to traverse.
283+
* @param input Input to pass to the first visitor.
284+
* @returns The output of the visitor.
285+
*/
286+
public visitHeaderCommand(
287+
node: import('../types').ESQLAstHeaderCommand,
288+
input: UndefinedToVoid<Parameters<NonNullable<Methods['visitHeaderCommand']>>[1]>
289+
) {
290+
this.ctx.assertMethodExists('visitHeaderCommand');
291+
return this.ctx.visitHeaderCommand(null, node, input);
292+
}
293+
273294
/**
274295
* Traverse starting from known expression node with default context.
275296
*

0 commit comments

Comments
 (0)