diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/index.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/index.ts index 9efef9289f6f3..6ef342439a240 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/index.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/index.ts @@ -12,7 +12,6 @@ export type { FunctionDefinition, CommandDefinition, CommandOptionsDefinition, - CommandModeDefinition, Literals, } from './src/definitions/types'; export type { ESQLCallbacks } from './src/shared/types'; @@ -51,7 +50,6 @@ export { printFunctionSignature, checkFunctionArgMatchesDefinition as isEqualType, isSourceItem, - isSettingItem, isFunctionItem, isOptionItem, isColumnItem, @@ -61,7 +59,6 @@ export { isAssignmentComplete, isSingleItem, } from './src/shared/helpers'; -export { ENRICH_MODES } from './src/definitions/settings'; export { timeUnits } from './src/definitions/literals'; export { aggFunctionDefinitions } from './src/definitions/generated/aggregation_functions'; export { getFunctionSignatures } from './src/definitions/helpers'; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/enrich/util.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/enrich/util.ts index b3af67aa29f3a..b787818bd2ab4 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/enrich/util.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/enrich/util.ts @@ -9,8 +9,8 @@ import { ESQLCommand } from '@kbn/esql-ast'; import { i18n } from '@kbn/i18n'; +import { ENRICH_MODES } from '../../../definitions/commands_helpers'; import { isSingleItem } from '../../../..'; -import { ENRICH_MODES } from '../../../definitions/settings'; import { SuggestionRawDefinition } from '../../types'; import { TRIGGER_SUGGESTION_COMMAND, getSafeInsertText } from '../../factories'; @@ -92,13 +92,25 @@ export const noPoliciesAvailableSuggestion: SuggestionRawDefinition = { }, }; -export const modeSuggestions: SuggestionRawDefinition[] = ENRICH_MODES.values.map( +export const modeDescription = i18n.translate( + 'kbn-esql-validation-autocomplete.esql.definitions.ccqMode', + { + defaultMessage: 'Cross-cluster query mode', + } +); + +export const modeSuggestions: SuggestionRawDefinition[] = ENRICH_MODES.map( ({ name, description }) => ({ - label: `${ENRICH_MODES.prefix || ''}${name}`, - text: `${ENRICH_MODES.prefix || ''}${name}:$0`, + label: `_${name}`, + text: `_${name}:$0`, asSnippet: true, kind: 'Reference', - detail: `${ENRICH_MODES.description} - ${description}`, + detail: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.ccqModeDoc', { + defaultMessage: 'Cross-cluster query mode - ${description}', + values: { + description, + }, + }), sortText: 'D', command: TRIGGER_SUGGESTION_COMMAND, }) diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/commands.ts index 2a8967be6f7d9..60364830b25a3 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -15,8 +15,15 @@ import { type ESQLFunction, isFunctionExpression, isWhereExpression, + ESQLCommandMode, } from '@kbn/esql-ast'; -import { isAssignment, isColumnItem, isFunctionItem } from '../shared/helpers'; +import { + isAssignment, + isColumnItem, + isFunctionItem, + isSingleItem, + noCaseCompare, +} from '../shared/helpers'; import { appendSeparatorOption, asOption, @@ -25,10 +32,9 @@ import { onOption, withOption, } from './options'; -import { ENRICH_MODES } from './settings'; import { type CommandDefinition } from './types'; -import { checkAggExistence, checkFunctionContent } from './commands_helpers'; +import { ENRICH_MODES, checkAggExistence, checkFunctionContent } from './commands_helpers'; import { suggest as suggestForDissect } from '../autocomplete/commands/dissect'; import { suggest as suggestForDrop } from '../autocomplete/commands/drop'; @@ -47,6 +53,8 @@ import { suggest as suggestForSort } from '../autocomplete/commands/sort'; import { suggest as suggestForStats } from '../autocomplete/commands/stats'; import { suggest as suggestForWhere } from '../autocomplete/commands/where'; +import { getMessageFromId } from '../validation/errors'; + const statsValidator = (command: ESQLCommand) => { const messages: ESQLMessage[] = []; const commandName = command.name.toUpperCase(); @@ -147,7 +155,6 @@ export const commandDefinitions: Array> = [ }, suggest: suggestForRow, options: [], - modes: [], }, { name: 'from', @@ -157,7 +164,6 @@ export const commandDefinitions: Array> = [ }), examples: ['from logs', 'from logs-*', 'from logs_*, events-*'], options: [metadataOption], - modes: [], signature: { multipleParams: true, params: [{ name: 'index', type: 'source', wildcards: true }], @@ -171,7 +177,6 @@ export const commandDefinitions: Array> = [ }), examples: ['SHOW INFO'], options: [], - modes: [], signature: { multipleParams: false, params: [{ name: 'functions', type: 'function' }], @@ -200,7 +205,6 @@ export const commandDefinitions: Array> = [ 'metrics src1, src2 agg1, agg2 by field1, field2', ], options: [], - modes: [], signature: { multipleParams: true, params: [ @@ -222,7 +226,6 @@ export const commandDefinitions: Array> = [ params: [{ name: 'expression', type: 'function', optional: true }], }, options: [byOption], - modes: [], validate: statsValidator, suggest: suggestForStats, }, @@ -242,7 +245,6 @@ export const commandDefinitions: Array> = [ params: [{ name: 'expression', type: 'function', optional: true }], }, options: [byOption], - modes: [], // Reusing the same validation logic as stats command validate: statsValidator, suggest: () => [], @@ -265,7 +267,6 @@ export const commandDefinitions: Array> = [ params: [{ name: 'expression', type: 'any' }], }, options: [], - modes: [], suggest: suggestForEval, }, { @@ -279,7 +280,6 @@ export const commandDefinitions: Array> = [ params: [{ name: 'renameClause', type: 'column' }], }, options: [asOption], - modes: [], suggest: suggestForRename, }, { @@ -294,7 +294,6 @@ export const commandDefinitions: Array> = [ params: [{ name: 'size', type: 'integer', constantOnly: true }], }, options: [], - modes: [], suggest: suggestForLimit, }, { @@ -306,7 +305,6 @@ export const commandDefinitions: Array> = [ examples: ['… | keep a', '… | keep a,b'], suggest: suggestForKeep, options: [], - modes: [], signature: { multipleParams: true, params: [{ name: 'column', type: 'column', wildcards: true }], @@ -319,7 +317,6 @@ export const commandDefinitions: Array> = [ }), examples: ['… | drop a', '… | drop a,b'], options: [], - modes: [], signature: { multipleParams: true, params: [{ name: 'column', type: 'column', wildcards: true }], @@ -376,7 +373,6 @@ export const commandDefinitions: Array> = [ '… | sort a - abs(b)', ], options: [], - modes: [], signature: { multipleParams: true, params: [{ name: 'expression', type: 'any' }], @@ -396,7 +392,6 @@ export const commandDefinitions: Array> = [ params: [{ name: 'expression', type: 'boolean' }], }, options: [], - modes: [], suggest: suggestForWhere, }, { @@ -407,7 +402,6 @@ export const commandDefinitions: Array> = [ }), examples: ['… | DISSECT a "%{b} %{c}" APPEND_SEPARATOR = ":"'], options: [appendSeparatorOption], - modes: [], signature: { multipleParams: false, params: [ @@ -425,7 +419,6 @@ export const commandDefinitions: Array> = [ }), examples: ['… | GROK a "%{IP:b} %{NUMBER:c}"'], options: [], - modes: [], signature: { multipleParams: false, params: [ @@ -442,7 +435,6 @@ export const commandDefinitions: Array> = [ }), examples: ['row a=[1,2,3] | mv_expand a'], options: [], - modes: [], preview: true, signature: { multipleParams: false, @@ -462,12 +454,37 @@ export const commandDefinitions: Array> = [ '… | enrich my-policy on pivotField with a = enrichFieldA, b = enrichFieldB', ], options: [onOption, withOption], - modes: [ENRICH_MODES], signature: { multipleParams: false, params: [{ name: 'policyName', type: 'source', innerTypes: ['policy'] }], }, suggest: suggestForEnrich, + validate: (command: ESQLCommand) => { + const modeArg = command.args.find((arg) => isSingleItem(arg) && arg.type === 'mode') as + | ESQLCommandMode + | undefined; + + if (!modeArg) { + return []; + } + + const acceptedValues = ENRICH_MODES.map(({ name }) => '_' + name); + if (acceptedValues.some((value) => noCaseCompare(modeArg.text, value))) { + return []; + } + + return [ + getMessageFromId({ + messageId: 'unsupportedMode', + values: { + command: 'ENRICH', + value: modeArg.text, + expected: acceptedValues.join(', '), + }, + locations: modeArg.location, + }), + ]; + }, }, { name: 'hidden_command', @@ -475,7 +492,6 @@ export const commandDefinitions: Array> = [ hidden: true, examples: [], options: [], - modes: [], signature: { params: [], multipleParams: false, @@ -527,7 +543,6 @@ export const commandDefinitions: Array> = [ // '… | JOIN index AS alias ON index.field = index2.field', // '… | JOIN index AS alias ON index.field = index2.field, index.field2 = index2.field2', ], - modes: [], signature: { multipleParams: true, params: [{ name: 'index', type: 'source', wildcards: true }], diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/commands_helpers.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/commands_helpers.ts index 77bbd4b477ca2..c2a361d272835 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/commands_helpers.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/commands_helpers.ts @@ -13,6 +13,7 @@ import { isFieldExpression, Walker, } from '@kbn/esql-ast'; +import { i18n } from '@kbn/i18n'; import { getFunctionDefinition, isFunctionItem, @@ -80,3 +81,27 @@ export function checkAggExistence(arg: ESQLFunction): boolean { return false; } + +export const ENRICH_MODES = [ + { + name: 'any', + description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.ccqAnyDoc', { + defaultMessage: 'Enrich takes place on any cluster', + }), + }, + { + name: 'coordinator', + description: i18n.translate( + 'kbn-esql-validation-autocomplete.esql.definitions.ccqCoordinatorDoc', + { + defaultMessage: 'Enrich takes place on the coordinating cluster receiving an ES|QL', + } + ), + }, + { + name: 'remote', + description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.ccqRemoteDoc', { + defaultMessage: 'Enrich takes place on the cluster hosting the target index.', + }), + }, +]; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/settings.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/settings.ts deleted file mode 100644 index a367d1cb65f7a..0000000000000 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/settings.ts +++ /dev/null @@ -1,45 +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 { i18n } from '@kbn/i18n'; -import type { CommandModeDefinition } from './types'; - -export const ENRICH_MODES: CommandModeDefinition = { - name: 'ccq.mode', - description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.ccqModeDoc', { - defaultMessage: 'Cross-clusters query mode', - }), - prefix: '_', - values: [ - { - name: 'any', - description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.ccqAnyDoc', { - defaultMessage: 'Enrich takes place on any cluster', - }), - }, - { - name: 'coordinator', - description: i18n.translate( - 'kbn-esql-validation-autocomplete.esql.definitions.ccqCoordinatorDoc', - { - defaultMessage: 'Enrich takes place on the coordinating cluster receiving an ES|QL', - } - ), - }, - { - name: 'remote', - description: i18n.translate( - 'kbn-esql-validation-autocomplete.esql.definitions.ccqRemoteDoc', - { - defaultMessage: 'Enrich takes place on the cluster hosting the target index.', - } - ), - }, - ], -}; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/types.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/types.ts index 83a518dcf87e6..3b4126d9e1c0e 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -16,7 +16,7 @@ import type { } from '@kbn/esql-ast'; import { ESQLControlVariable } from '@kbn/esql-types'; import { GetColumnsByTypeFn, SuggestionRawDefinition } from '../autocomplete/types'; -import type { ESQLPolicy } from '../validation/types'; +import type { ESQLPolicy, ReferenceMaps } from '../validation/types'; import { ESQLCallbacks, ESQLSourceResult } from '../shared/types'; /** @@ -339,21 +339,17 @@ export interface CommandOptionsDefinition ) => ESQLMessage[]; } -export interface CommandModeDefinition { - name: string; - description: string; - values: Array<{ name: string; description: string }>; - prefix?: string; -} - export interface CommandDefinition extends CommandBaseDefinition { examples: string[]; - validate?: (option: ESQLCommand) => ESQLMessage[]; + /** + * This function is run when the command is being validated, but it does not + * prevent the default behavior. If you need a full override, we are currently + * doing those directly in the validateCommand function in the validation module. + */ + validate?: (command: ESQLCommand, references: ReferenceMaps) => ESQLMessage[]; suggest: CommandSuggestFunction; /** @deprecated this property will disappear in the future */ - modes: CommandModeDefinition[]; - /** @deprecated this property will disappear in the future */ options: CommandOptionsDefinition[]; } diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/helpers.ts index 22d8da7bd1f13..62714a158cde3 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -19,7 +19,6 @@ import { type ESQLTimeInterval, } from '@kbn/esql-ast'; import { - ESQLCommandMode, ESQLIdentifier, ESQLInlineCast, ESQLParamLiteral, @@ -66,10 +65,6 @@ export function isSingleItem(arg: ESQLAstItem): arg is ESQLSingleAstItem { return arg && !Array.isArray(arg); } -/** @deprecated — a "setting" is a concept we will be getting rid of soon */ -export function isSettingItem(arg: ESQLAstItem): arg is ESQLCommandMode { - return isSingleItem(arg) && arg.type === 'mode'; -} export function isFunctionItem(arg: ESQLAstItem): arg is ESQLFunction { return isSingleItem(arg) && arg.type === 'function'; } diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/commands/join/index.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/commands/join/index.ts new file mode 100644 index 0000000000000..318aed0c13de2 --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/commands/join/index.ts @@ -0,0 +1,82 @@ +/* + * 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 { + ESQLAstJoinCommand, + ESQLMessage, + isBinaryExpression, + isIdentifier, + isSource, +} from '@kbn/esql-ast'; +import { ESQLIdentifier, ESQLProperNode, ESQLSource } from '@kbn/esql-ast/src/types'; +import { ReferenceMaps } from '../../types'; +import { errors } from '../../errors'; + +/** + * Validates the JOIN command: + * + * JOIN ON + * JOIN index [ = alias ] ON [, [, ...]] + */ +export const validate = (command: ESQLAstJoinCommand, references: ReferenceMaps): ESQLMessage[] => { + const messages: ESQLMessage[] = []; + const { commandType, args } = command; + const { joinIndices } = references; + + if (!['left', 'right', 'lookup'].includes(commandType)) { + return [errors.unexpected(command.location, 'JOIN command type')]; + } + + const target = args[0] as ESQLProperNode; + let index: ESQLSource; + let alias: ESQLIdentifier | undefined; + + if (isBinaryExpression(target)) { + if (target.name === 'as') { + alias = target.args[1] as ESQLIdentifier; + index = target.args[0] as ESQLSource; + + if (!isSource(index) || !isIdentifier(alias)) { + return [errors.unexpected(target.location)]; + } + } else { + return [errors.unexpected(target.location)]; + } + } else if (isSource(target)) { + index = target as ESQLSource; + } else { + return [errors.unexpected(target.location)]; + } + + let isIndexFound = false; + for (const { name, aliases } of joinIndices) { + if (index.name === name) { + isIndexFound = true; + break; + } + + if (aliases) { + for (const aliasName of aliases) { + if (index.name === aliasName) { + isIndexFound = true; + break; + } + } + } + } + + if (!isIndexFound) { + const error = errors.invalidJoinIndex(index); + messages.push(error); + + return messages; + } + + return messages; +}; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/commands/metrics/index.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/commands/metrics/index.ts new file mode 100644 index 0000000000000..e0827f65a5043 --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/commands/metrics/index.ts @@ -0,0 +1,218 @@ +/* + * 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 { ESQLAstMetricsCommand, ESQLCommand, ESQLMessage, isIdentifier, walk } from '@kbn/esql-ast'; +import { ESQLAstField, ESQLAstItem, ESQLFunction } from '@kbn/esql-ast/src/types'; +import { + isAggFunction, + isFunctionOperatorParam, + isMaybeAggFunction, +} from '../../../shared/helpers'; +import { FunctionDefinitionTypes } from '../../../definitions/types'; +import { ReferenceMaps } from '../../types'; +import { + getFunctionDefinition, + isAssignment, + isColumnItem, + isFunctionItem, + isLiteralItem, +} from '../../../..'; +import { errors } from '../../errors'; +import { validateFunction } from '../../function_validation'; +import { validateColumnForCommand, validateSources } from '../../validation'; + +/** + * Validates the METRICS source command: + * + * METRICS [ [ BY ]] + */ +export const validate = ( + command: ESQLAstMetricsCommand, + references: ReferenceMaps +): ESQLMessage[] => { + const messages: ESQLMessage[] = []; + const { sources, aggregates, grouping } = command; + + // METRICS ... + messages.push(...validateSources(command, sources, references)); + + // ... ... + if (aggregates && aggregates.length) { + messages.push(...validateAggregates(command, aggregates, references)); + + // ... BY + if (grouping && grouping.length) { + messages.push(...validateByGrouping(grouping, 'metrics', references, true)); + } + } + + return messages; +}; + +/** + * Validates aggregates fields: `... ...`. + */ +const validateAggregates = ( + command: ESQLCommand, + aggregates: ESQLAstField[], + references: ReferenceMaps +) => { + const messages: ESQLMessage[] = []; + + // Should never happen. + if (!aggregates.length) { + messages.push(errors.unexpected(command.location)); + return messages; + } + + let hasMissingAggregationFunctionError = false; + + for (const aggregate of aggregates) { + if (isFunctionItem(aggregate)) { + messages.push( + ...validateFunction({ + fn: aggregate, + parentCommand: command.name, + parentOption: undefined, + references, + }) + ); + + let hasAggregationFunction = false; + + walk(aggregate, { + visitFunction: (fn) => { + const definition = getFunctionDefinition(fn.name); + if (!definition) return; + if (definition.type === FunctionDefinitionTypes.AGG) hasAggregationFunction = true; + }, + }); + + if (!hasAggregationFunction) { + hasMissingAggregationFunctionError = true; + messages.push(errors.noAggFunction(command, aggregate)); + } + } else if (isColumnItem(aggregate) || isIdentifier(aggregate)) { + messages.push(errors.unknownAggFunction(aggregate)); + } else { + // Should never happen. + } + } + + if (hasMissingAggregationFunctionError) { + return messages; + } + + for (const aggregate of aggregates) { + if (isFunctionItem(aggregate)) { + const fn = isAssignment(aggregate) ? aggregate.args[1] : aggregate; + if (isFunctionItem(fn) && !isFunctionAggClosed(fn)) { + messages.push(errors.expressionNotAggClosed(command, fn)); + } + } + } + + if (messages.length) { + return messages; + } + + for (const aggregate of aggregates) { + if (isFunctionItem(aggregate)) { + const aggInAggFunction = findNestedAggFunction(aggregate); + if (aggInAggFunction) { + messages.push(errors.aggInAggFunction(aggInAggFunction)); + break; + } + } + } + + return messages; +}; + +/** + * Validates grouping fields of the BY clause: `... BY `. + */ +const validateByGrouping = ( + fields: ESQLAstItem[], + commandName: string, + referenceMaps: ReferenceMaps, + multipleParams: boolean +): ESQLMessage[] => { + const messages: ESQLMessage[] = []; + for (const field of fields) { + if (!Array.isArray(field)) { + if (!multipleParams) { + if (isColumnItem(field)) { + messages.push(...validateColumnForCommand(field, commandName, referenceMaps)); + } + } else { + if (isColumnItem(field)) { + messages.push(...validateColumnForCommand(field, commandName, referenceMaps)); + } + if (isFunctionItem(field)) { + messages.push( + ...validateFunction({ + fn: field, + parentCommand: commandName, + parentOption: 'by', + references: referenceMaps, + }) + ); + } + } + } + } + return messages; +}; + +/** + * Validate that a function is an aggregate function or that all children + * recursively terminate at either a literal or an aggregate function. + */ +const isFunctionAggClosed = (fn: ESQLFunction): boolean => + isMaybeAggFunction(fn) || areFunctionArgsAggClosed(fn); + +const areFunctionArgsAggClosed = (fn: ESQLFunction): boolean => + fn.args.every((arg) => isLiteralItem(arg) || (isFunctionItem(arg) && isFunctionAggClosed(arg))) || + isFunctionOperatorParam(fn); + +/** + * Looks for first nested aggregate function in an aggregate function, recursively. + */ +const findNestedAggFunctionInAggFunction = (agg: ESQLFunction): ESQLFunction | undefined => { + for (const arg of agg.args) { + if (isFunctionItem(arg)) { + return isMaybeAggFunction(arg) ? arg : findNestedAggFunctionInAggFunction(arg); + } + } +}; + +/** + * Looks for first nested aggregate function in another aggregate a function, + * recursively. + * + * @param fn Function to check for nested aggregate functions. + * @param parentIsAgg Whether the parent function of `fn` is an aggregate function. + * @returns The first nested aggregate function in `fn`, or `undefined` if none is found. + */ +const findNestedAggFunction = ( + fn: ESQLFunction, + parentIsAgg: boolean = false +): ESQLFunction | undefined => { + if (isMaybeAggFunction(fn)) { + return parentIsAgg ? fn : findNestedAggFunctionInAggFunction(fn); + } + + for (const arg of fn.args) { + if (isFunctionItem(arg)) { + const nestedAgg = findNestedAggFunction(arg, parentIsAgg || isAggFunction(fn)); + if (nestedAgg) return nestedAgg; + } + } +}; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/errors.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/errors.ts index af5392f5f5943..6d577d053d207 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/errors.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/errors.ts @@ -294,21 +294,7 @@ function getMessageAndTypeFromId({ ), type: 'warning', }; - case 'unsupportedSetting': - return { - message: i18n.translate( - 'kbn-esql-validation-autocomplete.esql.validation.unsupportedSetting', - { - defaultMessage: 'Unsupported setting [{setting}], expected [{expected}]', - values: { - setting: out.setting, - expected: out.expected, - }, - } - ), - type: 'error', - }; - case 'unsupportedSettingCommandValue': + case 'unsupportedMode': return { message: i18n.translate( 'kbn-esql-validation-autocomplete.esql.validation.unsupportedSettingValue', diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json index c10a35964add3..7f47401af0cff 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json @@ -9294,7 +9294,7 @@ { "query": "from a_index | enrich _:policy", "error": [ - "Unrecognized value [_] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]" + "Unrecognized value [_] for ENRICH, mode needs to be one of [_any, _coordinator, _remote]" ], "warning": [] }, @@ -9324,7 +9324,7 @@ { "query": "from a_index | enrich any:policy", "error": [ - "Unrecognized value [any] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]" + "Unrecognized value [any] for ENRICH, mode needs to be one of [_any, _coordinator, _remote]" ], "warning": [] }, @@ -9451,7 +9451,7 @@ { "query": "from a_index | enrich _unknown:policy", "error": [ - "Unrecognized value [_unknown] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]" + "Unrecognized value [_unknown] for ENRICH, mode needs to be one of [_any, _coordinator, _remote]" ], "warning": [] }, 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 new file mode 100644 index 0000000000000..20b5623d0a0eb --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/function_validation.ts @@ -0,0 +1,670 @@ +/* + * 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 { ESQLAstItem, ESQLCommand, ESQLFunction, ESQLMessage, isIdentifier } from '@kbn/esql-ast'; +import { uniqBy } from 'lodash'; +import { + isLiteralItem, + isTimeIntervalItem, + isFunctionItem, + isSupportedFunction, + getFunctionDefinition, + isColumnItem, + isAssignment, +} from '../..'; +import { FunctionParameter, FunctionDefinitionTypes } from '../definitions/types'; +import { + UNSUPPORTED_COMMANDS_BEFORE_MATCH, + UNSUPPORTED_COMMANDS_BEFORE_QSTR, +} from '../shared/constants'; +import { compareTypesWithLiterals } from '../shared/esql_types'; +import { + isValidLiteralOption, + checkFunctionArgMatchesDefinition, + inKnownTimeInterval, + isInlineCastItem, + getQuotedColumnName, + getColumnExists, + getColumnForASTNode, + isFunctionOperatorParam, + getSignaturesWithMatchingArity, + getParamAtPosition, + extractSingularType, + isArrayType, +} from '../shared/helpers'; +import { getMessageFromId, errors } from './errors'; +import { getMaxMinNumberOfParams, collapseWrongArgumentTypeMessages } from './helpers'; +import { ReferenceMaps } from './types'; + +const NO_MESSAGE: ESQLMessage[] = []; + +/** + * Performs validation on a function + */ +export function validateFunction({ + fn, + parentCommand, + parentOption, + references, + forceConstantOnly = false, + isNested, + parentAst, + currentCommandIndex, +}: { + fn: ESQLFunction; + parentCommand: string; + parentOption?: string; + references: ReferenceMaps; + forceConstantOnly?: boolean; + isNested?: boolean; + parentAst?: ESQLCommand[]; + currentCommandIndex?: number; +}): ESQLMessage[] { + const messages: ESQLMessage[] = []; + + if (fn.incomplete) { + return messages; + } + if (isFunctionOperatorParam(fn)) { + return messages; + } + const fnDefinition = getFunctionDefinition(fn.name)!; + + const isFnSupported = isSupportedFunction(fn.name, parentCommand, parentOption); + + if (typeof textSearchFunctionsValidators[fn.name] === 'function') { + const validator = textSearchFunctionsValidators[fn.name]; + messages.push( + ...validator({ + fn, + parentCommand, + parentOption, + references, + isNested, + parentAst, + currentCommandIndex, + }) + ); + } + if (!isFnSupported.supported) { + if (isFnSupported.reason === 'unknownFunction') { + messages.push(errors.unknownFunction(fn)); + } + // for nested functions skip this check and make the nested check fail later on + if (isFnSupported.reason === 'unsupportedFunction' && !isNested) { + messages.push( + parentOption + ? getMessageFromId({ + messageId: 'unsupportedFunctionForCommandOption', + values: { + name: fn.name, + command: parentCommand.toUpperCase(), + option: parentOption.toUpperCase(), + }, + locations: fn.location, + }) + : getMessageFromId({ + messageId: 'unsupportedFunctionForCommand', + values: { name: fn.name, command: parentCommand.toUpperCase() }, + locations: fn.location, + }) + ); + } + if (messages.length) { + return messages; + } + } + const matchingSignatures = getSignaturesWithMatchingArity(fnDefinition, fn); + if (!matchingSignatures.length) { + const { max, min } = getMaxMinNumberOfParams(fnDefinition); + if (max === min) { + messages.push( + getMessageFromId({ + messageId: 'wrongArgumentNumber', + values: { + fn: fn.name, + numArgs: max, + passedArgs: fn.args.length, + }, + locations: fn.location, + }) + ); + } else if (fn.args.length > max) { + messages.push( + getMessageFromId({ + messageId: 'wrongArgumentNumberTooMany', + values: { + fn: fn.name, + numArgs: max, + passedArgs: fn.args.length, + extraArgs: fn.args.length - max, + }, + locations: fn.location, + }) + ); + } else { + messages.push( + getMessageFromId({ + messageId: 'wrongArgumentNumberTooFew', + values: { + fn: fn.name, + numArgs: min, + passedArgs: fn.args.length, + missingArgs: min - fn.args.length, + }, + locations: fn.location, + }) + ); + } + } + // now perform the same check on all functions args + for (let i = 0; i < fn.args.length; i++) { + const arg = fn.args[i]; + + const allMatchingArgDefinitionsAreConstantOnly = matchingSignatures.every((signature) => { + return signature.params[i]?.constantOnly; + }); + const wrappedArray = Array.isArray(arg) ? arg : [arg]; + for (const _subArg of wrappedArray) { + /** + * we need to remove the inline casts + * to see if there's a function under there + * + * e.g. for ABS(CEIL(numberField)::int), we need to validate CEIL(numberField) + */ + const subArg = removeInlineCasts(_subArg); + + if (isFunctionItem(subArg)) { + const messagesFromArg = validateFunction({ + fn: subArg, + parentCommand, + parentOption, + references, + /** + * The constantOnly constraint needs to be enforced for arguments that + * are functions as well, regardless of whether the definition for the + * sub function's arguments includes the constantOnly flag. + * + * Example: + * bucket(@timestamp, abs(bytes), "", "") + * + * In the above example, the abs function is not defined with the + * constantOnly flag, but the second parameter in bucket _is_ defined + * with the constantOnly flag. + * + * Because of this, the abs function's arguments inherit the constraint + * and each should be validated as if each were constantOnly. + */ + forceConstantOnly: allMatchingArgDefinitionsAreConstantOnly || forceConstantOnly, + // use the nesting flag for now just for stats and metrics + // TODO: revisit this part later on to make it more generic + isNested: ['stats', 'inlinestats', 'metrics'].includes(parentCommand) + ? isNested || !isAssignment(fn) + : false, + parentAst, + }); + + if (messagesFromArg.some(({ code }) => code === 'expectedConstant')) { + const consolidatedMessage = getMessageFromId({ + messageId: 'expectedConstant', + values: { + fn: fn.name, + given: subArg.text, + }, + locations: subArg.location, + }); + + messages.push( + consolidatedMessage, + ...messagesFromArg.filter(({ code }) => code !== 'expectedConstant') + ); + } else { + messages.push(...messagesFromArg); + } + } + } + } + // check if the definition has some specific validation to apply: + if (fnDefinition.validate) { + const payloads = fnDefinition.validate(fn); + if (payloads.length) { + messages.push(...payloads); + } + } + // at this point we're sure that at least one signature is matching + const failingSignatures: ESQLMessage[][] = []; + let relevantFuncSignatures = matchingSignatures; + const enrichedArgs = fn.args; + + if (fn.name === 'in' || fn.name === 'not_in') { + for (let argIndex = 1; argIndex < fn.args.length; argIndex++) { + relevantFuncSignatures = fnDefinition.signatures.filter( + (s) => + s.params?.length >= argIndex && + s.params.slice(0, argIndex).every(({ type: dataType }, idx) => { + const arg = enrichedArgs[idx]; + + if (isLiteralItem(arg)) { + return ( + dataType === arg.literalType || compareTypesWithLiterals(dataType, arg.literalType) + ); + } + return false; // Non-literal arguments don't match + }) + ); + } + } + + for (const signature of relevantFuncSignatures) { + const failingSignature: ESQLMessage[] = []; + fn.args.forEach((outerArg, index) => { + const argDef = getParamAtPosition(signature, index); + if ((!outerArg && argDef?.optional) || !argDef) { + // that's ok, just skip it + // the else case is already catched with the argument counts check + // few lines above + return; + } + + // check every element of the argument (may be an array of elements, or may be a single element) + const hasMultipleElements = Array.isArray(outerArg); + const argElements = hasMultipleElements ? outerArg : [outerArg]; + const singularType = extractSingularType(argDef.type); + const messagesFromAllArgElements = argElements.flatMap((arg) => { + return [ + validateFunctionLiteralArg, + validateNestedFunctionArg, + validateFunctionColumnArg, + validateInlineCastArg, + ].flatMap((validateFn) => { + return validateFn( + fn, + arg, + { + ...argDef, + type: singularType, + constantOnly: forceConstantOnly || argDef.constantOnly, + }, + references, + parentCommand + ); + }); + }); + + const shouldCollapseMessages = isArrayType(argDef.type as string) && hasMultipleElements; + failingSignature.push( + ...(shouldCollapseMessages + ? collapseWrongArgumentTypeMessages( + messagesFromAllArgElements, + outerArg, + fn.name, + argDef.type as string, + parentCommand, + references + ) + : messagesFromAllArgElements) + ); + }); + if (failingSignature.length) { + failingSignatures.push(failingSignature); + } + } + + if (failingSignatures.length && failingSignatures.length === relevantFuncSignatures.length) { + const failingSignatureOrderedByErrorCount = failingSignatures + .map((arr, index) => ({ index, count: arr.length })) + .sort((a, b) => a.count - b.count); + const indexForShortestFailingsignature = failingSignatureOrderedByErrorCount[0].index; + messages.push(...failingSignatures[indexForShortestFailingsignature]); + } + // This is due to a special case in enrich where an implicit assignment is possible + // so the AST needs to store an explicit "columnX = columnX" which duplicates the message + return uniqBy(messages, ({ location }) => `${location.min}-${location.max}`); +} + +// #region Arg validation + +function validateFunctionLiteralArg( + astFunction: ESQLFunction, + actualArg: ESQLAstItem, + argDef: FunctionParameter, + references: ReferenceMaps, + parentCommand: string +) { + const messages: ESQLMessage[] = []; + if (isLiteralItem(actualArg)) { + if ( + actualArg.literalType === 'keyword' && + argDef.acceptedValues && + isValidLiteralOption(actualArg, argDef) + ) { + messages.push( + getMessageFromId({ + messageId: 'unsupportedLiteralOption', + values: { + name: astFunction.name, + value: actualArg.value, + supportedOptions: argDef.acceptedValues?.map((option) => `"${option}"`).join(', '), + }, + locations: actualArg.location, + }) + ); + } + + if (!checkFunctionArgMatchesDefinition(actualArg, argDef, references, parentCommand)) { + messages.push( + getMessageFromId({ + messageId: 'wrongArgumentType', + values: { + name: astFunction.name, + argType: argDef.type as string, + value: actualArg.text, + givenType: actualArg.literalType, + }, + locations: actualArg.location, + }) + ); + } + } + if (isTimeIntervalItem(actualArg)) { + // check first if it's a valid interval string + if (!inKnownTimeInterval(actualArg.unit)) { + messages.push( + getMessageFromId({ + messageId: 'unknownInterval', + values: { + value: actualArg.unit, + }, + locations: actualArg.location, + }) + ); + } else { + if (!checkFunctionArgMatchesDefinition(actualArg, argDef, references, parentCommand)) { + messages.push( + getMessageFromId({ + messageId: 'wrongArgumentType', + values: { + name: astFunction.name, + argType: argDef.type as string, + value: actualArg.name, + givenType: 'duration', + }, + locations: actualArg.location, + }) + ); + } + } + } + return messages; +} + +function validateInlineCastArg( + astFunction: ESQLFunction, + arg: ESQLAstItem, + parameterDefinition: FunctionParameter, + references: ReferenceMaps, + parentCommand: string +) { + if (!isInlineCastItem(arg)) { + return []; + } + + if (!checkFunctionArgMatchesDefinition(arg, parameterDefinition, references, parentCommand)) { + return [ + getMessageFromId({ + messageId: 'wrongArgumentType', + values: { + name: astFunction.name, + argType: parameterDefinition.type as string, + value: arg.text, + givenType: arg.castType, + }, + locations: arg.location, + }), + ]; + } + + return []; +} + +function validateNestedFunctionArg( + astFunction: ESQLFunction, + actualArg: ESQLAstItem, + parameterDefinition: FunctionParameter, + references: ReferenceMaps, + parentCommand: string +) { + const messages: ESQLMessage[] = []; + if ( + isFunctionItem(actualArg) && + // no need to check the reason here, it is checked already above + isSupportedFunction(actualArg.name, parentCommand).supported + ) { + // The isSupported check ensure the definition exists + const argFn = getFunctionDefinition(actualArg.name)!; + const fnDef = getFunctionDefinition(astFunction.name)!; + // no nestying criteria should be enforced only for same type function + if (fnDef.type === FunctionDefinitionTypes.AGG && argFn.type === FunctionDefinitionTypes.AGG) { + messages.push( + getMessageFromId({ + messageId: 'noNestedArgumentSupport', + values: { name: actualArg.text, argType: argFn.signatures[0].returnType as string }, + locations: actualArg.location, + }) + ); + } + if ( + !checkFunctionArgMatchesDefinition(actualArg, parameterDefinition, references, parentCommand) + ) { + messages.push( + getMessageFromId({ + messageId: 'wrongArgumentType', + values: { + name: astFunction.name, + argType: parameterDefinition.type as string, + value: actualArg.text, + givenType: argFn.signatures[0].returnType as string, + }, + locations: actualArg.location, + }) + ); + } + } + return messages; +} + +function validateFunctionColumnArg( + astFunction: ESQLFunction, + actualArg: ESQLAstItem, + parameterDefinition: FunctionParameter, + references: ReferenceMaps, + parentCommand: string +) { + const messages: ESQLMessage[] = []; + if (!(isColumnItem(actualArg) || isIdentifier(actualArg))) { + return messages; + } + + const columnName = getQuotedColumnName(actualArg); + const columnExists = getColumnExists(actualArg, references); + + if (parameterDefinition.constantOnly) { + messages.push( + getMessageFromId({ + messageId: 'expectedConstant', + values: { + fn: astFunction.name, + given: columnName, + }, + locations: actualArg.location, + }) + ); + + return messages; + } + + if (!columnExists) { + messages.push( + getMessageFromId({ + messageId: 'unknownColumn', + values: { + name: actualArg.name, + }, + locations: actualArg.location, + }) + ); + + return messages; + } + + if (actualArg.name === '*') { + // if function does not support wildcards return a specific error + if (!('supportsWildcard' in parameterDefinition) || !parameterDefinition.supportsWildcard) { + messages.push( + getMessageFromId({ + messageId: 'noWildcardSupportAsArg', + values: { + name: astFunction.name, + }, + locations: actualArg.location, + }) + ); + } + + return messages; + } + + if ( + !checkFunctionArgMatchesDefinition(actualArg, parameterDefinition, references, parentCommand) + ) { + const columnHit = getColumnForASTNode(actualArg, references); + messages.push( + getMessageFromId({ + messageId: 'wrongArgumentType', + values: { + name: astFunction.name, + argType: parameterDefinition.type as string, + value: actualArg.name, + givenType: columnHit!.type, + }, + locations: actualArg.location, + }) + ); + } + + return messages; +} + +function removeInlineCasts(arg: ESQLAstItem): ESQLAstItem { + if (isInlineCastItem(arg)) { + return removeInlineCasts(arg.value); + } + return arg; +} + +// #endregion + +// #region Specific functions + +function validateIfHasUnsupportedCommandPrior( + fn: ESQLFunction, + parentAst: ESQLCommand[] = [], + unsupportedCommands: Set, + currentCommandIndex?: number +) { + if (currentCommandIndex === undefined) { + return NO_MESSAGE; + } + const unsupportedCommandsPrior = parentAst.filter( + (cmd, idx) => idx <= currentCommandIndex && unsupportedCommands.has(cmd.name) + ); + + if (unsupportedCommandsPrior.length > 0) { + return [ + getMessageFromId({ + messageId: 'fnUnsupportedAfterCommand', + values: { + function: fn.name.toUpperCase(), + command: unsupportedCommandsPrior[0].name.toUpperCase(), + }, + locations: fn.location, + }), + ]; + } + return NO_MESSAGE; +} + +const validateMatchFunction: FunctionValidator = ({ + fn, + parentCommand, + parentOption, + references, + forceConstantOnly = false, + isNested, + parentAst, + currentCommandIndex, +}) => { + if (fn.name === 'match') { + if (parentCommand !== 'where') { + return [ + getMessageFromId({ + messageId: 'onlyWhereCommandSupported', + values: { fn: fn.name }, + locations: fn.location, + }), + ]; + } + return validateIfHasUnsupportedCommandPrior( + fn, + parentAst, + UNSUPPORTED_COMMANDS_BEFORE_MATCH, + currentCommandIndex + ); + } + return NO_MESSAGE; +}; + +type FunctionValidator = (args: { + fn: ESQLFunction; + parentCommand: string; + parentOption?: string; + references: ReferenceMaps; + forceConstantOnly?: boolean; + isNested?: boolean; + parentAst?: ESQLCommand[]; + currentCommandIndex?: number; +}) => ESQLMessage[]; + +const validateQSTRFunction: FunctionValidator = ({ + fn, + parentCommand, + parentOption, + references, + forceConstantOnly = false, + isNested, + parentAst, + currentCommandIndex, +}) => { + if (fn.name === 'qstr') { + return validateIfHasUnsupportedCommandPrior( + fn, + parentAst, + UNSUPPORTED_COMMANDS_BEFORE_QSTR, + currentCommandIndex + ); + } + return NO_MESSAGE; +}; + +const textSearchFunctionsValidators: Record = { + match: validateMatchFunction, + qstr: validateQSTRFunction, +}; + +// #endregion diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/types.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/types.ts index ef529d9398637..952dbcb4906f2 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/types.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/types.ts @@ -159,11 +159,7 @@ export interface ValidationErrors { message: string; type: { field: string }; }; - unsupportedSetting: { - message: string; - type: { setting: string; expected: string }; - }; - unsupportedSettingCommandValue: { + unsupportedMode: { message: string; type: { command: string; value: string; expected: string }; }; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index b05b1d3eeaf98..5a1e173287f05 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -1349,7 +1349,7 @@ describe('validation logic', () => { 'Unknown policy [_]', ]); testErrorsAndWarnings(`from a_index | enrich _:policy`, [ - 'Unrecognized value [_] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]', + 'Unrecognized value [_] for ENRICH, mode needs to be one of [_any, _coordinator, _remote]', ]); testErrorsAndWarnings(`from a_index | enrich :policy`, [ "SyntaxError: token recognition error at: ':'", @@ -1363,7 +1363,7 @@ describe('validation logic', () => { 'Unknown policy [_any]', ]); testErrorsAndWarnings(`from a_index | enrich any:policy`, [ - 'Unrecognized value [any] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]', + 'Unrecognized value [any] for ENRICH, mode needs to be one of [_any, _coordinator, _remote]', ]); testErrorsAndWarnings(`from a_index | enrich policy `, []); testErrorsAndWarnings('from a_index | enrich `this``is fine`', [ @@ -1391,7 +1391,7 @@ describe('validation logic', () => { } testErrorsAndWarnings(`from a_index | enrich _unknown:policy`, [ - 'Unrecognized value [_unknown] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]', + 'Unrecognized value [_unknown] for ENRICH, mode needs to be one of [_any, _coordinator, _remote]', ]); testErrorsAndWarnings(`from a_index |enrich missing-policy `, [ 'Unknown policy [missing-policy]', diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.ts index 51d731336923e..b48e8258db390 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/validation/validation.ts @@ -7,1199 +7,196 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import uniqBy from 'lodash/uniqBy'; import { AstProviderFn, ESQLAst, - ESQLAstItem, ESQLAstMetricsCommand, ESQLColumn, ESQLCommand, - ESQLCommandMode, ESQLCommandOption, - ESQLFunction, ESQLMessage, ESQLSource, - walk, - isBinaryExpression, isIdentifier, - isSource, + walk, } from '@kbn/esql-ast'; -import type { - ESQLAstField, - ESQLAstJoinCommand, - ESQLIdentifier, - ESQLProperNode, -} from '@kbn/esql-ast/src/types'; -import { - CommandModeDefinition, - CommandOptionsDefinition, - FunctionParameter, - FunctionDefinitionTypes, -} from '../definitions/types'; +import type { ESQLAstJoinCommand, ESQLIdentifier } from '@kbn/esql-ast/src/types'; +import { CommandOptionsDefinition } from '../definitions/types'; +import { METADATA_FIELDS } from '../shared/constants'; +import { compareTypesWithLiterals } from '../shared/esql_types'; import { areFieldAndVariableTypesCompatible, - extractSingularType, + getColumnExists, getColumnForASTNode, getCommandDefinition, - getFunctionDefinition, - isArrayType, + getQuotedColumnName, + hasWildcard, isColumnItem, - checkFunctionArgMatchesDefinition, isFunctionItem, - isLiteralItem, isOptionItem, + isParametrized, isSourceItem, - isSupportedFunction, isTimeIntervalItem, - inKnownTimeInterval, - sourceExists, - getColumnExists, - hasWildcard, - isSettingItem, - isAssignment, isVariable, - isValidLiteralOption, - isAggFunction, - getQuotedColumnName, - isInlineCastItem, - getSignaturesWithMatchingArity, - isFunctionOperatorParam, - isMaybeAggFunction, - isParametrized, + sourceExists, } from '../shared/helpers'; +import type { ESQLCallbacks } from '../shared/types'; import { collectVariables } from '../shared/variables'; -import { getMessageFromId, errors } from './errors'; +import { errors, getMessageFromId } from './errors'; +import { validateFunction } from './function_validation'; +import { + retrieveFields, + retrieveFieldsFromStringSources, + retrievePolicies, + retrievePoliciesFields, + retrieveSources, +} from './resources'; import type { - ErrorTypes, ESQLRealField, ESQLVariable, + ErrorTypes, ReferenceMaps, ValidationOptions, ValidationResult, } from './types'; -import type { ESQLCallbacks } from '../shared/types'; -import { - retrieveSources, - retrieveFields, - retrievePolicies, - retrievePoliciesFields, - retrieveFieldsFromStringSources, -} from './resources'; -import { collapseWrongArgumentTypeMessages, getMaxMinNumberOfParams } from './helpers'; -import { getParamAtPosition } from '../shared/helpers'; -import { - METADATA_FIELDS, - UNSUPPORTED_COMMANDS_BEFORE_MATCH, - UNSUPPORTED_COMMANDS_BEFORE_QSTR, -} from '../shared/constants'; -import { compareTypesWithLiterals } from '../shared/esql_types'; - -const NO_MESSAGE: ESQLMessage[] = []; -function validateFunctionLiteralArg( - astFunction: ESQLFunction, - actualArg: ESQLAstItem, - argDef: FunctionParameter, - references: ReferenceMaps, - parentCommand: string -) { - const messages: ESQLMessage[] = []; - if (isLiteralItem(actualArg)) { - if ( - actualArg.literalType === 'keyword' && - argDef.acceptedValues && - isValidLiteralOption(actualArg, argDef) - ) { - messages.push( - getMessageFromId({ - messageId: 'unsupportedLiteralOption', - values: { - name: astFunction.name, - value: actualArg.value, - supportedOptions: argDef.acceptedValues?.map((option) => `"${option}"`).join(', '), - }, - locations: actualArg.location, - }) - ); - } - - if (!checkFunctionArgMatchesDefinition(actualArg, argDef, references, parentCommand)) { - messages.push( - getMessageFromId({ - messageId: 'wrongArgumentType', - values: { - name: astFunction.name, - argType: argDef.type as string, - value: actualArg.text, - givenType: actualArg.literalType, - }, - locations: actualArg.location, - }) - ); - } - } - if (isTimeIntervalItem(actualArg)) { - // check first if it's a valid interval string - if (!inKnownTimeInterval(actualArg.unit)) { - messages.push( - getMessageFromId({ - messageId: 'unknownInterval', - values: { - value: actualArg.unit, - }, - locations: actualArg.location, - }) - ); - } else { - if (!checkFunctionArgMatchesDefinition(actualArg, argDef, references, parentCommand)) { - messages.push( - getMessageFromId({ - messageId: 'wrongArgumentType', - values: { - name: astFunction.name, - argType: argDef.type as string, - value: actualArg.name, - givenType: 'duration', - }, - locations: actualArg.location, - }) - ); - } - } - } - return messages; -} - -function validateInlineCastArg( - astFunction: ESQLFunction, - arg: ESQLAstItem, - parameterDefinition: FunctionParameter, - references: ReferenceMaps, - parentCommand: string -) { - if (!isInlineCastItem(arg)) { - return []; - } - - if (!checkFunctionArgMatchesDefinition(arg, parameterDefinition, references, parentCommand)) { - return [ - getMessageFromId({ - messageId: 'wrongArgumentType', - values: { - name: astFunction.name, - argType: parameterDefinition.type as string, - value: arg.text, - givenType: arg.castType, - }, - locations: arg.location, - }), - ]; - } - - return []; -} - -function validateNestedFunctionArg( - astFunction: ESQLFunction, - actualArg: ESQLAstItem, - parameterDefinition: FunctionParameter, - references: ReferenceMaps, - parentCommand: string -) { - const messages: ESQLMessage[] = []; - if ( - isFunctionItem(actualArg) && - // no need to check the reason here, it is checked already above - isSupportedFunction(actualArg.name, parentCommand).supported - ) { - // The isSupported check ensure the definition exists - const argFn = getFunctionDefinition(actualArg.name)!; - const fnDef = getFunctionDefinition(astFunction.name)!; - // no nestying criteria should be enforced only for same type function - if (fnDef.type === FunctionDefinitionTypes.AGG && argFn.type === FunctionDefinitionTypes.AGG) { - messages.push( - getMessageFromId({ - messageId: 'noNestedArgumentSupport', - values: { name: actualArg.text, argType: argFn.signatures[0].returnType as string }, - locations: actualArg.location, - }) - ); - } - if ( - !checkFunctionArgMatchesDefinition(actualArg, parameterDefinition, references, parentCommand) - ) { - messages.push( - getMessageFromId({ - messageId: 'wrongArgumentType', - values: { - name: astFunction.name, - argType: parameterDefinition.type as string, - value: actualArg.text, - givenType: argFn.signatures[0].returnType as string, - }, - locations: actualArg.location, - }) - ); - } - } - return messages; -} - -function validateFunctionColumnArg( - astFunction: ESQLFunction, - actualArg: ESQLAstItem, - parameterDefinition: FunctionParameter, - references: ReferenceMaps, - parentCommand: string -) { - const messages: ESQLMessage[] = []; - if (!(isColumnItem(actualArg) || isIdentifier(actualArg))) { - return messages; - } - - const columnName = getQuotedColumnName(actualArg); - const columnExists = getColumnExists(actualArg, references); - - if (parameterDefinition.constantOnly) { - messages.push( - getMessageFromId({ - messageId: 'expectedConstant', - values: { - fn: astFunction.name, - given: columnName, - }, - locations: actualArg.location, - }) - ); - - return messages; - } - - if (!columnExists) { - messages.push( - getMessageFromId({ - messageId: 'unknownColumn', - values: { - name: actualArg.name, - }, - locations: actualArg.location, - }) - ); - - return messages; - } - - if (actualArg.name === '*') { - // if function does not support wildcards return a specific error - if (!('supportsWildcard' in parameterDefinition) || !parameterDefinition.supportsWildcard) { - messages.push( - getMessageFromId({ - messageId: 'noWildcardSupportAsArg', - values: { - name: astFunction.name, - }, - locations: actualArg.location, - }) - ); - } - - return messages; - } - - if ( - !checkFunctionArgMatchesDefinition(actualArg, parameterDefinition, references, parentCommand) - ) { - const columnHit = getColumnForASTNode(actualArg, references); - messages.push( - getMessageFromId({ - messageId: 'wrongArgumentType', - values: { - name: astFunction.name, - argType: parameterDefinition.type as string, - value: actualArg.name, - givenType: columnHit!.type, - }, - locations: actualArg.location, - }) - ); - } - - return messages; -} - -function removeInlineCasts(arg: ESQLAstItem): ESQLAstItem { - if (isInlineCastItem(arg)) { - return removeInlineCasts(arg.value); - } - return arg; -} - -function validateIfHasUnsupportedCommandPrior( - fn: ESQLFunction, - parentAst: ESQLCommand[] = [], - unsupportedCommands: Set, - currentCommandIndex?: number -) { - if (currentCommandIndex === undefined) { - return NO_MESSAGE; - } - const unsupportedCommandsPrior = parentAst.filter( - (cmd, idx) => idx <= currentCommandIndex && unsupportedCommands.has(cmd.name) - ); - - if (unsupportedCommandsPrior.length > 0) { - return [ - getMessageFromId({ - messageId: 'fnUnsupportedAfterCommand', - values: { - function: fn.name.toUpperCase(), - command: unsupportedCommandsPrior[0].name.toUpperCase(), - }, - locations: fn.location, - }), - ]; - } - return NO_MESSAGE; -} - -const validateMatchFunction: FunctionValidator = ({ - fn, - parentCommand, - parentOption, - references, - forceConstantOnly = false, - isNested, - parentAst, - currentCommandIndex, -}) => { - if (fn.name === 'match') { - if (parentCommand !== 'where') { - return [ - getMessageFromId({ - messageId: 'onlyWhereCommandSupported', - values: { fn: fn.name }, - locations: fn.location, - }), - ]; - } - return validateIfHasUnsupportedCommandPrior( - fn, - parentAst, - UNSUPPORTED_COMMANDS_BEFORE_MATCH, - currentCommandIndex - ); - } - return NO_MESSAGE; -}; - -type FunctionValidator = (args: { - fn: ESQLFunction; - parentCommand: string; - parentOption?: string; - references: ReferenceMaps; - forceConstantOnly?: boolean; - isNested?: boolean; - parentAst?: ESQLCommand[]; - currentCommandIndex?: number; -}) => ESQLMessage[]; - -const validateQSTRFunction: FunctionValidator = ({ - fn, - parentCommand, - parentOption, - references, - forceConstantOnly = false, - isNested, - parentAst, - currentCommandIndex, -}) => { - if (fn.name === 'qstr') { - return validateIfHasUnsupportedCommandPrior( - fn, - parentAst, - UNSUPPORTED_COMMANDS_BEFORE_QSTR, - currentCommandIndex - ); - } - return NO_MESSAGE; -}; - -const textSearchFunctionsValidators: Record = { - match: validateMatchFunction, - qstr: validateQSTRFunction, -}; - -function validateFunction({ - fn, - parentCommand, - parentOption, - references, - forceConstantOnly = false, - isNested, - parentAst, - currentCommandIndex, -}: { - fn: ESQLFunction; - parentCommand: string; - parentOption?: string; - references: ReferenceMaps; - forceConstantOnly?: boolean; - isNested?: boolean; - parentAst?: ESQLCommand[]; - currentCommandIndex?: number; -}): ESQLMessage[] { - const messages: ESQLMessage[] = []; - - if (fn.incomplete) { - return messages; - } - if (isFunctionOperatorParam(fn)) { - return messages; - } - const fnDefinition = getFunctionDefinition(fn.name)!; - - const isFnSupported = isSupportedFunction(fn.name, parentCommand, parentOption); - - if (typeof textSearchFunctionsValidators[fn.name] === 'function') { - const validator = textSearchFunctionsValidators[fn.name]; - messages.push( - ...validator({ - fn, - parentCommand, - parentOption, - references, - isNested, - parentAst, - currentCommandIndex, - }) - ); - } - if (!isFnSupported.supported) { - if (isFnSupported.reason === 'unknownFunction') { - messages.push(errors.unknownFunction(fn)); - } - // for nested functions skip this check and make the nested check fail later on - if (isFnSupported.reason === 'unsupportedFunction' && !isNested) { - messages.push( - parentOption - ? getMessageFromId({ - messageId: 'unsupportedFunctionForCommandOption', - values: { - name: fn.name, - command: parentCommand.toUpperCase(), - option: parentOption.toUpperCase(), - }, - locations: fn.location, - }) - : getMessageFromId({ - messageId: 'unsupportedFunctionForCommand', - values: { name: fn.name, command: parentCommand.toUpperCase() }, - locations: fn.location, - }) - ); - } - if (messages.length) { - return messages; - } - } - const matchingSignatures = getSignaturesWithMatchingArity(fnDefinition, fn); - if (!matchingSignatures.length) { - const { max, min } = getMaxMinNumberOfParams(fnDefinition); - if (max === min) { - messages.push( - getMessageFromId({ - messageId: 'wrongArgumentNumber', - values: { - fn: fn.name, - numArgs: max, - passedArgs: fn.args.length, - }, - locations: fn.location, - }) - ); - } else if (fn.args.length > max) { - messages.push( - getMessageFromId({ - messageId: 'wrongArgumentNumberTooMany', - values: { - fn: fn.name, - numArgs: max, - passedArgs: fn.args.length, - extraArgs: fn.args.length - max, - }, - locations: fn.location, - }) - ); - } else { - messages.push( - getMessageFromId({ - messageId: 'wrongArgumentNumberTooFew', - values: { - fn: fn.name, - numArgs: min, - passedArgs: fn.args.length, - missingArgs: min - fn.args.length, - }, - locations: fn.location, - }) - ); - } - } - // now perform the same check on all functions args - for (let i = 0; i < fn.args.length; i++) { - const arg = fn.args[i]; - - const allMatchingArgDefinitionsAreConstantOnly = matchingSignatures.every((signature) => { - return signature.params[i]?.constantOnly; - }); - const wrappedArray = Array.isArray(arg) ? arg : [arg]; - for (const _subArg of wrappedArray) { - /** - * we need to remove the inline casts - * to see if there's a function under there - * - * e.g. for ABS(CEIL(numberField)::int), we need to validate CEIL(numberField) - */ - const subArg = removeInlineCasts(_subArg); - - if (isFunctionItem(subArg)) { - const messagesFromArg = validateFunction({ - fn: subArg, - parentCommand, - parentOption, - references, - /** - * The constantOnly constraint needs to be enforced for arguments that - * are functions as well, regardless of whether the definition for the - * sub function's arguments includes the constantOnly flag. - * - * Example: - * bucket(@timestamp, abs(bytes), "", "") - * - * In the above example, the abs function is not defined with the - * constantOnly flag, but the second parameter in bucket _is_ defined - * with the constantOnly flag. - * - * Because of this, the abs function's arguments inherit the constraint - * and each should be validated as if each were constantOnly. - */ - forceConstantOnly: allMatchingArgDefinitionsAreConstantOnly || forceConstantOnly, - // use the nesting flag for now just for stats and metrics - // TODO: revisit this part later on to make it more generic - isNested: ['stats', 'inlinestats', 'metrics'].includes(parentCommand) - ? isNested || !isAssignment(fn) - : false, - parentAst, - }); - - if (messagesFromArg.some(({ code }) => code === 'expectedConstant')) { - const consolidatedMessage = getMessageFromId({ - messageId: 'expectedConstant', - values: { - fn: fn.name, - given: subArg.text, - }, - locations: subArg.location, - }); - - messages.push( - consolidatedMessage, - ...messagesFromArg.filter(({ code }) => code !== 'expectedConstant') - ); - } else { - messages.push(...messagesFromArg); - } - } - } - } - // check if the definition has some specific validation to apply: - if (fnDefinition.validate) { - const payloads = fnDefinition.validate(fn); - if (payloads.length) { - messages.push(...payloads); - } - } - // at this point we're sure that at least one signature is matching - const failingSignatures: ESQLMessage[][] = []; - let relevantFuncSignatures = matchingSignatures; - const enrichedArgs = fn.args; - - if (fn.name === 'in' || fn.name === 'not_in') { - for (let argIndex = 1; argIndex < fn.args.length; argIndex++) { - relevantFuncSignatures = fnDefinition.signatures.filter( - (s) => - s.params?.length >= argIndex && - s.params.slice(0, argIndex).every(({ type: dataType }, idx) => { - const arg = enrichedArgs[idx]; - - if (isLiteralItem(arg)) { - return ( - dataType === arg.literalType || compareTypesWithLiterals(dataType, arg.literalType) - ); - } - return false; // Non-literal arguments don't match - }) - ); - } - } - - for (const signature of relevantFuncSignatures) { - const failingSignature: ESQLMessage[] = []; - fn.args.forEach((outerArg, index) => { - const argDef = getParamAtPosition(signature, index); - if ((!outerArg && argDef?.optional) || !argDef) { - // that's ok, just skip it - // the else case is already catched with the argument counts check - // few lines above - return; - } - - // check every element of the argument (may be an array of elements, or may be a single element) - const hasMultipleElements = Array.isArray(outerArg); - const argElements = hasMultipleElements ? outerArg : [outerArg]; - const singularType = extractSingularType(argDef.type); - const messagesFromAllArgElements = argElements.flatMap((arg) => { - return [ - validateFunctionLiteralArg, - validateNestedFunctionArg, - validateFunctionColumnArg, - validateInlineCastArg, - ].flatMap((validateFn) => { - return validateFn( - fn, - arg, - { - ...argDef, - type: singularType, - constantOnly: forceConstantOnly || argDef.constantOnly, - }, - references, - parentCommand - ); - }); - }); - - const shouldCollapseMessages = isArrayType(argDef.type as string) && hasMultipleElements; - failingSignature.push( - ...(shouldCollapseMessages - ? collapseWrongArgumentTypeMessages( - messagesFromAllArgElements, - outerArg, - fn.name, - argDef.type as string, - parentCommand, - references - ) - : messagesFromAllArgElements) - ); - }); - if (failingSignature.length) { - failingSignatures.push(failingSignature); - } - } - - if (failingSignatures.length && failingSignatures.length === relevantFuncSignatures.length) { - const failingSignatureOrderedByErrorCount = failingSignatures - .map((arr, index) => ({ index, count: arr.length })) - .sort((a, b) => a.count - b.count); - const indexForShortestFailingsignature = failingSignatureOrderedByErrorCount[0].index; - messages.push(...failingSignatures[indexForShortestFailingsignature]); - } - // This is due to a special case in enrich where an implicit assignment is possible - // so the AST needs to store an explicit "columnX = columnX" which duplicates the message - return uniqBy(messages, ({ location }) => `${location.min}-${location.max}`); -} - -/** @deprecated — "command settings" will be removed soon */ -function validateSetting( - setting: ESQLCommandMode, - settingDef: CommandModeDefinition | undefined, - command: ESQLCommand, - referenceMaps: ReferenceMaps -): ESQLMessage[] { - const messages: ESQLMessage[] = []; - if (setting.incomplete || command.incomplete) { - return messages; - } - if (!settingDef) { - const commandDef = getCommandDefinition(command.name); - messages.push( - getMessageFromId({ - messageId: 'unsupportedSetting', - values: { - setting: setting.name, - expected: commandDef.modes.map(({ name }) => name).join(', '), - }, - locations: setting.location, - }) - ); - return messages; - } - if ( - settingDef.values.every(({ name }) => name !== setting.name) || - // enforce the check on the prefix if present - (settingDef.prefix && !setting.text.startsWith(settingDef.prefix)) - ) { - messages.push( - getMessageFromId({ - messageId: 'unsupportedSettingCommandValue', - values: { - command: command.name.toUpperCase(), - value: setting.text, - // for some reason all this enums are uppercase in ES - expected: settingDef.values - .map(({ name }) => `${settingDef.prefix || ''}${name}`) - .join(', ') - .toUpperCase(), - }, - locations: setting.location, - }) - ); - } - return messages; -} - -/** - * Validate that a function is an aggregate function or that all children - * recursively terminate at either a literal or an aggregate function. - */ -const isFunctionAggClosed = (fn: ESQLFunction): boolean => - isMaybeAggFunction(fn) || areFunctionArgsAggClosed(fn); - -const areFunctionArgsAggClosed = (fn: ESQLFunction): boolean => - fn.args.every((arg) => isLiteralItem(arg) || (isFunctionItem(arg) && isFunctionAggClosed(arg))) || - isFunctionOperatorParam(fn); - -/** - * Looks for first nested aggregate function in an aggregate function, recursively. - */ -const findNestedAggFunctionInAggFunction = (agg: ESQLFunction): ESQLFunction | undefined => { - for (const arg of agg.args) { - if (isFunctionItem(arg)) { - return isMaybeAggFunction(arg) ? arg : findNestedAggFunctionInAggFunction(arg); - } - } -}; - -/** - * Looks for first nested aggregate function in another aggregate a function, - * recursively. - * - * @param fn Function to check for nested aggregate functions. - * @param parentIsAgg Whether the parent function of `fn` is an aggregate function. - * @returns The first nested aggregate function in `fn`, or `undefined` if none is found. - */ -const findNestedAggFunction = ( - fn: ESQLFunction, - parentIsAgg: boolean = false -): ESQLFunction | undefined => { - if (isMaybeAggFunction(fn)) { - return parentIsAgg ? fn : findNestedAggFunctionInAggFunction(fn); - } - - for (const arg of fn.args) { - if (isFunctionItem(arg)) { - const nestedAgg = findNestedAggFunction(arg, parentIsAgg || isAggFunction(fn)); - if (nestedAgg) return nestedAgg; - } - } -}; - -/** - * Validates aggregates fields: `... ...`. - */ -const validateAggregates = ( - command: ESQLCommand, - aggregates: ESQLAstField[], - references: ReferenceMaps -) => { - const messages: ESQLMessage[] = []; - - // Should never happen. - if (!aggregates.length) { - messages.push(errors.unexpected(command.location)); - return messages; - } - - let hasMissingAggregationFunctionError = false; - - for (const aggregate of aggregates) { - if (isFunctionItem(aggregate)) { - messages.push( - ...validateFunction({ - fn: aggregate, - parentCommand: command.name, - parentOption: undefined, - references, - }) - ); - - let hasAggregationFunction = false; - - walk(aggregate, { - visitFunction: (fn) => { - const definition = getFunctionDefinition(fn.name); - if (!definition) return; - if (definition.type === FunctionDefinitionTypes.AGG) hasAggregationFunction = true; - }, - }); - - if (!hasAggregationFunction) { - hasMissingAggregationFunctionError = true; - messages.push(errors.noAggFunction(command, aggregate)); - } - } else if (isColumnItem(aggregate) || isIdentifier(aggregate)) { - messages.push(errors.unknownAggFunction(aggregate)); - } else { - // Should never happen. - } - } - - if (hasMissingAggregationFunctionError) { - return messages; - } - - for (const aggregate of aggregates) { - if (isFunctionItem(aggregate)) { - const fn = isAssignment(aggregate) ? aggregate.args[1] : aggregate; - if (isFunctionItem(fn) && !isFunctionAggClosed(fn)) { - messages.push(errors.expressionNotAggClosed(command, fn)); - } - } - } - - if (messages.length) { - return messages; - } - - for (const aggregate of aggregates) { - if (isFunctionItem(aggregate)) { - const aggInAggFunction = findNestedAggFunction(aggregate); - if (aggInAggFunction) { - messages.push(errors.aggInAggFunction(aggInAggFunction)); - break; - } - } - } - - return messages; -}; - -/** - * Validates grouping fields of the BY clause: `... BY `. - */ -const validateByGrouping = ( - fields: ESQLAstItem[], - commandName: string, - referenceMaps: ReferenceMaps, - multipleParams: boolean -): ESQLMessage[] => { - const messages: ESQLMessage[] = []; - for (const field of fields) { - if (!Array.isArray(field)) { - if (!multipleParams) { - if (isColumnItem(field)) { - messages.push(...validateColumnForCommand(field, commandName, referenceMaps)); - } - } else { - if (isColumnItem(field)) { - messages.push(...validateColumnForCommand(field, commandName, referenceMaps)); - } - if (isFunctionItem(field)) { - messages.push( - ...validateFunction({ - fn: field, - parentCommand: commandName, - parentOption: 'by', - references: referenceMaps, - }) - ); - } - } - } - } - return messages; -}; - -function validateOption( - option: ESQLCommandOption, - optionDef: CommandOptionsDefinition | undefined, - command: ESQLCommand, - referenceMaps: ReferenceMaps -): ESQLMessage[] { - // check if the arguments of the option are of the correct type - const messages: ESQLMessage[] = []; - if (option.incomplete || command.incomplete) { - return messages; - } - if (!optionDef) { - messages.push( - getMessageFromId({ - messageId: 'unknownOption', - values: { command: command.name.toUpperCase(), option: option.name }, - locations: option.location, - }) - ); - return messages; - } - // use dedicate validate fn if provided - if (optionDef.validate) { - const fields = METADATA_FIELDS; - messages.push(...optionDef.validate(option, command, new Set(fields))); - } - if (!optionDef.skipCommonValidation) { - option.args.forEach((arg) => { - if (!Array.isArray(arg)) { - if (!optionDef.signature.multipleParams) { - if (isColumnItem(arg)) { - messages.push(...validateColumnForCommand(arg, command.name, referenceMaps)); - } - } else { - if (isColumnItem(arg)) { - messages.push(...validateColumnForCommand(arg, command.name, referenceMaps)); - } - if (isFunctionItem(arg)) { - messages.push( - ...validateFunction({ - fn: arg, - parentCommand: command.name, - parentOption: option.name, - references: referenceMaps, - }) - ); - } - } - } - }); - } - - return messages; -} - -function validateSource( - source: ESQLSource, - commandName: string, - { sources, policies }: ReferenceMaps -) { - const messages: ESQLMessage[] = []; - if (source.incomplete) { - return messages; - } - - const commandDef = getCommandDefinition(commandName); - const isWildcardAndNotSupported = - hasWildcard(source.name) && !commandDef.signature.params.some(({ wildcards }) => wildcards); - if (isWildcardAndNotSupported) { - messages.push( - getMessageFromId({ - messageId: 'wildcardNotSupportedForCommand', - values: { command: commandName.toUpperCase(), value: source.name }, - locations: source.location, - }) - ); - } else { - if (source.sourceType === 'index' && !sourceExists(source.name, sources)) { - messages.push( - getMessageFromId({ - messageId: 'unknownIndex', - values: { name: source.name }, - locations: source.location, - }) - ); - } else if (source.sourceType === 'policy' && !policies.has(source.name)) { - messages.push( - getMessageFromId({ - messageId: 'unknownPolicy', - values: { name: source.name }, - locations: source.location, - }) - ); - } - } - - return messages; -} - -function validateColumnForCommand( - column: ESQLColumn | ESQLIdentifier, - commandName: string, - references: ReferenceMaps -): ESQLMessage[] { - const messages: ESQLMessage[] = []; - if (commandName === 'row') { - if (!references.variables.has(column.name) && !isParametrized(column)) { - messages.push(errors.unknownColumn(column)); - } - } else { - const columnName = getQuotedColumnName(column); - if (getColumnExists(column, references)) { - const commandDef = getCommandDefinition(commandName); - const columnParamsWithInnerTypes = commandDef.signature.params.filter( - ({ type, innerTypes }) => type === 'column' && innerTypes - ); - // this should be guaranteed by the columnCheck above - const columnRef = getColumnForASTNode(column, references)!; - if (columnParamsWithInnerTypes.length) { - const hasSomeWrongInnerTypes = columnParamsWithInnerTypes.every( - ({ innerTypes }) => - innerTypes && - !innerTypes.includes('any') && - !innerTypes.some((type) => compareTypesWithLiterals(type, columnRef.type)) - ); - if (hasSomeWrongInnerTypes) { - const supportedTypes: string[] = columnParamsWithInnerTypes - .map(({ innerTypes }) => innerTypes) - .flat() - .filter((type) => type !== undefined) as string[]; +import { validate as validateJoinCommand } from './commands/join'; +import { validate as validateMetricsCommand } from './commands/metrics'; - messages.push( - getMessageFromId({ - messageId: 'unsupportedColumnTypeForCommand', - values: { - command: commandName.toUpperCase(), - type: supportedTypes.join(', '), - typeCount: supportedTypes.length, - givenType: columnRef.type, - column: columnName, - }, - locations: column.location, - }) - ); - } - } - if ( - hasWildcard(columnName) && - !isVariable(columnRef) && - !commandDef.signature.params.some(({ type, wildcards }) => type === 'column' && wildcards) - ) { - messages.push( - getMessageFromId({ - messageId: 'wildcardNotSupportedForCommand', - values: { - command: commandName.toUpperCase(), - value: columnName, - }, - locations: column.location, - }) - ); - } - } else { - if (column.name) { - messages.push(errors.unknownColumn(column)); +/** + * ES|QL validation public API + * It takes a query string and returns a list of messages (errors and warnings) after validate + * The astProvider is optional, but if not provided the default one from '@kbn/esql-validation-autocomplete' will be used. + * This is useful for async loading the ES|QL parser and reduce the bundle size, or to swap grammar version. + * As for the callbacks, while optional, the validation function will selectively ignore some errors types based on each callback missing. + */ +export async function validateQuery( + queryString: string, + astProvider: AstProviderFn, + options: ValidationOptions = {}, + callbacks?: ESQLCallbacks +): Promise { + const result = await validateAst(queryString, astProvider, callbacks); + // early return if we do not want to ignore errors + if (!options.ignoreOnMissingCallbacks) { + return result; + } + const finalCallbacks = callbacks || {}; + const errorTypoesToIgnore = Object.entries(ignoreErrorsMap).reduce((acc, [key, errorCodes]) => { + if ( + !(key in finalCallbacks) || + (key in finalCallbacks && finalCallbacks[key as keyof ESQLCallbacks] == null) + ) { + for (const e of errorCodes) { + acc[e] = true; } } - } - return messages; -} - -export function validateSources( - command: ESQLCommand, - sources: ESQLSource[], - references: ReferenceMaps -): ESQLMessage[] { - const messages: ESQLMessage[] = []; - - for (const source of sources) { - messages.push(...validateSource(source, command.name, references)); - } - - return messages; + return acc; + }, {} as Partial>); + const filteredErrors = result.errors + .filter((error) => { + if ('severity' in error) { + return true; + } + return !errorTypoesToIgnore[error.code as ErrorTypes]; + }) + .map((error) => + 'severity' in error + ? { + text: error.message, + code: error.code!, + type: 'error' as const, + location: { min: error.startColumn, max: error.endColumn }, + } + : error + ); + return { errors: filteredErrors, warnings: result.warnings }; } /** - * Validates the METRICS source command: - * - * METRICS [ [ BY ]] + * @private */ -const validateMetricsCommand = ( - command: ESQLAstMetricsCommand, - references: ReferenceMaps -): ESQLMessage[] => { - const messages: ESQLMessage[] = []; - const { sources, aggregates, grouping } = command; - - // METRICS ... - messages.push(...validateSources(command, sources, references)); - - // ... ... - if (aggregates && aggregates.length) { - messages.push(...validateAggregates(command, aggregates, references)); - - // ... BY - if (grouping && grouping.length) { - messages.push(...validateByGrouping(grouping, 'metrics', references, true)); - } - } - - return messages; +export const ignoreErrorsMap: Record = { + getColumnsFor: ['unknownColumn', 'wrongArgumentType', 'unsupportedFieldType'], + getSources: ['unknownIndex'], + getPolicies: ['unknownPolicy'], + getPreferences: [], + getFieldsMetadata: [], + getVariables: [], + canSuggestVariables: [], + getJoinIndices: [], }; /** - * Validates the JOIN command: - * - * JOIN ON - * JOIN index [ = alias ] ON [, [, ...]] + * This function will perform an high level validation of the + * query AST. An initial syntax validation is already performed by the parser + * while here it can detect things like function names, types correctness and potential warnings + * @param ast A valid AST data structure */ -const validateJoinCommand = ( - command: ESQLAstJoinCommand, - references: ReferenceMaps -): ESQLMessage[] => { +async function validateAst( + queryString: string, + astProvider: AstProviderFn, + callbacks?: ESQLCallbacks +): Promise { const messages: ESQLMessage[] = []; - const { commandType, args } = command; - const { joinIndices } = references; - if (!['left', 'right', 'lookup'].includes(commandType)) { - return [errors.unexpected(command.location, 'JOIN command type')]; - } + const parsingResult = await astProvider(queryString); - const target = args[0] as ESQLProperNode; - let index: ESQLSource; - let alias: ESQLIdentifier | undefined; + const { ast } = parsingResult; - if (isBinaryExpression(target)) { - if (target.name === 'as') { - alias = target.args[1] as ESQLIdentifier; - index = target.args[0] as ESQLSource; + const [sources, availableFields, availablePolicies, joinIndices] = await Promise.all([ + // retrieve the list of available sources + retrieveSources(ast, callbacks), + // retrieve available fields (if a source command has been defined) + retrieveFields(queryString, ast, callbacks), + // retrieve available policies (if an enrich command has been defined) + retrievePolicies(ast, callbacks), + // retrieve indices for join command + callbacks?.getJoinIndices?.(), + ]); - if (!isSource(index) || !isIdentifier(alias)) { - return [errors.unexpected(target.location)]; - } - } else { - return [errors.unexpected(target.location)]; - } - } else if (isSource(target)) { - index = target as ESQLSource; - } else { - return [errors.unexpected(target.location)]; + if (availablePolicies.size) { + const fieldsFromPoliciesMap = await retrievePoliciesFields(ast, availablePolicies, callbacks); + fieldsFromPoliciesMap.forEach((value, key) => availableFields.set(key, value)); } - let isIndexFound = false; - for (const { name, aliases } of joinIndices) { - if (index.name === name) { - isIndexFound = true; - break; - } - - if (aliases) { - for (const aliasName of aliases) { - if (index.name === aliasName) { - isIndexFound = true; - break; - } + if (ast.some(({ name }) => ['grok', 'dissect'].includes(name))) { + const fieldsFromGrokOrDissect = await retrieveFieldsFromStringSources( + queryString, + ast, + callbacks + ); + fieldsFromGrokOrDissect.forEach((value, key) => { + // if the field is already present, do not overwrite it + // Note: this can also overlap with some variables + if (!availableFields.has(key)) { + availableFields.set(key, value); } - } + }); } - if (!isIndexFound) { - const error = errors.invalidJoinIndex(index); - messages.push(error); + const variables = collectVariables(ast, availableFields, queryString); + // notify if the user is rewriting a column as variable with another type + messages.push(...validateFieldsShadowing(availableFields, variables)); + messages.push(...validateUnsupportedTypeFields(availableFields, ast)); - return messages; + for (const [index, command] of ast.entries()) { + const references: ReferenceMaps = { + sources, + fields: availableFields, + policies: availablePolicies, + variables, + query: queryString, + joinIndices: joinIndices?.indices || [], + }; + const commandMessages = validateCommand(command, references, ast, index); + messages.push(...commandMessages); } - return messages; -}; + return { + errors: [...parsingResult.errors, ...messages.filter(({ type }) => type === 'error')], + warnings: messages.filter(({ type }) => type === 'warning'), + }; +} function validateCommand( command: ESQLCommand, @@ -1219,13 +216,14 @@ function validateCommand( } if (commandDef.validate) { - messages.push(...commandDef.validate(command)); + messages.push(...commandDef.validate(command, references)); } switch (commandDef.name) { case 'metrics': { const metrics = command as ESQLAstMetricsCommand; - messages.push(...validateMetricsCommand(metrics, references)); + const metricsCommandErrors = validateMetricsCommand(metrics, references); + messages.push(...metricsCommandErrors); break; } case 'join': { @@ -1250,8 +248,6 @@ function validateCommand( currentCommandIndex, }) ); - } else if (isSettingItem(arg)) { - messages.push(...validateSetting(arg, commandDef.modes[0], command, references)); } else if (isOptionItem(arg)) { messages.push( ...validateOption( @@ -1269,26 +265,81 @@ function validateCommand( } } else if (isTimeIntervalItem(arg)) { messages.push( - getMessageFromId({ - messageId: 'unsupportedTypeForCommand', - values: { - command: command.name.toUpperCase(), - type: 'date_period', - value: arg.name, - }, - locations: arg.location, + getMessageFromId({ + messageId: 'unsupportedTypeForCommand', + values: { + command: command.name.toUpperCase(), + type: 'date_period', + value: arg.name, + }, + locations: arg.location, + }) + ); + } else if (isSourceItem(arg)) { + messages.push(...validateSource(arg, command.name, references)); + } + } + } + } + } + + // no need to check for mandatory options passed + // as they are already validated at syntax level + return messages; +} + +function validateOption( + option: ESQLCommandOption, + optionDef: CommandOptionsDefinition | undefined, + command: ESQLCommand, + referenceMaps: ReferenceMaps +): ESQLMessage[] { + // check if the arguments of the option are of the correct type + const messages: ESQLMessage[] = []; + if (option.incomplete || command.incomplete) { + return messages; + } + if (!optionDef) { + messages.push( + getMessageFromId({ + messageId: 'unknownOption', + values: { command: command.name.toUpperCase(), option: option.name }, + locations: option.location, + }) + ); + return messages; + } + // use dedicate validate fn if provided + if (optionDef.validate) { + const fields = METADATA_FIELDS; + messages.push(...optionDef.validate(option, command, new Set(fields))); + } + if (!optionDef.skipCommonValidation) { + option.args.forEach((arg) => { + if (!Array.isArray(arg)) { + if (!optionDef.signature.multipleParams) { + if (isColumnItem(arg)) { + messages.push(...validateColumnForCommand(arg, command.name, referenceMaps)); + } + } else { + if (isColumnItem(arg)) { + messages.push(...validateColumnForCommand(arg, command.name, referenceMaps)); + } + if (isFunctionItem(arg)) { + messages.push( + ...validateFunction({ + fn: arg, + parentCommand: command.name, + parentOption: option.name, + references: referenceMaps, }) ); - } else if (isSourceItem(arg)) { - messages.push(...validateSource(arg, command.name, references)); } } } - } + }); } - // no need to check for mandatory options passed - // as they are already validated at syntax level return messages; } @@ -1346,135 +397,133 @@ function validateUnsupportedTypeFields(fields: Map, ast: return messages; } -export const ignoreErrorsMap: Record = { - getColumnsFor: ['unknownColumn', 'wrongArgumentType', 'unsupportedFieldType'], - getSources: ['unknownIndex'], - getPolicies: ['unknownPolicy'], - getPreferences: [], - getFieldsMetadata: [], - getVariables: [], - canSuggestVariables: [], - getJoinIndices: [], -}; +export function validateSources( + command: ESQLCommand, + sources: ESQLSource[], + references: ReferenceMaps +): ESQLMessage[] { + const messages: ESQLMessage[] = []; -/** - * ES|QL validation public API - * It takes a query string and returns a list of messages (errors and warnings) after validate - * The astProvider is optional, but if not provided the default one from '@kbn/esql-validation-autocomplete' will be used. - * This is useful for async loading the ES|QL parser and reduce the bundle size, or to swap grammar version. - * As for the callbacks, while optional, the validation function will selectively ignore some errors types based on each callback missing. - */ -export async function validateQuery( - queryString: string, - astProvider: AstProviderFn, - options: ValidationOptions = {}, - callbacks?: ESQLCallbacks -): Promise { - const result = await validateAst(queryString, astProvider, callbacks); - // early return if we do not want to ignore errors - if (!options.ignoreOnMissingCallbacks) { - return result; + for (const source of sources) { + messages.push(...validateSource(source, command.name, references)); } - const finalCallbacks = callbacks || {}; - const errorTypoesToIgnore = Object.entries(ignoreErrorsMap).reduce((acc, [key, errorCodes]) => { - if ( - !(key in finalCallbacks) || - (key in finalCallbacks && finalCallbacks[key as keyof ESQLCallbacks] == null) - ) { - for (const e of errorCodes) { - acc[e] = true; - } - } - return acc; - }, {} as Partial>); - const filteredErrors = result.errors - .filter((error) => { - if ('severity' in error) { - return true; - } - return !errorTypoesToIgnore[error.code as ErrorTypes]; - }) - .map((error) => - 'severity' in error - ? { - text: error.message, - code: error.code!, - type: 'error' as const, - location: { min: error.startColumn, max: error.endColumn }, - } - : error - ); - return { errors: filteredErrors, warnings: result.warnings }; + + return messages; } -/** - * This function will perform an high level validation of the - * query AST. An initial syntax validation is already performed by the parser - * while here it can detect things like function names, types correctness and potential warnings - * @param ast A valid AST data structure - */ -async function validateAst( - queryString: string, - astProvider: AstProviderFn, - callbacks?: ESQLCallbacks -): Promise { +function validateSource( + source: ESQLSource, + commandName: string, + { sources, policies }: ReferenceMaps +) { const messages: ESQLMessage[] = []; - - const parsingResult = await astProvider(queryString); - - const { ast } = parsingResult; - - const [sources, availableFields, availablePolicies, joinIndices] = await Promise.all([ - // retrieve the list of available sources - retrieveSources(ast, callbacks), - // retrieve available fields (if a source command has been defined) - retrieveFields(queryString, ast, callbacks), - // retrieve available policies (if an enrich command has been defined) - retrievePolicies(ast, callbacks), - // retrieve indices for join command - callbacks?.getJoinIndices?.(), - ]); - - if (availablePolicies.size) { - const fieldsFromPoliciesMap = await retrievePoliciesFields(ast, availablePolicies, callbacks); - fieldsFromPoliciesMap.forEach((value, key) => availableFields.set(key, value)); + if (source.incomplete) { + return messages; } - if (ast.some(({ name }) => ['grok', 'dissect'].includes(name))) { - const fieldsFromGrokOrDissect = await retrieveFieldsFromStringSources( - queryString, - ast, - callbacks + const commandDef = getCommandDefinition(commandName); + const isWildcardAndNotSupported = + hasWildcard(source.name) && !commandDef.signature.params.some(({ wildcards }) => wildcards); + if (isWildcardAndNotSupported) { + messages.push( + getMessageFromId({ + messageId: 'wildcardNotSupportedForCommand', + values: { command: commandName.toUpperCase(), value: source.name }, + locations: source.location, + }) ); - fieldsFromGrokOrDissect.forEach((value, key) => { - // if the field is already present, do not overwrite it - // Note: this can also overlap with some variables - if (!availableFields.has(key)) { - availableFields.set(key, value); - } - }); + } else { + if (source.sourceType === 'index' && !sourceExists(source.name, sources)) { + messages.push( + getMessageFromId({ + messageId: 'unknownIndex', + values: { name: source.name }, + locations: source.location, + }) + ); + } else if (source.sourceType === 'policy' && !policies.has(source.name)) { + messages.push( + getMessageFromId({ + messageId: 'unknownPolicy', + values: { name: source.name }, + locations: source.location, + }) + ); + } } - const variables = collectVariables(ast, availableFields, queryString); - // notify if the user is rewriting a column as variable with another type - messages.push(...validateFieldsShadowing(availableFields, variables)); - messages.push(...validateUnsupportedTypeFields(availableFields, ast)); + return messages; +} - for (const [index, command] of ast.entries()) { - const references: ReferenceMaps = { - sources, - fields: availableFields, - policies: availablePolicies, - variables, - query: queryString, - joinIndices: joinIndices?.indices || [], - }; - const commandMessages = validateCommand(command, references, ast, index); - messages.push(...commandMessages); - } +export function validateColumnForCommand( + column: ESQLColumn | ESQLIdentifier, + commandName: string, + references: ReferenceMaps +): ESQLMessage[] { + const messages: ESQLMessage[] = []; + if (commandName === 'row') { + if (!references.variables.has(column.name) && !isParametrized(column)) { + messages.push(errors.unknownColumn(column)); + } + } else { + const columnName = getQuotedColumnName(column); + if (getColumnExists(column, references)) { + const commandDef = getCommandDefinition(commandName); + const columnParamsWithInnerTypes = commandDef.signature.params.filter( + ({ type, innerTypes }) => type === 'column' && innerTypes + ); + // this should be guaranteed by the columnCheck above + const columnRef = getColumnForASTNode(column, references)!; - return { - errors: [...parsingResult.errors, ...messages.filter(({ type }) => type === 'error')], - warnings: messages.filter(({ type }) => type === 'warning'), - }; + if (columnParamsWithInnerTypes.length) { + const hasSomeWrongInnerTypes = columnParamsWithInnerTypes.every( + ({ innerTypes }) => + innerTypes && + !innerTypes.includes('any') && + !innerTypes.some((type) => compareTypesWithLiterals(type, columnRef.type)) + ); + if (hasSomeWrongInnerTypes) { + const supportedTypes: string[] = columnParamsWithInnerTypes + .map(({ innerTypes }) => innerTypes) + .flat() + .filter((type) => type !== undefined) as string[]; + + messages.push( + getMessageFromId({ + messageId: 'unsupportedColumnTypeForCommand', + values: { + command: commandName.toUpperCase(), + type: supportedTypes.join(', '), + typeCount: supportedTypes.length, + givenType: columnRef.type, + column: columnName, + }, + locations: column.location, + }) + ); + } + } + if ( + hasWildcard(columnName) && + !isVariable(columnRef) && + !commandDef.signature.params.some(({ type, wildcards }) => type === 'column' && wildcards) + ) { + messages.push( + getMessageFromId({ + messageId: 'wildcardNotSupportedForCommand', + values: { + command: commandName.toUpperCase(), + value: columnName, + }, + locations: column.location, + }) + ); + } + } else { + if (column.name) { + messages.push(errors.unknownColumn(column)); + } + } + } + return messages; } diff --git a/src/platform/packages/shared/kbn-monaco/src/esql/lib/hover/hover.test.ts b/src/platform/packages/shared/kbn-monaco/src/esql/lib/hover/hover.test.ts index 35a62303339d0..a0c5e6b536dfa 100644 --- a/src/platform/packages/shared/kbn-monaco/src/esql/lib/hover/hover.test.ts +++ b/src/platform/packages/shared/kbn-monaco/src/esql/lib/hover/hover.test.ts @@ -11,11 +11,12 @@ import { monaco } from '../../../monaco_imports'; import { getHoverItem } from './hover'; import { getAstAndSyntaxErrors } from '@kbn/esql-ast'; import { - ENRICH_MODES, ESQLRealField, getFunctionDefinition, getFunctionSignatures, } from '@kbn/esql-validation-autocomplete'; +import { modeDescription } from '@kbn/esql-validation-autocomplete/src/autocomplete/commands/enrich/util'; +import { ENRICH_MODES } from '@kbn/esql-validation-autocomplete/src/definitions/commands_helpers'; import { FieldType } from '@kbn/esql-validation-autocomplete/src/definitions/types'; const types: FieldType[] = ['keyword', 'double', 'date', 'boolean', 'ip']; @@ -187,12 +188,11 @@ describe('hover', () => { testSuggestions(`from a | enrich policy on b `, 'non-policy', createPolicyContent); describe('ccq mode', () => { - for (const mode of ENRICH_MODES.values) { - testSuggestions( - `from a | enrich ${ENRICH_MODES.prefix || ''}${mode.name}:policy`, - `${ENRICH_MODES.prefix || ''}${mode.name}`, - () => [ENRICH_MODES.description, `**${mode.name}**: ${mode.description}`] - ); + for (const mode of ENRICH_MODES) { + testSuggestions(`from a | enrich _${mode.name}:policy`, `_${mode.name}`, () => [ + modeDescription, + `**${mode.name}**: ${mode.description}`, + ]); } }); }); diff --git a/src/platform/packages/shared/kbn-monaco/src/esql/lib/hover/hover.ts b/src/platform/packages/shared/kbn-monaco/src/esql/lib/hover/hover.ts index 871ab62227577..bdd9cf72fe6b0 100644 --- a/src/platform/packages/shared/kbn-monaco/src/esql/lib/hover/hover.ts +++ b/src/platform/packages/shared/kbn-monaco/src/esql/lib/hover/hover.ts @@ -14,8 +14,6 @@ import { getFunctionDefinition, getFunctionSignatures, isSourceItem, - isSettingItem, - getCommandDefinition, type ESQLCallbacks, getPolicyHelper, collectVariables, @@ -34,6 +32,8 @@ import { TIME_SYSTEM_PARAMS, } from '@kbn/esql-validation-autocomplete/src/autocomplete/factories'; import { isESQLFunction, isESQLNamedParamLiteral } from '@kbn/esql-ast/src/types'; +import { ENRICH_MODES } from '@kbn/esql-validation-autocomplete/src/definitions/commands_helpers'; +import { modeDescription } from '@kbn/esql-validation-autocomplete/src/autocomplete/commands/enrich/util'; import { monacoPositionToOffset } from '../shared/utils'; import { monaco } from '../../../monaco_imports'; import { getVariablesHoverContent } from './helpers'; @@ -217,22 +217,16 @@ export async function getHoverItem( ); } } - if (isSettingItem(astContext.node)) { - const commandDef = getCommandDefinition(astContext.command.name); - const settingDef = commandDef?.modes.find(({ values }) => - values.some(({ name }) => name === astContext.node!.name) + if (astContext.node.type === 'mode') { + const mode = ENRICH_MODES.find(({ name }) => name === astContext.node!.name)!; + hoverContent.contents.push( + ...[ + { value: modeDescription }, + { + value: `**${mode.name}**: ${mode.description}`, + }, + ] ); - if (settingDef) { - const mode = settingDef.values.find(({ name }) => name === astContext.node!.name)!; - hoverContent.contents.push( - ...[ - { value: settingDef.description }, - { - value: `**${mode.name}**: ${mode.description}`, - }, - ] - ); - } } } } diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 9f65825fa41c2..2421e9e1de039 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -5653,7 +5653,6 @@ "kbn-esql-validation-autocomplete.esql.definitions.cbrt": "Renvoie la racine cubique d'un nombre. La valeur de renvoi est toujours un double, quelle que soit la valeur numérique de l'entrée. La racine cubique de l’infini est nulle.", "kbn-esql-validation-autocomplete.esql.definitions.ccqAnyDoc": "L'enrichissement a lieu sur n'importe quel cluster", "kbn-esql-validation-autocomplete.esql.definitions.ccqCoordinatorDoc": "L'enrichissement a lieu sur le cluster de coordination qui reçoit une requête ES|QL", - "kbn-esql-validation-autocomplete.esql.definitions.ccqModeDoc": "Mode de requête inter-clusters", "kbn-esql-validation-autocomplete.esql.definitions.ccqRemoteDoc": "L'enrichissement a lieu sur le cluster qui héberge l'index cible.", "kbn-esql-validation-autocomplete.esql.definitions.ceil": "Arrondir un nombre à l'entier supérieur.", "kbn-esql-validation-autocomplete.esql.definitions.cidr_match": "Renvoie true si l'IP fournie est contenue dans l'un des blocs CIDR fournis.", @@ -5863,7 +5862,6 @@ "kbn-esql-validation-autocomplete.esql.validation.unsupportedFunctionForCommand": "{command} n'est pas compatible avec la fonction {name}", "kbn-esql-validation-autocomplete.esql.validation.unsupportedFunctionforCommandOption": "{command} {option} n'est pas compatible avec la fonction {name}", "kbn-esql-validation-autocomplete.esql.validation.unsupportedLiteralOption": "Option non valide [{value}] pour {name}. Options prises en charge : [{supportedOptions}].", - "kbn-esql-validation-autocomplete.esql.validation.unsupportedSetting": "Paramètre non pris en charge [{setting}], [{expected}] attendu", "kbn-esql-validation-autocomplete.esql.validation.unsupportedSettingValue": "Valeur [{value}] non reconnue pour {command}, le mode doit être l'un de [{expected}]", "kbn-esql-validation-autocomplete.esql.validation.unsupportedTypeForCommand": "{command} n'est pas compatible avec [{type}] dans l'expression [{value}]", "kbn-esql-validation-autocomplete.esql.validation.wildcardNotSupportedForCommand": "L'utilisation de caractères génériques (*) dans {command} n'est pas autorisée [{value}]", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 5fa94f32146a8..7426e0adb7aa9 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -5648,7 +5648,6 @@ "kbn-esql-validation-autocomplete.esql.definitions.cbrt": "数値の立方根を返します。入力は任意の数値で、戻り値は常にdoubleです。無限大の立方根はnullです。", "kbn-esql-validation-autocomplete.esql.definitions.ccqAnyDoc": "エンリッチは任意のクラスターで発生します", "kbn-esql-validation-autocomplete.esql.definitions.ccqCoordinatorDoc": "エンリッチは、ES|QLを受信する調整クラスターで実行されます", - "kbn-esql-validation-autocomplete.esql.definitions.ccqModeDoc": "クラスター横断クエリーモード", "kbn-esql-validation-autocomplete.esql.definitions.ccqRemoteDoc": "エンリッチはターゲットインデックスをホスティングするクラスターで発生します。", "kbn-esql-validation-autocomplete.esql.definitions.ceil": "最も近い整数に数値を切り上げます。", "kbn-esql-validation-autocomplete.esql.definitions.cidr_match": "指定されたIPが指定されたCIDRブロックのいずれかに含まれていればtrueを返します。", @@ -5857,7 +5856,6 @@ "kbn-esql-validation-autocomplete.esql.validation.unsupportedColumnTypeForCommand": "{command}は{type} {typeCount, plural, other {型}}の値のみをサポートしていますが、[{givenType}]型の[{column}]が見つかりました", "kbn-esql-validation-autocomplete.esql.validation.unsupportedFieldType": "フィールド[{field}]を取得できません。サポートされていないか、インデックス化されていません。NULLが返されます", "kbn-esql-validation-autocomplete.esql.validation.unsupportedLiteralOption": "{name}の無効なオプション [{value}]です。サポートされているオプション:[{supportedOptions}]。", - "kbn-esql-validation-autocomplete.esql.validation.unsupportedSetting": "サポートされていない設定[{setting}]です。[{expected}]でなければなりません", "kbn-esql-validation-autocomplete.esql.validation.unsupportedSettingValue": "{command}の認識されていない値[{value}]です。モードは[{expected}]のいずれかでなければなりません", "kbn-esql-validation-autocomplete.esql.validation.unsupportedTypeForCommand": "{command}は式[{value}]で[{type}]をサポートしていません", "kbn-esql-validation-autocomplete.esql.validation.wildcardNotSupportedForCommand": "{command}でのワイルドカード(*)の使用は許可されていません[{value}]", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 728bebcba2d2b..9ac8c7558a05f 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -5658,7 +5658,6 @@ "kbn-esql-validation-autocomplete.esql.definitions.cbrt": "返回数字的立方根。输入可以为任何数字值,返回值始终为双精度值。无穷大的立方根为 null。", "kbn-esql-validation-autocomplete.esql.definitions.ccqAnyDoc": "扩充在任何集群上发生", "kbn-esql-validation-autocomplete.esql.definitions.ccqCoordinatorDoc": "扩充在接收 ES|QL 的协调集群上发生", - "kbn-esql-validation-autocomplete.esql.definitions.ccqModeDoc": "跨集群查询模式", "kbn-esql-validation-autocomplete.esql.definitions.ccqRemoteDoc": "扩充在托管目标索引的集群上发生。", "kbn-esql-validation-autocomplete.esql.definitions.ceil": "将数字四舍五入为最近的整数。", "kbn-esql-validation-autocomplete.esql.definitions.cidr_match": "如果提供的 IP 包含在所提供的其中一个 CIDR 块中,则返回 true。", @@ -5869,7 +5868,6 @@ "kbn-esql-validation-autocomplete.esql.validation.unsupportedFunctionForCommand": "{command} 不支持函数 {name}", "kbn-esql-validation-autocomplete.esql.validation.unsupportedFunctionforCommandOption": "{command} {option} 不支持函数 {name}", "kbn-esql-validation-autocomplete.esql.validation.unsupportedLiteralOption": "{name} 的选项 [{value}] 无效。支持的选项:[{supportedOptions}]。", - "kbn-esql-validation-autocomplete.esql.validation.unsupportedSetting": "不支持设置 [{setting}],应为 [{expected}]", "kbn-esql-validation-autocomplete.esql.validation.unsupportedSettingValue": "无法识别 {command} 的值 [{value}],模式需要为 [{expected}] 之一", "kbn-esql-validation-autocomplete.esql.validation.unsupportedTypeForCommand": "{command} 不支持表达式 [{value}] 中的 [{type}]", "kbn-esql-validation-autocomplete.esql.validation.wildcardNotSupportedForCommand": "不允许在 {command} 中使用通配符 (*) [{value}]",