diff --git a/packages/kbn-esql-editor/src/esql_editor.tsx b/packages/kbn-esql-editor/src/esql_editor.tsx index 97340dc20d422..e8ca582ac5229 100644 --- a/packages/kbn-esql-editor/src/esql_editor.tsx +++ b/packages/kbn-esql-editor/src/esql_editor.tsx @@ -336,7 +336,7 @@ export const ESQLEditor = memo(function ESQLEditor({ const sources = await memoizedSources(dataViews, core).result; return sources; }, - getFieldsFor: async ({ query: queryToExecute }: { query?: string } | undefined = {}) => { + getColumnsFor: async ({ query: queryToExecute }: { query?: string } | undefined = {}) => { if (queryToExecute) { // ES|QL with limit 0 returns only the columns and is more performant const esqlQuery = { diff --git a/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts index abac86ab0e323..2f46356acee37 100644 --- a/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts @@ -56,7 +56,7 @@ export const policies = [ export function getCallbackMocks() { return { - getFieldsFor: jest.fn(async ({ query }) => { + getColumnsFor: jest.fn(async ({ query }) => { if (/enrich/.test(query)) { return enrichFields; } diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts index 51302d0d4cde5..c7bf9079f9155 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts @@ -42,6 +42,6 @@ describe('autocomplete.suggest', () => { await suggest('sHoW ?'); await suggest('row ? |'); - expect(callbacks.getFieldsFor.mock.calls.length).toBe(0); + expect(callbacks.getColumnsFor.mock.calls.length).toBe(0); }); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts index fa16a3df7026f..3234417c1f1a4 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts @@ -244,7 +244,17 @@ export function getDateLiteralsByFieldType(_requestedType: FieldType | FieldType } export function createCustomCallbackMocks( - customFields?: ESQLRealField[], + /** + * Columns that will come from Elasticsearch since the last command + * e.g. the test case may be `FROM index | EVAL foo = 1 | KEEP /` + * + * In this case, the columns available for the KEEP command will be the ones + * that were available after the EVAL command + * + * `FROM index | EVAL foo = 1 | LIMIT 0` will be used to fetch columns. The response + * will include "foo" as a column. + */ + customColumnsSinceLastCommand?: ESQLRealField[], customSources?: Array<{ name: string; hidden: boolean }>, customPolicies?: Array<{ name: string; @@ -253,11 +263,11 @@ export function createCustomCallbackMocks( enrichFields: string[]; }> ) { - const finalFields = customFields || fields; + const finalColumnsSinceLastCommand = customColumnsSinceLastCommand || fields; const finalSources = customSources || indexes; const finalPolicies = customPolicies || policies; return { - getFieldsFor: jest.fn(async () => finalFields), + getColumnsFor: jest.fn(async () => finalColumnsSinceLastCommand), getSources: jest.fn(async () => finalSources), getPolicies: jest.fn(async () => finalPolicies), }; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index deb4592428089..b89be15d670b1 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -31,6 +31,8 @@ import { TIME_PICKER_SUGGESTION, setup, attachTriggerCommand, + SuggestOptions, + fields, } from './__tests__/helpers'; import { METADATA_FIELDS } from '../shared/constants'; import { ESQL_COMMON_NUMERIC_TYPES, ESQL_STRING_TYPES } from '../shared/esql_types'; @@ -385,24 +387,56 @@ describe('autocomplete', () => { '```````round(doubleField) + 1```` + 1`` + 1`', '```````````````round(doubleField) + 1```````` + 1```` + 1`` + 1`', '```````````````````````````````round(doubleField) + 1```````````````` + 1```````` + 1```` + 1`` + 1`', + ], + undefined, + [ + [ + ...fields, + // the following non-field columns will come over the wire as part of the response + { + name: 'round(doubleField) + 1', + type: 'double', + }, + { + name: '`round(doubleField) + 1` + 1', + type: 'double', + }, + { + name: '```round(doubleField) + 1`` + 1` + 1', + type: 'double', + }, + { + name: '```````round(doubleField) + 1```` + 1`` + 1` + 1', + type: 'double', + }, + { + name: '```````````````round(doubleField) + 1```````` + 1```` + 1`` + 1` + 1', + type: 'double', + }, + ], ] ); it('should not suggest already-used fields and variables', async () => { const { suggest: suggestTest } = await setup(); - const getSuggestions = async (query: string) => - (await suggestTest(query)).map((value) => value.text); + const getSuggestions = async (query: string, opts?: SuggestOptions) => + (await suggestTest(query, opts)).map((value) => value.text); - expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP /')).toContain('foo'); - expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP foo, /')).not.toContain( - 'foo' - ); - expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP /')).toContain( + expect( + await getSuggestions('from a_index | EVAL foo = 1 | KEEP /', { + callbacks: { getColumnsFor: () => [...fields, { name: 'foo', type: 'integer' }] }, + }) + ).toContain('foo'); + expect( + await getSuggestions('from a_index | EVAL foo = 1 | KEEP foo, /', { + callbacks: { getColumnsFor: () => [...fields, { name: 'foo', type: 'integer' }] }, + }) + ).not.toContain('foo'); + + expect(await getSuggestions('from a_index | KEEP /')).toContain('doubleField'); + expect(await getSuggestions('from a_index | KEEP doubleField, /')).not.toContain( 'doubleField' ); - expect( - await getSuggestions('from a_index | EVAL foo = 1 | KEEP doubleField, /') - ).not.toContain('doubleField'); }); }); } @@ -504,7 +538,7 @@ describe('autocomplete', () => { }); describe('callbacks', () => { - it('should send the fields query without the last command', async () => { + it('should send the columns query without the last command', async () => { const callbackMocks = createCustomCallbackMocks(undefined, undefined, undefined); const statement = 'from a | drop keywordField | eval var0 = abs(doubleField) '; const triggerOffset = statement.lastIndexOf(' '); @@ -516,7 +550,7 @@ describe('autocomplete', () => { async (text) => (text ? getAstAndSyntaxErrors(text) : { ast: [], errors: [] }), callbackMocks ); - expect(callbackMocks.getFieldsFor).toHaveBeenCalledWith({ + expect(callbackMocks.getColumnsFor).toHaveBeenCalledWith({ query: 'from a | drop keywordField', }); }); @@ -532,7 +566,7 @@ describe('autocomplete', () => { async (text) => (text ? getAstAndSyntaxErrors(text) : { ast: [], errors: [] }), callbackMocks ); - expect(callbackMocks.getFieldsFor).toHaveBeenCalledWith({ query: 'from a' }); + expect(callbackMocks.getColumnsFor).toHaveBeenCalledWith({ query: 'from a' }); }); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 98a26b0c8dd4b..5bdbd9d995fc9 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -14,12 +14,11 @@ import type { ESQLCommand, ESQLCommandOption, ESQLFunction, - ESQLLiteral, ESQLSingleAstItem, } from '@kbn/esql-ast'; import { i18n } from '@kbn/i18n'; import { ESQL_NUMBER_TYPES, isNumericType } from '../shared/esql_types'; -import type { EditorContext, ItemKind, SuggestionRawDefinition, GetFieldsByTypeFn } from './types'; +import type { EditorContext, ItemKind, SuggestionRawDefinition, GetColumnsByTypeFn } from './types'; import { getColumnForASTNode, getCommandDefinition, @@ -49,6 +48,7 @@ import { getColumnByName, sourceExists, findFinalWord, + getAllCommands, } from '../shared/helpers'; import { collectVariables, excludeVariablesFromCurrentCommand } from '../shared/variables'; import type { ESQLPolicy, ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; @@ -56,9 +56,9 @@ import { allStarConstant, colonCompleteItem, commaCompleteItem, - commandAutocompleteDefinitions, getAssignmentDefinitionCompletitionItem, getBuiltinCompatibleFunctionDefinition, + getCommandAutocompleteDefinitions, getNextTokenForNot, listCompleteItem, pipeCompleteItem, @@ -101,15 +101,12 @@ import { isAggFunctionUsedAlready, removeQuoteForSuggestedSources, getValidSignaturesAndTypesToSuggestNext, + handleFragment, + getFieldsOrFunctionsSuggestions, + pushItUpInTheList, + extractTypeFromASTArg, } from './helper'; -import { getSortPos } from './commands/sort/helper'; -import { - FunctionParameter, - FunctionReturnType, - SupportedDataType, - isParameterType, - isReturnType, -} from '../definitions/types'; +import { FunctionParameter, isParameterType, isReturnType } from '../definitions/types'; import { metadataOption } from '../definitions/options'; import { comparisonFunctions } from '../definitions/builtin'; import { countBracketsUnclosed } from '../shared/helpers'; @@ -181,7 +178,7 @@ export async function suggest( if (astContext.type === 'newCommand') { // propose main commands here // filter source commands if already defined - const suggestions = commandAutocompleteDefinitions; + const suggestions = getCommandAutocompleteDefinitions(getAllCommands()); if (!ast.length) { // Display the recommended queries if there are no commands (empty state) const recommendedQueriesSuggestions: SuggestionRawDefinition[] = []; @@ -211,7 +208,7 @@ export async function suggest( if (astContext.type === 'expression') { // suggest next possible argument, or option // otherwise a variable - return getExpressionSuggestionsByType( + return getSuggestionsWithinCommand( innerText, ast, astContext, @@ -275,7 +272,7 @@ export async function suggest( export function getFieldsByTypeRetriever( queryString: string, resourceRetriever?: ESQLCallbacks -): { getFieldsByType: GetFieldsByTypeFn; getFieldsMap: GetFieldsMapFn } { +): { getFieldsByType: GetColumnsByTypeFn; getFieldsMap: GetFieldsMapFn } { const helpers = getFieldsByTypeHelper(queryString, resourceRetriever); return { getFieldsByType: async ( @@ -389,43 +386,6 @@ function areCurrentArgsValid( return true; } -export function extractTypeFromASTArg( - arg: ESQLAstItem, - references: Pick -): - | ESQLLiteral['literalType'] - | SupportedDataType - | FunctionReturnType - | 'timeInterval' - | string // @TODO remove this - | undefined { - if (Array.isArray(arg)) { - return extractTypeFromASTArg(arg[0], references); - } - if (isColumnItem(arg) || isLiteralItem(arg)) { - if (isLiteralItem(arg)) { - return arg.literalType; - } - if (isColumnItem(arg)) { - const hit = getColumnForASTNode(arg, references); - if (hit) { - return hit.type; - } - } - } - if (isTimeIntervalItem(arg)) { - return arg.type; - } - if (isFunctionItem(arg)) { - const fnDef = getFunctionDefinition(arg.name); - if (fnDef) { - // @TODO: improve this to better filter down the correct return type based on existing arguments - // just mind that this can be highly recursive... - return fnDef.signatures[0].returnType; - } - } -} - // @TODO: refactor this to be shared with validation function isFunctionArgComplete( arg: ESQLFunction, @@ -484,6 +444,55 @@ function extractArgMeta( return { argIndex, prevIndex, lastArg, nodeArg }; } +async function getSuggestionsWithinCommand( + innerText: string, + commands: ESQLCommand[], + { + command, + option, + node, + }: { + command: ESQLCommand; + option: ESQLCommandOption | undefined; + node: ESQLSingleAstItem | undefined; + }, + getSources: () => Promise, + getColumnsByType: GetColumnsByTypeFn, + getFieldsMap: GetFieldsMapFn, + getPolicies: GetPoliciesFn, + getPolicyMetadata: GetPolicyMetadataFn +) { + const commandDef = getCommandDefinition(command.name); + + // collect all fields + variables to suggest + const fieldsMap: Map = await getFieldsMap(); + const anyVariables = collectVariables(commands, fieldsMap, innerText); + + const references = { fields: fieldsMap, variables: anyVariables }; + if (commandDef.suggest) { + // The new path. + return commandDef.suggest(innerText, command, getColumnsByType, (col: string) => + Boolean(getColumnByName(col, references)) + ); + } else { + // The deprecated path. + return getExpressionSuggestionsByType( + innerText, + commands, + { command, option, node }, + getSources, + getColumnsByType, + getFieldsMap, + getPolicies, + getPolicyMetadata + ); + } +} + +/** + * @deprecated — this generic logic will be replaced with the command-specific suggest functions + * from each command definition. + */ async function getExpressionSuggestionsByType( innerText: string, commands: ESQLCommand[], @@ -497,7 +506,7 @@ async function getExpressionSuggestionsByType( node: ESQLSingleAstItem | undefined; }, getSources: () => Promise, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, getFieldsMap: GetFieldsMapFn, getPolicies: GetPoliciesFn, getPolicyMetadata: GetPolicyMetadataFn @@ -505,6 +514,15 @@ async function getExpressionSuggestionsByType( const commandDef = getCommandDefinition(command.name); const { argIndex, prevIndex, lastArg, nodeArg } = extractArgMeta(command, node); + // collect all fields + variables to suggest + const fieldsMap: Map = await getFieldsMap(); + const anyVariables = collectVariables(commands, fieldsMap, innerText); + + const references = { fields: fieldsMap, variables: anyVariables }; + if (!commandDef.signature || !commandDef.options) { + return []; + } + // TODO - this is a workaround because it was too difficult to handle this case in a generic way :( if (commandDef.name === 'from' && node && isSourceItem(node) && /\s/.test(node.name)) { // FROM " " @@ -537,7 +555,7 @@ async function getExpressionSuggestionsByType( command.args.filter((arg) => isOptionItem(arg)) as ESQLCommandOption[] ).map(({ name }) => ({ name, - index: commandDef.options.findIndex(({ name: defName }) => defName === name), + index: commandDef.options!.findIndex(({ name: defName }) => defName === name), })); const optionsAvailable = commandDef.options.filter(({ name }, index) => { const optArg = optionsAlreadyDeclared.find(({ name: optionName }) => optionName === name); @@ -577,23 +595,12 @@ async function getExpressionSuggestionsByType( } } - // collect all fields + variables to suggest - const fieldsMap: Map = await (argDef ? getFieldsMap() : new Map()); - const anyVariables = collectVariables(commands, fieldsMap, innerText); - const previousWord = findPreviousWord(innerText); // enrich with assignment has some special rules who are handled somewhere else const canHaveAssignments = ['eval', 'stats', 'row'].includes(command.name) && !comparisonFunctions.map((fn) => fn.name).includes(previousWord); - const references = { fields: fieldsMap, variables: anyVariables }; - if (command.name === 'sort') { - return await suggestForSortCmd(innerText, getFieldsByType, (col) => - Boolean(getColumnByName(col, references)) - ); - } - const suggestions: SuggestionRawDefinition[] = []; // When user types and accepts autocomplete suggestion, and cursor is placed at the end of a valid field @@ -1075,7 +1082,7 @@ async function getBuiltinFunctionNextArgument( nodeArg: ESQLFunction, nodeArgType: string, references: Pick, - getFieldsByType: GetFieldsByTypeFn + getFieldsByType: GetColumnsByTypeFn ) { const suggestions = []; const isFnComplete = isFunctionArgComplete(nodeArg, references); @@ -1171,96 +1178,6 @@ async function getBuiltinFunctionNextArgument( }); } -function pushItUpInTheList(suggestions: SuggestionRawDefinition[], shouldPromote: boolean) { - if (!shouldPromote) { - return suggestions; - } - return suggestions.map(({ sortText, ...rest }) => ({ - ...rest, - sortText: `1${sortText}`, - })); -} - -/** - * TODO — split this into distinct functions, one for fields, one for functions, one for literals - */ -async function getFieldsOrFunctionsSuggestions( - types: string[], - commandName: string, - optionName: string | undefined, - getFieldsByType: GetFieldsByTypeFn, - { - functions, - fields, - variables, - literals = false, - }: { - functions: boolean; - fields: boolean; - variables?: Map; - literals?: boolean; - }, - { - ignoreFn = [], - ignoreColumns = [], - }: { - ignoreFn?: string[]; - ignoreColumns?: string[]; - } = {} -): Promise { - const filteredFieldsByType = pushItUpInTheList( - (await (fields - ? getFieldsByType(types, ignoreColumns, { - advanceCursor: commandName === 'sort', - openSuggestions: commandName === 'sort', - }) - : [])) as SuggestionRawDefinition[], - functions - ); - - const filteredVariablesByType: string[] = []; - if (variables) { - for (const variable of variables.values()) { - if ( - (types.includes('any') || types.includes(variable[0].type)) && - !ignoreColumns.includes(variable[0].name) - ) { - filteredVariablesByType.push(variable[0].name); - } - } - // due to a bug on the ES|QL table side, filter out fields list with underscored variable names (??) - // avg( numberField ) => avg_numberField_ - const ALPHANUMERIC_REGEXP = /[^a-zA-Z\d]/g; - if ( - filteredVariablesByType.length && - filteredVariablesByType.some((v) => ALPHANUMERIC_REGEXP.test(v)) - ) { - for (const variable of filteredVariablesByType) { - const underscoredName = variable.replace(ALPHANUMERIC_REGEXP, '_'); - const index = filteredFieldsByType.findIndex( - ({ label }) => underscoredName === label || `_${underscoredName}_` === label - ); - if (index >= 0) { - filteredFieldsByType.splice(index); - } - } - } - } - // could also be in stats (bucket) but our autocomplete is not great yet - const displayDateSuggestions = types.includes('date') && ['where', 'eval'].includes(commandName); - - const suggestions = filteredFieldsByType.concat( - displayDateSuggestions ? getDateLiterals() : [], - functions ? getCompatibleFunctionDefinition(commandName, optionName, types, ignoreFn) : [], - variables - ? pushItUpInTheList(buildVariablesDefinitions(filteredVariablesByType), functions) - : [], - literals ? getCompatibleLiterals(commandName, types) : [] - ); - - return suggestions; -} - const addCommaIf = (condition: boolean, text: string) => (condition ? `${text},` : text); async function getFunctionArgsSuggestions( @@ -1275,7 +1192,7 @@ async function getFunctionArgsSuggestions( option: ESQLCommandOption | undefined; node: ESQLFunction; }, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, getFieldsMap: GetFieldsMapFn, getPolicyMetadata: GetPolicyMetadataFn, fullText: string, @@ -1504,7 +1421,7 @@ async function getListArgsSuggestions( command: ESQLCommand; node: ESQLSingleAstItem | undefined; }, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, getFieldsMaps: GetFieldsMapFn, getPolicyMetadata: GetPolicyMetadataFn ) { @@ -1559,13 +1476,13 @@ async function getSettingArgsSuggestions( command: ESQLCommand; node: ESQLSingleAstItem | undefined; }, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, getFieldsMaps: GetFieldsMapFn, getPolicyMetadata: GetPolicyMetadataFn ) { const suggestions = []; - const settingDefs = getCommandDefinition(command.name).modes; + const settingDefs = getCommandDefinition(command.name).modes || []; if (settingDefs.length) { const lastChar = getLastCharFromTrimmed(innerText); @@ -1590,7 +1507,7 @@ async function getOptionArgsSuggestions( option: ESQLCommandOption; node: ESQLSingleAstItem | undefined; }, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, getFieldsMaps: GetFieldsMapFn, getPolicyMetadata: GetPolicyMetadataFn, getPreferences?: () => Promise<{ histogramBarTarget: number } | undefined> @@ -1601,6 +1518,9 @@ async function getOptionArgsSuggestions( } const optionDef = getCommandOption(option.name); + if (!optionDef || !optionDef.signature) { + return []; + } const { nodeArg, argIndex, lastArg } = extractArgMeta(option, node); const suggestions = []; const isNewExpression = isRestartingExpression(innerText) || option.args.length === 0; @@ -1899,236 +1819,3 @@ async function getOptionArgsSuggestions( } return suggestions; } - -/** - * This function handles the logic to suggest completions - * for a given fragment of text in a generic way. A good example is - * a field name. - * - * When typing a field name, there are 2 scenarios - * - * 1. field name is incomplete (includes the empty string) - * KEEP / - * KEEP fie/ - * - * 2. field name is complete - * KEEP field/ - * - * This function provides a framework for detecting and handling both scenarios in a clean way. - * - * @param innerText - the query text before the current cursor position - * @param isFragmentComplete — return true if the fragment is complete - * @param getSuggestionsForIncomplete — gets suggestions for an incomplete fragment - * @param getSuggestionsForComplete - gets suggestions for a complete fragment - * @returns - */ -function handleFragment( - innerText: string, - isFragmentComplete: (fragment: string) => boolean, - getSuggestionsForIncomplete: ( - fragment: string, - rangeToReplace?: { start: number; end: number } - ) => SuggestionRawDefinition[] | Promise, - getSuggestionsForComplete: ( - fragment: string, - rangeToReplace: { start: number; end: number } - ) => SuggestionRawDefinition[] | Promise -): SuggestionRawDefinition[] | Promise { - /** - * @TODO — this string manipulation is crude and can't support all cases - * Checking for a partial word and computing the replacement range should - * really be done using the AST node, but we'll have to refactor further upstream - * to make that available. This is a quick fix to support the most common case. - */ - const fragment = findFinalWord(innerText); - if (!fragment) { - return getSuggestionsForIncomplete(''); - } else { - const rangeToReplace = { - start: innerText.length - fragment.length + 1, - end: innerText.length + 1, - }; - if (isFragmentComplete(fragment)) { - return getSuggestionsForComplete(fragment, rangeToReplace); - } else { - return getSuggestionsForIncomplete(fragment, rangeToReplace); - } - } -} - -const sortModifierSuggestions = { - ASC: { - label: 'ASC', - text: 'ASC', - detail: '', - kind: 'Keyword', - sortText: '1-ASC', - command: TRIGGER_SUGGESTION_COMMAND, - } as SuggestionRawDefinition, - DESC: { - label: 'DESC', - text: 'DESC', - detail: '', - kind: 'Keyword', - sortText: '1-DESC', - command: TRIGGER_SUGGESTION_COMMAND, - } as SuggestionRawDefinition, - NULLS_FIRST: { - label: 'NULLS FIRST', - text: 'NULLS FIRST', - detail: '', - kind: 'Keyword', - sortText: '2-NULLS FIRST', - command: TRIGGER_SUGGESTION_COMMAND, - } as SuggestionRawDefinition, - NULLS_LAST: { - label: 'NULLS LAST', - text: 'NULLS LAST', - detail: '', - kind: 'Keyword', - sortText: '2-NULLS LAST', - command: TRIGGER_SUGGESTION_COMMAND, - } as SuggestionRawDefinition, -}; - -export const suggestForSortCmd = async ( - innerText: string, - getFieldsByType: GetFieldsByTypeFn, - columnExists: (column: string) => boolean -): Promise => { - const prependSpace = (s: SuggestionRawDefinition) => ({ ...s, text: ' ' + s.text }); - - const { pos, nulls } = getSortPos(innerText); - - switch (pos) { - case 'space2': { - return [ - sortModifierSuggestions.ASC, - sortModifierSuggestions.DESC, - sortModifierSuggestions.NULLS_FIRST, - sortModifierSuggestions.NULLS_LAST, - pipeCompleteItem, - { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, - ]; - } - case 'order': { - return handleFragment( - innerText, - (fragment) => ['ASC', 'DESC'].some((completeWord) => noCaseCompare(completeWord, fragment)), - (_fragment, rangeToReplace) => { - return Object.values(sortModifierSuggestions).map((suggestion) => ({ - ...suggestion, - rangeToReplace, - })); - }, - (fragment, rangeToReplace) => { - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - prependSpace(sortModifierSuggestions.NULLS_FIRST), - prependSpace(sortModifierSuggestions.NULLS_LAST), - ].map((suggestion) => ({ - ...suggestion, - filterText: fragment, - text: fragment + suggestion.text, - rangeToReplace, - command: TRIGGER_SUGGESTION_COMMAND, - })); - } - ); - } - case 'space3': { - return [ - sortModifierSuggestions.NULLS_FIRST, - sortModifierSuggestions.NULLS_LAST, - pipeCompleteItem, - { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, - ]; - } - case 'nulls': { - return handleFragment( - innerText, - (fragment) => - ['FIRST', 'LAST'].some((completeWord) => noCaseCompare(completeWord, fragment)), - (_fragment) => { - const end = innerText.length + 1; - const start = end - nulls.length; - return Object.values(sortModifierSuggestions).map((suggestion) => ({ - ...suggestion, - // we can't use the range generated by handleFragment here - // because it doesn't really support multi-word completions - rangeToReplace: { start, end }, - })); - }, - (fragment, rangeToReplace) => { - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - ].map((suggestion) => ({ - ...suggestion, - filterText: fragment, - text: fragment + suggestion.text, - rangeToReplace, - command: TRIGGER_SUGGESTION_COMMAND, - })); - } - ); - } - case 'space4': { - return [ - pipeCompleteItem, - { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, - ]; - } - } - - const fieldSuggestions = await getFieldsByType('any', [], { - openSuggestions: true, - }); - const functionSuggestions = await getFieldsOrFunctionsSuggestions( - ['any'], - 'sort', - undefined, - getFieldsByType, - { - functions: true, - fields: false, - } - ); - - return await handleFragment( - innerText, - columnExists, - (_fragment: string, rangeToReplace?: { start: number; end: number }) => { - // SORT fie - return [ - ...pushItUpInTheList( - fieldSuggestions.map((suggestion) => ({ - ...suggestion, - command: TRIGGER_SUGGESTION_COMMAND, - rangeToReplace, - })), - true - ), - ...functionSuggestions, - ]; - }, - (fragment: string, rangeToReplace: { start: number; end: number }) => { - // SORT field - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - prependSpace(sortModifierSuggestions.ASC), - prependSpace(sortModifierSuggestions.DESC), - prependSpace(sortModifierSuggestions.NULLS_FIRST), - prependSpace(sortModifierSuggestions.NULLS_LAST), - ].map((s) => ({ - ...s, - filterText: fragment, - text: fragment + s.text, - command: TRIGGER_SUGGESTION_COMMAND, - rangeToReplace, - })); - } - ); -}; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts new file mode 100644 index 0000000000000..ed5f0ee3d3f6b --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ESQLCommand } from '@kbn/esql-ast'; +import { + findPreviousWord, + getLastCharFromTrimmed, + isColumnItem, + noCaseCompare, +} from '../../../shared/helpers'; +import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; +import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; +import { handleFragment } from '../../helper'; +import { TRIGGER_SUGGESTION_COMMAND } from '../../factories'; + +export async function suggest( + innerText: string, + command: ESQLCommand<'drop'>, + getColumnsByType: GetColumnsByTypeFn, + columnExists: (column: string) => boolean +): Promise { + if ( + /\s/.test(innerText[innerText.length - 1]) && + getLastCharFromTrimmed(innerText) !== ',' && + !noCaseCompare(findPreviousWord(innerText), 'drop') + ) { + return [pipeCompleteItem, commaCompleteItem]; + } + + const alreadyDeclaredFields = command.args.filter(isColumnItem).map((arg) => arg.name); + const fieldSuggestions = await getColumnsByType('any', alreadyDeclaredFields); + + return handleFragment( + innerText, + (fragment) => columnExists(fragment), + (_fragment: string, rangeToReplace?: { start: number; end: number }) => { + // KEEP fie + return fieldSuggestions.map((suggestion) => ({ + ...suggestion, + text: suggestion.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + }, + (fragment: string, rangeToReplace: { start: number; end: number }) => { + // KEEP field + const finalSuggestions = [{ ...pipeCompleteItem, text: ' | ' }]; + if (fieldSuggestions.length > 1) + // when we fix the editor marker, this should probably be checked against 0 instead of 1 + // this is because the last field in the AST is currently getting removed (because it contains + // the editor marker) so it is not included in the ignored list which is used to filter out + // existing fields above. + finalSuggestions.push({ ...commaCompleteItem, text: ', ' }); + + return finalSuggestions.map((s) => ({ + ...s, + filterText: fragment, + text: fragment + s.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + } + ); +} diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts new file mode 100644 index 0000000000000..c2480ffbcde72 --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ESQLCommand } from '@kbn/esql-ast'; +import { + findPreviousWord, + getLastCharFromTrimmed, + isColumnItem, + noCaseCompare, +} from '../../../shared/helpers'; +import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; +import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; +import { handleFragment } from '../../helper'; +import { TRIGGER_SUGGESTION_COMMAND } from '../../factories'; + +export async function suggest( + innerText: string, + command: ESQLCommand<'keep'>, + getColumnsByType: GetColumnsByTypeFn, + columnExists: (column: string) => boolean +): Promise { + if ( + /\s/.test(innerText[innerText.length - 1]) && + getLastCharFromTrimmed(innerText) !== ',' && + !noCaseCompare(findPreviousWord(innerText), 'keep') + ) { + return [pipeCompleteItem, commaCompleteItem]; + } + + const alreadyDeclaredFields = command.args.filter(isColumnItem).map((arg) => arg.name); + const fieldSuggestions = await getColumnsByType('any', alreadyDeclaredFields); + + return handleFragment( + innerText, + (fragment) => columnExists(fragment), + (_fragment: string, rangeToReplace?: { start: number; end: number }) => { + // KEEP fie + return fieldSuggestions.map((suggestion) => ({ + ...suggestion, + text: suggestion.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + }, + (fragment: string, rangeToReplace: { start: number; end: number }) => { + // KEEP field + const finalSuggestions = [{ ...pipeCompleteItem, text: ' | ' }]; + if (fieldSuggestions.length > 1) + // when we fix the editor marker, this should probably be checked against 0 instead of 1 + // this is because the last field in the AST is currently getting removed (because it contains + // the editor marker) so it is not included in the ignored list which is used to filter out + // existing fields above. + finalSuggestions.push({ ...commaCompleteItem, text: ', ' }); + + return finalSuggestions.map((s) => ({ + ...s, + filterText: fragment, + text: fragment + s.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + } + ); +} diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/helper.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/helper.ts index 96546eff7d391..63dea06667cd8 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/helper.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/helper.ts @@ -7,6 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { TRIGGER_SUGGESTION_COMMAND } from '../../factories'; +import { SuggestionRawDefinition } from '../../types'; + const regexStart = /.+\|\s*so?r?(?t?)(.+,)?(?\s+)?/i; const regex = /.+\|\s*sort(.+,)?((?\s+)(?[^\s]+)(?\s*)(?(AS?C?)|(DE?S?C?))?(?\s*)(?NU?L?L?S? ?(FI?R?S?T?|LA?S?T?)?)?(?\s*))?/i; @@ -43,6 +46,41 @@ export interface SortCaretPosition { nulls: string; } +export const sortModifierSuggestions = { + ASC: { + label: 'ASC', + text: 'ASC', + detail: '', + kind: 'Keyword', + sortText: '1-ASC', + command: TRIGGER_SUGGESTION_COMMAND, + } as SuggestionRawDefinition, + DESC: { + label: 'DESC', + text: 'DESC', + detail: '', + kind: 'Keyword', + sortText: '1-DESC', + command: TRIGGER_SUGGESTION_COMMAND, + } as SuggestionRawDefinition, + NULLS_FIRST: { + label: 'NULLS FIRST', + text: 'NULLS FIRST', + detail: '', + kind: 'Keyword', + sortText: '2-NULLS FIRST', + command: TRIGGER_SUGGESTION_COMMAND, + } as SuggestionRawDefinition, + NULLS_LAST: { + label: 'NULLS LAST', + text: 'NULLS LAST', + detail: '', + kind: 'Keyword', + sortText: '2-NULLS LAST', + command: TRIGGER_SUGGESTION_COMMAND, + } as SuggestionRawDefinition, +}; + export const getSortPos = (query: string): SortCaretPosition => { const match = query.match(regex); let pos: SortCaretPosition['pos'] = 'none'; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts new file mode 100644 index 0000000000000..61561dea96b72 --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ESQLCommand } from '@kbn/esql-ast'; +import { noCaseCompare } from '../../../shared/helpers'; +import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; +import { TRIGGER_SUGGESTION_COMMAND } from '../../factories'; +import { getFieldsOrFunctionsSuggestions, handleFragment, pushItUpInTheList } from '../../helper'; +import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; +import { getSortPos, sortModifierSuggestions } from './helper'; + +export async function suggest( + innerText: string, + _command: ESQLCommand<'sort'>, + getColumnsByType: GetColumnsByTypeFn, + columnExists: (column: string) => boolean +): Promise { + const prependSpace = (s: SuggestionRawDefinition) => ({ ...s, text: ' ' + s.text }); + + const { pos, nulls } = getSortPos(innerText); + + switch (pos) { + case 'space2': { + return [ + sortModifierSuggestions.ASC, + sortModifierSuggestions.DESC, + sortModifierSuggestions.NULLS_FIRST, + sortModifierSuggestions.NULLS_LAST, + pipeCompleteItem, + { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, + ]; + } + case 'order': { + return handleFragment( + innerText, + (fragment) => ['ASC', 'DESC'].some((completeWord) => noCaseCompare(completeWord, fragment)), + (_fragment, rangeToReplace) => { + return Object.values(sortModifierSuggestions).map((suggestion) => ({ + ...suggestion, + rangeToReplace, + })); + }, + (fragment, rangeToReplace) => { + return [ + { ...pipeCompleteItem, text: ' | ' }, + { ...commaCompleteItem, text: ', ' }, + prependSpace(sortModifierSuggestions.NULLS_FIRST), + prependSpace(sortModifierSuggestions.NULLS_LAST), + ].map((suggestion) => ({ + ...suggestion, + filterText: fragment, + text: fragment + suggestion.text, + rangeToReplace, + command: TRIGGER_SUGGESTION_COMMAND, + })); + } + ); + } + case 'space3': { + return [ + sortModifierSuggestions.NULLS_FIRST, + sortModifierSuggestions.NULLS_LAST, + pipeCompleteItem, + { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, + ]; + } + case 'nulls': { + return handleFragment( + innerText, + (fragment) => + ['FIRST', 'LAST'].some((completeWord) => noCaseCompare(completeWord, fragment)), + (_fragment) => { + const end = innerText.length + 1; + const start = end - nulls.length; + return Object.values(sortModifierSuggestions).map((suggestion) => ({ + ...suggestion, + // we can't use the range generated by handleFragment here + // because it doesn't really support multi-word completions + rangeToReplace: { start, end }, + })); + }, + (fragment, rangeToReplace) => { + return [ + { ...pipeCompleteItem, text: ' | ' }, + { ...commaCompleteItem, text: ', ' }, + ].map((suggestion) => ({ + ...suggestion, + filterText: fragment, + text: fragment + suggestion.text, + rangeToReplace, + command: TRIGGER_SUGGESTION_COMMAND, + })); + } + ); + } + case 'space4': { + return [ + pipeCompleteItem, + { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, + ]; + } + } + + const fieldSuggestions = await getColumnsByType('any', [], { + openSuggestions: true, + }); + const functionSuggestions = await getFieldsOrFunctionsSuggestions( + ['any'], + 'sort', + undefined, + getColumnsByType, + { + functions: true, + fields: false, + } + ); + + return await handleFragment( + innerText, + columnExists, + (_fragment: string, rangeToReplace?: { start: number; end: number }) => { + // SORT fie + return [ + ...pushItUpInTheList( + fieldSuggestions.map((suggestion) => ({ + ...suggestion, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })), + true + ), + ...functionSuggestions, + ]; + }, + (fragment: string, rangeToReplace: { start: number; end: number }) => { + // SORT field + return [ + { ...pipeCompleteItem, text: ' | ' }, + { ...commaCompleteItem, text: ', ' }, + prependSpace(sortModifierSuggestions.ASC), + prependSpace(sortModifierSuggestions.DESC), + prependSpace(sortModifierSuggestions.NULLS_FIRST), + prependSpace(sortModifierSuggestions.NULLS_LAST), + ].map((s) => ({ + ...s, + filterText: fragment, + text: fragment + s.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + } + ); +} diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts index 000c196b49e5e..b115e30c47efe 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts @@ -10,14 +10,13 @@ import { i18n } from '@kbn/i18n'; import type { ItemKind, SuggestionRawDefinition } from './types'; import { builtinFunctions } from '../definitions/builtin'; -import { getAllCommands } from '../shared/helpers'; import { getSuggestionBuiltinDefinition, getSuggestionCommandDefinition, TRIGGER_SUGGESTION_COMMAND, buildConstantsDefinitions, } from './factories'; -import { FunctionParameterType, FunctionReturnType } from '../definitions/types'; +import { CommandDefinition, FunctionParameterType, FunctionReturnType } from '../definitions/types'; import { getTestFunctions } from '../shared/test_functions'; export function getAssignmentDefinitionCompletitionItem() { @@ -87,9 +86,10 @@ export const getBuiltinCompatibleFunctionDefinition = ( .map(getSuggestionBuiltinDefinition); }; -export const commandAutocompleteDefinitions: SuggestionRawDefinition[] = getAllCommands() - .filter(({ hidden }) => !hidden) - .map(getSuggestionCommandDefinition); +export const getCommandAutocompleteDefinitions = ( + commands: Array> +): SuggestionRawDefinition[] => + commands.filter(({ hidden }) => !hidden).map(getSuggestionCommandDefinition); function buildCharCompleteItem( label: string, diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index 85c8d035d33b1..f522e9bc65863 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -123,7 +123,7 @@ export const getCompatibleFunctionDefinition = ( }; export function getSuggestionCommandDefinition( - command: CommandDefinition + command: CommandDefinition ): SuggestionRawDefinition { const commandDefinition = getCommandDefinition(command.name); const commandSignature = getCommandSignature(commandDefinition); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts index dd450e28b66a9..6585a04c98c59 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts @@ -7,21 +7,40 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { ESQLAstItem, ESQLCommand, ESQLFunction, ESQLSource } from '@kbn/esql-ast'; +import type { + ESQLAstItem, + ESQLCommand, + ESQLFunction, + ESQLLiteral, + ESQLSource, +} from '@kbn/esql-ast'; import { uniqBy } from 'lodash'; -import type { FunctionDefinition } from '../definitions/types'; +import type { + FunctionDefinition, + FunctionReturnType, + SupportedDataType, +} from '../definitions/types'; import { + findFinalWord, + getColumnForASTNode, getFunctionDefinition, isAssignment, + isColumnItem, isFunctionItem, isLiteralItem, + isTimeIntervalItem, } from '../shared/helpers'; -import type { SuggestionRawDefinition } from './types'; +import type { GetColumnsByTypeFn, SuggestionRawDefinition } from './types'; import { compareTypesWithLiterals } from '../shared/esql_types'; -import { TIME_SYSTEM_PARAMS } from './factories'; +import { + TIME_SYSTEM_PARAMS, + buildVariablesDefinitions, + getCompatibleFunctionDefinition, + getCompatibleLiterals, + getDateLiterals, +} from './factories'; import { EDITOR_MARKER } from '../shared/constants'; -import { extractTypeFromASTArg } from './autocomplete'; -import { ESQLRealField, ESQLVariable } from '../validation/types'; +import { ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; function extractFunctionArgs(args: ESQLAstItem[]): ESQLFunction[] { return args.flatMap((arg) => (isAssignment(arg) ? arg.args[1] : arg)).filter(isFunctionItem); @@ -272,3 +291,185 @@ export function getValidSignaturesAndTypesToSuggestNext( currentArg, }; } + +/** + * This function handles the logic to suggest completions + * for a given fragment of text in a generic way. A good example is + * a field name. + * + * When typing a field name, there are 2 scenarios + * + * 1. field name is incomplete (includes the empty string) + * KEEP / + * KEEP fie/ + * + * 2. field name is complete + * KEEP field/ + * + * This function provides a framework for detecting and handling both scenarios in a clean way. + * + * @param innerText - the query text before the current cursor position + * @param isFragmentComplete — return true if the fragment is complete + * @param getSuggestionsForIncomplete — gets suggestions for an incomplete fragment + * @param getSuggestionsForComplete - gets suggestions for a complete fragment + * @returns + */ +export function handleFragment( + innerText: string, + isFragmentComplete: (fragment: string) => boolean, + getSuggestionsForIncomplete: ( + fragment: string, + rangeToReplace?: { start: number; end: number } + ) => SuggestionRawDefinition[] | Promise, + getSuggestionsForComplete: ( + fragment: string, + rangeToReplace: { start: number; end: number } + ) => SuggestionRawDefinition[] | Promise +): SuggestionRawDefinition[] | Promise { + /** + * @TODO — this string manipulation is crude and can't support all cases + * Checking for a partial word and computing the replacement range should + * really be done using the AST node, but we'll have to refactor further upstream + * to make that available. This is a quick fix to support the most common case. + */ + const fragment = findFinalWord(innerText); + if (!fragment) { + return getSuggestionsForIncomplete(''); + } else { + const rangeToReplace = { + start: innerText.length - fragment.length + 1, + end: innerText.length + 1, + }; + if (isFragmentComplete(fragment)) { + return getSuggestionsForComplete(fragment, rangeToReplace); + } else { + return getSuggestionsForIncomplete(fragment, rangeToReplace); + } + } +} +/** + * TODO — split this into distinct functions, one for fields, one for functions, one for literals + */ +export async function getFieldsOrFunctionsSuggestions( + types: string[], + commandName: string, + optionName: string | undefined, + getFieldsByType: GetColumnsByTypeFn, + { + functions, + fields, + variables, + literals = false, + }: { + functions: boolean; + fields: boolean; + variables?: Map; + literals?: boolean; + }, + { + ignoreFn = [], + ignoreColumns = [], + }: { + ignoreFn?: string[]; + ignoreColumns?: string[]; + } = {} +): Promise { + const filteredFieldsByType = pushItUpInTheList( + (await (fields + ? getFieldsByType(types, ignoreColumns, { + advanceCursor: commandName === 'sort', + openSuggestions: commandName === 'sort', + }) + : [])) as SuggestionRawDefinition[], + functions + ); + + const filteredVariablesByType: string[] = []; + if (variables) { + for (const variable of variables.values()) { + if ( + (types.includes('any') || types.includes(variable[0].type)) && + !ignoreColumns.includes(variable[0].name) + ) { + filteredVariablesByType.push(variable[0].name); + } + } + // due to a bug on the ES|QL table side, filter out fields list with underscored variable names (??) + // avg( numberField ) => avg_numberField_ + const ALPHANUMERIC_REGEXP = /[^a-zA-Z\d]/g; + if ( + filteredVariablesByType.length && + filteredVariablesByType.some((v) => ALPHANUMERIC_REGEXP.test(v)) + ) { + for (const variable of filteredVariablesByType) { + const underscoredName = variable.replace(ALPHANUMERIC_REGEXP, '_'); + const index = filteredFieldsByType.findIndex( + ({ label }) => underscoredName === label || `_${underscoredName}_` === label + ); + if (index >= 0) { + filteredFieldsByType.splice(index); + } + } + } + } + // could also be in stats (bucket) but our autocomplete is not great yet + const displayDateSuggestions = types.includes('date') && ['where', 'eval'].includes(commandName); + + const suggestions = filteredFieldsByType.concat( + displayDateSuggestions ? getDateLiterals() : [], + functions ? getCompatibleFunctionDefinition(commandName, optionName, types, ignoreFn) : [], + variables + ? pushItUpInTheList(buildVariablesDefinitions(filteredVariablesByType), functions) + : [], + literals ? getCompatibleLiterals(commandName, types) : [] + ); + + return suggestions; +} + +export function pushItUpInTheList(suggestions: SuggestionRawDefinition[], shouldPromote: boolean) { + if (!shouldPromote) { + return suggestions; + } + return suggestions.map(({ sortText, ...rest }) => ({ + ...rest, + sortText: `1${sortText}`, + })); +} + +export function extractTypeFromASTArg( + arg: ESQLAstItem, + references: Pick +): + | ESQLLiteral['literalType'] + | SupportedDataType + | FunctionReturnType + | 'timeInterval' + | string // @TODO remove this + | undefined { + if (Array.isArray(arg)) { + return extractTypeFromASTArg(arg[0], references); + } + if (isColumnItem(arg) || isLiteralItem(arg)) { + if (isLiteralItem(arg)) { + return arg.literalType; + } + if (isColumnItem(arg)) { + const hit = getColumnForASTNode(arg, references); + if (hit) { + return hit.type; + } + } + } + if (isTimeIntervalItem(arg)) { + return arg.type; + } + if (isFunctionItem(arg)) { + const fnDef = getFunctionDefinition(arg.name); + if (fnDef) { + // @TODO: improve this to better filter down the correct return type based on existing arguments + // just mind that this can be highly recursive... + return fnDef.signatures[0].returnType; + } + } +} diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts index fbcfbabb2b63c..29c598af93501 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts @@ -7,11 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { SuggestionRawDefinition, GetFieldsByTypeFn } from '../types'; +import type { SuggestionRawDefinition, GetColumnsByTypeFn } from '../types'; import { getRecommendedQueries } from './templates'; export const getRecommendedQueriesSuggestions = async ( - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, fromCommand: string = '' ): Promise => { const fieldSuggestions = await getFieldsByType('date', [], { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts index 030bff4da181c..cbd6ead535932 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts @@ -81,7 +81,7 @@ export interface EditorContext { triggerKind: number; } -export type GetFieldsByTypeFn = ( +export type GetColumnsByTypeFn = ( type: string | string[], ignored?: string[], options?: { advanceCursor?: boolean; openSuggestions?: boolean; addComma?: boolean } diff --git a/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts b/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts index b608570854950..4563379642767 100644 --- a/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts @@ -18,7 +18,7 @@ import type { ESQLCallbacks, PartialFieldsMetadataClient } from '../shared/types function getCallbackMocks(): jest.Mocked { return { - getFieldsFor: jest.fn, any>(async ({ query }) => { + getColumnsFor: jest.fn, any>(async ({ query }) => { if (/enrich/.test(query)) { const fields: ESQLRealField[] = [ { name: 'otherField', type: 'keyword' }, @@ -375,11 +375,11 @@ describe('quick fixes logic', () => { const statement = `FROM index | DROP any#Char$Field`; const { errors } = await validateQuery(statement, getAstAndSyntaxErrors, undefined, { ...callbackMocks, - getFieldsFor: undefined, + getColumnsFor: undefined, }); const edits = await getActions(statement, errors, getAstAndSyntaxErrors, undefined, { ...callbackMocks, - getFieldsFor: undefined, + getColumnsFor: undefined, }); expect(edits.length).toBe(0); }); @@ -400,7 +400,7 @@ describe('quick fixes logic', () => { const statement = `FROM index | DROP any#Char$Field`; const { errors } = await validateQuery(statement, getAstAndSyntaxErrors, undefined, { ...callbackMocks, - getFieldsFor: undefined, + getColumnsFor: undefined, getFieldsMetadata: undefined, }); const actions = await getActions( @@ -412,7 +412,7 @@ describe('quick fixes logic', () => { }, { ...callbackMocks, - getFieldsFor: undefined, + getColumnsFor: undefined, getFieldsMetadata: undefined, } ); @@ -435,7 +435,7 @@ describe('quick fixes logic', () => { ); try { await getActions(statement, errors, getAstAndSyntaxErrors, undefined, { - getFieldsFor: undefined, + getColumnsFor: undefined, getSources: undefined, getPolicies: undefined, }); @@ -460,7 +460,7 @@ describe('quick fixes logic', () => { getAstAndSyntaxErrors, { relaxOnMissingCallbacks: true }, { - getFieldsFor: undefined, + getColumnsFor: undefined, getSources: undefined, getPolicies: undefined, getFieldsMetadata: undefined, diff --git a/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts b/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts index 02627c5f1abdf..37ab56350ffb2 100644 --- a/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts +++ b/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts @@ -403,7 +403,7 @@ export async function getActions( const { getPolicies, getPolicyFields } = getPolicyRetriever(resourceRetriever); const callbacks = { - getFieldsByType: resourceRetriever?.getFieldsFor ? getFieldsByType : undefined, + getFieldsByType: resourceRetriever?.getColumnsFor ? getFieldsByType : undefined, getSources: resourceRetriever?.getSources ? getSources : undefined, getPolicies: resourceRetriever?.getPolicies ? getPolicies : undefined, getPolicyFields: resourceRetriever?.getPolicies ? getPolicyFields : undefined, diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index 54504ac1a2a18..f4482a5b33c17 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -32,6 +32,9 @@ import { withOption, } from './options'; import type { CommandDefinition } from './types'; +import { suggest as suggestForSort } from '../autocomplete/commands/sort'; +import { suggest as suggestForKeep } from '../autocomplete/commands/keep'; +import { suggest as suggestForDrop } from '../autocomplete/commands/drop'; const statsValidator = (command: ESQLCommand) => { const messages: ESQLMessage[] = []; @@ -148,7 +151,7 @@ const statsValidator = (command: ESQLCommand) => { } return messages; }; -export const commandDefinitions: CommandDefinition[] = [ +export const commandDefinitions: Array> = [ { name: 'row', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.rowDoc', { @@ -311,6 +314,7 @@ export const commandDefinitions: CommandDefinition[] = [ defaultMessage: 'Rearranges fields in the input table by applying the keep clauses in fields', }), examples: ['… | keep a', '… | keep a,b'], + suggest: suggestForKeep, options: [], modes: [], signature: { @@ -330,6 +334,7 @@ export const commandDefinitions: CommandDefinition[] = [ multipleParams: true, params: [{ name: 'column', type: 'column', wildcards: true }], }, + suggest: suggestForDrop, validate: (command: ESQLCommand) => { const messages: ESQLMessage[] = []; const wildcardItems = command.args.filter((arg) => isColumnItem(arg) && arg.name === '*'); @@ -386,7 +391,9 @@ export const commandDefinitions: CommandDefinition[] = [ multipleParams: true, params: [{ name: 'expression', type: 'any' }], }, + suggest: suggestForSort, }, + { name: 'where', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.whereDoc', { diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts index 867c68ab4f1df..2b50c9da541ce 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts @@ -56,14 +56,13 @@ function handleAdditionalArgs( } export function getCommandSignature( - { name, signature, options, examples }: CommandDefinition, + { name, signature, options, examples }: CommandDefinition, { withTypes }: { withTypes: boolean } = { withTypes: true } ) { return { - declaration: `${name.toUpperCase()} ${printCommandArguments( - signature, - withTypes - )} ${options.map( + declaration: `${name.toUpperCase()} ${printCommandArguments(signature, withTypes)} ${( + options || [] + ).map( (option) => `${ option.wrapped ? option.wrapped[0] : '' @@ -76,7 +75,7 @@ export function getCommandSignature( } function printCommandArguments( - { multipleParams, params }: CommandDefinition['signature'], + { multipleParams, params }: CommandDefinition['signature'], withTypes: boolean ): string { return `${params.map((arg) => printCommandArgument(arg, withTypes)).join(', `')}${ @@ -87,7 +86,7 @@ function printCommandArguments( } function printCommandArgument( - param: CommandDefinition['signature']['params'][number], + param: CommandDefinition['signature']['params'][number], withTypes: boolean ): string { if (!withTypes) { diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts index dee08766745df..a83908b41617f 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -8,6 +8,7 @@ */ import type { ESQLCommand, ESQLCommandOption, ESQLFunction, ESQLMessage } from '@kbn/esql-ast'; +import { GetColumnsByTypeFn, SuggestionRawDefinition } from '../autocomplete/types'; /** * All supported field types in ES|QL. This is all the types @@ -158,14 +159,21 @@ export interface FunctionDefinition { validate?: (fnDef: ESQLFunction) => ESQLMessage[]; } -export interface CommandBaseDefinition { - name: string; +export interface CommandBaseDefinition { + name: CommandName; alias?: string; description: string; /** * Whether to show or hide in autocomplete suggestion list */ hidden?: boolean; + suggest?: ( + innerText: string, + command: ESQLCommand, + getColumnsByType: GetColumnsByTypeFn, + columnExists: (column: string) => boolean + ) => Promise; + /** @deprecated this property will disappear in the future */ signature: { multipleParams: boolean; // innerTypes here is useful to drill down the type in case of "column" @@ -183,7 +191,8 @@ export interface CommandBaseDefinition { }; } -export interface CommandOptionsDefinition extends CommandBaseDefinition { +export interface CommandOptionsDefinition + extends CommandBaseDefinition { wrapped?: string[]; optional: boolean; skipCommonValidation?: boolean; @@ -201,12 +210,15 @@ export interface CommandModeDefinition { prefix?: string; } -export interface CommandDefinition extends CommandBaseDefinition { - options: CommandOptionsDefinition[]; +export interface CommandDefinition + extends CommandBaseDefinition { examples: string[]; validate?: (option: ESQLCommand) => ESQLMessage[]; - modes: CommandModeDefinition[]; hasRecommendedQueries?: boolean; + /** @deprecated this property will disappear in the future */ + modes: CommandModeDefinition[]; + /** @deprecated this property will disappear in the future */ + options: CommandOptionsDefinition[]; } export interface Literals { diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts index e2e6397005e22..b5f14ecfd0227 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts @@ -9,7 +9,7 @@ import { parse } from '@kbn/esql-ast'; import { getExpressionType, shouldBeQuotedSource } from './helpers'; -import { SupportedDataType } from '../definitions/types'; +import type { SupportedDataType } from '../definitions/types'; import { setTestFunctions } from './test_functions'; describe('shouldBeQuotedSource', () => { diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index 18d6ae6faa246..02dff9720cd9b 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -132,7 +132,7 @@ export function isSourceCommand({ label }: { label: string }) { } let fnLookups: Map | undefined; -let commandLookups: Map | undefined; +let commandLookups: Map> | undefined; function buildFunctionLookup() { // we always refresh if we have test functions @@ -197,7 +197,7 @@ export function getFunctionDefinition(name: string) { const unwrapStringLiteralQuotes = (value: string) => value.slice(1, -1); -function buildCommandLookup() { +function buildCommandLookup(): Map> { if (!commandLookups) { commandLookups = commandDefinitions.reduce((memo, def) => { memo.set(def.name, def); @@ -205,12 +205,12 @@ function buildCommandLookup() { memo.set(def.alias, def); } return memo; - }, new Map()); + }, new Map>()); } - return commandLookups; + return commandLookups!; } -export function getCommandDefinition(name: string): CommandDefinition { +export function getCommandDefinition(name: string): CommandDefinition { return buildCommandLookup().get(name.toLowerCase())!; } @@ -218,7 +218,7 @@ export function getAllCommands() { return Array.from(buildCommandLookup().values()); } -export function getCommandOption(optionName: CommandOptionsDefinition['name']) { +export function getCommandOption(optionName: CommandOptionsDefinition['name']) { return [byOption, metadataOption, asOption, onOption, withOption, appendSeparatorOption].find( ({ name }) => name === optionName ); diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts index a4da3907a4d6b..5e7d951d8bdbf 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts @@ -36,7 +36,7 @@ export function getFieldsByTypeHelper(queryText: string, resourceRetriever?: ESQ const getFields = async () => { const metadata = await getEcsMetadata(); if (!cacheFields.size && queryText) { - const fieldsOfType = await resourceRetriever?.getFieldsFor?.({ query: queryText }); + const fieldsOfType = await resourceRetriever?.getColumnsFor?.({ query: queryText }); const fieldsWithMetadata = enrichFieldsWithECSInfo(fieldsOfType || [], metadata); for (const field of fieldsWithMetadata || []) { cacheFields.set(field.name, field); diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/types.ts b/packages/kbn-esql-validation-autocomplete/src/shared/types.ts index bc1e1d337e4b3..1caa2c480864e 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/types.ts @@ -39,7 +39,7 @@ export interface ESQLSourceResult { export interface ESQLCallbacks { getSources?: CallbackFn<{}, ESQLSourceResult>; - getFieldsFor?: CallbackFn<{ query: string }, ESQLRealField>; + getColumnsFor?: CallbackFn<{ query: string }, ESQLRealField>; getPolicies?: CallbackFn< {}, { name: string; sourceIndices: string[]; matchField: string; enrichFields: string[] } diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/callbacks.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/callbacks.test.ts index aaa7a3d88f5ca..61c0455fa1b0d 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/callbacks.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/callbacks.test.ts @@ -19,7 +19,7 @@ describe('FROM', () => { await validate('SHOW'); await validate('ROW \t'); - expect(callbacks.getFieldsFor.mock.calls.length).toBe(0); + expect(callbacks.getColumnsFor.mock.calls.length).toBe(0); }); test('loads fields with FROM source when commands after pipe present', async () => { @@ -27,6 +27,6 @@ describe('FROM', () => { await validate('FROM kibana_ecommerce METADATA _id | eval'); - expect(callbacks.getFieldsFor.mock.calls.length).toBe(1); + expect(callbacks.getColumnsFor.mock.calls.length).toBe(1); }); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index fae4ca16797cc..a9ecac9663609 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -1532,7 +1532,7 @@ describe('validation logic', () => { it(`should not fetch source and fields list when a row command is set`, async () => { const callbackMocks = getCallbackMocks(); await validateQuery(`row a = 1 | eval a`, getAstAndSyntaxErrors, undefined, callbackMocks); - expect(callbackMocks.getFieldsFor).not.toHaveBeenCalled(); + expect(callbackMocks.getColumnsFor).not.toHaveBeenCalled(); expect(callbackMocks.getSources).not.toHaveBeenCalled(); }); @@ -1545,7 +1545,7 @@ describe('validation logic', () => { it(`should not fetch source and fields for empty command`, async () => { const callbackMocks = getCallbackMocks(); await validateQuery(` `, getAstAndSyntaxErrors, undefined, callbackMocks); - expect(callbackMocks.getFieldsFor).not.toHaveBeenCalled(); + expect(callbackMocks.getColumnsFor).not.toHaveBeenCalled(); expect(callbackMocks.getSources).not.toHaveBeenCalled(); }); @@ -1559,8 +1559,8 @@ describe('validation logic', () => { ); expect(callbackMocks.getSources).not.toHaveBeenCalled(); expect(callbackMocks.getPolicies).toHaveBeenCalled(); - expect(callbackMocks.getFieldsFor).toHaveBeenCalledTimes(1); - expect(callbackMocks.getFieldsFor).toHaveBeenLastCalledWith({ + expect(callbackMocks.getColumnsFor).toHaveBeenCalledTimes(1); + expect(callbackMocks.getColumnsFor).toHaveBeenLastCalledWith({ query: `from enrich_index | keep otherField, yetAnotherField`, }); }); @@ -1575,8 +1575,8 @@ describe('validation logic', () => { ); expect(callbackMocks.getSources).not.toHaveBeenCalled(); expect(callbackMocks.getPolicies).not.toHaveBeenCalled(); - expect(callbackMocks.getFieldsFor).toHaveBeenCalledTimes(1); - expect(callbackMocks.getFieldsFor).toHaveBeenLastCalledWith({ + expect(callbackMocks.getColumnsFor).toHaveBeenCalledTimes(1); + expect(callbackMocks.getColumnsFor).toHaveBeenLastCalledWith({ query: 'show info', }); }); @@ -1591,8 +1591,8 @@ describe('validation logic', () => { ); expect(callbackMocks.getSources).toHaveBeenCalled(); expect(callbackMocks.getPolicies).toHaveBeenCalled(); - expect(callbackMocks.getFieldsFor).toHaveBeenCalledTimes(2); - expect(callbackMocks.getFieldsFor).toHaveBeenLastCalledWith({ + expect(callbackMocks.getColumnsFor).toHaveBeenCalledTimes(2); + expect(callbackMocks.getColumnsFor).toHaveBeenLastCalledWith({ query: `from enrich_index | keep otherField, yetAnotherField`, }); }); @@ -1604,7 +1604,7 @@ describe('validation logic', () => { getAstAndSyntaxErrors, undefined, { - getFieldsFor: undefined, + getColumnsFor: undefined, getSources: undefined, getPolicies: undefined, } @@ -1718,7 +1718,7 @@ describe('validation logic', () => { const contentByCallback = { getSources: /Unknown index/, getPolicies: /Unknown policy/, - getFieldsFor: /Unknown column|Argument of|it is unsupported or not indexed/, + getColumnsFor: /Unknown column|Argument of|it is unsupported or not indexed/, getPreferences: /Unknown/, getFieldsMetadata: /Unknown/, }; @@ -1761,7 +1761,7 @@ describe('validation logic', () => { }); // test excluding one callback at the time - it.each(['getSources', 'getFieldsFor', 'getPolicies'] as Array)( + it.each(['getSources', 'getColumnsFor', 'getPolicies'] as Array)( `should not error if %s is missing`, async (excludedCallback) => { const filteredTestCases = fixtures.testCases.filter((t) => @@ -1790,7 +1790,7 @@ describe('validation logic', () => { ); it('should work if no callback passed', async () => { - const excludedCallbacks = ['getSources', 'getPolicies', 'getFieldsFor'] as Array< + const excludedCallbacks = ['getSources', 'getPolicies', 'getColumnsFor'] as Array< keyof typeof ignoreErrorsMap >; for (const testCase of fixtures.testCases.filter((t) => diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts index 9605da8460eed..111fe79b3f5e0 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts @@ -1074,7 +1074,7 @@ function validateUnsupportedTypeFields(fields: Map) { } export const ignoreErrorsMap: Record = { - getFieldsFor: ['unknownColumn', 'wrongArgumentType', 'unsupportedFieldType'], + getColumnsFor: ['unknownColumn', 'wrongArgumentType', 'unsupportedFieldType'], getSources: ['unknownIndex'], getPolicies: ['unknownPolicy'], getPreferences: [],