[8.x] [ES|QL] Restructure validation code, remove command settings (#215056) (#215491)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ES|QL] Restructure validation code, remove command settings
(#215056)](https://github.com/elastic/kibana/pull/215056)

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

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

<!--BACKPORT [{"author":{"name":"Drew
Tate","email":"drew.tate@elastic.co"},"sourceCommit":{"committedDate":"2025-03-20T13:16:34Z","message":"[ES|QL]
Restructure validation code, remove command settings (#215056)\n\n##
Summary\n\nFollow on to several recent efforts
including\nhttps://github.com/elastic/kibana/issues/195418
and\nhttps://github.com/elastic/kibana/pull/213325\n\nThis PR\n-
reorganizes validation code to make dependencies clearer and make
it\nless overwhelming... it's not perfect but it's better\n- removes the
deprecated notion of a command \"setting\" which only ever\napplied to
`ENRICH`.\n\nNo regression in `ENRICH` mode validation:\n<img
width=\"874\" alt=\"Screenshot 2025-03-18 at 1 04
46 PM\"\nsrc=\"https://github.com/user-attachments/assets/e6639d8a-d129-440f-ac30-64a2ef6ab65c\"\n/>\n\nOr
hover\n<img width=\"419\" alt=\"Screenshot 2025-03-18 at 7 43
04 PM\"\nsrc=\"https://github.com/user-attachments/assets/8f9c020c-dcfd-42dc-8e14-4b1c4311457b\"\n/>\n\n\n\n\n\n###
Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"9073b194072cee0ef5290982c05cfdb84662c673","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["technical
debt","release_note:skip","Feature:ES|QL","Team:ESQL","backport:version","v9.1.0","v8.19.0"],"title":"[ES|QL]
Restructure validation code, remove command
settings","number":215056,"url":"https://github.com/elastic/kibana/pull/215056","mergeCommit":{"message":"[ES|QL]
Restructure validation code, remove command settings (#215056)\n\n##
Summary\n\nFollow on to several recent efforts
including\nhttps://github.com/elastic/kibana/issues/195418
and\nhttps://github.com/elastic/kibana/pull/213325\n\nThis PR\n-
reorganizes validation code to make dependencies clearer and make
it\nless overwhelming... it's not perfect but it's better\n- removes the
deprecated notion of a command \"setting\" which only ever\napplied to
`ENRICH`.\n\nNo regression in `ENRICH` mode validation:\n<img
width=\"874\" alt=\"Screenshot 2025-03-18 at 1 04
46 PM\"\nsrc=\"https://github.com/user-attachments/assets/e6639d8a-d129-440f-ac30-64a2ef6ab65c\"\n/>\n\nOr
hover\n<img width=\"419\" alt=\"Screenshot 2025-03-18 at 7 43
04 PM\"\nsrc=\"https://github.com/user-attachments/assets/8f9c020c-dcfd-42dc-8e14-4b1c4311457b\"\n/>\n\n\n\n\n\n###
Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"9073b194072cee0ef5290982c05cfdb84662c673"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/215056","number":215056,"mergeCommit":{"message":"[ES|QL]
Restructure validation code, remove command settings (#215056)\n\n##
Summary\n\nFollow on to several recent efforts
including\nhttps://github.com/elastic/kibana/issues/195418
and\nhttps://github.com/elastic/kibana/pull/213325\n\nThis PR\n-
reorganizes validation code to make dependencies clearer and make
it\nless overwhelming... it's not perfect but it's better\n- removes the
deprecated notion of a command \"setting\" which only ever\napplied to
`ENRICH`.\n\nNo regression in `ENRICH` mode validation:\n<img
width=\"874\" alt=\"Screenshot 2025-03-18 at 1 04
46 PM\"\nsrc=\"https://github.com/user-attachments/assets/e6639d8a-d129-440f-ac30-64a2ef6ab65c\"\n/>\n\nOr
hover\n<img width=\"419\" alt=\"Screenshot 2025-03-18 at 7 43
04 PM\"\nsrc=\"https://github.com/user-attachments/assets/8f9c020c-dcfd-42dc-8e14-4b1c4311457b\"\n/>\n\n\n\n\n\n###
Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"9073b194072cee0ef5290982c05cfdb84662c673"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Drew Tate <drew.tate@elastic.co>
This commit is contained in:
Kibana Machine 2025-03-21 16:17:46 +01:00 committed by GitHub
parent 04433e758e
commit 7b1eefddd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1453 additions and 1469 deletions

View file

@ -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';

View file

@ -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,
})

View file

@ -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<CommandDefinition<any>> = [
},
suggest: suggestForRow,
options: [],
modes: [],
},
{
name: 'from',
@ -157,7 +164,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
}),
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<CommandDefinition<any>> = [
}),
examples: ['SHOW INFO'],
options: [],
modes: [],
signature: {
multipleParams: false,
params: [{ name: 'functions', type: 'function' }],
@ -200,7 +205,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
'metrics src1, src2 agg1, agg2 by field1, field2',
],
options: [],
modes: [],
signature: {
multipleParams: true,
params: [
@ -222,7 +226,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
params: [{ name: 'expression', type: 'function', optional: true }],
},
options: [byOption],
modes: [],
validate: statsValidator,
suggest: suggestForStats,
},
@ -242,7 +245,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
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<CommandDefinition<any>> = [
params: [{ name: 'expression', type: 'any' }],
},
options: [],
modes: [],
suggest: suggestForEval,
},
{
@ -279,7 +280,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
params: [{ name: 'renameClause', type: 'column' }],
},
options: [asOption],
modes: [],
suggest: suggestForRename,
},
{
@ -294,7 +294,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
params: [{ name: 'size', type: 'integer', constantOnly: true }],
},
options: [],
modes: [],
suggest: suggestForLimit,
},
{
@ -306,7 +305,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
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<CommandDefinition<any>> = [
}),
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<CommandDefinition<any>> = [
'… | sort a - abs(b)',
],
options: [],
modes: [],
signature: {
multipleParams: true,
params: [{ name: 'expression', type: 'any' }],
@ -396,7 +392,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
params: [{ name: 'expression', type: 'boolean' }],
},
options: [],
modes: [],
suggest: suggestForWhere,
},
{
@ -407,7 +402,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
}),
examples: ['… | DISSECT a "%{b} %{c}" APPEND_SEPARATOR = ":"'],
options: [appendSeparatorOption],
modes: [],
signature: {
multipleParams: false,
params: [
@ -425,7 +419,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
}),
examples: ['… | GROK a "%{IP:b} %{NUMBER:c}"'],
options: [],
modes: [],
signature: {
multipleParams: false,
params: [
@ -442,7 +435,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
}),
examples: ['row a=[1,2,3] | mv_expand a'],
options: [],
modes: [],
preview: true,
signature: {
multipleParams: false,
@ -462,12 +454,37 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
'… | 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<CommandDefinition<any>> = [
hidden: true,
examples: [],
options: [],
modes: [],
signature: {
params: [],
multipleParams: false,
@ -527,7 +543,6 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
// '… | <LEFT | RIGHT | LOOKUP> JOIN index AS alias ON index.field = index2.field',
// '… | <LEFT | RIGHT | LOOKUP> JOIN index AS alias ON index.field = index2.field, index.field2 = index2.field2',
],
modes: [],
signature: {
multipleParams: true,
params: [{ name: 'index', type: 'source', wildcards: true }],

View file

@ -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.',
}),
},
];

View file

@ -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.',
}
),
},
],
};

View file

@ -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<CommandName extends string = string>
) => ESQLMessage[];
}
export interface CommandModeDefinition {
name: string;
description: string;
values: Array<{ name: string; description: string }>;
prefix?: string;
}
export interface CommandDefinition<CommandName extends string>
extends CommandBaseDefinition<CommandName> {
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<CommandName>, references: ReferenceMaps) => ESQLMessage[];
suggest: CommandSuggestFunction<CommandName>;
/** @deprecated this property will disappear in the future */
modes: CommandModeDefinition[];
/** @deprecated this property will disappear in the future */
options: CommandOptionsDefinition[];
}

View file

@ -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';
}

View file

@ -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:
*
* <LEFT | RIGHT | LOOKUP> JOIN <target> ON <conditions>
* <LEFT | RIGHT | LOOKUP> JOIN index [ = alias ] ON <condition> [, <condition> [, ...]]
*/
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;
};

View file

@ -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 <sources> [ <aggregates> [ BY <grouping> ]]
*/
export const validate = (
command: ESQLAstMetricsCommand,
references: ReferenceMaps
): ESQLMessage[] => {
const messages: ESQLMessage[] = [];
const { sources, aggregates, grouping } = command;
// METRICS <sources> ...
messages.push(...validateSources(command, sources, references));
// ... <aggregates> ...
if (aggregates && aggregates.length) {
messages.push(...validateAggregates(command, aggregates, references));
// ... BY <grouping>
if (grouping && grouping.length) {
messages.push(...validateByGrouping(grouping, 'metrics', references, true));
}
}
return messages;
};
/**
* Validates aggregates fields: `... <aggregates> ...`.
*/
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 <grouping>`.
*/
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;
}
}
};

View file

@ -294,21 +294,7 @@ function getMessageAndTypeFromId<K extends ErrorTypes>({
),
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',

View file

@ -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": []
},

View file

@ -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<string>,
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<string, FunctionValidator> = {
match: validateMatchFunction,
qstr: validateQSTRFunction,
};
// #endregion

View file

@ -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 };
};

View file

@ -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]',

View file

@ -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}`,
]);
}
});
});

View file

@ -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}`,
},
]
);
}
}
}
}

View file

@ -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 linfini 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}]",

View file

@ -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}]",

View file

@ -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}]",