[8.x] [ES|QL] Match expression support in the ES|QL AST package (#215336) (#215367)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ES|QL] Match expression support in the ES|QL AST package
(#215336)](https://github.com/elastic/kibana/pull/215336)

<!--- Backport version: 9.6.6 -->

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

<!--BACKPORT [{"author":{"name":"Vadim
Kibana","email":"82822460+vadimkibana@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-03-20T16:19:45Z","message":"[ES|QL]
Match expression support in the ES|QL AST package (#215336)\n\n##
Summary\n\nPartially addresses
https://github.com/elastic/kibana/issues/214359\n\n- Adds support for
*MatchExpression* in `WHERE` command.\n - `WHERE column :: cast :
condition`\n - Support for cast and condition parsing\n - Support for
pretty-printing\n\n\n\n### Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"c8abafc6e7a63beeed404b0ea1d2a4dfd7777dc2","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["review","release_note:skip","Feature:ES|QL","Team:ESQL","backport:version","v9.1.0","v8.19.0"],"title":"[ES|QL]
Match expression support in the ES|QL AST
package","number":215336,"url":"https://github.com/elastic/kibana/pull/215336","mergeCommit":{"message":"[ES|QL]
Match expression support in the ES|QL AST package (#215336)\n\n##
Summary\n\nPartially addresses
https://github.com/elastic/kibana/issues/214359\n\n- Adds support for
*MatchExpression* in `WHERE` command.\n - `WHERE column :: cast :
condition`\n - Support for cast and condition parsing\n - Support for
pretty-printing\n\n\n\n### Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"c8abafc6e7a63beeed404b0ea1d2a4dfd7777dc2"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/215336","number":215336,"mergeCommit":{"message":"[ES|QL]
Match expression support in the ES|QL AST package (#215336)\n\n##
Summary\n\nPartially addresses
https://github.com/elastic/kibana/issues/214359\n\n- Adds support for
*MatchExpression* in `WHERE` command.\n - `WHERE column :: cast :
condition`\n - Support for cast and condition parsing\n - Support for
pretty-printing\n\n\n\n### Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"c8abafc6e7a63beeed404b0ea1d2a4dfd7777dc2"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2025-03-20 19:23:46 +01:00 committed by GitHub
parent 915a7f8549
commit 0a7f9f74dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 187 additions and 4 deletions

View file

@ -8,6 +8,7 @@
*/
import { parse } from '..';
import { ESQLColumn, ESQLCommand, ESQLFunction, ESQLInlineCast } from '../../types';
describe('WHERE', () => {
describe('correctly formatted', () => {
@ -35,5 +36,114 @@ describe('WHERE', () => {
},
]);
});
describe('match expression', () => {
it('simple column name', () => {
const text = `FROM index | WHERE abc`;
const { root } = parse(text);
expect(root.commands[1]).toMatchObject({
type: 'command',
name: 'where',
args: [
{
type: 'column',
name: 'abc',
},
],
});
});
it('simple column with match expression', () => {
const text = `FROM index | WHERE abc : 123`;
const { root } = parse(text);
expect(root.commands[1]).toMatchObject({
type: 'command',
name: 'where',
args: [
{
type: 'function',
subtype: 'binary-expression',
name: ':',
args: [
{
type: 'column',
name: 'abc',
},
{
type: 'literal',
literalType: 'integer',
value: 123,
},
],
},
],
});
});
it('correctly reports match expression location', () => {
const text = `FROM index | WHERE abc /*a*/ : /*a*/ 123`;
const { root } = parse(text);
const expression = root.commands[1].args[0] as ESQLFunction;
expect(expression.name).toBe(':');
expect(text.slice(expression.location.min, expression.location.max + 1)).toBe(
'abc /*a*/ : /*a*/ 123'
);
});
it('simple column with match expression and inline cast', () => {
const text = `FROM index | WHERE abc :: INTEGER : 123`;
const { root } = parse(text);
expect(root.commands[1]).toMatchObject({
type: 'command',
name: 'where',
args: [
{
type: 'function',
subtype: 'binary-expression',
name: ':',
args: [
{
type: 'inlineCast',
castType: 'integer',
value: {
type: 'column',
name: 'abc',
},
},
{
type: 'literal',
literalType: 'integer',
value: 123,
},
],
},
],
});
});
it('correctly reports match expression with inline cast location', () => {
const text = `FROM index | WHERE abc /*a*/ :: /*a*/ INTEGER : 123`;
const { root } = parse(text);
const command = root.commands[1] as ESQLCommand;
const match = command.args[0] as ESQLFunction;
const cast = match.args[0] as ESQLInlineCast;
const column = cast.value as ESQLColumn;
expect(text.slice(command.location.min, command.location.max + 1)).toBe(
'WHERE abc /*a*/ :: /*a*/ INTEGER : 123'
);
expect(text.slice(match.location.min, match.location.max + 1)).toBe(
'abc /*a*/ :: /*a*/ INTEGER : 123'
);
expect(text.slice(cast.location.min, cast.location.max + 1)).toBe(
'abc /*a*/ :: /*a*/ INTEGER'
);
expect(text.slice(column.location.min, column.location.max + 1)).toBe('abc');
});
});
});
});

View file

@ -51,7 +51,6 @@ import {
visitByOption,
collectAllColumnIdentifiers,
visitRenameClauses,
collectBooleanExpression,
visitOrderExpressions,
getPolicyName,
getMatchField,
@ -63,6 +62,7 @@ import { createDissectCommand } from './factories/dissect';
import { createGrokCommand } from './factories/grok';
import { createStatsCommand } from './factories/stats';
import { createChangePointCommand } from './factories/change_point';
import { createWhereCommand } from './factories/where';
export class ESQLAstBuilderListener implements ESQLParserListener {
private ast: ESQLAst = [];
@ -102,9 +102,9 @@ export class ESQLAstBuilderListener implements ESQLParserListener {
* @param ctx the parse tree
*/
exitWhereCommand(ctx: WhereCommandContext) {
const command = createCommand('where', ctx);
const command = createWhereCommand(ctx);
this.ast.push(command);
command.args.push(...collectBooleanExpression(ctx.booleanExpression()));
}
/**

View file

@ -0,0 +1,23 @@
/*
* 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 { WhereCommandContext } from '../../antlr/esql_parser';
import { ESQLCommand } from '../../types';
import { createCommand } from '../factories';
import { collectBooleanExpression } from '../walkers';
export const createWhereCommand = (ctx: WhereCommandContext): ESQLCommand<'where'> => {
const command = createCommand('where', ctx);
const expressions = collectBooleanExpression(ctx.booleanExpression());
command.args.push(expressions[0]);
return command;
};

View file

@ -59,6 +59,8 @@ import {
InlineCastContext,
IndexPatternContext,
InlinestatsCommandContext,
MatchExpressionContext,
MatchBooleanExpressionContext,
} from '../antlr/esql_parser';
import {
createSource,
@ -83,6 +85,7 @@ import {
createFunctionCall,
createParam,
createLiteralString,
createBinaryExpression,
} from './factories';
import {
@ -94,8 +97,12 @@ import {
ESQLAstField,
ESQLInlineCast,
ESQLOrderExpression,
ESQLBinaryExpression,
InlineCastingType,
} from '../types';
import { firstItem, lastItem } from '../visitor/utils';
import { Builder } from '../builder';
import { getPosition } from './helpers';
export function collectAllSourceIdentifiers(ctx: FromCommandContext): ESQLAstItem[] {
const fromContexts = ctx.getTypedRuleContexts(IndexPatternContext);
@ -512,9 +519,15 @@ function collectDefaultExpression(ctx: BooleanExpressionContext) {
export function collectBooleanExpression(ctx: BooleanExpressionContext | undefined): ESQLAstItem[] {
const ast: ESQLAstItem[] = [];
if (!ctx) {
return ast;
}
if (ctx instanceof MatchExpressionContext) {
return [visitMatchExpression(ctx)];
}
return ast
.concat(
collectLogicalExpression(ctx),
@ -525,6 +538,41 @@ export function collectBooleanExpression(ctx: BooleanExpressionContext | undefin
.flat();
}
type ESQLAstMatchBooleanExpression = ESQLColumn | ESQLBinaryExpression | ESQLInlineCast;
const visitMatchExpression = (ctx: MatchExpressionContext): ESQLAstMatchBooleanExpression => {
return visitMatchBooleanExpression(ctx.matchBooleanExpression());
};
const visitMatchBooleanExpression = (
ctx: MatchBooleanExpressionContext
): ESQLAstMatchBooleanExpression => {
let expression: ESQLAstMatchBooleanExpression = createColumn(ctx.qualifiedName());
const dataTypeCtx = ctx.dataType();
const constantCtx = ctx.constant();
if (dataTypeCtx) {
expression = Builder.expression.inlineCast(
{
castType: dataTypeCtx.getText().toLowerCase() as InlineCastingType,
value: expression,
},
{
location: getPosition(ctx.start, dataTypeCtx.stop),
incomplete: Boolean(ctx.exception),
}
);
}
if (constantCtx) {
const constantExpression = getConstant(constantCtx);
expression = createBinaryExpression(':', ctx, [expression, constantExpression]);
}
return expression;
};
export function visitField(ctx: FieldContext) {
if (ctx.qualifiedName() && ctx.ASSIGN()) {
const fn = createFunction(ctx.ASSIGN()!.getText(), ctx, undefined, 'binary-expression');

View file

@ -215,7 +215,8 @@ export type BinaryExpressionOperator =
| BinaryExpressionComparisonOperator
| BinaryExpressionRegexOperator
| BinaryExpressionRenameOperator
| BinaryExpressionWhereOperator;
| BinaryExpressionWhereOperator
| BinaryExpressionMatchOperator;
export type BinaryExpressionArithmeticOperator = '+' | '-' | '*' | '/' | '%';
export type BinaryExpressionAssignmentOperator = '=';
@ -223,6 +224,7 @@ export type BinaryExpressionComparisonOperator = '==' | '=~' | '!=' | '<' | '<='
export type BinaryExpressionRegexOperator = 'like' | 'not_like' | 'rlike' | 'not_rlike';
export type BinaryExpressionRenameOperator = 'as';
export type BinaryExpressionWhereOperator = 'where';
export type BinaryExpressionMatchOperator = ':';
// from https://github.com/elastic/elasticsearch/blob/122e7288200ee03e9087c98dff6cebbc94e774aa/docs/reference/esql/functions/kibana/inline_cast.json
export type InlineCastingType =