Skip to content

Commit

Permalink
[8.x] [ES|QL] More AST mutation APIs (#196240) (#196355)
Browse files Browse the repository at this point in the history
# Backport

This will backport the following commits from `main` to `8.x`:
- [[ES|QL] More AST mutation APIs
(#196240)](#196240)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Vadim
Kibana","email":"82822460+vadimkibana@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-15T15:45:03Z","message":"[ES|QL]
More AST mutation APIs (#196240)\n\n## Summary\r\n\r\nPartially
addresses
https://github.com/elastic/kibana/issues/191812\r\n\r\nImplements the
following high-level ES|QL AST manipulation methods:\r\n\r\n\r\n-
`.generic`\r\n- `.appendCommandArgument()` &mdash; Add a new main
command argument to\r\na command.\r\n- `.removeCommandArgument()`
&mdash; Remove a command argument from the\r\nAST.\r\n- `.commands`\r\n
- `.from`\r\n - `.sources`\r\n - `.list()` &mdash; List all `FROM`
sources.\r\n - `.find()` &mdash; Find a source by name.\r\n -
`.remove()` &mdash; Remove a source by name.\r\n - `.insert()` &mdash;
Insert a source.\r\n - `.upsert()` &mdash; Insert a source, if it does
not exist.\r\n - `.limit`\r\n - `.list()` &mdash; List all `LIMIT`
commands.\r\n - `.byIndex()` &mdash; Find a `LIMIT` command by
index.\r\n - `.find()` &mdash; Find a `LIMIT` command by a predicate
function.\r\n - `.remove()` &mdash; Remove a `LIMIT` command by
index.\r\n- `.set()` &mdash; Set the limit value of a specific `LIMIT`
command.\r\n- `.upsert()` &mdash; Insert a `LIMIT` command, or update
the limit\r\nvalue if it already exists.\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n-
[x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n\r\n### For
maintainers\r\n\r\n- [x] This was checked for breaking API changes and
was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels)","sha":"10364fba2db8bb2080a97173c76a9d1aef1e80ed","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["review","release_note:skip","v9.0.0","backport:prev-minor","Feature:ES|QL","Team:ESQL","v8.16.0"],"title":"[ES|QL]
More AST mutation
APIs","number":196240,"url":"https://github.com/elastic/kibana/pull/196240","mergeCommit":{"message":"[ES|QL]
More AST mutation APIs (#196240)\n\n## Summary\r\n\r\nPartially
addresses
https://github.com/elastic/kibana/issues/191812\r\n\r\nImplements the
following high-level ES|QL AST manipulation methods:\r\n\r\n\r\n-
`.generic`\r\n- `.appendCommandArgument()` &mdash; Add a new main
command argument to\r\na command.\r\n- `.removeCommandArgument()`
&mdash; Remove a command argument from the\r\nAST.\r\n- `.commands`\r\n
- `.from`\r\n - `.sources`\r\n - `.list()` &mdash; List all `FROM`
sources.\r\n - `.find()` &mdash; Find a source by name.\r\n -
`.remove()` &mdash; Remove a source by name.\r\n - `.insert()` &mdash;
Insert a source.\r\n - `.upsert()` &mdash; Insert a source, if it does
not exist.\r\n - `.limit`\r\n - `.list()` &mdash; List all `LIMIT`
commands.\r\n - `.byIndex()` &mdash; Find a `LIMIT` command by
index.\r\n - `.find()` &mdash; Find a `LIMIT` command by a predicate
function.\r\n - `.remove()` &mdash; Remove a `LIMIT` command by
index.\r\n- `.set()` &mdash; Set the limit value of a specific `LIMIT`
command.\r\n- `.upsert()` &mdash; Insert a `LIMIT` command, or update
the limit\r\nvalue if it already exists.\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n-
[x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n\r\n### For
maintainers\r\n\r\n- [x] This was checked for breaking API changes and
was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels)","sha":"10364fba2db8bb2080a97173c76a9d1aef1e80ed"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196240","number":196240,"mergeCommit":{"message":"[ES|QL]
More AST mutation APIs (#196240)\n\n## Summary\r\n\r\nPartially
addresses
https://github.com/elastic/kibana/issues/191812\r\n\r\nImplements the
following high-level ES|QL AST manipulation methods:\r\n\r\n\r\n-
`.generic`\r\n- `.appendCommandArgument()` &mdash; Add a new main
command argument to\r\na command.\r\n- `.removeCommandArgument()`
&mdash; Remove a command argument from the\r\nAST.\r\n- `.commands`\r\n
- `.from`\r\n - `.sources`\r\n - `.list()` &mdash; List all `FROM`
sources.\r\n - `.find()` &mdash; Find a source by name.\r\n -
`.remove()` &mdash; Remove a source by name.\r\n - `.insert()` &mdash;
Insert a source.\r\n - `.upsert()` &mdash; Insert a source, if it does
not exist.\r\n - `.limit`\r\n - `.list()` &mdash; List all `LIMIT`
commands.\r\n - `.byIndex()` &mdash; Find a `LIMIT` command by
index.\r\n - `.find()` &mdash; Find a `LIMIT` command by a predicate
function.\r\n - `.remove()` &mdash; Remove a `LIMIT` command by
index.\r\n- `.set()` &mdash; Set the limit value of a specific `LIMIT`
command.\r\n- `.upsert()` &mdash; Insert a `LIMIT` command, or update
the limit\r\nvalue if it already exists.\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n-
[x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n\r\n### For
maintainers\r\n\r\n- [x] This was checked for breaking API changes and
was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels)","sha":"10364fba2db8bb2080a97173c76a9d1aef1e80ed"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com>
  • Loading branch information
kibanamachine and vadimkibana authored Oct 15, 2024
1 parent 11f535a commit 79f9d05
Show file tree
Hide file tree
Showing 12 changed files with 1,003 additions and 13 deletions.
14 changes: 14 additions & 0 deletions packages/kbn-esql-ast/src/ast/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { ESQLAstNode, ESQLCommandOption } from '../types';

export const isOptionNode = (node: ESQLAstNode): node is ESQLCommandOption => {
return !!node && typeof node === 'object' && !Array.isArray(node) && node.type === 'option';
};
17 changes: 17 additions & 0 deletions packages/kbn-esql-ast/src/builder/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,23 @@ export namespace Builder {
};
};

export const indexSource = (
index: string,
cluster?: string,
template?: Omit<AstNodeTemplate<ESQLSource>, 'name' | 'index' | 'cluster'>,
fromParser?: Partial<AstNodeParserFields>
): ESQLSource => {
return {
...template,
...Builder.parserFields(fromParser),
index,
cluster,
name: (cluster ? cluster + ':' : '') + index,
sourceType: 'index',
type: 'source',
};
};

export const column = (
template: Omit<AstNodeTemplate<ESQLColumn>, 'name' | 'quoted'>,
fromParser?: Partial<AstNodeParserFields>
Expand Down
42 changes: 34 additions & 8 deletions packages/kbn-esql-ast/src/mutate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,37 @@ console.log(src); // FROM index METADATA _lang, _id

## API

- `.commands.from.metadata.list()` &mdash; List all `METADATA` fields.
- `.commands.from.metadata.find()` &mdash; Find a `METADATA` field by name.
- `.commands.from.metadata.removeByPredicate()` &mdash; Remove a `METADATA`
field by matching a predicate.
- `.commands.from.metadata.remove()` &mdash; Remove a `METADATA` field by name.
- `.commands.from.metadata.insert()` &mdash; Insert a `METADATA` field.
- `.commands.from.metadata.upsert()` &mdash; Insert `METADATA` field, if it does
not exist.
- `.generic`
- `.listCommands()` &mdash; Lists all commands. Returns an iterator.
- `.findCommand()` &mdash; Finds a specific command by a predicate function.
- `.findCommandOption()` &mdash; Finds a specific command option by a predicate function.
- `.findCommandByName()` &mdash; Finds a specific command by name.
- `.findCommandOptionByName()` &mdash; Finds a specific command option by name.
- `.appendCommand()` &mdash; Add a new command to the AST.
- `.appendCommandOption()` &mdash; Add a new command option to a command.
- `.appendCommandArgument()` &mdash; Add a new main command argument to a command.
- `.removeCommand()` &mdash; Remove a command from the AST.
- `.removeCommandOption()` &mdash; Remove a command option from the AST.
- `.removeCommandArgument()` &mdash; Remove a command argument from the AST.
- `.commands`
- `.from`
- `.sources`
- `.list()` &mdash; List all `FROM` sources.
- `.find()` &mdash; Find a source by name.
- `.remove()` &mdash; Remove a source by name.
- `.insert()` &mdash; Insert a source.
- `.upsert()` &mdash; Insert a source, if it does not exist.
- `.metadata`
- `.list()` &mdash; List all `METADATA` fields.
- `.find()` &mdash; Find a `METADATA` field by name.
- `.removeByPredicate()` &mdash; Remove a `METADATA` field by matching a predicate function.
- `.remove()` &mdash; Remove a `METADATA` field by name.
- `.insert()` &mdash; Insert a `METADATA` field.
- `.upsert()` &mdash; Insert `METADATA` field, if it does not exist.
- `.limit`
- `.list()` &mdash; List all `LIMIT` commands.
- `.byIndex()` &mdash; Find a `LIMIT` command by index.
- `.find()` &mdash; Find a `LIMIT` command by a predicate function.
- `.remove()` &mdash; Remove a `LIMIT` command by index.
- `.set()` &mdash; Set the limit value of a specific `LIMIT` command.
- `.upsert()` &mdash; Insert a `LIMIT` command, or update the limit value if it already exists.
3 changes: 2 additions & 1 deletion packages/kbn-esql-ast/src/mutate/commands/from/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import * as sources from './sources';
import * as metadata from './metadata';

export { metadata };
export { sources, metadata };
2 changes: 1 addition & 1 deletion packages/kbn-esql-ast/src/mutate/commands/from/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export const insert = (
return;
}

option = generic.insertCommandOption(command, 'metadata');
option = generic.appendCommandOption(command, 'metadata');
}

const parts: string[] = typeof fieldName === 'string' ? [fieldName] : fieldName;
Expand Down
246 changes: 246 additions & 0 deletions packages/kbn-esql-ast/src/mutate/commands/from/sources.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { parse } from '../../../parser';
import { BasicPrettyPrinter } from '../../../pretty_print';
import * as commands from '..';

describe('commands.from.sources', () => {
describe('.list()', () => {
it('returns empty array, if there are no sources', () => {
const src = 'ROW 123';
const { root } = parse(src);
const list = [...commands.from.sources.list(root)];

expect(list.length).toBe(0);
});

it('returns a single source', () => {
const src = 'FROM index METADATA a';
const { root } = parse(src);
const list = [...commands.from.sources.list(root)];

expect(list.length).toBe(1);
expect(list[0]).toMatchObject({
type: 'source',
});
});

it('returns all source fields', () => {
const src = 'FROM index, index2, cl:index3 METADATA a | LIMIT 88';
const { root } = parse(src);
const list = [...commands.from.sources.list(root)];

expect(list).toMatchObject([
{
type: 'source',
index: 'index',
},
{
type: 'source',
index: 'index2',
},
{
type: 'source',
index: 'index3',
cluster: 'cl',
},
]);
});
});

describe('.find()', () => {
it('returns undefined if source is not found', () => {
const src = 'FROM index | WHERE a = b | LIMIT 123';
const { root } = parse(src);
const source = commands.from.sources.find(root, 'abc');

expect(source).toBe(undefined);
});

it('can find a single source', () => {
const src = 'FROM index METADATA a';
const { root } = parse(src);
const source = commands.from.sources.find(root, 'index')!;

expect(source).toMatchObject({
type: 'source',
name: 'index',
index: 'index',
});
});

it('can find a source withing other sources', () => {
const src = 'FROM index, a, b, c:s1, s1, s2 METADATA a, b, c, _lang, _id';
const { root } = parse(src);
const source1 = commands.from.sources.find(root, 's2')!;
const source2 = commands.from.sources.find(root, 's1', 'c')!;

expect(source1).toMatchObject({
type: 'source',
name: 's2',
index: 's2',
});
expect(source2).toMatchObject({
type: 'source',
name: 'c:s1',
index: 's1',
cluster: 'c',
});
});
});

describe('.remove()', () => {
it('can remove a source from a list', () => {
const src1 = 'FROM a, b, c';
const { root } = parse(src1);
const src2 = BasicPrettyPrinter.print(root);

expect(src2).toBe('FROM a, b, c');

commands.from.sources.remove(root, 'b');

const src3 = BasicPrettyPrinter.print(root);

expect(src3).toBe('FROM a, c');
});

it('does nothing if source-to-delete does not exist', () => {
const src1 = 'FROM a, b, c';
const { root } = parse(src1);
const src2 = BasicPrettyPrinter.print(root);

expect(src2).toBe('FROM a, b, c');

commands.from.sources.remove(root, 'd');

const src3 = BasicPrettyPrinter.print(root);

expect(src3).toBe('FROM a, b, c');
});
});

describe('.insert()', () => {
it('can append a source', () => {
const src1 = 'FROM index METADATA a';
const { root } = parse(src1);

commands.from.sources.insert(root, 'index2');

const src2 = BasicPrettyPrinter.print(root);

expect(src2).toBe('FROM index, index2 METADATA a');
});

it('can insert at specified position', () => {
const src1 = 'FROM a1, a2, a3';
const { root } = parse(src1);

commands.from.sources.insert(root, 'x', '', 0);

const src2 = BasicPrettyPrinter.print(root);

expect(src2).toBe('FROM x, a1, a2, a3');

commands.from.sources.insert(root, 'y', '', 2);

const src3 = BasicPrettyPrinter.print(root);

expect(src3).toBe('FROM x, a1, y, a2, a3');

commands.from.sources.insert(root, 'z', '', 4);

const src4 = BasicPrettyPrinter.print(root);

expect(src4).toBe('FROM x, a1, y, a2, z, a3');
});

it('appends element, when insert position too high', () => {
const src1 = 'FROM a1, a2, a3';
const { root } = parse(src1);

commands.from.sources.insert(root, 'x', '', 999);

const src2 = BasicPrettyPrinter.print(root);

expect(src2).toBe('FROM a1, a2, a3, x');
});

it('can inset the same source twice', () => {
const src1 = 'FROM index';
const { root } = parse(src1);

commands.from.sources.insert(root, 'x', '', 999);
commands.from.sources.insert(root, 'x', '', 999);

const src2 = BasicPrettyPrinter.print(root);

expect(src2).toBe('FROM index, x, x');
});
});

describe('.upsert()', () => {
it('can append a source', () => {
const src1 = 'FROM index METADATA a';
const { root } = parse(src1);

commands.from.sources.upsert(root, 'index2');

const src2 = BasicPrettyPrinter.print(root);

expect(src2).toBe('FROM index, index2 METADATA a');
});

it('can upsert at specified position', () => {
const src1 = 'FROM a1, a2, a3';
const { root } = parse(src1);

commands.from.sources.upsert(root, 'x', '', 0);

const src2 = BasicPrettyPrinter.print(root);

expect(src2).toBe('FROM x, a1, a2, a3');

commands.from.sources.upsert(root, 'y', '', 2);

const src3 = BasicPrettyPrinter.print(root);

expect(src3).toBe('FROM x, a1, y, a2, a3');

commands.from.sources.upsert(root, 'z', '', 4);

const src4 = BasicPrettyPrinter.print(root);

expect(src4).toBe('FROM x, a1, y, a2, z, a3');
});

it('appends element, when upsert position too high', () => {
const src1 = 'FROM a1, a2, a3';
const { root } = parse(src1);

commands.from.sources.upsert(root, 'x', '', 999);

const src2 = BasicPrettyPrinter.print(root);

expect(src2).toBe('FROM a1, a2, a3, x');
});

it('inserting already existing source is a no-op', () => {
const src1 = 'FROM index';
const { root } = parse(src1);

commands.from.sources.upsert(root, 'x', '', 999);
commands.from.sources.upsert(root, 'x', '', 999);

const src2 = BasicPrettyPrinter.print(root);

expect(src2).toBe('FROM index, x');
});
});
});
Loading

0 comments on commit 79f9d05

Please sign in to comment.