Skip to content

Commit

Permalink
[ES|QL] Implements Visitor pattern for ES|QL AST (elastic#189516)
Browse files Browse the repository at this point in the history
## Summary

Partially addresses elastic#182255

- Implements the `Visitor` pattern for ES|QL AST trees. Unlike the
`Walker` (which automatically traverses the whole tree exactly once),
the `Visitor` pattern allows to control the traversal. The developer has
to manually call children "visitor" routines. This manual handling
enables:
  - The AST tree can be traversed any number of times.
  - Only a specific subset of the tree can be travered.
- Each visitor receives a *context* object, which can provide the global
context as well as a linked list to all parent nodes.
- The context object also provides node-specific read/write
functionality.
  - Each visitor can receive *input* from its parent node.
  - Each visitor can return *output* to its parent node.
- The visitor nodes are strictly typed: the context object as well as
inputs and outputs have specific types. Also the inputs and outputs
TypeScript types are inferred automatically from the callback signature
the developer specifies and then the correct input/output usage is
enforced in other callbacks.
- The "scenarios" test file contains real-world usage scenarios, like:
- [Changing the
`LIMIT`](https://github.com/elastic/kibana/pull/189516/files#diff-571e21fd50dbdb664e71297e2edd72c1a1b2b96f346248f0360558ef8ceb75f7R20)
- [Removing a "filter", a `WHERE`
command](https://github.com/elastic/kibana/pull/189516/files#diff-571e21fd50dbdb664e71297e2edd72c1a1b2b96f346248f0360558ef8ceb75f7R57)
 


### Checklist

Delete any items that are not applicable to this PR.

- [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


### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
vadimkibana and elasticmachine committed Jul 31, 2024
1 parent 6de843b commit e49e89e
Show file tree
Hide file tree
Showing 18 changed files with 2,466 additions and 0 deletions.
369 changes: 369 additions & 0 deletions packages/kbn-esql-ast/src/__tests__/ast_parser.commands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { getAstAndSyntaxErrors as parse } from '../ast_parser';

describe('commands', () => {
describe('correctly formatted, basic usage', () => {
it('SHOW', () => {
const query = 'SHOW info';
const { ast } = parse(query);

expect(ast).toMatchObject([
{
type: 'command',
name: 'show',
args: [
{
type: 'function',
name: 'info',
},
],
},
]);
});

it('META', () => {
const query = 'META functions';
const { ast } = parse(query);

expect(ast).toMatchObject([
{
type: 'command',
name: 'meta',
args: [
{
type: 'function',
name: 'functions',
},
],
},
]);
});

it('FROM', () => {
const query = 'FROM index';
const { ast } = parse(query);

expect(ast).toMatchObject([
{
type: 'command',
name: 'from',
args: [
{
type: 'source',
name: 'index',
},
],
},
]);
});

it('ROW', () => {
const query = 'ROW 1';
const { ast } = parse(query);

expect(ast).toMatchObject([
{
type: 'command',
name: 'row',
args: [
{
type: 'literal',
value: 1,
},
],
},
]);
});

it('EVAL', () => {
const query = 'FROM index | EVAL 1';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'eval',
args: [
{
type: 'literal',
value: 1,
},
],
},
]);
});

it('STATS', () => {
const query = 'FROM index | STATS 1';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'stats',
args: [
{
type: 'literal',
value: 1,
},
],
},
]);
});

it('LIMIT', () => {
const query = 'FROM index | LIMIT 1';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'limit',
args: [
{
type: 'literal',
value: 1,
},
],
},
]);
});

it('KEEP', () => {
const query = 'FROM index | KEEP abc';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'keep',
args: [
{
type: 'column',
name: 'abc',
},
],
},
]);
});

it('SORT', () => {
const query = 'FROM index | SORT 1';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'sort',
args: [
{
type: 'literal',
value: 1,
},
],
},
]);
});

it('WHERE', () => {
const query = 'FROM index | WHERE 1';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'where',
args: [
{
type: 'literal',
value: 1,
},
],
},
]);
});

it('DROP', () => {
const query = 'FROM index | DROP abc';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'drop',
args: [
{
type: 'column',
name: 'abc',
},
],
},
]);
});

it('RENAME', () => {
const query = 'FROM index | RENAME a AS b, c AS d';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'rename',
args: [
{
type: 'option',
name: 'as',
args: [
{
type: 'column',
name: 'a',
},
{
type: 'column',
name: 'b',
},
],
},
{
type: 'option',
name: 'as',
args: [
{
type: 'column',
name: 'c',
},
{
type: 'column',
name: 'd',
},
],
},
],
},
]);
});

it('DISSECT', () => {
const query = 'FROM index | DISSECT a "b" APPEND_SEPARATOR="c"';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'dissect',
args: [
{
type: 'column',
name: 'a',
},
{
type: 'literal',
value: '"b"',
},
{
type: 'option',
name: 'append_separator',
args: [
{
type: 'literal',
value: '"c"',
},
],
},
],
},
]);
});

it('GROK', () => {
const query = 'FROM index | GROK a "b"';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'grok',
args: [
{
type: 'column',
name: 'a',
},
{
type: 'literal',
value: '"b"',
},
],
},
]);
});

it('ENRICH', () => {
const query = 'FROM index | ENRICH a ON b WITH c, d';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'enrich',
args: [
{
type: 'source',
name: 'a',
},
{
type: 'option',
name: 'on',
args: [
{
type: 'column',
name: 'b',
},
],
},
{
type: 'option',
name: 'with',
},
],
},
]);
});

it('MV_EXPAND', () => {
const query = 'FROM index | MV_EXPAND a ';
const { ast } = parse(query);

expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'mv_expand',
args: [
{
type: 'column',
name: 'a',
},
],
},
]);
});
});
});
25 changes: 25 additions & 0 deletions packages/kbn-esql-ast/src/__tests__/ast_parser.literal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { getAstAndSyntaxErrors as parse } from '../ast_parser';
import { ESQLLiteral } from '../types';

describe('literal expression', () => {
it('numeric expression captures "value", and "name" fields', () => {
const text = 'ROW 1';
const { ast } = parse(text);
const literal = ast[0].args[0] as ESQLLiteral;

expect(literal).toMatchObject({
type: 'literal',
literalType: 'number',
name: '1',
value: 1,
});
});
});
Loading

0 comments on commit e49e89e

Please sign in to comment.