diff --git a/src/platform/packages/shared/kbn-esql-ast/index.ts b/src/platform/packages/shared/kbn-esql-ast/index.ts index d4aa17a867018..d16d786a14a1f 100644 --- a/src/platform/packages/shared/kbn-esql-ast/index.ts +++ b/src/platform/packages/shared/kbn-esql-ast/index.ts @@ -32,20 +32,7 @@ export type { ESQLAstChangePointCommand, } from './src/types'; -export { - isColumn, - isDoubleLiteral, - isFunctionExpression, - isBinaryExpression, - isWhereExpression, - isFieldExpression, - isSource, - isIdentifier, - isIntegerLiteral, - isLiteral, - isParamLiteral, - isProperNode, -} from './src/ast/helpers'; +export * from './src/ast/is'; export { Builder, type AstNodeParserFields, type AstNodeTemplate } from './src/builder'; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/__tests__/walk_json.ts b/src/platform/packages/shared/kbn-esql-ast/src/__tests__/walk_json.ts new file mode 100644 index 0000000000000..10257ef60d62d --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-ast/src/__tests__/walk_json.ts @@ -0,0 +1,65 @@ +/* + * 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". + */ + +interface JsonWalkerOptions { + visitObject?: (node: Record) => void; + visitArray?: (node: unknown[]) => void; + visitString?: (node: string) => void; + visitNumber?: (node: number) => void; + visitBigint?: (node: bigint) => void; + visitBoolean?: (node: boolean) => void; + visitNull?: () => void; + visitUndefined?: () => void; +} + +const walkJson = (json: unknown, options: JsonWalkerOptions = {}) => { + switch (typeof json) { + case 'string': { + options.visitString?.(json); + break; + } + case 'number': { + options.visitNumber?.(json); + break; + } + case 'bigint': { + options.visitBigint?.(json as bigint); + break; + } + case 'boolean': { + options.visitBoolean?.(json); + break; + } + case 'undefined': { + options.visitUndefined?.(); + break; + } + case 'object': { + if (!json) { + options.visitNull?.(); + } else if (Array.isArray(json)) { + options.visitArray?.(json); + const length = json.length; + + for (let i = 0; i < length; i++) { + walkJson(json[i], options); + } + } else { + options.visitObject?.(json as Record); + const values = Object.values(json as Record); + const length = values.length; + + for (let i = 0; i < length; i++) { + const value = values[i]; + walkJson(value, options); + } + } + } + } +}; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/ast/constants.ts b/src/platform/packages/shared/kbn-esql-ast/src/ast/constants.ts deleted file mode 100644 index d5c027114e845..0000000000000 --- a/src/platform/packages/shared/kbn-esql-ast/src/ast/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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". - */ - -/** - * The group name of a binary expression. Groups are ordered by precedence. - */ -export enum BinaryExpressionGroup { - unknown = 0, - additive = 10, - multiplicative = 20, - assignment = 30, - comparison = 40, - regex = 50, -} diff --git a/src/platform/packages/shared/kbn-esql-ast/src/ast/grouping.ts b/src/platform/packages/shared/kbn-esql-ast/src/ast/grouping.ts new file mode 100644 index 0000000000000..9167f007b331b --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-ast/src/ast/grouping.ts @@ -0,0 +1,99 @@ +/* + * 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 { isBinaryExpression } from './is'; +import type { ESQLAstNode } from '../types'; + +/** + * The group name of a binary expression. Groups are ordered by precedence. + */ +export enum BinaryExpressionGroup { + /** + * No group, not a binary expression. + */ + none = 0, + + /** + * Binary expression, but its group is unknown. + */ + unknown = 1, + + /** + * Logical: `and`, `or` + */ + or = 10, + and = 11, + + /** + * Regular expression: `like`, `not like`, `rlike`, `not rlike` + */ + regex = 20, + + /** + * Assignment: `=`, `:=` + */ + assignment = 30, + + /** + * Comparison: `==`, `=~`, `!=`, `<`, `<=`, `>`, `>=` + */ + comparison = 40, + + /** + * Additive: `+`, `-` + */ + additive = 50, + + /** + * Multiplicative: `*`, `/`, `%` + */ + multiplicative = 60, +} + +/** + * Returns the group of a binary expression. + * + * @param node Any ES|QL AST node. + * @returns Binary expression group or undefined if the node is + * not a binary expression. + */ +export const binaryExpressionGroup = (node: ESQLAstNode): BinaryExpressionGroup => { + if (isBinaryExpression(node)) { + switch (node.name) { + case '+': + case '-': + return BinaryExpressionGroup.additive; + case '*': + case '/': + case '%': + return BinaryExpressionGroup.multiplicative; + case '=': + return BinaryExpressionGroup.assignment; + case '==': + case '=~': + case '!=': + case '<': + case '<=': + case '>': + case '>=': + return BinaryExpressionGroup.comparison; + case 'like': + case 'not like': + case 'rlike': + case 'not rlike': + return BinaryExpressionGroup.regex; + case 'or': + return BinaryExpressionGroup.or; + case 'and': + return BinaryExpressionGroup.and; + } + return BinaryExpressionGroup.unknown; + } + return BinaryExpressionGroup.none; +}; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/ast/helpers.ts b/src/platform/packages/shared/kbn-esql-ast/src/ast/helpers.ts deleted file mode 100644 index 93a7fa9ea9fbc..0000000000000 --- a/src/platform/packages/shared/kbn-esql-ast/src/ast/helpers.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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 type { - BinaryExpressionRenameOperator, - BinaryExpressionWhereOperator, - ESQLAstNode, - ESQLBinaryExpression, - ESQLColumn, - ESQLFunction, - ESQLIdentifier, - ESQLIntegerLiteral, - ESQLList, - ESQLLiteral, - ESQLParamLiteral, - ESQLProperNode, - ESQLSource, - ESQLStringLiteral, -} from '../types'; -import { BinaryExpressionGroup } from './constants'; - -export const isProperNode = (node: unknown): node is ESQLProperNode => - !!node && - typeof node === 'object' && - !Array.isArray(node) && - typeof (node as ESQLProperNode).type === 'string' && - !!(node as ESQLProperNode).type; - -export const isFunctionExpression = (node: unknown): node is ESQLFunction => - isProperNode(node) && node.type === 'function'; - -/** - * Returns true if the given node is a binary expression, i.e. an operator - * surrounded by two operands: - * - * ``` - * 1 + 1 - * column LIKE "foo" - * foo = "bar" - * ``` - * - * @param node Any ES|QL AST node. - */ -export const isBinaryExpression = (node: unknown): node is ESQLBinaryExpression => - isFunctionExpression(node) && node.subtype === 'binary-expression'; - -export const isWhereExpression = ( - node: unknown -): node is ESQLBinaryExpression => - isBinaryExpression(node) && node.name === 'where'; - -export const isAsExpression = ( - node: unknown -): node is ESQLBinaryExpression => - isBinaryExpression(node) && node.name === 'as'; - -export const isFieldExpression = ( - node: unknown -): node is ESQLBinaryExpression => - isBinaryExpression(node) && node.name === '='; - -export const isLiteral = (node: unknown): node is ESQLLiteral => - isProperNode(node) && node.type === 'literal'; - -export const isStringLiteral = (node: unknown): node is ESQLStringLiteral => - isLiteral(node) && node.literalType === 'keyword'; - -export const isIntegerLiteral = (node: unknown): node is ESQLIntegerLiteral => - isLiteral(node) && node.literalType === 'integer'; - -export const isDoubleLiteral = (node: unknown): node is ESQLIntegerLiteral => - isLiteral(node) && node.literalType === 'double'; - -export const isBooleanLiteral = (node: unknown): node is ESQLStringLiteral => - isLiteral(node) && node.literalType === 'boolean'; - -export const isParamLiteral = (node: unknown): node is ESQLParamLiteral => - isLiteral(node) && node.literalType === 'param'; - -export const isColumn = (node: unknown): node is ESQLColumn => - isProperNode(node) && node.type === 'column'; - -export const isSource = (node: unknown): node is ESQLSource => - isProperNode(node) && node.type === 'source'; - -export const isIdentifier = (node: unknown): node is ESQLIdentifier => - isProperNode(node) && node.type === 'identifier'; - -export const isList = (node: unknown): node is ESQLList => - isProperNode(node) && node.type === 'list'; - -/** - * Returns the group of a binary expression: - * - * - `additive`: `+`, `-` - * - `multiplicative`: `*`, `/`, `%` - * - `assignment`: `=` - * - `comparison`: `==`, `=~`, `!=`, `<`, `<=`, `>`, `>=` - * - `regex`: `like`, `not_like`, `rlike`, `not_rlike` - * @param node Any ES|QL AST node. - * @returns Binary expression group or undefined if the node is not a binary expression. - */ -export const binaryExpressionGroup = (node: ESQLAstNode): BinaryExpressionGroup => { - if (isBinaryExpression(node)) { - switch (node.name) { - case '+': - case '-': - return BinaryExpressionGroup.additive; - case '*': - case '/': - case '%': - return BinaryExpressionGroup.multiplicative; - case '=': - return BinaryExpressionGroup.assignment; - case '==': - case '=~': - case '!=': - case '<': - case '<=': - case '>': - case '>=': - return BinaryExpressionGroup.comparison; - case 'like': - case 'not like': - case 'rlike': - case 'not rlike': - return BinaryExpressionGroup.regex; - } - } - return BinaryExpressionGroup.unknown; -}; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/ast/is.ts b/src/platform/packages/shared/kbn-esql-ast/src/ast/is.ts new file mode 100644 index 0000000000000..8efb1ca62b1d3 --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-ast/src/ast/is.ts @@ -0,0 +1,84 @@ +/* + * 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 type * as types from '../types'; + +export const isProperNode = (node: unknown): node is types.ESQLProperNode => + !!node && + typeof node === 'object' && + !Array.isArray(node) && + typeof (node as types.ESQLProperNode).type === 'string' && + !!(node as types.ESQLProperNode).type; + +export const isFunctionExpression = (node: unknown): node is types.ESQLFunction => + isProperNode(node) && node.type === 'function'; + +/** + * Returns true if the given node is a binary expression, i.e. an operator + * surrounded by two operands: + * + * ``` + * 1 + 1 + * column LIKE "foo" + * foo = "bar" + * ``` + * + * @param node Any ES|QL AST node. + */ +export const isBinaryExpression = (node: unknown): node is types.ESQLBinaryExpression => + isFunctionExpression(node) && node.subtype === 'binary-expression'; + +export const isWhereExpression = ( + node: unknown +): node is types.ESQLBinaryExpression => + isBinaryExpression(node) && node.name === 'where'; + +export const isAsExpression = ( + node: unknown +): node is types.ESQLBinaryExpression => + isBinaryExpression(node) && node.name === 'as'; + +export const isFieldExpression = ( + node: unknown +): node is types.ESQLBinaryExpression => + isBinaryExpression(node) && node.name === '='; + +export const isLiteral = (node: unknown): node is types.ESQLLiteral => + isProperNode(node) && node.type === 'literal'; + +export const isStringLiteral = (node: unknown): node is types.ESQLStringLiteral => + isLiteral(node) && node.literalType === 'keyword'; + +export const isIntegerLiteral = (node: unknown): node is types.ESQLIntegerLiteral => + isLiteral(node) && node.literalType === 'integer'; + +export const isDoubleLiteral = (node: unknown): node is types.ESQLIntegerLiteral => + isLiteral(node) && node.literalType === 'double'; + +export const isBooleanLiteral = (node: unknown): node is types.ESQLStringLiteral => + isLiteral(node) && node.literalType === 'boolean'; + +export const isParamLiteral = (node: unknown): node is types.ESQLParamLiteral => + isLiteral(node) && node.literalType === 'param'; + +export const isColumn = (node: unknown): node is types.ESQLColumn => + isProperNode(node) && node.type === 'column'; + +export const isSource = (node: unknown): node is types.ESQLSource => + isProperNode(node) && node.type === 'source'; + +export const isIdentifier = (node: unknown): node is types.ESQLIdentifier => + isProperNode(node) && node.type === 'identifier'; + +export const isList = (node: unknown): node is types.ESQLList => + isProperNode(node) && node.type === 'list'; + +export const isOptionNode = (node: types.ESQLAstNode): node is types.ESQLCommandOption => { + return !!node && typeof node === 'object' && !Array.isArray(node) && node.type === 'option'; +}; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/ast/util.ts b/src/platform/packages/shared/kbn-esql-ast/src/ast/util.ts deleted file mode 100644 index 0cd94aba85cf1..0000000000000 --- a/src/platform/packages/shared/kbn-esql-ast/src/ast/util.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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'; -}; diff --git a/src/platform/packages/shared/kbn-esql-ast/src/builder/builder.ts b/src/platform/packages/shared/kbn-esql-ast/src/builder/builder.ts index de697715d2d25..bd4f06449a896 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/builder/builder.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/builder/builder.ts @@ -9,7 +9,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import { isStringLiteral } from '../ast/helpers'; +import { isStringLiteral } from '../ast/is'; import { LeafPrinter } from '../pretty_print'; import { ESQLAstComment, diff --git a/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/join/index.ts b/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/join/index.ts index 7216bce95ba63..908b33d353aab 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/join/index.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/join/index.ts @@ -8,7 +8,7 @@ */ import type { WalkerAstNode } from '../../../walker/walker'; -import { isAsExpression } from '../../../ast/helpers'; +import { isAsExpression } from '../../../ast/is'; import { Walker } from '../../../walker'; import type { ESQLAstExpression, diff --git a/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/stats/index.ts b/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/stats/index.ts index ae766bb2369d0..0ef56edc51501 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/stats/index.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/stats/index.ts @@ -23,7 +23,7 @@ import type { ESQLTimeInterval, } from '../../../types'; import * as generic from '../../generic'; -import { isColumn, isFunctionExpression, isParamLiteral } from '../../../ast/helpers'; +import { isColumn, isFunctionExpression, isParamLiteral } from '../../../ast/is'; import type { EsqlQuery } from '../../../query'; /** diff --git a/src/platform/packages/shared/kbn-esql-ast/src/mutate/generic/commands/args/index.ts b/src/platform/packages/shared/kbn-esql-ast/src/mutate/generic/commands/args/index.ts index 7072c38a5f1a8..eaf8c4749da08 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/mutate/generic/commands/args/index.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/mutate/generic/commands/args/index.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { isOptionNode } from '../../../../ast/util'; +import { isOptionNode } from '../../../../ast/is'; import { ESQLAstQueryExpression, ESQLCommand, diff --git a/src/platform/packages/shared/kbn-esql-ast/src/parser/__tests__/binary_expression_grouping.test.ts b/src/platform/packages/shared/kbn-esql-ast/src/parser/__tests__/binary_expression_grouping.test.ts new file mode 100644 index 0000000000000..92211979866de --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-ast/src/parser/__tests__/binary_expression_grouping.test.ts @@ -0,0 +1,95 @@ +/* + * 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 { EsqlQuery } from '../../query'; +import { ESQLAstQueryExpression } from '../../types'; +import { singleItems } from '../../visitor/utils'; +import { Walker } from '../../walker'; + +const removeParserFields = (tree: ESQLAstQueryExpression): void => { + Walker.walk(tree, { + visitAny: (node) => { + delete (node as any).text; + delete (node as any).location; + delete (node as any).incomplete; + const args = (node as any).args; + if (Array.isArray(args)) { + (node as any).args = [...singleItems(args)]; + } + }, + }); +}; + +const assertSameAst = (src1: string, src2: string) => { + const { ast: ast1, errors: errors1 } = EsqlQuery.fromSrc(src1); + const { ast: ast2, errors: errors2 } = EsqlQuery.fromSrc(src2); + + expect(errors1.length).toBe(0); + expect(errors2.length).toBe(0); + + removeParserFields(ast1); + removeParserFields(ast2); + + expect(ast1).toEqual(ast2); +}; + +const assertDifferentAst = (src1: string, src2: string) => { + expect(() => assertSameAst(src1, src2)).toThrow(); +}; + +describe('binary operator precedence', () => { + it('AND has higher precedence than OR', () => { + assertSameAst('FROM a | WHERE a AND b OR c', 'FROM a | WHERE (a AND b) OR c'); + assertSameAst('FROM a | WHERE a OR b OR c', 'FROM a | WHERE (a OR b) OR c'); + assertSameAst('FROM a | WHERE a AND b AND c', 'FROM a | WHERE (a AND b) AND c'); + assertDifferentAst('FROM a | WHERE a OR b AND c', 'FROM a | WHERE (a OR b) AND c'); + }); + + it('LIKE (regex) has higher precedence than AND', () => { + assertSameAst('FROM a | WHERE a LIKE "b" OR c', 'FROM a | WHERE (a LIKE "b") OR c'); + assertDifferentAst('FROM a | WHERE a AND b LIKE "c"', 'FROM a | WHERE (a AND b) LIKE "c"'); + }); + + it('comparison has higher precedence than AND', () => { + assertSameAst('FROM a | WHERE a AND b < c', 'FROM a | WHERE a AND (b < c)'); + assertSameAst('FROM a | WHERE a < b AND c', 'FROM a | WHERE (a < b) AND c'); + assertDifferentAst('FROM a | WHERE a AND b < c', 'FROM a | WHERE (a AND b) < c'); + assertDifferentAst('FROM a | WHERE a < b AND c', 'FROM a | WHERE a < (b AND c)'); + }); + + it('addition has higher precedence than comparison', () => { + assertSameAst('FROM a | WHERE a > b + c', 'FROM a | WHERE a > (b + c)'); + assertSameAst('FROM a | WHERE a + b > c', 'FROM a | WHERE (a + b) > c'); + assertDifferentAst('FROM a | WHERE a > b + c', 'FROM a | WHERE (a > b) + c'); + assertDifferentAst('FROM a | WHERE a + b > c', 'FROM a | WHERE a + (b > c)'); + }); + + it('addition has higher precedence than AND (and LIKE)', () => { + assertSameAst('FROM a | WHERE a + b AND c', 'FROM a | WHERE (a + b) AND c'); + // TODO: this test should work once right side of LIKE does not return a list of "single items" + // assertSameAst('FROM a | WHERE a + b LIKE "c"', 'FROM a | WHERE (a + b) LIKE "c"'); + assertSameAst('FROM a | WHERE a AND b + c', 'FROM a | WHERE a AND (b + c)'); + assertDifferentAst('FROM a | WHERE a + b AND c', 'FROM a | WHERE a + (b AND c)'); + assertDifferentAst('FROM a | WHERE a AND b + c', 'FROM a | WHERE (a AND b) + c'); + }); + + it('multiplication has higher precedence than addition', () => { + assertSameAst('FROM a | WHERE a * b + c', 'FROM a | WHERE (a * b) + c'); + assertSameAst('FROM a | WHERE a + b * c', 'FROM a | WHERE a + (b * c)'); + assertDifferentAst('FROM a | WHERE a * b + c', 'FROM a | WHERE a * (b + c)'); + assertDifferentAst('FROM a | WHERE a + b * c', 'FROM a | WHERE (a + b) * c'); + }); + + it('grouping addition in comparison is not necessary', () => { + assertSameAst( + 'FROM a | EVAL key = CASE(timestamp < t - 1 hour AND timestamp > t - 2 hour)', + 'FROM a | EVAL key = CASE(timestamp < (t - 1 hour) AND timestamp > (t - 2 hour))' + ); + }); +}); diff --git a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.comments.test.ts b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.comments.test.ts index 89e6e25764652..a9e2121e859c0 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.comments.test.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.comments.test.ts @@ -186,6 +186,32 @@ describe('binary expressions', () => { 'FROM a | STATS /* a */ /* a.2 */ 1 /* b */ + /* c */ 2 /* d */ + /* e */ 3 /* f */ + /* g */ 4 /* h */ /* h.2 */' ); }); + + describe('grouping', () => { + test('AND has higher precedence than OR', () => { + assertPrint('FROM a | WHERE /* a */ b /* b */ AND (c /* d */ OR /* e */ d)'); + assertPrint('FROM a | WHERE (b /* a */ OR /* b */ c) AND /* c */ d'); + }); + + test('addition has higher precedence than AND', () => { + assertPrint('FROM a | WHERE b /* a */ + (/* b */ c /* c */ AND /* d */ d /* e */)'); + assertPrint('FROM a | WHERE (/* a */ b /* b */ AND /* c */ c /* d */) + /* e */ d /* f */'); + }); + + test('multiplication (division) has higher precedence than addition (subtraction)', () => { + assertPrint( + 'FROM a | WHERE /* a */ b /* b */ / (/* c */ c /* d */ - /* e */ d /* f */) /* h */' + ); + assertPrint('FROM a | WHERE (/* a */ b /* b */ - /* c */ c /* d */) / /* e */ d /* f */'); + }); + + test('issue: https://github.com/elastic/kibana/issues/224990', () => { + assertPrint('FROM a | WHERE b AND (c OR d)'); + assertPrint( + 'FROM kibana_sample_data_logs | WHERE agent.keyword /* a */ == /* b */ "meow" AND (geo.dest == "GR" /* c */ OR geo.dest == "ES")' + ); + }); + }); }); describe('unary expressions', () => { diff --git a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts index 6660fed6c0f2f..d62658a44e4d6 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts @@ -21,6 +21,12 @@ const reprint = (src: string) => { return { text }; }; +const assertReprint = (src: string, expected: string = src) => { + const { text } = reprint(src); + + expect(text).toBe(expected); +}; + describe('single line query', () => { describe('commands', () => { describe('FROM', () => { @@ -517,46 +523,82 @@ describe('single line query', () => { expect(text).toBe('FROM a | WHERE a LIKE "b"'); }); - test('inserts brackets where necessary due precedence', () => { - const { text } = reprint('FROM a | WHERE (1 + 2) * 3'); + test('formats WHERE binary-expression', () => { + const { text } = reprint('FROM a | STATS a WHERE b'); - expect(text).toBe('FROM a | WHERE (1 + 2) * 3'); + expect(text).toBe('FROM a | STATS a WHERE b'); }); - test('inserts brackets where necessary due precedence - 2', () => { - const { text } = reprint('FROM a | WHERE (1 + 2) * (3 - 4)'); + test('formats complex WHERE binary-expression', () => { + const { text } = reprint('FROM a | STATS a = agg(123) WHERE b == test(c, 123)'); - expect(text).toBe('FROM a | WHERE (1 + 2) * (3 - 4)'); + expect(text).toBe('FROM a | STATS a = AGG(123) WHERE b == TEST(c, 123)'); }); - test('inserts brackets where necessary due precedence - 3', () => { - const { text } = reprint('FROM a | WHERE (1 + 2) * (3 - 4) / (5 + 6 + 7)'); + describe('grouping', () => { + test('inserts brackets where necessary due precedence', () => { + const { text } = reprint('FROM a | WHERE (1 + 2) * 3'); - expect(text).toBe('FROM a | WHERE (1 + 2) * (3 - 4) / (5 + 6 + 7)'); - }); + expect(text).toBe('FROM a | WHERE (1 + 2) * 3'); + }); - test('inserts brackets where necessary due precedence - 4', () => { - const { text } = reprint('FROM a | WHERE (1 + (1 + 2)) * ((3 - 4) / (5 + 6 + 7))'); + test('inserts brackets where necessary due precedence - 2', () => { + const { text } = reprint('FROM a | WHERE (1 + 2) * (3 - 4)'); - expect(text).toBe('FROM a | WHERE (1 + 1 + 2) * (3 - 4) / (5 + 6 + 7)'); - }); + expect(text).toBe('FROM a | WHERE (1 + 2) * (3 - 4)'); + }); - test('inserts brackets where necessary due precedence - 5', () => { - const { text } = reprint('FROM a | WHERE (1 + (1 + 2)) * (((3 - 4) / (5 + 6 + 7)) + 1)'); + test('inserts brackets where necessary due precedence - 3', () => { + const { text } = reprint('FROM a | WHERE (1 + 2) * (3 - 4) / (5 + 6 + 7)'); - expect(text).toBe('FROM a | WHERE (1 + 1 + 2) * ((3 - 4) / (5 + 6 + 7) + 1)'); - }); + expect(text).toBe('FROM a | WHERE (1 + 2) * (3 - 4) / (5 + 6 + 7)'); + }); - test('formats WHERE binary-expression', () => { - const { text } = reprint('FROM a | STATS a WHERE b'); + test('inserts brackets where necessary due precedence - 4', () => { + const { text } = reprint('FROM a | WHERE (1 + (1 + 2)) * ((3 - 4) / (5 + 6 + 7))'); - expect(text).toBe('FROM a | STATS a WHERE b'); - }); + expect(text).toBe('FROM a | WHERE (1 + 1 + 2) * (3 - 4) / (5 + 6 + 7)'); + }); - test('formats complex WHERE binary-expression', () => { - const { text } = reprint('FROM a | STATS a = agg(123) WHERE b == test(c, 123)'); + test('inserts brackets where necessary due precedence - 5', () => { + const { text } = reprint( + 'FROM a | WHERE (1 + (1 + 2)) * (((3 - 4) / (5 + 6 + 7)) + 1)' + ); - expect(text).toBe('FROM a | STATS a = AGG(123) WHERE b == TEST(c, 123)'); + expect(text).toBe('FROM a | WHERE (1 + 1 + 2) * ((3 - 4) / (5 + 6 + 7) + 1)'); + }); + + test('AND has higher precedence than OR', () => { + assertReprint('FROM a | WHERE b AND (c OR d)'); + assertReprint('FROM a | WHERE (b AND c) OR d', 'FROM a | WHERE b AND c OR d'); + assertReprint('FROM a | WHERE b OR c AND d'); + assertReprint('FROM a | WHERE (b OR c) AND d'); + }); + + test('addition has higher precedence than AND', () => { + assertReprint('FROM a | WHERE b + (c AND d)'); + assertReprint('FROM a | WHERE (b + c) AND d', 'FROM a | WHERE b + c AND d'); + assertReprint('FROM a | WHERE b AND c + d'); + assertReprint('FROM a | WHERE (b AND c) + d'); + }); + + test('multiplication (division) has higher precedence than addition (subtraction)', () => { + assertReprint('FROM a | WHERE b / (c - d)'); + assertReprint('FROM a | WHERE b * (c - d)'); + assertReprint('FROM a | WHERE b * (c + d)'); + assertReprint('FROM a | WHERE (b / c) - d', 'FROM a | WHERE b / c - d'); + assertReprint('FROM a | WHERE (b * c) - d', 'FROM a | WHERE b * c - d'); + assertReprint('FROM a | WHERE (b * c) + d', 'FROM a | WHERE b * c + d'); + assertReprint('FROM a | WHERE b - c / d'); + assertReprint('FROM a | WHERE (b - c) / d'); + }); + + test('issue: https://github.com/elastic/kibana/issues/224990', () => { + assertReprint('FROM a | WHERE b AND (c OR d)'); + assertReprint( + 'FROM kibana_sample_data_logs | WHERE agent.keyword == "meow" AND (geo.dest == "GR" OR geo.dest == "ES")' + ); + }); }); }); }); @@ -790,7 +832,7 @@ describe('multiline query', () => { const query = `FROM kibana_sample_data_logs | SORT @timestamp | EVAL t = NOW() -| EVAL key = CASE(timestamp < (t - 1 hour) AND timestamp > (t - 2 hour), "Last hour", "Other") +| EVAL key = CASE(timestamp < t - 1 hour AND timestamp > t - 2 hour, "Last hour", "Other") | STATS sum = SUM(bytes), count = COUNT_DISTINCT(clientip) BY key, extension.keyword | EVAL sum_last_hour = CASE(key == "Last hour", sum), sum_rest = CASE(key == "Other", sum), count_last_hour = CASE(key == "Last hour", count), count_rest = CASE(key == "Other", count) | STATS sum_last_hour = MAX(sum_last_hour), sum_rest = MAX(sum_rest), count_last_hour = MAX(count_last_hour), count_rest = MAX(count_rest) BY key, extension.keyword diff --git a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts index d84cd9e9efdfc..b1410f09a13b7 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts @@ -8,14 +8,14 @@ */ import { - binaryExpressionGroup, isBinaryExpression, isColumn, isDoubleLiteral, isIntegerLiteral, isLiteral, isProperNode, -} from '../ast/helpers'; +} from '../ast/is'; +import { BinaryExpressionGroup, binaryExpressionGroup } from '../ast/grouping'; import { ESQLAstBaseItem, ESQLAstCommand, ESQLAstQueryExpression } from '../types'; import { ESQLAstExpressionNode, Visitor } from '../visitor'; import { resolveItem } from '../visitor/utils'; @@ -344,11 +344,17 @@ export class BasicPrettyPrinter { let leftFormatted = ctx.visitArgument(0); let rightFormatted = ctx.visitArgument(1); - if (groupLeft && groupLeft < group) { + const shouldGroupLeftExpressions = + groupLeft && (groupLeft === BinaryExpressionGroup.unknown || groupLeft < group); + + if (shouldGroupLeftExpressions) { leftFormatted = `(${leftFormatted})`; } - if (groupRight && groupRight < group) { + const shouldGroupRightExpressions = + groupRight && (groupRight === BinaryExpressionGroup.unknown || groupRight < group); + + if (shouldGroupRightExpressions) { rightFormatted = `(${rightFormatted})`; } diff --git a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts index e80878258b32f..89d6ae1b5d47c 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { BinaryExpressionGroup } from '../ast/constants'; -import { binaryExpressionGroup, isBinaryExpression } from '../ast/helpers'; +import { BinaryExpressionGroup, binaryExpressionGroup } from '../ast/grouping'; +import { isBinaryExpression } from '../ast/is'; import type { ESQLAstBaseItem, ESQLAstQueryExpression } from '../types'; import { CommandOptionVisitorContext, diff --git a/src/platform/packages/shared/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts b/src/platform/packages/shared/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts index 0295f6f11fa30..871b4a014515d 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/synth/__tests__/expr_function.test.ts @@ -111,7 +111,7 @@ describe('can generate various expression types', () => { ['assignment expression', 'bytes_transform = ROUND(total_bytes / 1000000.0, 1)'], [ 'assignment with time intervals', - 'key = CASE(timestamp < (t - 1 hour) AND timestamp > (t - 2 hour), "Last hour", "Other")', + 'key = CASE(timestamp < t - 1 hour AND timestamp > t - 2 hour, "Last hour", "Other")', ], [ 'assignment with casts', diff --git a/src/platform/packages/shared/kbn-esql-ast/src/types.ts b/src/platform/packages/shared/kbn-esql-ast/src/types.ts index 63cbc08b6c473..b66cc9fb139a5 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/types.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/types.ts @@ -225,7 +225,8 @@ export type BinaryExpressionOperator = | BinaryExpressionRenameOperator | BinaryExpressionWhereOperator | BinaryExpressionMatchOperator - | BinaryExpressionIn; + | BinaryExpressionIn + | BinaryExpressionLogical; export type BinaryExpressionArithmeticOperator = '+' | '-' | '*' | '/' | '%'; export type BinaryExpressionAssignmentOperator = '='; @@ -235,6 +236,7 @@ export type BinaryExpressionRenameOperator = 'as'; export type BinaryExpressionWhereOperator = 'where'; export type BinaryExpressionMatchOperator = ':'; export type BinaryExpressionIn = 'in' | 'not in'; +export type BinaryExpressionLogical = 'and' | 'or'; // from https://github.com/elastic/elasticsearch/blob/122e7288200ee03e9087c98dff6cebbc94e774aa/docs/reference/esql/functions/kibana/inline_cast.json export type InlineCastingType = diff --git a/src/platform/packages/shared/kbn-esql-ast/src/visitor/contexts.ts b/src/platform/packages/shared/kbn-esql-ast/src/visitor/contexts.ts index 3fc5e22b60893..4b94fe653ecd2 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/visitor/contexts.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/visitor/contexts.ts @@ -49,7 +49,7 @@ import type { VisitorOutput, } from './types'; import { Builder } from '../builder'; -import { isProperNode } from '../ast/helpers'; +import { isProperNode } from '../ast/is'; export class VisitorContext< Methods extends VisitorMethods = VisitorMethods, diff --git a/src/platform/packages/shared/kbn-esql-ast/src/walker/__tests__/walker_all_nodes.test.ts b/src/platform/packages/shared/kbn-esql-ast/src/walker/__tests__/walker_all_nodes.test.ts index db9f1154b487d..b8fe005b3d33c 100644 --- a/src/platform/packages/shared/kbn-esql-ast/src/walker/__tests__/walker_all_nodes.test.ts +++ b/src/platform/packages/shared/kbn-esql-ast/src/walker/__tests__/walker_all_nodes.test.ts @@ -11,7 +11,7 @@ import { EsqlQuery } from '../../query'; import * as fixtures from '../../__tests__/fixtures'; import { Walker } from '../walker'; import { ESQLAstExpression, ESQLProperNode } from '../../types'; -import { isProperNode } from '../../ast/helpers'; +import { isProperNode } from '../../ast/is'; interface JsonWalkerOptions { visitObject?: (node: Record) => void; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 662ea7a715b15..392159216dc9f 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -16,9 +16,9 @@ import { type ESQLFunction, type ESQLSingleAstItem, Walker, + isList, } from '@kbn/esql-ast'; import { type ESQLControlVariable, ESQLVariableType } from '@kbn/esql-types'; -import { isList } from '@kbn/esql-ast/src/ast/helpers'; import { isNumericType } from '../shared/esql_types'; import type { EditorContext, ItemKind, SuggestionRawDefinition, GetColumnsByTypeFn } from './types'; import { diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts index f49e9ddca814b..c5be4aeaab490 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts @@ -11,13 +11,13 @@ import { ESQLCommandOption, Walker, isIdentifier, + isList, type ESQLAst, type ESQLAstItem, type ESQLCommand, type ESQLFunction, type ESQLSingleAstItem, } from '@kbn/esql-ast'; -import { isList } from '@kbn/esql-ast/src/ast/helpers'; import { ESQLAstExpression } from '@kbn/esql-ast/src/types'; import { FunctionDefinitionTypes } from '../definitions/types'; import { EDITOR_MARKER } from './constants'; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/function_validation.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/function_validation.ts index f18f16153f1a7..85f5fb20130d3 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/function_validation.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/function_validation.ts @@ -7,9 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ESQLAstItem, ESQLCommand, ESQLFunction, ESQLMessage, isIdentifier } from '@kbn/esql-ast'; +import { + ESQLAstItem, + ESQLCommand, + ESQLFunction, + ESQLMessage, + isIdentifier, + isList, +} from '@kbn/esql-ast'; import { uniqBy } from 'lodash'; -import { isList } from '@kbn/esql-ast/src/ast/helpers'; import { isLiteralItem, isTimeIntervalItem, diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/queries/helpers.ts b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/queries/helpers.ts index 40ceafe87042e..b95221164ca24 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/queries/helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/privileged_user_monitoring/queries/helpers.ts @@ -11,7 +11,7 @@ import type { DataViewFieldMap } from '@kbn/data-views-plugin/common'; import { partition } from 'lodash/fp'; import type { ESQLProperNode } from '@kbn/esql-ast/src/types'; import { Parser } from '@kbn/esql-ast/src/parser/parser'; -import { isAsExpression, isFieldExpression } from '@kbn/esql-ast/src/ast/helpers'; +import { isAsExpression, isFieldExpression } from '@kbn/esql-ast/src/ast/is'; import { getPrivilegedMonitorUsersIndex } from '../../../../../common/entity_analytics/privilege_monitoring/constants'; export const getPrivilegedMonitorUsersJoin = (