From cd2d6ca16e6cfd1faae10212a553610a454d75f4 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Mon, 3 Mar 2025 13:24:23 -0700 Subject: [PATCH] [ES|QL] Separate `ENRICH` autocomplete routine (#211657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Part of https://github.com/elastic/kibana/issues/195418 Gives `ENRICH` autocomplete logic its own home 🏡 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### Identify risks - [ ] As with any refactor, there's a possibility this will introduce a regression in the behavior of commands. However, all automated tests are passing and I have tested the behavior manually and can detect no regression. --------- Co-authored-by: Stratoula Kalafateli (cherry picked from commit f2a91732d8f8d20a22bf761bfe9ec85e8a8e1c0c) --- .../autocomplete.command.enrich.test.ts | 142 +++++++++++ .../src/autocomplete/autocomplete.test.ts | 90 +------ .../src/autocomplete/autocomplete.ts | 222 ++---------------- .../src/autocomplete/commands/enrich/index.ts | 165 +++++++++++++ .../src/autocomplete/commands/enrich/util.ts | 148 ++++++++++++ .../src/autocomplete/commands/join/index.ts | 1 - .../src/autocomplete/factories.ts | 66 +----- .../src/definitions/commands.ts | 7 +- .../src/definitions/options.ts | 6 + .../src/definitions/types.ts | 29 ++- .../src/shared/context.ts | 25 +- .../src/shared/helpers.ts | 23 +- .../src/validation/validation.ts | 1 + 13 files changed, 549 insertions(+), 376 deletions(-) create mode 100644 src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.enrich.test.ts create mode 100644 src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/enrich/index.ts create mode 100644 src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/enrich/util.ts diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.enrich.test.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.enrich.test.ts new file mode 100644 index 0000000000000..847c7dbc2f1a8 --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.enrich.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { camelCase } from 'lodash'; +import { getFieldNamesByType, getPolicyFields, policies, setup } from './helpers'; + +describe('autocomplete.suggest', () => { + describe('ENRICH', () => { + const modes = ['any', 'coordinator', 'remote']; + const expectedPolicyNameSuggestions = policies + .map(({ name, suggestedAs }) => suggestedAs || name) + .map((name) => `${name} `); + + let assertSuggestions: Awaited>['assertSuggestions']; + beforeEach(async () => { + const setupResult = await setup(); + assertSuggestions = setupResult.assertSuggestions; + }); + + it('suggests policy names', async () => { + await assertSuggestions(`from a | enrich /`, expectedPolicyNameSuggestions); + await assertSuggestions(`from a | enrich po/`, expectedPolicyNameSuggestions); + }); + + test('modes', async () => { + await assertSuggestions( + `from a | enrich _/`, + modes.map((mode) => `_${mode}:$0`), + { triggerCharacter: '_' } + ); + await assertSuggestions('from a | enrich _any: /', []); + for (const mode of modes) { + await assertSuggestions(`from a | enrich _${mode}:/`, expectedPolicyNameSuggestions, { + triggerCharacter: ':', + }); + + await assertSuggestions( + `from a | enrich _${mode.toUpperCase()}:/`, + expectedPolicyNameSuggestions, + { triggerCharacter: ':' } + ); + + await assertSuggestions( + `from a | enrich _${camelCase(mode)}:/`, + expectedPolicyNameSuggestions, + { triggerCharacter: ':' } + ); + } + }); + + it('suggests ON and WITH after policy name', async () => { + await assertSuggestions(`from a | enrich policy /`, ['ON ', 'WITH ', '| ']); + await assertSuggestions(`from a | enrich policy O/`, ['ON ', 'WITH ', '| ']); + }); + + it('suggests fields after ON', async () => { + await assertSuggestions( + `from a | enrich policy on /`, + getFieldNamesByType('any').map((v) => `${v} `) + ); + await assertSuggestions( + `from a | enrich policy on fi/`, + getFieldNamesByType('any').map((v) => `${v} `) + ); + }); + + describe('WITH', () => { + it('suggests WITH after ON ', async () => { + await assertSuggestions(`from a | enrich policy on field /`, ['WITH ', '| ']); + }); + + it('suggests fields for new WITH clauses', async () => { + await assertSuggestions(`from a | enrich policy on field with /`, [ + 'var0 = ', + ...getPolicyFields('policy').map((name) => ({ + text: name, + // Makes sure the suggestion menu isn't opened when a field is accepted + command: undefined, + })), + ]); + await assertSuggestions(`from a | enrich policy on field with fi/`, [ + 'var0 = ', + ...getPolicyFields('policy'), + ]); + await assertSuggestions(`from a | enrich policy on b with var0 = otherField, /`, [ + 'var1 = ', + ...getPolicyFields('policy'), + ]); + await assertSuggestions(`from a | enrich policy on b with var0 = otherField, fi/`, [ + 'var1 = ', + ...getPolicyFields('policy'), + ]); + }); + + test('waits to suggest fields until space', async () => { + await assertSuggestions(`from a | enrich policy on b with var0 = otherField,/`, []); + await assertSuggestions(`from a | enrich policy on b with/`, []); + }); + + test('after first word', async () => { + // not a recognized column name + await assertSuggestions(`from a | enrich policy on b with var0 /`, ['= $0']); + // recognized column name + await assertSuggestions(`from a | enrich policy on b with otherField /`, [',', '| ']); + }); + + test('suggests enrich fields after open assignment', async () => { + await assertSuggestions(`from a | enrich policy on b with var0 = /`, [ + ...getPolicyFields('policy'), + ]); + await assertSuggestions(`from a | enrich policy on b with var0 = fi/`, [ + ...getPolicyFields('policy'), + ]); + await assertSuggestions(`from a | enrich policy on b with var0 = otherField, var1 = /`, [ + ...getPolicyFields('policy'), + ]); + }); + + test('after complete clause', async () => { + // works with escaped field names + await assertSuggestions(`from a | enrich policy on b with var0 = \`otherField\` /`, [ + ',', + '| ', + ]); + await assertSuggestions(`from a | enrich policy on b with var0=otherField /`, [',', '| ']); + await assertSuggestions(`from a | enrich policy on b with otherField /`, [',', '| ']); + }); + + test('after user-defined column name', async () => { + await assertSuggestions(`from a | enrich policy on b with var0 = otherField, var1 /`, [ + '= $0', + ]); + }); + }); + }); +}); diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index 18443d14218c3..beaed77d9612b 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -12,7 +12,6 @@ import { scalarFunctionDefinitions } from '../definitions/generated/scalar_funct import { timeUnitsToSuggest } from '../definitions/literals'; import { commandDefinitions as unmodifiedCommandDefinitions } from '../definitions/commands'; import { getSafeInsertText, TIME_SYSTEM_PARAMS, TRIGGER_SUGGESTION_COMMAND } from './factories'; -import { camelCase } from 'lodash'; import { getAstAndSyntaxErrors } from '@kbn/esql-ast'; import { policies, @@ -206,84 +205,6 @@ describe('autocomplete', () => { }); } - describe('enrich', () => { - const modes = ['any', 'coordinator', 'remote']; - const expectedPolicyNameSuggestions = policies - .map(({ name, suggestedAs }) => suggestedAs || name) - .map((name) => `${name} `); - for (const prevCommand of [ - '', - // '| enrich other-policy ', - // '| enrich other-policy on b ', - // '| enrich other-policy with c ', - ]) { - testSuggestions(`from a ${prevCommand}| enrich /`, expectedPolicyNameSuggestions); - testSuggestions( - `from a ${prevCommand}| enrich _/`, - modes.map((mode) => `_${mode}:$0`), - '_' - ); - for (const mode of modes) { - testSuggestions( - `from a ${prevCommand}| enrich _${mode}:/`, - expectedPolicyNameSuggestions, - ':' - ); - testSuggestions( - `from a ${prevCommand}| enrich _${mode.toUpperCase()}:/`, - expectedPolicyNameSuggestions, - ':' - ); - testSuggestions( - `from a ${prevCommand}| enrich _${camelCase(mode)}:/`, - expectedPolicyNameSuggestions, - ':' - ); - } - testSuggestions(`from a ${prevCommand}| enrich policy /`, ['ON $0', 'WITH $0', '| ']); - testSuggestions( - `from a ${prevCommand}| enrich policy on /`, - getFieldNamesByType('any').map((v) => `${v} `) - ); - testSuggestions(`from a ${prevCommand}| enrich policy on b /`, ['WITH $0', '| ']); - testSuggestions( - `from a ${prevCommand}| enrich policy on b with /`, - ['var0 = ', ...getPolicyFields('policy')], - ' ' - ); - testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 /`, ['= $0', ',', '| ']); - testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = /`, [ - ...getPolicyFields('policy'), - ]); - testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = keywordField /`, [ - ',', - '| ', - ]); - testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = keywordField, /`, [ - 'var1 = ', - ...getPolicyFields('policy'), - ]); - testSuggestions( - `from a ${prevCommand}| enrich policy on b with var0 = keywordField, var1 /`, - ['= $0', ',', '| '] - ); - testSuggestions( - `from a ${prevCommand}| enrich policy on b with var0 = keywordField, var1 = /`, - [...getPolicyFields('policy')] - ); - testSuggestions( - `from a ${prevCommand}| enrich policy with /`, - ['var0 = ', ...getPolicyFields('policy')], - ' ' - ); - testSuggestions(`from a ${prevCommand}| enrich policy with keywordField /`, [ - '= $0', - ',', - '| ', - ]); - } - }); - // @TODO: get updated eval block from main describe('values suggestions', () => { testSuggestions('FROM "i/"', []); @@ -449,7 +370,7 @@ describe('autocomplete', () => { ); // ENRICH policy ON - testSuggestions('FROM index1 | ENRICH policy O/', ['ON $0', 'WITH $0', '| ']); + testSuggestions('FROM index1 | ENRICH policy O/', ['ON ', 'WITH ', '| ']); // ENRICH policy ON field testSuggestions( @@ -816,10 +737,7 @@ describe('autocomplete', () => { .map(attachTriggerCommand) .map((s) => ({ ...s, rangeToReplace: { start: 17, end: 20 } })) ); - testSuggestions( - 'FROM a | ENRICH policy /', - ['ON $0', 'WITH $0', '| '].map(attachTriggerCommand) - ); + testSuggestions('FROM a | ENRICH policy /', ['ON ', 'WITH ', '| '].map(attachTriggerCommand)); testSuggestions( 'FROM a | ENRICH policy ON /', @@ -829,12 +747,12 @@ describe('autocomplete', () => { ); testSuggestions( 'FROM a | ENRICH policy ON @timestamp /', - ['WITH $0', '| '].map(attachTriggerCommand) + ['WITH ', '| '].map(attachTriggerCommand) ); // nothing fancy with this field list testSuggestions('FROM a | ENRICH policy ON @timestamp WITH /', [ 'var0 = ', - ...getPolicyFields('policy').map((name) => ({ text: name, command: undefined })), + ...getPolicyFields('policy'), ]); describe('replacement range', () => { testSuggestions('FROM a | ENRICH policy ON @timestamp WITH othe/', [ diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index f628458f25e47..4f74b0c0c5a72 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -24,7 +24,6 @@ import { getCommandDefinition, getCommandOption, getFunctionDefinition, - getLastNonWhitespaceChar, isAssignment, isAssignmentComplete, isColumnItem, @@ -34,23 +33,19 @@ import { isOptionItem, isRestartingExpression, isSourceCommand, - isSettingItem, - isSourceItem, isTimeIntervalItem, getAllFunctions, isSingleItem, nonNullable, getColumnExists, findPreviousWord, - noCaseCompare, correctQuerySyntax, getColumnByName, - findFinalWord, getAllCommands, getExpressionType, } from '../shared/helpers'; import { collectVariables, excludeVariablesFromCurrentCommand } from '../shared/variables'; -import type { ESQLPolicy, ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; +import type { ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; import { allStarConstant, commaCompleteItem, @@ -59,17 +54,13 @@ import { pipeCompleteItem, } from './complete_items'; import { - buildFieldsDefinitions, buildPoliciesDefinitions, getNewVariableSuggestion, - buildNoPoliciesAvailableDefinition, getFunctionSuggestions, - buildMatchingFieldsDefinition, getCompatibleLiterals, buildConstantsDefinitions, buildVariablesDefinitions, buildOptionDefinition, - buildSettingDefinitions, buildValueDefinitions, getDateLiterals, buildFieldsDefinitionsWithMetadata, @@ -99,37 +90,17 @@ import { getSuggestionsToRightOfOperatorExpression, checkFunctionInvocationComplete, } from './helper'; -import { FunctionParameter, isParameterType, FunctionDefinitionTypes } from '../definitions/types'; +import { + FunctionParameter, + isParameterType, + FunctionDefinitionTypes, + GetPolicyMetadataFn, +} from '../definitions/types'; import { comparisonFunctions } from '../definitions/all_operators'; import { getRecommendedQueriesSuggestions } from './recommended_queries/suggestions'; type GetFieldsMapFn = () => Promise>; type GetPoliciesFn = () => Promise; -type GetPolicyMetadataFn = (name: string) => Promise; - -function hasSameArgBothSides(assignFn: ESQLFunction) { - if (assignFn.name === '=' && isColumnItem(assignFn.args[0]) && assignFn.args[1]) { - const assignValue = assignFn.args[1]; - if (Array.isArray(assignValue) && isColumnItem(assignValue[0])) { - return assignFn.args[0].name === assignValue[0].name; - } - } -} - -function appendEnrichFields( - fieldsMap: Map, - policyMetadata: ESQLPolicy | undefined -) { - if (!policyMetadata) { - return fieldsMap; - } - // @TODO: improve this - const newMap: Map = new Map(fieldsMap); - for (const field of policyMetadata.enrichFields) { - newMap.set(field, { name: field, type: 'double' }); - } - return newMap; -} function getFinalSuggestions({ comma }: { comma?: boolean } = { comma: true }) { const finalSuggestions = [pipeCompleteItem]; @@ -205,7 +176,8 @@ export async function suggest( astContext.type === 'expression' || (astContext.type === 'option' && astContext.command?.name === 'join') || (astContext.type === 'option' && astContext.command?.name === 'dissect') || - (astContext.type === 'option' && astContext.command?.name === 'from') + (astContext.type === 'option' && astContext.command?.name === 'from') || + (astContext.type === 'option' && astContext.command?.name === 'enrich') ) { return getSuggestionsWithinCommandExpression( innerText, @@ -215,22 +187,13 @@ export async function suggest( getFieldsByType, getFieldsMap, getPolicies, + getPolicyMetadata, getVariablesByType, resourceRetriever?.getPreferences, resourceRetriever, supportsControls ); } - if (astContext.type === 'setting') { - return getSettingArgsSuggestions( - innerText, - ast, - astContext, - getFieldsByType, - getFieldsMap, - getPolicyMetadata - ); - } if (astContext.type === 'option') { // need this wrap/unwrap thing to make TS happy const { option, ...rest } = astContext; @@ -391,6 +354,7 @@ async function getSuggestionsWithinCommandExpression( getColumnsByType: GetColumnsByTypeFn, getFieldsMap: GetFieldsMapFn, getPolicies: GetPoliciesFn, + getPolicyMetadata: GetPolicyMetadataFn, getVariablesByType?: (type: ESQLVariableType) => ESQLControlVariable[] | undefined, getPreferences?: () => Promise<{ histogramBarTarget: number } | undefined>, callbacks?: ESQLCallbacks, @@ -409,8 +373,19 @@ async function getSuggestionsWithinCommandExpression( innerText, command, getColumnsByType, + getAllColumnNames: () => Array.from(fieldsMap.keys()), columnExists: (col: string) => Boolean(getColumnByName(col, references)), - getSuggestedVariableName: () => findNewVariable(anyVariables), + getSuggestedVariableName: (extraFieldNames?: string[]) => { + if (!extraFieldNames?.length) { + return findNewVariable(anyVariables); + } + + const augmentedFieldsMap = new Map(fieldsMap); + extraFieldNames.forEach((name) => { + augmentedFieldsMap.set(name, { name, type: 'double' }); + }); + return findNewVariable(collectVariables(commands, augmentedFieldsMap, innerText)); + }, getExpressionType: (expression: ESQLAstItem | undefined) => getExpressionType(expression, references.fields, references.variables), getPreferences, @@ -423,6 +398,8 @@ async function getSuggestionsWithinCommandExpression( callbacks, getVariablesByType, supportsControls, + getPolicies, + getPolicyMetadata, }); } else { // The deprecated path. @@ -866,29 +843,10 @@ async function getExpressionSuggestionsByType( } } } - if (argDef.type === 'source') { - if (argDef.innerTypes?.includes('policy')) { - // ... | ENRICH - const policies = await getPolicies(); - const lastWord = findFinalWord(innerText); - if (lastWord !== '') { - policies.forEach((suggestion) => { - suggestions.push({ - ...suggestion, - rangeToReplace: { - start: innerText.length - lastWord.length + 1, - end: innerText.length + 1, - }, - }); - }); - } - suggestions.push(...(policies.length ? policies : [buildNoPoliciesAvailableDefinition()])); - } - } } const nonOptionArgs = command.args.filter( - (arg) => !isOptionItem(arg) && !isSettingItem(arg) && !Array.isArray(arg) && !arg.incomplete + (arg) => !isOptionItem(arg) && !Array.isArray(arg) && !arg.incomplete ); // Perform some checks on mandatory arguments const mandatoryArgsAlreadyPresent = @@ -1234,35 +1192,6 @@ async function getListArgsSuggestions( return suggestions; } -async function getSettingArgsSuggestions( - innerText: string, - commands: ESQLCommand[], - { - command, - node, - }: { - command: ESQLCommand; - node: ESQLSingleAstItem | undefined; - }, - getFieldsByType: GetColumnsByTypeFn, - getFieldsMaps: GetFieldsMapFn, - getPolicyMetadata: GetPolicyMetadataFn -) { - const suggestions = []; - - const settingDefs = getCommandDefinition(command.name).modes || []; - - if (settingDefs.length) { - const lastChar = getLastNonWhitespaceChar(innerText); - const matchingSettingDefs = settingDefs.filter(({ prefix }) => lastChar === prefix); - if (matchingSettingDefs.length) { - // COMMAND _ - suggestions.push(...matchingSettingDefs.flatMap(buildSettingDefinitions)); - } - } - return suggestions; -} - /** * @deprecated — this will disappear when https://github.com/elastic/kibana/issues/195418 is complete * because "options" will be handled in imperative command-specific routines instead of being independent. @@ -1294,105 +1223,6 @@ async function getOptionArgsSuggestions( const fieldsMap = await getFieldsMaps(); const anyVariables = collectVariables(commands, fieldsMap, innerText); - if (command.name === 'enrich') { - if (option.name === 'on') { - // if it's a new expression, suggest fields to match on - if ( - isNewExpression || - noCaseCompare(findPreviousWord(innerText), 'ON') || - (option && isAssignment(option.args[0]) && !option.args[1]) - ) { - const policyName = isSourceItem(command.args[0]) ? command.args[0].name : undefined; - if (policyName) { - const policyMetadata = await getPolicyMetadata(policyName); - if (policyMetadata) { - suggestions.push( - ...buildMatchingFieldsDefinition( - policyMetadata.matchField, - Array.from(fieldsMap.keys()) - ) - ); - } - } - } else { - // propose the with option - suggestions.push( - buildOptionDefinition(getCommandOption('with')!), - ...getFinalSuggestions({ - comma: false, - }) - ); - } - } - if (option.name === 'with') { - const policyName = isSourceItem(command.args[0]) ? command.args[0].name : undefined; - if (policyName) { - const policyMetadata = await getPolicyMetadata(policyName); - const anyEnhancedVariables = collectVariables( - commands, - appendEnrichFields(fieldsMap, policyMetadata), - innerText - ); - - if (isNewExpression || noCaseCompare(findPreviousWord(innerText), 'WITH')) { - suggestions.push(getNewVariableSuggestion(findNewVariable(anyEnhancedVariables))); - } - - // make sure to remove the marker arg from the assign fn - const assignFn = isAssignment(lastArg) - ? (removeMarkerArgFromArgsList(lastArg) as ESQLFunction) - : undefined; - - if (policyMetadata) { - if (isNewExpression || (assignFn && !isAssignmentComplete(assignFn))) { - // ... | ENRICH ... WITH a = - // ... | ENRICH ... WITH b - const fieldSuggestions = buildFieldsDefinitions(policyMetadata.enrichFields); - // in this case, we don't want to open the suggestions menu when the field is accepted - // because we're keeping the suggestions simple here for now. Could always revisit. - fieldSuggestions.forEach((s) => (s.command = undefined)); - - // attach the replacement range if needed - const lastWord = findFinalWord(innerText); - if (lastWord) { - // ENRICH ... WITH a - const rangeToReplace = { - start: innerText.length - lastWord.length + 1, - end: innerText.length + 1, - }; - fieldSuggestions.forEach((s) => (s.rangeToReplace = rangeToReplace)); - } - suggestions.push(...fieldSuggestions); - } - } - - if ( - assignFn && - hasSameArgBothSides(assignFn) && - !isNewExpression && - !isIncompleteItem(assignFn) - ) { - // ... | ENRICH ... WITH a - // effectively only assign will apper - suggestions.push( - ...pushItUpInTheList(getOperatorSuggestions({ command: command.name }), true) - ); - } - - if ( - assignFn && - (isAssignmentComplete(assignFn) || hasSameArgBothSides(assignFn)) && - !isNewExpression - ) { - suggestions.push( - ...getFinalSuggestions({ - comma: true, - }) - ); - } - } - } - } if (command.name === 'rename') { if (option.args.length < 2) { suggestions.push(...buildVariablesDefinitions([findNewVariable(anyVariables)])); diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/enrich/index.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/enrich/index.ts new file mode 100644 index 0000000000000..7e288304a1694 --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/enrich/index.ts @@ -0,0 +1,165 @@ +/* + * 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 { ESQLSource } from '@kbn/esql-ast'; +import { + findFinalWord, + findPreviousWord, + isSingleItem, + unescapeColumnName, +} from '../../../shared/helpers'; +import { CommandSuggestParams } from '../../../definitions/types'; +import type { SuggestionRawDefinition } from '../../types'; +import { + Position, + buildMatchingFieldsDefinition, + getPosition, + modeSuggestions, + noPoliciesAvailableSuggestion, + onSuggestion, + withSuggestion, +} from './util'; +import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; +import { + TRIGGER_SUGGESTION_COMMAND, + buildFieldsDefinitions, + getNewVariableSuggestion, + getOperatorSuggestions, +} from '../../factories'; + +export async function suggest({ + innerText, + command, + getPolicies, + getPolicyMetadata, + getAllColumnNames, + getSuggestedVariableName, +}: CommandSuggestParams<'enrich'>): Promise { + const pos = getPosition(innerText, command); + + const policyName = ( + command.args.find((arg) => isSingleItem(arg) && arg.type === 'source') as ESQLSource | undefined + )?.name; + + const getFieldSuggestionsForWithClause = async () => { + if (!policyName) { + return []; + } + + const policyMetadata = await getPolicyMetadata(policyName); + if (!policyMetadata) { + return []; + } + + const fieldSuggestions = buildFieldsDefinitions(policyMetadata.enrichFields, false); + + const lastWord = findFinalWord(innerText); + if (lastWord) { + // ENRICH ... WITH a + const rangeToReplace = { + start: innerText.length - lastWord.length + 1, + end: innerText.length + 1, + }; + fieldSuggestions.forEach((s) => { + s.rangeToReplace = rangeToReplace; + }); + } + + return fieldSuggestions; + }; + + switch (pos) { + case Position.MODE: + return modeSuggestions; + + case Position.POLICY: { + const policies = await getPolicies(); + const lastWord = findFinalWord(innerText); + if (lastWord !== '') { + policies.forEach((policySuggestion) => { + policySuggestion.rangeToReplace = { + start: innerText.length - lastWord.length + 1, + end: innerText.length + 1, + }; + }); + } + return policies.length ? policies : [noPoliciesAvailableSuggestion]; + } + + case Position.AFTER_POLICY: + return [onSuggestion, withSuggestion, pipeCompleteItem]; + + case Position.MATCH_FIELD: { + if (!policyName) { + return []; + } + + const policyMetadata = await getPolicyMetadata(policyName); + if (!policyMetadata) { + return []; + } + + return buildMatchingFieldsDefinition(policyMetadata.matchField, getAllColumnNames()); + } + + case Position.AFTER_ON_CLAUSE: + return [withSuggestion, pipeCompleteItem]; + + case Position.WITH_NEW_CLAUSE: { + if (!policyName) { + return []; + } + + const policyMetadata = await getPolicyMetadata(policyName); + if (!policyMetadata) { + return []; + } + + const suggestions: SuggestionRawDefinition[] = []; + suggestions.push( + getNewVariableSuggestion(getSuggestedVariableName(policyMetadata.enrichFields)) + ); + suggestions.push(...(await getFieldSuggestionsForWithClause())); + return suggestions; + } + + case Position.WITH_AFTER_FIRST_WORD: { + if (!policyName) { + return []; + } + const policyMetadata = await getPolicyMetadata(policyName); + + if (!policyMetadata) { + return []; + } + + const word = findPreviousWord(innerText); + if (policyMetadata.enrichFields.includes(unescapeColumnName(word))) { + // complete field name + return [pipeCompleteItem, { ...commaCompleteItem, command: TRIGGER_SUGGESTION_COMMAND }]; + } else { + // not recognized as a field name, assume new user-defined column name + return getOperatorSuggestions({ command: 'enrich' }); + } + } + + case Position.WITH_AFTER_ASSIGNMENT: { + const suggestions: SuggestionRawDefinition[] = []; + suggestions.push(...(await getFieldSuggestionsForWithClause())); + return suggestions; + } + + case Position.WITH_AFTER_COMPLETE_CLAUSE: { + return [pipeCompleteItem, { ...commaCompleteItem, command: TRIGGER_SUGGESTION_COMMAND }]; + } + + default: + return []; + } +} 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 new file mode 100644 index 0000000000000..b3af67aa29f3a --- /dev/null +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/enrich/util.ts @@ -0,0 +1,148 @@ +/* + * 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 { ESQLCommand } from '@kbn/esql-ast'; +import { i18n } from '@kbn/i18n'; +import { isSingleItem } from '../../../..'; +import { ENRICH_MODES } from '../../../definitions/settings'; +import { SuggestionRawDefinition } from '../../types'; +import { TRIGGER_SUGGESTION_COMMAND, getSafeInsertText } from '../../factories'; + +export enum Position { + MODE = 'mode', + POLICY = 'policy', + AFTER_POLICY = 'after_policy', + MATCH_FIELD = 'match_field', + AFTER_ON_CLAUSE = 'after_on_clause', + WITH_NEW_CLAUSE = 'with_new_clause', + WITH_AFTER_FIRST_WORD = 'with_after_first_word', + WITH_AFTER_ASSIGNMENT = 'with_after_assignment', + WITH_AFTER_COMPLETE_CLAUSE = 'with_after_complete_clause', +} + +export const getPosition = ( + innerText: string, + command: ESQLCommand<'enrich'> +): Position | undefined => { + if (command.args.length < 2) { + if (innerText.match(/_[^:\s]*$/)) { + return Position.MODE; + } + if (innerText.match(/(:|ENRICH\s+)\S*$/i)) { + return Position.POLICY; + } + if (innerText.match(/:\s+$/)) { + return undefined; + } + if (innerText.match(/\s+\S*$/)) { + return Position.AFTER_POLICY; + } + } + + const lastArg = command.args[command.args.length - 1]; + if (isSingleItem(lastArg) && lastArg.name === 'on') { + if (innerText.match(/on\s+\S*$/i)) { + return Position.MATCH_FIELD; + } + if (innerText.match(/on\s+\S+\s+$/i)) { + return Position.AFTER_ON_CLAUSE; + } + } + + if (isSingleItem(lastArg) && lastArg.name === 'with') { + if (innerText.match(/[,|with]\s+\S*$/i)) { + return Position.WITH_NEW_CLAUSE; + } + if (innerText.match(/[,|with]\s+\S+\s*=\s*\S+\s+$/i)) { + return Position.WITH_AFTER_COMPLETE_CLAUSE; + } + if (innerText.match(/[,|with]\s+\S+\s+$/i)) { + return Position.WITH_AFTER_FIRST_WORD; + } + if (innerText.match(/=\s+[^,\s]*$/i)) { + return Position.WITH_AFTER_ASSIGNMENT; + } + } +}; + +export const noPoliciesAvailableSuggestion: SuggestionRawDefinition = { + label: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.noPoliciesLabel', { + defaultMessage: 'No available policy', + }), + text: '', + kind: 'Issue', + detail: i18n.translate( + 'kbn-esql-validation-autocomplete.esql.autocomplete.noPoliciesLabelsFound', + { + defaultMessage: 'Click to create', + } + ), + sortText: 'D', + command: { + id: 'esql.policies.create', + title: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.createNewPolicy', { + defaultMessage: 'Click to create', + }), + }, +}; + +export const modeSuggestions: SuggestionRawDefinition[] = ENRICH_MODES.values.map( + ({ name, description }) => ({ + label: `${ENRICH_MODES.prefix || ''}${name}`, + text: `${ENRICH_MODES.prefix || ''}${name}:$0`, + asSnippet: true, + kind: 'Reference', + detail: `${ENRICH_MODES.description} - ${description}`, + sortText: 'D', + command: TRIGGER_SUGGESTION_COMMAND, + }) +); + +export const onSuggestion: SuggestionRawDefinition = { + label: 'ON', + text: 'ON ', + kind: 'Reference', + detail: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.onDoc', { + defaultMessage: 'On', + }), + sortText: '1', + command: TRIGGER_SUGGESTION_COMMAND, +}; + +export const withSuggestion: SuggestionRawDefinition = { + label: 'WITH', + text: 'WITH ', + kind: 'Reference', + detail: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.withDoc', { + defaultMessage: 'With', + }), + sortText: '1', + command: TRIGGER_SUGGESTION_COMMAND, +}; + +export const buildMatchingFieldsDefinition = ( + matchingField: string, + fields: string[] +): SuggestionRawDefinition[] => + fields.map((label) => ({ + label, + text: getSafeInsertText(label) + ' ', + kind: 'Variable', + detail: i18n.translate( + 'kbn-esql-validation-autocomplete.esql.autocomplete.matchingFieldDefinition', + { + defaultMessage: `Use to match on {matchingField} on the policy`, + values: { + matchingField, + }, + } + ), + sortText: 'D', + command: TRIGGER_SUGGESTION_COMMAND, + })); diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/join/index.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/join/index.ts index 80f98c5215aa2..5fb82556819b8 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/join/index.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/commands/join/index.ts @@ -102,7 +102,6 @@ export const suggest: CommandBaseDefinition<'join'>['suggest'] = async ({ getColumnsByType, definition, callbacks, - previousCommands, }: CommandSuggestParams<'join'>): Promise => { let commandText: string = innerText; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index a73d9d646a9bd..0b6b9ff401df2 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -19,7 +19,6 @@ import { timeUnitsToSuggest } from '../definitions/literals'; import { FunctionDefinition, CommandOptionsDefinition, - CommandModeDefinition, FunctionParameterType, FunctionDefinitionTypes, } from '../definitions/types'; @@ -245,7 +244,10 @@ export const buildFieldsDefinitionsWithMetadata = ( return [...suggestions]; }; -export const buildFieldsDefinitions = (fields: string[]): SuggestionRawDefinition[] => { +export const buildFieldsDefinitions = ( + fields: string[], + openSuggestions = true +): SuggestionRawDefinition[] => { return fields.map((label) => ({ label, text: getSafeInsertText(label), @@ -254,7 +256,7 @@ export const buildFieldsDefinitions = (fields: string[]): SuggestionRawDefinitio defaultMessage: `Field specified by the input table`, }), sortText: 'D', - command: TRIGGER_SUGGESTION_COMMAND, + command: openSuggestions ? TRIGGER_SUGGESTION_COMMAND : undefined, })); }; export const buildVariablesDefinitions = (variables: string[]): SuggestionRawDefinition[] => @@ -365,27 +367,7 @@ export const buildPoliciesDefinitions = ( command: TRIGGER_SUGGESTION_COMMAND, })); -export const buildMatchingFieldsDefinition = ( - matchingField: string, - fields: string[] -): SuggestionRawDefinition[] => - fields.map((label) => ({ - label, - text: getSafeInsertText(label) + ' ', - kind: 'Variable', - detail: i18n.translate( - 'kbn-esql-validation-autocomplete.esql.autocomplete.matchingFieldDefinition', - { - defaultMessage: `Use to match on {matchingField} on the policy`, - values: { - matchingField, - }, - } - ), - sortText: 'D', - command: TRIGGER_SUGGESTION_COMMAND, - })); - +/** @deprecated — options will be removed */ export const buildOptionDefinition = ( option: CommandOptionsDefinition, isAssignType: boolean = false @@ -407,42 +389,6 @@ export const buildOptionDefinition = ( return completeItem; }; -export const buildSettingDefinitions = ( - setting: CommandModeDefinition -): SuggestionRawDefinition[] => { - // for now there's just a single setting with one argument - return setting.values.map(({ name, description }) => ({ - label: `${setting.prefix || ''}${name}`, - text: `${setting.prefix || ''}${name}:$0`, - asSnippet: true, - kind: 'Reference', - detail: description ? `${setting.description} - ${description}` : setting.description, - sortText: 'D', - command: TRIGGER_SUGGESTION_COMMAND, - })); -}; - -export const buildNoPoliciesAvailableDefinition = (): SuggestionRawDefinition => ({ - label: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.noPoliciesLabel', { - defaultMessage: 'No available policy', - }), - text: '', - kind: 'Issue', - detail: i18n.translate( - 'kbn-esql-validation-autocomplete.esql.autocomplete.noPoliciesLabelsFound', - { - defaultMessage: 'Click to create', - } - ), - sortText: 'D', - command: { - id: 'esql.policies.create', - title: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.createNewPolicy', { - defaultMessage: 'Click to create', - }), - }, -}); - export function getUnitDuration(unit: number = 1) { const filteredTimeLiteral = timeUnitsToSuggest.filter(({ name }) => { const result = /s$/.test(name); 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 9e85cecb6f096..46ef56af1e802 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 @@ -27,7 +27,6 @@ import { isFunctionOperatorParam, isLiteralItem, } from '../shared/helpers'; -import { ENRICH_MODES } from './settings'; import { appendSeparatorOption, asOption, @@ -36,6 +35,8 @@ import { onOption, withOption, } from './options'; +import { ENRICH_MODES } from './settings'; + import { type CommandDefinition, FunctionDefinitionTypes } from './types'; import { suggest as suggestForSort } from '../autocomplete/commands/sort'; import { suggest as suggestForKeep } from '../autocomplete/commands/keep'; @@ -48,6 +49,7 @@ import { suggest as suggestForRow } from '../autocomplete/commands/row'; import { suggest as suggestForShow } from '../autocomplete/commands/show'; import { suggest as suggestForGrok } from '../autocomplete/commands/grok'; import { suggest as suggestForDissect } from '../autocomplete/commands/dissect'; +import { suggest as suggestForEnrich } from '../autocomplete/commands/enrich'; const statsValidator = (command: ESQLCommand) => { const messages: ESQLMessage[] = []; @@ -521,14 +523,15 @@ export const commandDefinitions: Array> = [ multipleParams: false, params: [{ name: 'policyName', type: 'source', innerTypes: ['policy'] }], }, + suggest: suggestForEnrich, }, { name: 'hidden_command', description: 'A test fixture to test hidden-ness', hidden: true, examples: [], - modes: [], options: [], + modes: [], signature: { params: [], multipleParams: false, diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/options.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/options.ts index 31d443a8cbb2b..3f067493b15d0 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/options.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/options.ts @@ -13,6 +13,7 @@ import { isLiteralItem, isColumnItem, isInlineCastItem } from '../shared/helpers import { getMessageFromId } from '../validation/errors'; import type { CommandOptionsDefinition } from './types'; +/** @deprecated — options are going away */ export const byOption: CommandOptionsDefinition = { name: 'by', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.byDoc', { @@ -25,6 +26,7 @@ export const byOption: CommandOptionsDefinition = { optional: true, }; +/** @deprecated — options are going away */ export const metadataOption: CommandOptionsDefinition = { name: 'metadata', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.metadataDoc', { @@ -70,6 +72,7 @@ export const metadataOption: CommandOptionsDefinition = { }, }; +/** @deprecated — options are going away */ export const asOption: CommandOptionsDefinition = { name: 'as', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.asDoc', { @@ -85,6 +88,7 @@ export const asOption: CommandOptionsDefinition = { optional: false, }; +/** @deprecated — options are going away */ export const onOption: CommandOptionsDefinition = { name: 'on', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.onDoc', { @@ -97,6 +101,7 @@ export const onOption: CommandOptionsDefinition = { optional: true, }; +/** @deprecated — options are going away */ export const withOption: CommandOptionsDefinition = { name: 'with', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.withDoc', { @@ -109,6 +114,7 @@ export const withOption: CommandOptionsDefinition = { optional: true, }; +/** @deprecated — options are going away */ export const appendSeparatorOption: CommandOptionsDefinition = { name: 'append_separator', description: i18n.translate( 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 02e9aaf00a81a..e71b915a044ca 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 @@ -6,7 +6,6 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { ESQLVariableType, ESQLControlVariable } from '@kbn/esql-types'; import type { ESQLAstItem, ESQLCommand, @@ -15,8 +14,10 @@ import type { ESQLMessage, ESQLSource, } from '@kbn/esql-ast'; +import { ESQLControlVariable, ESQLVariableType } from '@kbn/esql-types'; import { GetColumnsByTypeFn, SuggestionRawDefinition } from '../autocomplete/types'; -import type { ESQLCallbacks, ESQLSourceResult } from '../shared/types'; +import type { ESQLPolicy } from '../validation/types'; +import { ESQLCallbacks, ESQLSourceResult } from '../shared/types'; /** * All supported field types in ES|QL. This is all the types @@ -192,6 +193,8 @@ export interface FunctionDefinition { customParametersSnippet?: string; } +export type GetPolicyMetadataFn = (name: string) => Promise; + export interface CommandSuggestParams { /** * The text of the query to the left of the cursor. @@ -202,10 +205,14 @@ export interface CommandSuggestParams { */ command: ESQLCommand; /** - * Get a list of columns by type. This includes fields from any sources as well as - * variables defined in the query. + * Get suggestions for columns by type. This includes fields from any sources as well as + * user-defined columns in the query. */ getColumnsByType: GetColumnsByTypeFn; + /** + * Gets the names of all columns + */ + getAllColumnNames: () => string[]; /** * Check for the existence of a column by name. * @param column @@ -214,9 +221,13 @@ export interface CommandSuggestParams { columnExists: (column: string) => boolean; /** * Gets the name that should be used for the next variable. + * + * @param extraFieldNames — names that should be recognized as columns + * but that won't be found in the current table from Elasticsearch. This is currently only + * used to recognize enrichment fields from a policy in the ENRICH command. * @returns */ - getSuggestedVariableName: () => string; + getSuggestedVariableName: (extraFieldNames?: string[]) => string; /** * Examine the AST to determine the type of an expression. * @param expression @@ -237,6 +248,14 @@ export interface CommandSuggestParams { * @returns */ getSources: () => Promise; + /** + * Fetch suggestions for all available policies + */ + getPolicies: () => Promise; + /** + * Get metadata for a policy by name + */ + getPolicyMetadata: GetPolicyMetadataFn; /** * Inspect the AST and returns the sources that are used in the query. * @param type diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts index 95d3a9fa2d33b..da3c05e6dfb56 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts @@ -18,14 +18,12 @@ import { Walker, isIdentifier, } from '@kbn/esql-ast'; -import { ENRICH_MODES } from '../definitions/settings'; import { FunctionDefinitionTypes } from '../definitions/types'; import { EDITOR_MARKER } from './constants'; import { isOptionItem, isColumnItem, isSourceItem, - isSettingItem, pipePrecedesCurrentWord, getFunctionDefinition, } from './helpers'; @@ -66,10 +64,6 @@ function findOption(nodes: ESQLAstItem[], offset: number): ESQLCommandOption | u return findCommandSubType(nodes, offset, isOptionItem); } -function findSetting(nodes: ESQLAstItem[], offset: number): ESQLCommandMode | undefined { - return findCommandSubType(nodes, offset, isSettingItem); -} - function findCommandSubType( nodes: ESQLAstItem[], offset: number, @@ -131,7 +125,6 @@ function findAstPosition(ast: ESQLAst, offset: number) { command: removeMarkerArgFromArgsList(command)!, option: removeMarkerArgFromArgsList(findOption(command.args, offset)), node: removeMarkerArgFromArgsList(cleanMarkerNode(findNode(command.args, offset))), - setting: removeMarkerArgFromArgsList(findSetting(command.args, offset)), }; } @@ -171,16 +164,16 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number) }; } - const { command, option, setting, node } = findAstPosition(ast, offset); + const { command, option, node } = findAstPosition(ast, offset); if (node) { if (node.type === 'literal' && node.literalType === 'keyword') { // command ... "" - return { type: 'value' as const, command, node, option, setting }; + return { type: 'value' as const, command, node, option }; } if (node.type === 'function') { if (['in', 'not_in'].includes(node.name) && Array.isArray(node.args[1])) { // command ... a in ( ) - return { type: 'list' as const, command, node, option, setting }; + return { type: 'list' as const, command, node, option }; } if ( isNotEnrichClauseAssigment(node, command) && @@ -191,24 +184,19 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number) !(isOperator(node) && command.name !== 'stats') ) { // command ... fn( ) - return { type: 'function' as const, command, node, option, setting }; + return { type: 'function' as const, command, node, option }; } } - // for now it's only an enrich thing - if (node.type === 'source' && node.text === ENRICH_MODES.prefix) { - // command _ - return { type: 'setting' as const, command, node, option, setting }; - } } if (!command || (queryString.length <= offset && pipePrecedesCurrentWord(queryString))) { // // ... | - return { type: 'newCommand' as const, command: undefined, node, option, setting }; + return { type: 'newCommand' as const, command: undefined, node, option }; } // TODO — remove this option branch once https://github.com/elastic/kibana/issues/195418 is complete if (command && isOptionItem(command.args[command.args.length - 1]) && command.name !== 'stats') { if (option) { - return { type: 'option' as const, command, node, option, setting }; + return { type: 'option' as const, command, node, option }; } } @@ -218,6 +206,5 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number) command, option, node, - setting, }; } 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 0aa03b3d37999..14b476b7f80c1 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 @@ -11,7 +11,6 @@ import { Walker, type ESQLAstItem, type ESQLColumn, - type ESQLCommandMode, type ESQLCommandOption, type ESQLFunction, type ESQLLiteral, @@ -20,6 +19,7 @@ import { type ESQLTimeInterval, } from '@kbn/esql-ast'; import { + ESQLCommandMode, ESQLIdentifier, ESQLInlineCast, ESQLParamLiteral, @@ -66,6 +66,7 @@ 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'; } @@ -268,6 +269,18 @@ export function getColumnForASTNode( return getColumnByName(formatted, { fields, variables }); } +/** + * Take a column name like "`my``column`"" and return "my`column" + */ +export function unescapeColumnName(columnName: string) { + // TODO this doesn't cover all escaping scenarios... the best thing to do would be + // to use the AST column node parts array, but in some cases the AST node isn't available. + if (columnName.startsWith(SINGLE_BACKTICK) && columnName.endsWith(SINGLE_BACKTICK)) { + return columnName.slice(1, -1).replace(DOUBLE_TICKS_REGEX, SINGLE_BACKTICK); + } + return columnName; +} + /** * This function returns the variable or field matching a column */ @@ -275,12 +288,8 @@ export function getColumnByName( columnName: string, { fields, variables }: Pick ): ESQLRealField | ESQLVariable | undefined { - // TODO this doesn't cover all escaping scenarios... the best thing to do would be - // to use the AST column node parts array, but in some cases the AST node isn't available. - if (columnName.startsWith(SINGLE_BACKTICK) && columnName.endsWith(SINGLE_BACKTICK)) { - columnName = columnName.slice(1, -1).replace(DOUBLE_TICKS_REGEX, SINGLE_BACKTICK); - } - return fields.get(columnName) || variables.get(columnName)?.[0]; + const unescaped = unescapeColumnName(columnName); + return fields.get(unescaped) || variables.get(unescaped)?.[0]; } const ARRAY_REGEXP = /\[\]$/; 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 21c4df26d55e8..8da29c1cc9679 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 @@ -710,6 +710,7 @@ function validateFunction({ return uniqBy(messages, ({ location }) => `${location.min}-${location.max}`); } +/** @deprecated — "command settings" will be removed soon */ function validateSetting( setting: ESQLCommandMode, settingDef: CommandModeDefinition | undefined,