Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[8.x] [ES|QL] AST node synthesizer (elastic#201814) (elastic#202409)
# Backport This will backport the following commits from `main` to `8.x`: - [[ES|QL] AST node synthesizer (elastic#201814)](elastic#201814) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Vadim Kibana","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-12-02T09:12:36Z","message":"[ES|QL] AST node synthesizer (elastic#201814)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/191812\r\n\r\nAdds ability to create ES|QL AST nodes from plain strings and compose\r\nthem.\r\n\r\nCreate an integer literal:\r\n\r\n```js\r\nconst node = expr `42`;\r\n```\r\n\r\nCreate any expression:\r\n\r\n```js\r\nconst node = expr `nested.field = fn(123)`;\r\n```\r\n\r\nCompose AST nodes:\r\n\r\n```js\r\nconst value = expr `123`;\r\nconst node = expr `nested.field = fn(${value})`;\r\n```\r\n\r\n## Usage\r\n\r\nYou can create an assignment expression AST node as simle as:\r\n\r\n```ts\r\nimport { synth } from '@kbn/esql-ast';\r\n\r\nconst node = synth.expr `my.field = max(10, ?my_param)`;\r\n// { type: 'function', name: '=', args: [ ... ]}\r\n```\r\n\r\nTo construct an equivalent AST node using the `Builder` class, you would\r\nneed to\r\nwrite the following code:\r\n\r\n```ts\r\nimport { Builder } from '@kbn/esql-ast';\r\n\r\nconst node = Builder.expression.func.binary('=', [\r\n Builder.expression.column({\r\n args: [Builder.identifier({ name: 'my' }), Builder.identifier({ name: 'field' })],\r\n }),\r\n Builder.expression.func.call('max', [\r\n Builder.expression.literal.integer(10),\r\n Builder.param.named({ value: 'my_param' }),\r\n ]),\r\n]);\r\n// { type: 'function', name: '=', args: [ ... ]}\r\n```\r\n\r\nYou can nest template strings to create more complex AST nodes:\r\n\r\n```ts\r\nconst field = synth.expr `my.field`;\r\nconst value = synth.expr `max(10, 20)`;\r\n\r\nconst assignment = synth.expr`${field} = ${value}`;\r\n// { type: 'function', name: '=', args: [ \r\n// { type: 'column', args: [ ... ] },\r\n// { type: 'function', name: 'max', args: [ ... ] }\r\n// ]}\r\n```\r\n\r\nUse the `synth.cmd` method to create command nodes:\r\n\r\n```ts\r\nconst command = synth.cmd `WHERE my.field == 10`;\r\n// { type: 'command', name: 'WHERE', args: [ ... ]}\r\n```\r\n\r\nAST nodes created by the synthesizer are pretty-printed when you coerce\r\nthem to\r\na string or call the `toString` method:\r\n\r\n```ts\r\nconst command = synth.cmd ` WHERE my.field == 10 `; // { type: 'command', ... }\r\nString(command); // \"WHERE my.field == 10\"\r\n```\r\n\r\n\r\n## Reference\r\n\r\n### `synth.expr`\r\n\r\nThe `synth.expr` synthesizes an expression AST nodes. (*Expressions* are\r\nbasically any thing that can go into a `WHERE` command, like boolean\r\nexpression,\r\nfunction call, literal, params, etc.)\r\n\r\nUse it as a function:\r\n\r\n```ts\r\nconst node = synth.expr('my.field = max(10, 20)');\r\n```\r\n\r\nSpecify parser options:\r\n\r\n```ts\r\nconst node = synth.expr('my.field = max(10, 20)', { withFormatting: false });\r\n```\r\n\r\nUse it as a template string tag:\r\n\r\n```ts\r\nconst node = synth.expr `my.field = max(10, 20)`;\r\n```\r\n\r\nSpecify parser options, when using as a template string tag:\r\n\r\n```ts\r\nconst node = synth.expr({ withFormatting: false }) `my.field = max(10, 20)`;\r\n```\r\n\r\nCombine nodes using template strings:\r\n\r\n```ts\r\nconst field = synth.expr `my.field`;\r\nconst node = synth.expr `${field} = max(10, 20)`;\r\n```\r\n\r\nPrint the node as a string:\r\n\r\n```ts\r\nconst node = synth.expr `my.field = max(10, 20)`;\r\nString(node); // 'my.field = max(10, 20)'\r\n```\r\n\r\n\r\n### `synth.cmd`\r\n\r\nThe `synth.cmd` synthesizes a command AST node (such as `SELECT`,\r\n`WHERE`,\r\netc.). You use it the same as the `synth.expr` function or template\r\nstring tag.\r\nThe only difference is that the `synth.cmd` function or tag creates a\r\ncommand\r\nAST node.\r\n\r\n```ts\r\nconst node = synth.cmd `WHERE my.field == 10`;\r\n// { type: 'command', name: 'where', args: [ ... ]}\r\n```\r\n\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] [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":"378002f9f60356d1d5fe04a6fdf0a7641175ca14","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["review","release_note:skip","v9.0.0","Feature:ES|QL","Team:ESQL","backport:version","v8.18.0"],"title":"[ES|QL] AST node synthesizer","number":201814,"url":"https://github.com/elastic/kibana/pull/201814","mergeCommit":{"message":"[ES|QL] AST node synthesizer (elastic#201814)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/191812\r\n\r\nAdds ability to create ES|QL AST nodes from plain strings and compose\r\nthem.\r\n\r\nCreate an integer literal:\r\n\r\n```js\r\nconst node = expr `42`;\r\n```\r\n\r\nCreate any expression:\r\n\r\n```js\r\nconst node = expr `nested.field = fn(123)`;\r\n```\r\n\r\nCompose AST nodes:\r\n\r\n```js\r\nconst value = expr `123`;\r\nconst node = expr `nested.field = fn(${value})`;\r\n```\r\n\r\n## Usage\r\n\r\nYou can create an assignment expression AST node as simle as:\r\n\r\n```ts\r\nimport { synth } from '@kbn/esql-ast';\r\n\r\nconst node = synth.expr `my.field = max(10, ?my_param)`;\r\n// { type: 'function', name: '=', args: [ ... ]}\r\n```\r\n\r\nTo construct an equivalent AST node using the `Builder` class, you would\r\nneed to\r\nwrite the following code:\r\n\r\n```ts\r\nimport { Builder } from '@kbn/esql-ast';\r\n\r\nconst node = Builder.expression.func.binary('=', [\r\n Builder.expression.column({\r\n args: [Builder.identifier({ name: 'my' }), Builder.identifier({ name: 'field' })],\r\n }),\r\n Builder.expression.func.call('max', [\r\n Builder.expression.literal.integer(10),\r\n Builder.param.named({ value: 'my_param' }),\r\n ]),\r\n]);\r\n// { type: 'function', name: '=', args: [ ... ]}\r\n```\r\n\r\nYou can nest template strings to create more complex AST nodes:\r\n\r\n```ts\r\nconst field = synth.expr `my.field`;\r\nconst value = synth.expr `max(10, 20)`;\r\n\r\nconst assignment = synth.expr`${field} = ${value}`;\r\n// { type: 'function', name: '=', args: [ \r\n// { type: 'column', args: [ ... ] },\r\n// { type: 'function', name: 'max', args: [ ... ] }\r\n// ]}\r\n```\r\n\r\nUse the `synth.cmd` method to create command nodes:\r\n\r\n```ts\r\nconst command = synth.cmd `WHERE my.field == 10`;\r\n// { type: 'command', name: 'WHERE', args: [ ... ]}\r\n```\r\n\r\nAST nodes created by the synthesizer are pretty-printed when you coerce\r\nthem to\r\na string or call the `toString` method:\r\n\r\n```ts\r\nconst command = synth.cmd ` WHERE my.field == 10 `; // { type: 'command', ... }\r\nString(command); // \"WHERE my.field == 10\"\r\n```\r\n\r\n\r\n## Reference\r\n\r\n### `synth.expr`\r\n\r\nThe `synth.expr` synthesizes an expression AST nodes. (*Expressions* are\r\nbasically any thing that can go into a `WHERE` command, like boolean\r\nexpression,\r\nfunction call, literal, params, etc.)\r\n\r\nUse it as a function:\r\n\r\n```ts\r\nconst node = synth.expr('my.field = max(10, 20)');\r\n```\r\n\r\nSpecify parser options:\r\n\r\n```ts\r\nconst node = synth.expr('my.field = max(10, 20)', { withFormatting: false });\r\n```\r\n\r\nUse it as a template string tag:\r\n\r\n```ts\r\nconst node = synth.expr `my.field = max(10, 20)`;\r\n```\r\n\r\nSpecify parser options, when using as a template string tag:\r\n\r\n```ts\r\nconst node = synth.expr({ withFormatting: false }) `my.field = max(10, 20)`;\r\n```\r\n\r\nCombine nodes using template strings:\r\n\r\n```ts\r\nconst field = synth.expr `my.field`;\r\nconst node = synth.expr `${field} = max(10, 20)`;\r\n```\r\n\r\nPrint the node as a string:\r\n\r\n```ts\r\nconst node = synth.expr `my.field = max(10, 20)`;\r\nString(node); // 'my.field = max(10, 20)'\r\n```\r\n\r\n\r\n### `synth.cmd`\r\n\r\nThe `synth.cmd` synthesizes a command AST node (such as `SELECT`,\r\n`WHERE`,\r\netc.). You use it the same as the `synth.expr` function or template\r\nstring tag.\r\nThe only difference is that the `synth.cmd` function or tag creates a\r\ncommand\r\nAST node.\r\n\r\n```ts\r\nconst node = synth.cmd `WHERE my.field == 10`;\r\n// { type: 'command', name: 'where', args: [ ... ]}\r\n```\r\n\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] [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":"378002f9f60356d1d5fe04a6fdf0a7641175ca14"}},"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/201814","number":201814,"mergeCommit":{"message":"[ES|QL] AST node synthesizer (elastic#201814)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/191812\r\n\r\nAdds ability to create ES|QL AST nodes from plain strings and compose\r\nthem.\r\n\r\nCreate an integer literal:\r\n\r\n```js\r\nconst node = expr `42`;\r\n```\r\n\r\nCreate any expression:\r\n\r\n```js\r\nconst node = expr `nested.field = fn(123)`;\r\n```\r\n\r\nCompose AST nodes:\r\n\r\n```js\r\nconst value = expr `123`;\r\nconst node = expr `nested.field = fn(${value})`;\r\n```\r\n\r\n## Usage\r\n\r\nYou can create an assignment expression AST node as simle as:\r\n\r\n```ts\r\nimport { synth } from '@kbn/esql-ast';\r\n\r\nconst node = synth.expr `my.field = max(10, ?my_param)`;\r\n// { type: 'function', name: '=', args: [ ... ]}\r\n```\r\n\r\nTo construct an equivalent AST node using the `Builder` class, you would\r\nneed to\r\nwrite the following code:\r\n\r\n```ts\r\nimport { Builder } from '@kbn/esql-ast';\r\n\r\nconst node = Builder.expression.func.binary('=', [\r\n Builder.expression.column({\r\n args: [Builder.identifier({ name: 'my' }), Builder.identifier({ name: 'field' })],\r\n }),\r\n Builder.expression.func.call('max', [\r\n Builder.expression.literal.integer(10),\r\n Builder.param.named({ value: 'my_param' }),\r\n ]),\r\n]);\r\n// { type: 'function', name: '=', args: [ ... ]}\r\n```\r\n\r\nYou can nest template strings to create more complex AST nodes:\r\n\r\n```ts\r\nconst field = synth.expr `my.field`;\r\nconst value = synth.expr `max(10, 20)`;\r\n\r\nconst assignment = synth.expr`${field} = ${value}`;\r\n// { type: 'function', name: '=', args: [ \r\n// { type: 'column', args: [ ... ] },\r\n// { type: 'function', name: 'max', args: [ ... ] }\r\n// ]}\r\n```\r\n\r\nUse the `synth.cmd` method to create command nodes:\r\n\r\n```ts\r\nconst command = synth.cmd `WHERE my.field == 10`;\r\n// { type: 'command', name: 'WHERE', args: [ ... ]}\r\n```\r\n\r\nAST nodes created by the synthesizer are pretty-printed when you coerce\r\nthem to\r\na string or call the `toString` method:\r\n\r\n```ts\r\nconst command = synth.cmd ` WHERE my.field == 10 `; // { type: 'command', ... }\r\nString(command); // \"WHERE my.field == 10\"\r\n```\r\n\r\n\r\n## Reference\r\n\r\n### `synth.expr`\r\n\r\nThe `synth.expr` synthesizes an expression AST nodes. (*Expressions* are\r\nbasically any thing that can go into a `WHERE` command, like boolean\r\nexpression,\r\nfunction call, literal, params, etc.)\r\n\r\nUse it as a function:\r\n\r\n```ts\r\nconst node = synth.expr('my.field = max(10, 20)');\r\n```\r\n\r\nSpecify parser options:\r\n\r\n```ts\r\nconst node = synth.expr('my.field = max(10, 20)', { withFormatting: false });\r\n```\r\n\r\nUse it as a template string tag:\r\n\r\n```ts\r\nconst node = synth.expr `my.field = max(10, 20)`;\r\n```\r\n\r\nSpecify parser options, when using as a template string tag:\r\n\r\n```ts\r\nconst node = synth.expr({ withFormatting: false }) `my.field = max(10, 20)`;\r\n```\r\n\r\nCombine nodes using template strings:\r\n\r\n```ts\r\nconst field = synth.expr `my.field`;\r\nconst node = synth.expr `${field} = max(10, 20)`;\r\n```\r\n\r\nPrint the node as a string:\r\n\r\n```ts\r\nconst node = synth.expr `my.field = max(10, 20)`;\r\nString(node); // 'my.field = max(10, 20)'\r\n```\r\n\r\n\r\n### `synth.cmd`\r\n\r\nThe `synth.cmd` synthesizes a command AST node (such as `SELECT`,\r\n`WHERE`,\r\netc.). You use it the same as the `synth.expr` function or template\r\nstring tag.\r\nThe only difference is that the `synth.cmd` function or tag creates a\r\ncommand\r\nAST node.\r\n\r\n```ts\r\nconst node = synth.cmd `WHERE my.field == 10`;\r\n// { type: 'command', name: 'where', args: [ ... ]}\r\n```\r\n\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] [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":"378002f9f60356d1d5fe04a6fdf0a7641175ca14"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Vadim Kibana <[email protected]>
- Loading branch information