diff --git a/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx b/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx index b5f075fb2f095..49881e4ef8e6b 100644 --- a/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx +++ b/src/platform/packages/private/kbn-esql-editor/src/esql_editor.tsx @@ -618,11 +618,6 @@ export const ESQLEditor = memo(function ESQLEditor({ const hoverProvider = useMemo(() => ESQLLang.getHoverProvider?.(esqlCallbacks), [esqlCallbacks]); - const codeActionProvider = useMemo( - () => ESQLLang.getCodeActionProvider?.(esqlCallbacks), - [esqlCallbacks] - ); - const onErrorClick = useCallback(({ startLineNumber, startColumn }: MonacoMessage) => { if (!editor1.current) { return; @@ -770,7 +765,6 @@ export const ESQLEditor = memo(function ESQLEditor({ return hoverProvider?.provideHover(model, position, token); }, }} - codeActions={codeActionProvider} onChange={onQueryUpdate} editorDidMount={(editor) => { editor1.current = editor; diff --git a/src/platform/packages/private/kbn-esql-editor/src/helpers.ts b/src/platform/packages/private/kbn-esql-editor/src/helpers.ts index 8a3618784dd31..fa204ef49ca07 100644 --- a/src/platform/packages/private/kbn-esql-editor/src/helpers.ts +++ b/src/platform/packages/private/kbn-esql-editor/src/helpers.ts @@ -318,6 +318,9 @@ export const getEditorOverwrites = (theme: UseEuiTheme<{}>) => { .monaco-hover { display: block !important; } + .hover-row.status-bar { + display: none; + } .margin-view-overlays .line-numbers { color: ${theme.euiTheme.colors.textDisabled}; } diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/README.md b/src/platform/packages/shared/kbn-esql-validation-autocomplete/README.md index eaa97898a8e89..5eb8c9b0adbc0 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/README.md +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/README.md @@ -8,7 +8,6 @@ The package is structure as follow: ``` src | autocomplete // => the autocomplete/suggest service logic - | code_actions // => the quick fixes service logic | definitions // => static assets to define all components behaviour of a ES|QL query: commands, functions, etc... | validation // => the validation logic diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/index.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/index.ts index 181195b3978cd..9efef9289f6f3 100644 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/index.ts +++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/index.ts @@ -8,7 +8,6 @@ */ export type { SuggestionRawDefinition, ItemKind } from './src/autocomplete/types'; -export type { CodeAction } from './src/code_actions/types'; export type { FunctionDefinition, CommandDefinition, @@ -28,9 +27,6 @@ export { getAstContext } from './src/shared/context'; export { validateQuery } from './src/validation/validation'; // Autocomplete function export { suggest } from './src/autocomplete/autocomplete'; -// Quick fixes function -export { getActions } from './src/code_actions/actions'; - /** * Some utility functions that can be useful to build more feature * for the ES|QL language @@ -76,6 +72,4 @@ export { getSourcesHelper, } from './src/shared/resources_helpers'; -export { wrapAsEditorMessage } from './src/code_actions/utils'; - export { getRecommendedQueries } from './src/autocomplete/recommended_queries/templates'; diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts deleted file mode 100644 index 85317fb44c59f..0000000000000 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts +++ /dev/null @@ -1,516 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { getActions } from './actions'; -import { validateQuery } from '../validation/validation'; -import { getAllFunctions } from '../shared/helpers'; -import { getAstAndSyntaxErrors } from '@kbn/esql-ast'; -import type { CodeActionOptions } from './types'; -import type { ESQLRealField } from '../validation/types'; -import { type FieldType, FunctionDefinitionTypes } from '../definitions/types'; -import type { ESQLCallbacks, PartialFieldsMetadataClient } from '../shared/types'; -import { FULL_TEXT_SEARCH_FUNCTIONS } from '../shared/constants'; - -function getCallbackMocks(): jest.Mocked { - return { - getColumnsFor: jest.fn, any>(async ({ query }) => { - if (/enrich/.test(query)) { - const fields: ESQLRealField[] = [ - { name: 'otherField', type: 'keyword' }, - { name: 'yetAnotherField', type: 'double' }, - ]; - return fields; - } - - if (/unsupported_index/.test(query)) { - const fields: ESQLRealField[] = [{ name: 'unsupported_field', type: 'unsupported' }]; - return fields; - } - - const localDataTypes: FieldType[] = ['keyword', 'double', 'date', 'boolean', 'ip']; - const fields: ESQLRealField[] = [ - ...localDataTypes.map((type) => ({ - name: `${type}Field`, - type, - })), - { name: 'geoPointField', type: 'geo_point' }, - { name: 'any#Char$Field', type: 'double' }, - { name: 'kubernetes.something.something', type: 'double' }, - { name: '@timestamp', type: 'date' }, - ]; - - return fields; - }), - getSources: jest.fn(async () => - ['index', '.secretIndex', 'my-index'].map((name) => ({ - name, - hidden: name.startsWith('.'), - })) - ), - getPolicies: jest.fn(async () => [ - { - name: 'policy', - sourceIndices: ['enrichIndex1'], - matchField: 'otherStringField', - enrichFields: ['other-field', 'yetAnotherField'], - }, - { - name: 'policy[]', - sourceIndices: ['enrichIndex1'], - matchField: 'otherStringField', - enrichFields: ['other-field', 'yetAnotherField'], - }, - ]), - getFieldsMetadata: jest.fn(async () => ({ - find: jest.fn(async () => ({ - fields: {}, - })), - })) as unknown as Promise, - }; -} - -export type Simplify = { [KeyType in keyof T]: T[KeyType] } & {}; - -/** - * There are different wats to test the code here: one is a direct unit test of the feature, another is - * an integration test passing from the query statement validation. The latter is more realistic, but - * a little bit more tricky to setup. This function will encapsulate all the complexity - */ -function testQuickFixesFn( - statement: string, - expectedFixes: string[] = [], - options: Simplify<{ equalityCheck?: 'include' | 'equal' } & CodeActionOptions> = {}, - { only, skip }: { only?: boolean; skip?: boolean } = {} -) { - const testFn = only ? it.only : skip ? it.skip : it; - testFn( - `${statement} => ["${expectedFixes.join('","')}"]${ - options.relaxOnMissingCallbacks != null - ? ` (Relaxed = ${options.relaxOnMissingCallbacks})` - : '' - } `, - async () => { - const callbackMocks = getCallbackMocks(); - const { errors } = await validateQuery( - statement, - getAstAndSyntaxErrors, - undefined, - callbackMocks - ); - const { equalityCheck, ...fnOptions } = options || {}; - - const actions = await getActions( - statement, - errors, - getAstAndSyntaxErrors, - fnOptions, - callbackMocks - ); - const edits = actions.map(({ edits: actionEdits }) => actionEdits[0].text); - expect(edits).toEqual( - !equalityCheck || equalityCheck === 'equal' - ? expectedFixes - : expect.arrayContaining(expectedFixes) - ); - } - ); -} - -type TestArgs = [string, string[], { equalityCheck?: 'include' | 'equal' }?]; - -// Make only and skip work with our custom wrapper -const testQuickFixes = Object.assign(testQuickFixesFn, { - skip: (...args: TestArgs) => { - const paddingArgs = ['equal'].slice(args.length - 2); - return testQuickFixesFn(...((args.length > 1 ? [...args, ...paddingArgs] : args) as TestArgs), { - skip: true, - }); - }, - only: (...args: TestArgs) => { - const paddingArgs = ['equal'].slice(args.length - 2); - return testQuickFixesFn(...((args.length > 1 ? [...args, ...paddingArgs] : args) as TestArgs), { - only: true, - }); - }, -}); - -describe('quick fixes logic', () => { - describe('fixing index spellchecks', () => { - for (const options of [ - undefined, - { relaxOnMissingCallbacks: false }, - { relaxOnMissingCallbacks: false }, - ]) { - // No error, no quick action - testQuickFixes('FROM index', [], options); - testQuickFixes('FROM index2', ['index'], options); - testQuickFixes('FROM myindex', ['index', 'my-index'], options); - // wildcards - testQuickFixes('FROM index*', [], options); - testQuickFixes('FROM ind*', [], options); - testQuickFixes('FROM end*', ['ind*']); - testQuickFixes('FROM endex*', ['index'], options); - // Too far for the levenstein distance and should not fix with a hidden index - testQuickFixes('FROM secretIndex', [], options); - testQuickFixes('FROM secretIndex2', [], options); - testQuickFixes('from index | stats var0 = aveg(bytes) | eval ab(var0) | limit 1', [ - 'avg(bytes)', - 'abs(var0)', - 'e(var0)', - 'pi(var0)', - 'tan(var0)', - 'tau(var0)', - ]); - } - }); - - describe('fixing fields spellchecks', () => { - for (const options of [ - undefined, - { relaxOnMissingCallbacks: false }, - { relaxOnMissingCallbacks: false }, - ]) { - for (const command of ['KEEP', 'DROP', 'EVAL']) { - testQuickFixes(`FROM index | ${command} keywordField`, [], options); - // koywordField => keywordField - testQuickFixes(`FROM index | ${command} koywordField`, ['keywordField'], options); - testQuickFixes( - `FROM index | ${command} numberField, koywordField`, - ['keywordField'], - options - ); - } - testQuickFixes(`FROM index | EVAL round(koywordField)`, ['keywordField'], options); - testQuickFixes(`FROM index | EVAL var0 = round(koywordField)`, ['keywordField'], options); - testQuickFixes(`FROM index | WHERE round(koywordField) > 0`, ['keywordField'], options); - testQuickFixes(`FROM index | WHERE 0 < round(koywordField)`, ['keywordField'], options); - testQuickFixes(`FROM index | RENAME koywordField as newField`, ['keywordField'], options); - // This levarage the knowledge of the enrich policy fields to suggest the right field - testQuickFixes( - `FROM index | ENRICH policy | KEEP yetAnotherField2`, - ['yetAnotherField'], - options - ); - testQuickFixes(`FROM index | ENRICH policy ON koywordField`, ['keywordField'], options); - testQuickFixes( - `FROM index | ENRICH policy ON keywordField WITH yetAnotherField2`, - ['yetAnotherField'], - options - ); - - describe('metafields spellchecks', () => { - testQuickFixes(`FROM index ${'metadata _i_ndex'}`, ['_index'], options); - testQuickFixes(`FROM index ${'metadata _id, _i_ndex'}`, ['_index'], options); - testQuickFixes(`FROM index ${'METADATA _id, _i_ndex'}`, ['_index'], options); - }); - } - }); - - describe('fixing meta fields spellchecks', () => { - for (const options of [ - undefined, - { relaxOnMissingCallbacks: false }, - { relaxOnMissingCallbacks: false }, - ]) { - for (const command of ['KEEP', 'DROP', 'EVAL']) { - testQuickFixes(`FROM index | ${command} keywordField`, [], options); - // koywordField => keywordField - testQuickFixes(`FROM index | ${command} koywordField`, ['keywordField'], options); - testQuickFixes( - `FROM index | ${command} numberField, koywordField`, - ['keywordField'], - options - ); - } - testQuickFixes(`FROM index | EVAL round(koywordField)`, ['keywordField'], options); - testQuickFixes(`FROM index | EVAL var0 = round(koywordField)`, ['keywordField'], options); - testQuickFixes(`FROM index | WHERE round(koywordField) > 0`, ['keywordField'], options); - testQuickFixes(`FROM index | WHERE 0 < round(koywordField)`, ['keywordField'], options); - testQuickFixes(`FROM index | RENAME koywordField as newField`, ['keywordField'], options); - // This levarage the knowledge of the enrich policy fields to suggest the right field - testQuickFixes( - `FROM index | ENRICH policy | KEEP yetAnotherField2`, - ['yetAnotherField'], - options - ); - testQuickFixes(`FROM index | ENRICH policy ON koywordField`, ['keywordField'], options); - testQuickFixes( - `FROM index | ENRICH policy ON keywordField WITH yetAnotherField2`, - ['yetAnotherField'], - options - ); - } - }); - - describe('fixing policies spellchecks', () => { - for (const options of [ - undefined, - { relaxOnMissingCallbacks: false }, - { relaxOnMissingCallbacks: false }, - ]) { - testQuickFixes(`FROM index | ENRICH poli`, ['policy'], options); - testQuickFixes(`FROM index | ENRICH mypolicy`, ['policy'], options); - testQuickFixes(`FROM index | ENRICH policy[`, ['policy', 'policy[]'], options); - - describe('modes', () => { - testQuickFixes(`FROM index | ENRICH _ann:policy`, ['_any'], options); - const modes = ['_any', '_coordinator', '_remote']; - for (const mode of modes) { - testQuickFixes(`FROM index | ENRICH ${mode.replace('_', '@')}:policy`, [mode], options); - } - testQuickFixes(`FROM index | ENRICH unknown:policy`, modes, options); - }); - } - }); - - describe('fixing function spellchecks', () => { - function toFunctionSignature(name: string) { - return `${name}()`; - } - // it should be strange enough to make the function invalid - const BROKEN_PREFIX = 'Q'; - for (const options of [ - undefined, - { relaxOnMissingCallbacks: false }, - { relaxOnMissingCallbacks: false }, - ]) { - for (const fn of getAllFunctions({ type: FunctionDefinitionTypes.SCALAR })) { - if (FULL_TEXT_SEARCH_FUNCTIONS.includes(fn.name)) { - testQuickFixes( - `FROM index | WHERE ${BROKEN_PREFIX}${fn.name}()`, - [fn.name].map(toFunctionSignature), - { equalityCheck: 'include', ...options } - ); - } - } - for (const fn of getAllFunctions({ type: FunctionDefinitionTypes.SCALAR })) { - if (FULL_TEXT_SEARCH_FUNCTIONS.includes(fn.name)) continue; - // add an A to the function name to make it invalid - testQuickFixes( - `FROM index | EVAL ${BROKEN_PREFIX}${fn.name}()`, - [fn.name].map(toFunctionSignature), - { equalityCheck: 'include', ...options } - ); - testQuickFixes( - `FROM index | EVAL var0 = ${BROKEN_PREFIX}${fn.name}()`, - [fn.name].map(toFunctionSignature), - { equalityCheck: 'include', ...options } - ); - testQuickFixes( - `FROM index | STATS avg(${BROKEN_PREFIX}${fn.name}())`, - [fn.name].map(toFunctionSignature), - { equalityCheck: 'include', ...options } - ); - testQuickFixes( - `FROM index | STATS avg(numberField) BY ${BROKEN_PREFIX}${fn.name}()`, - [fn.name].map(toFunctionSignature), - { equalityCheck: 'include', ...options } - ); - testQuickFixes( - `FROM index | STATS avg(numberField) BY var0 = ${BROKEN_PREFIX}${fn.name}()`, - [fn.name].map(toFunctionSignature), - { equalityCheck: 'include', ...options } - ); - } - for (const fn of getAllFunctions({ type: FunctionDefinitionTypes.AGG })) { - if (FULL_TEXT_SEARCH_FUNCTIONS.includes(fn.name)) continue; - - // add an A to the function name to make it invalid - testQuickFixes( - `FROM index | STATS ${BROKEN_PREFIX}${fn.name}()`, - [fn.name].map(toFunctionSignature), - { equalityCheck: 'include', ...options } - ); - testQuickFixes( - `FROM index | STATS var0 = ${BROKEN_PREFIX}${fn.name}()`, - [fn.name].map(toFunctionSignature), - { equalityCheck: 'include', ...options } - ); - } - // it should preserve the arguments - testQuickFixes(`FROM index | EVAL rAund(numberField)`, ['round(numberField)'], { - equalityCheck: 'include', - ...options, - }); - testQuickFixes(`FROM index | STATS AVVG(numberField)`, ['avg(numberField)'], { - equalityCheck: 'include', - ...options, - }); - } - }); - - describe('fixing wrong quotes', () => { - for (const options of [ - undefined, - { relaxOnMissingCallbacks: false }, - { relaxOnMissingCallbacks: false }, - ]) { - testQuickFixes(`FROM index | WHERE keywordField like 'asda'`, ['"asda"'], options); - testQuickFixes(`FROM index | WHERE keywordField not like 'asda'`, ['"asda"'], options); - } - }); - - describe('fixing unquoted field names', () => { - for (const options of [ - undefined, - { relaxOnMissingCallbacks: false }, - { relaxOnMissingCallbacks: false }, - ]) { - testQuickFixes('FROM index | DROP any#Char$Field', ['`any#Char$Field`'], options); - testQuickFixes( - 'FROM index | DROP numberField, any#Char$Field', - ['`any#Char$Field`'], - options - ); - } - describe('with no callbacks', () => { - describe('with no relaxed option', () => { - it('return no result without callbacks and relaxed option', async () => { - const statement = `FROM index | DROP any#Char$Field`; - const { errors } = await validateQuery(statement, getAstAndSyntaxErrors); - const edits = await getActions(statement, errors, getAstAndSyntaxErrors); - expect(edits.length).toBe(0); - }); - - it('return no result without specific callback and relaxed option', async () => { - const callbackMocks = getCallbackMocks(); - const statement = `FROM index | DROP any#Char$Field`; - const { errors } = await validateQuery(statement, getAstAndSyntaxErrors, undefined, { - ...callbackMocks, - getColumnsFor: undefined, - }); - const edits = await getActions(statement, errors, getAstAndSyntaxErrors, undefined, { - ...callbackMocks, - getColumnsFor: undefined, - }); - expect(edits.length).toBe(0); - }); - }); - describe('with relaxed option', () => { - it('return a result without callbacks and relaxed option', async () => { - const statement = `FROM index | DROP any#Char$Field`; - const { errors } = await validateQuery(statement, getAstAndSyntaxErrors); - const actions = await getActions(statement, errors, getAstAndSyntaxErrors, { - relaxOnMissingCallbacks: true, - }); - const edits = actions.map(({ edits: actionEdits }) => actionEdits[0].text); - expect(edits).toEqual(['`any#Char$Field`']); - }); - - it('return a result without specific callback and relaxed option', async () => { - const callbackMocks = getCallbackMocks(); - const statement = `FROM index | DROP any#Char$Field`; - const { errors } = await validateQuery(statement, getAstAndSyntaxErrors, undefined, { - ...callbackMocks, - getColumnsFor: undefined, - getFieldsMetadata: undefined, - }); - const actions = await getActions( - statement, - errors, - getAstAndSyntaxErrors, - { - relaxOnMissingCallbacks: true, - }, - { - ...callbackMocks, - getColumnsFor: undefined, - getFieldsMetadata: undefined, - } - ); - const edits = actions.map(({ edits: actionEdits }) => actionEdits[0].text); - expect(edits).toEqual(['`any#Char$Field`']); - }); - }); - }); - }); - - describe('callbacks', () => { - it('should not crash if specific callback functions are not passed', async () => { - const callbackMocks = getCallbackMocks(); - const statement = `from a | eval b = a | enrich policy | dissect keywordField "%{firstWord}"`; - const { errors } = await validateQuery( - statement, - getAstAndSyntaxErrors, - undefined, - callbackMocks - ); - try { - await getActions(statement, errors, getAstAndSyntaxErrors, undefined, { - getColumnsFor: undefined, - getSources: undefined, - getPolicies: undefined, - }); - } catch { - fail('Should not throw'); - } - }); - - it('should not crash if specific callback functions are not passed with relaxed option', async () => { - const callbackMocks = getCallbackMocks(); - const statement = `from a | eval b = a | enrich policy | dissect keywordField "%{firstWord}"`; - const { errors } = await validateQuery( - statement, - getAstAndSyntaxErrors, - undefined, - callbackMocks - ); - try { - await getActions( - statement, - errors, - getAstAndSyntaxErrors, - { relaxOnMissingCallbacks: true }, - { - getColumnsFor: undefined, - getSources: undefined, - getPolicies: undefined, - getFieldsMetadata: undefined, - } - ); - } catch { - fail('Should not throw'); - } - }); - - it('should not crash no callbacks are passed', async () => { - const callbackMocks = getCallbackMocks(); - const statement = `from a | eval b = a | enrich policy | dissect keywordField "%{firstWord}"`; - const { errors } = await validateQuery( - statement, - getAstAndSyntaxErrors, - undefined, - callbackMocks - ); - try { - await getActions(statement, errors, getAstAndSyntaxErrors); - } catch { - fail('Should not throw'); - } - }); - - it('should not crash no callbacks are passed with relaxed option', async () => { - const callbackMocks = getCallbackMocks(); - const statement = `from a | eval b = a | enrich policy | dissect keywordField "%{firstWord}"`; - const { errors } = await validateQuery( - statement, - getAstAndSyntaxErrors, - undefined, - callbackMocks - ); - try { - await getActions(statement, errors, getAstAndSyntaxErrors, { - relaxOnMissingCallbacks: true, - }); - } catch { - fail('Should not throw'); - } - }); - }); -}); diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/actions.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/actions.ts deleted file mode 100644 index 1c9951ffeb2df..0000000000000 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/actions.ts +++ /dev/null @@ -1,511 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { i18n } from '@kbn/i18n'; -import { distance } from 'fastest-levenshtein'; -import { - type AstProviderFn, - type ESQLAst, - type EditorError, - type ESQLMessage, - isIdentifier, -} from '@kbn/esql-ast'; -import { uniqBy } from 'lodash'; -import { - getFieldsByTypeHelper, - getPolicyHelper, - getSourcesHelper, -} from '../shared/resources_helpers'; -import { - getAllFunctions, - getCommandDefinition, - isColumnItem, - isSourceItem, - shouldBeQuotedText, -} from '../shared/helpers'; -import { FunctionDefinitionTypes } from '../definitions/types'; -import { ESQLCallbacks } from '../shared/types'; -import { buildQueryForFieldsFromSource } from '../validation/helpers'; -import { DOUBLE_BACKTICK, SINGLE_TICK_REGEX, METADATA_FIELDS } from '../shared/constants'; -import type { CodeAction, Callbacks, CodeActionOptions } from './types'; -import { getAstContext } from '../shared/context'; -import { wrapAsEditorMessage } from './utils'; - -function getFieldsByTypeRetriever(queryString: string, resourceRetriever?: ESQLCallbacks) { - const helpers = getFieldsByTypeHelper(queryString, resourceRetriever); - return { - getFieldsByType: async (expectedType: string | string[] = 'any', ignored: string[] = []) => { - const fields = await helpers.getFieldsByType(expectedType, ignored); - return fields; - }, - getFieldsMap: helpers.getFieldsMap, - }; -} - -function getPolicyRetriever(resourceRetriever?: ESQLCallbacks) { - const helpers = getPolicyHelper(resourceRetriever); - return { - getPolicies: async () => { - const policies = await helpers.getPolicies(); - return policies.map(({ name }) => name); - }, - getPolicyFields: async (policy: string) => { - const metadata = await helpers.getPolicyMetadata(policy); - return metadata?.enrichFields || []; - }, - }; -} - -function getSourcesRetriever(resourceRetriever?: ESQLCallbacks) { - const helper = getSourcesHelper(resourceRetriever); - return async () => { - const list = (await helper()) || []; - // hide indexes that start with . - return list.filter(({ hidden }) => !hidden).map(({ name }) => name); - }; -} - -export const getCompatibleFunctionDefinitions = ( - command: string, - option: string | undefined -): string[] => { - const fnSupportedByCommand = getAllFunctions({ - type: [FunctionDefinitionTypes.SCALAR, FunctionDefinitionTypes.AGG], - }).filter(({ name, supportedCommands, supportedOptions }) => - option ? supportedOptions?.includes(option) : supportedCommands.includes(command) - ); - return fnSupportedByCommand.map(({ name }) => name); -}; - -function createAction(title: string, solution: string, error: EditorError): CodeAction { - return { - title, - diagnostics: [error], - kind: 'quickfix', - edits: [ - { - range: error, - text: solution, - }, - ], - }; -} - -async function getSpellingPossibilities(fn: () => Promise, errorText: string) { - const allPossibilities = await fn(); - const allSolutions = allPossibilities.reduce((solutions, item) => { - if (distance(item, errorText) < 3) { - solutions.push(item); - } - return solutions; - }, [] as string[]); - // filter duplicates - return Array.from(new Set(allSolutions)); -} - -async function getSpellingActionForColumns( - error: EditorError, - queryString: string, - ast: ESQLAst, - options: CodeActionOptions, - { getFieldsByType, getPolicies, getPolicyFields }: Partial -) { - const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); - if (!getFieldsByType || !getPolicyFields) { - return []; - } - // @TODO add variables support - const possibleFields = await getSpellingPossibilities(async () => { - const availableFields = (await getFieldsByType('any')).map(({ name }) => name); - const enrichPolicies = ast.filter(({ name }) => name === 'enrich'); - if (enrichPolicies.length) { - const enrichPolicyNames = enrichPolicies.flatMap(({ args }) => - args.filter(isSourceItem).map(({ name }) => name) - ); - const enrichFields = await Promise.all(enrichPolicyNames.map(getPolicyFields)); - availableFields.push(...enrichFields.flat()); - } - return availableFields; - }, errorText); - return wrapIntoSpellingChangeAction(error, possibleFields); -} - -function extractUnquotedFieldText( - query: string, - errorType: string, - ast: ESQLAst, - possibleStart: number, - end: number -) { - if (errorType === 'syntaxError') { - // scope it down to column items for now - const { node } = getAstContext(query, ast, possibleStart - 1); - if (node && (isColumnItem(node) || isIdentifier(node))) { - return { - start: node.location.min + 1, - name: query.substring(node.location.min, end).trimEnd(), - }; - } - } - return { start: possibleStart + 1, name: query.substring(possibleStart, end - 1).trimEnd() }; -} - -async function getQuotableActionForColumns( - error: EditorError, - queryString: string, - ast: ESQLAst, - options: CodeActionOptions, - { getFieldsByType }: Partial -): Promise { - const commandEndIndex = ast.find( - (command) => - error.startColumn > command.location.min && error.startColumn < command.location.max - )?.location.max; - - // the error received is unknwonColumn here, but look around the column to see if there's more - // which broke the grammar and the validation code couldn't identify as unquoted column - const remainingCommandText = queryString.substring( - error.endColumn - 1, - commandEndIndex ? commandEndIndex + 1 : undefined - ); - const stopIndex = Math.max( - /[()]/.test(remainingCommandText) - ? remainingCommandText.indexOf(')') - : /,/.test(remainingCommandText) - ? remainingCommandText.indexOf(',') - 1 - : /\s/.test(remainingCommandText) - ? remainingCommandText.indexOf(' ') - : remainingCommandText.length, - 0 - ); - const possibleUnquotedText = queryString.substring( - error.endColumn - 1, - error.endColumn + stopIndex - ); - const { start, name: errorText } = extractUnquotedFieldText( - queryString, - error.code || 'syntaxError', - ast, - error.startColumn - 1, - error.endColumn + possibleUnquotedText.length - 1 - ); - const actions: CodeAction[] = []; - if (shouldBeQuotedText(errorText)) { - const solution = `\`${errorText.replace(SINGLE_TICK_REGEX, DOUBLE_BACKTICK)}\``; - if (!getFieldsByType) { - if (!options.relaxOnMissingCallbacks) { - return []; - } - const textHasAlreadyQuotes = /`/.test(errorText); - if (textHasAlreadyQuotes) { - return []; - } - actions.push( - createAction( - i18n.translate('kbn-esql-validation-autocomplete.esql.quickfix.replaceWithSolution', { - defaultMessage: 'Did you mean {solution} ?', - values: { - solution, - }, - }), - solution, - { ...error, startColumn: start, endColumn: start + errorText.length } // override the location - ) - ); - } else { - const availableFields = new Set((await getFieldsByType('any')).map(({ name }) => name)); - if (availableFields.has(errorText) || availableFields.has(solution)) { - actions.push( - createAction( - i18n.translate('kbn-esql-validation-autocomplete.esql.quickfix.replaceWithSolution', { - defaultMessage: 'Did you mean {solution} ?', - values: { - solution, - }, - }), - solution, - { ...error, startColumn: start, endColumn: start + errorText.length } // override the location - ) - ); - } - } - } - return actions; -} - -async function getSpellingActionForIndex( - error: EditorError, - queryString: string, - ast: ESQLAst, - options: CodeActionOptions, - { getSources }: Partial -) { - if (!getSources) { - return []; - } - const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); - const possibleSources = await getSpellingPossibilities(async () => { - // Handle fuzzy names via truncation to test levenstein distance - const sources = await getSources(); - if (errorText.endsWith('*')) { - return sources.map((source) => - source.length > errorText.length ? source.substring(0, errorText.length - 1) + '*' : source - ); - } - return sources; - }, errorText); - return wrapIntoSpellingChangeAction(error, possibleSources); -} - -async function getSpellingActionForPolicies( - error: EditorError, - queryString: string, - ast: ESQLAst, - options: CodeActionOptions, - { getPolicies }: Partial -) { - if (!getPolicies) { - return []; - } - const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); - const possiblePolicies = await getSpellingPossibilities(getPolicies, errorText); - return wrapIntoSpellingChangeAction(error, possiblePolicies); -} - -async function getSpellingActionForFunctions( - error: EditorError, - queryString: string, - ast: ESQLAst -) { - const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); - // fallback to the last command if not found - const commandContext = - ast.find( - (command) => - error.startColumn > command.location.min && error.startColumn < command.location.max - ) || ast[ast.length - 1]; - if (!commandContext) { - return []; - } - const possibleSolutions = await getSpellingPossibilities( - async () => - getCompatibleFunctionDefinitions(commandContext.name, undefined).concat( - // support nested expressions in STATS - commandContext.name === 'stats' ? getCompatibleFunctionDefinitions('eval', undefined) : [] - ), - errorText.substring(0, errorText.lastIndexOf('(')).toLowerCase() // reduce a bit the distance check making al lowercase - ); - return wrapIntoSpellingChangeAction( - error, - possibleSolutions.map((fn) => `${fn}${errorText.substring(errorText.lastIndexOf('('))}`) - ); -} - -async function getSpellingActionForMetadata( - error: EditorError, - queryString: string, - ast: ESQLAst, - options: CodeActionOptions -) { - const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); - const allSolutions = METADATA_FIELDS.reduce((solutions, item) => { - const dist = distance(item, errorText); - if (dist < 3) { - solutions.push(item); - } - return solutions; - }, [] as string[]); - // filter duplicates - const possibleMetafields = Array.from(new Set(allSolutions)); - return wrapIntoSpellingChangeAction(error, possibleMetafields); -} - -async function getSpellingActionForEnrichMode( - error: EditorError, - queryString: string, - ast: ESQLAst, - options: CodeActionOptions, - _callbacks: Partial -) { - const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1); - const commandContext = - ast.find((command) => command.location.max > error.endColumn) || ast[ast.length - 1]; - if (!commandContext) { - return []; - } - const commandDef = getCommandDefinition(commandContext.name); - const allModes = - commandDef.modes?.flatMap(({ values, prefix }) => - values.map(({ name }) => `${prefix || ''}${name}`) - ) || []; - const possibleEnrichModes = await getSpellingPossibilities(async () => allModes, errorText); - // if no possible solution is found, push all modes - if (!possibleEnrichModes.length) { - possibleEnrichModes.push(...allModes); - } - return wrapIntoSpellingChangeAction(error, possibleEnrichModes); -} - -function wrapIntoSpellingChangeAction( - error: EditorError, - possibleSolution: string[] -): CodeAction[] { - return possibleSolution.map((solution) => - createAction( - // @TODO: workout why the tooltip is truncating the title here - i18n.translate('kbn-esql-validation-autocomplete.esql.quickfix.replaceWithSolution', { - defaultMessage: 'Did you mean {solution} ?', - values: { - solution, - }, - }), - solution, - error - ) - ); -} - -function extractQuotedText(rawText: string, error: EditorError) { - return rawText.substring(error.startColumn - 2, error.endColumn); -} - -function inferCodeFromError( - error: EditorError & { owner?: string }, - ast: ESQLAst, - rawText: string -) { - if (error.message.endsWith('expecting QUOTED_STRING')) { - const value = extractQuotedText(rawText, error); - return /^'(.)*'$/.test(value) ? 'wrongQuotes' : undefined; - } - if (error.message.startsWith('SyntaxError: token recognition error at:')) { - // scope it down to column items for now - const { node } = getAstContext(rawText, ast, error.startColumn - 2); - return node && (isColumnItem(node) || isIdentifier(node)) ? 'quotableFields' : undefined; - } -} - -export async function getActions( - innerText: string, - markers: Array, - astProvider: AstProviderFn, - options: CodeActionOptions = {}, - resourceRetriever?: ESQLCallbacks -): Promise { - const actions: CodeAction[] = []; - if (markers.length === 0) { - return actions; - } - const editorMarkers = wrapAsEditorMessage('error', markers); - const { ast } = await astProvider(innerText); - - const queryForFields = buildQueryForFieldsFromSource(innerText, ast); - const { getFieldsByType } = getFieldsByTypeRetriever(queryForFields, resourceRetriever); - const getSources = getSourcesRetriever(resourceRetriever); - const { getPolicies, getPolicyFields } = getPolicyRetriever(resourceRetriever); - - const callbacks = { - getFieldsByType: resourceRetriever?.getColumnsFor ? getFieldsByType : undefined, - getSources: resourceRetriever?.getSources ? getSources : undefined, - getPolicies: resourceRetriever?.getPolicies ? getPolicies : undefined, - getPolicyFields: resourceRetriever?.getPolicies ? getPolicyFields : undefined, - }; - - // Markers are sent only on hover and are limited to the hovered area - // so unless there are multiple error/markers for the same area, there's just one - // in some cases, like syntax + semantic errors (i.e. unquoted fields eval field-1 ), there might be more than one - for (const error of editorMarkers) { - const code = error.code ?? inferCodeFromError(error, ast, innerText); - switch (code) { - case 'unknownColumn': { - const [columnsSpellChanges, columnsQuotedChanges] = await Promise.all([ - getSpellingActionForColumns(error, innerText, ast, options, callbacks), - getQuotableActionForColumns(error, innerText, ast, options, callbacks), - ]); - actions.push(...(columnsQuotedChanges.length ? columnsQuotedChanges : columnsSpellChanges)); - break; - } - case 'quotableFields': { - const columnsQuotedChanges = await getQuotableActionForColumns( - error, - innerText, - ast, - options, - callbacks - ); - actions.push(...columnsQuotedChanges); - break; - } - case 'unknownIndex': - const indexSpellChanges = await getSpellingActionForIndex( - error, - innerText, - ast, - options, - callbacks - ); - actions.push(...indexSpellChanges); - break; - case 'unknownPolicy': - const policySpellChanges = await getSpellingActionForPolicies( - error, - innerText, - ast, - options, - callbacks - ); - actions.push(...policySpellChanges); - break; - case 'unknownFunction': - const fnsSpellChanges = await getSpellingActionForFunctions(error, innerText, ast); - actions.push(...fnsSpellChanges); - break; - case 'unknownMetadataField': - const metadataSpellChanges = await getSpellingActionForMetadata( - error, - innerText, - ast, - options - ); - actions.push(...metadataSpellChanges); - break; - case 'wrongQuotes': - // it is a syntax error, so location won't be helpful here - const errorText = extractQuotedText(innerText, error); - actions.push( - createAction( - i18n.translate('kbn-esql-validation-autocomplete.esql.quickfix.replaceWithQuote', { - defaultMessage: 'Change quote to " (double)', - }), - errorText.replaceAll("'", '"'), - // override the location - { - ...error, - startColumn: error.startColumn - 1, - endColumn: error.startColumn + errorText.length, - } - ) - ); - break; - case 'unsupportedSettingCommandValue': - const enrichModeSpellChanges = await getSpellingActionForEnrichMode( - error, - innerText, - ast, - options, - callbacks - ); - actions.push(...enrichModeSpellChanges); - break; - default: - break; - } - } - return uniqBy(actions, ({ edits }) => edits[0].text); -} diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/types.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/types.ts deleted file mode 100644 index e76d5d124cb33..0000000000000 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/types.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { EditorError } from '../types'; -import type { ESQLRealField } from '../validation/types'; - -type GetSourceFn = () => Promise; -type GetFieldsByTypeFn = (type: string | string[], ignored?: string[]) => Promise; -type GetPoliciesFn = () => Promise; -type GetPolicyFieldsFn = (name: string) => Promise; - -export interface Callbacks { - getSources: GetSourceFn; - getFieldsByType: GetFieldsByTypeFn; - getPolicies: GetPoliciesFn; - getPolicyFields: GetPolicyFieldsFn; -} - -export interface CodeAction { - title: string; - diagnostics: EditorError[]; - kind: 'quickfix'; - edits: Array<{ - range: EditorError; - text: string; - }>; -} - -export interface CodeActionOptions { - relaxOnMissingCallbacks?: boolean; -} diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/utils.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/utils.ts deleted file mode 100644 index eb1c6d6b25357..0000000000000 --- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/code_actions/utils.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { EditorError, ESQLMessage } from '@kbn/esql-ast'; - -export function wrapAsEditorMessage( - type: 'error' | 'warning', - messages: Array -): EditorError[] { - return messages.map((e) => { - if ('severity' in e) { - return e; - } - const startPosition = e.location ? e.location.min + 1 : 0; - const endPosition = e.location ? e.location.max + 1 : 0; - return { - message: e.text, - startColumn: startPosition, - startLineNumber: 1, - endColumn: endPosition + 1, - endLineNumber: 1, - severity: type, - _source: 'client' as const, - code: e.code, - }; - }); -} diff --git a/src/platform/packages/shared/kbn-monaco/src/languages/esql/language.ts b/src/platform/packages/shared/kbn-monaco/src/languages/esql/language.ts index 231ca135a4deb..e3a122bace036 100644 --- a/src/platform/packages/shared/kbn-monaco/src/languages/esql/language.ts +++ b/src/platform/packages/shared/kbn-monaco/src/languages/esql/language.ts @@ -19,7 +19,6 @@ import { WorkerProxyService } from '../../common/worker_proxy'; import { buildESQLTheme } from './lib/esql_theme'; import { ESQLAstAdapter } from './lib/esql_ast_provider'; import { wrapAsMonacoSuggestions } from './lib/converters/suggestions'; -import { wrapAsMonacoCodeActions } from './lib/converters/actions'; const workerProxyService = new WorkerProxyService(); const removeKeywordSuffix = (name: string) => { @@ -150,25 +149,4 @@ export const ESQLLang: CustomLangModuleType = { }, }; }, - - getCodeActionProvider: (callbacks?: ESQLCallbacks): monaco.languages.CodeActionProvider => { - return { - async provideCodeActions( - model /** ITextModel*/, - range /** Range*/, - context /** CodeActionContext*/, - token /** CancellationToken*/ - ) { - const astAdapter = new ESQLAstAdapter( - (...uris) => workerProxyService.getWorker(uris), - callbacks - ); - const actions = await astAdapter.codeAction(model, range, context); - return { - actions: wrapAsMonacoCodeActions(model, actions), - dispose: () => {}, - }; - }, - }; - }, }; diff --git a/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/converters/actions.ts b/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/converters/actions.ts deleted file mode 100644 index a302b2576d279..0000000000000 --- a/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/converters/actions.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { CodeAction } from '@kbn/esql-validation-autocomplete'; -import { monaco } from '../../../../monaco_imports'; -import { MonacoCodeAction } from '../types'; -import { wrapAsMonacoMessages } from './positions'; - -export function wrapAsMonacoCodeActions( - model: monaco.editor.ITextModel, - actions: CodeAction[] -): MonacoCodeAction[] { - const queryString = model.getValue(); - const uri = model.uri; - return actions.map((action) => { - const [error] = wrapAsMonacoMessages(queryString, action.diagnostics); - return { - title: action.title, - diagnostics: [error], - kind: action.kind, - edit: { - edits: action.edits.map((edit) => { - return { - resource: uri, - textEdit: { - range: error, - text: edit.text, - }, - versionId: undefined, - }; - }), - }, - }; - }); -} diff --git a/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/esql_ast_provider.ts b/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/esql_ast_provider.ts index f702f8070822c..2b8b585a4ffdf 100644 --- a/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/esql_ast_provider.ts +++ b/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/esql_ast_provider.ts @@ -7,13 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { EditorError } from '@kbn/esql-ast'; -import { - type ESQLCallbacks, - getActions, - suggest, - validateQuery, -} from '@kbn/esql-validation-autocomplete'; +import { type ESQLCallbacks, suggest, validateQuery } from '@kbn/esql-validation-autocomplete'; import { monaco } from '../../../monaco_imports'; import type { ESQLWorker } from '../worker/esql_worker'; import { wrapAsMonacoMessages } from './converters/positions'; @@ -81,20 +75,4 @@ export class ESQLAstAdapter { } return suggestions; } - - async codeAction( - model: monaco.editor.ITextModel, - range: monaco.Range, - context: monaco.languages.CodeActionContext - ) { - const getAstFn = await this.getAstWorker(model); - const codeActions = await getActions( - model.getValue(), - context.markers as EditorError[], - getAstFn, - undefined, - this.callbacks - ); - return codeActions; - } } diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index d73b347d18964..5b0f124c268af 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -5464,8 +5464,6 @@ "kbn-esql-validation-autocomplete.esql.divide.warning.divideByZero": "Impossible de diviser par zéro : {left}/{right}", "kbn-esql-validation-autocomplete.esql.divide.warning.logOfNegativeValue": "Le log d'un nombre négatif est null : {value}", "kbn-esql-validation-autocomplete.esql.divide.warning.zeroModule": "Le module par zéro peut renvoyer une valeur null : {left} % {right}", - "kbn-esql-validation-autocomplete.esql.quickfix.replaceWithQuote": "Remplacer les guillemets par le signe \" (double)", - "kbn-esql-validation-autocomplete.esql.quickfix.replaceWithSolution": "Vouliez-vous dire {solution} ?", "kbn-esql-validation-autocomplete.esql.validation.aggInAggFunction": "La fonction d'agrégation [{nestedAgg}] ne peut pas être utilisée comme argument pour une autre fonction d'agrégation", "kbn-esql-validation-autocomplete.esql.validation.dropAllColumnsError": "Il est interdit de supprimer tous les champs [*]", "kbn-esql-validation-autocomplete.esql.validation.dropTimestampWarning": "La suppression de [@timestamp] effacera tous les filtres de temps des résultats de la recherche", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 57817c38cc1ae..a5a58de63dd0c 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -5460,8 +5460,6 @@ "kbn-esql-validation-autocomplete.esql.divide.warning.divideByZero": "ゼロで除算できません:{left}/{right}", "kbn-esql-validation-autocomplete.esql.divide.warning.logOfNegativeValue": "負数の対数はNULLになります:{value}", "kbn-esql-validation-autocomplete.esql.divide.warning.zeroModule": "ゼロによる加群はNULL値を返すことがあります:{left}%{right}", - "kbn-esql-validation-autocomplete.esql.quickfix.replaceWithQuote": "引用符を\"(二重引用符)に変更", - "kbn-esql-validation-autocomplete.esql.quickfix.replaceWithSolution": "次の結果を表示しています:{solution}", "kbn-esql-validation-autocomplete.esql.validation.aggInAggFunction": "集約関数[{nestedAgg}]は、別の集約関数の引数として使用できません", "kbn-esql-validation-autocomplete.esql.validation.dropAllColumnsError": "すべてのフィールドの削除はできません[*]", "kbn-esql-validation-autocomplete.esql.validation.dropTimestampWarning": "[@timestamp]を破棄すると、検索結果に対するすべての時間フィルターが削除されます", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index f673700d552bd..c888005344721 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -5427,8 +5427,6 @@ "kbn-esql-validation-autocomplete.esql.divide.warning.divideByZero": "不能除以零:{left}/{right}", "kbn-esql-validation-autocomplete.esql.divide.warning.logOfNegativeValue": "负数的对数将生成 null:{value}", "kbn-esql-validation-autocomplete.esql.divide.warning.zeroModule": "对零取余数会返回 null 值:{left}%{right}", - "kbn-esql-validation-autocomplete.esql.quickfix.replaceWithQuote": "将引号更改为 \"(双引号)", - "kbn-esql-validation-autocomplete.esql.quickfix.replaceWithSolution": "您是指 {solution} 吗?", "kbn-esql-validation-autocomplete.esql.validation.aggInAggFunction": "聚合函数 [{nestedAgg}] 不能用作另一聚合函数中的参数", "kbn-esql-validation-autocomplete.esql.validation.dropAllColumnsError": "不允许移除所有字段 [*]", "kbn-esql-validation-autocomplete.esql.validation.dropTimestampWarning": "丢弃 [@timestamp] 会将所有时间筛选移除到搜索结果", diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/query/correct_query_with_actions.test.ts b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/query/correct_query_with_actions.test.ts deleted file mode 100644 index 818f4854e038d..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/query/correct_query_with_actions.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { correctQueryWithActions } from './correct_query_with_actions'; - -describe('correctQueryWithActions', () => { - it(`fixes errors correctly for a query with one syntax error for stats`, async () => { - const fixedQuery = await correctQueryWithActions('from logstash-* | stats aveg(bytes)'); - expect(fixedQuery).toBe('from logstash-* | stats avg(bytes)'); - }); - - it(`fixes errors correctly for a query with 2 syntax errors for stats`, async () => { - const fixedQuery = await correctQueryWithActions( - 'from logstash-* | stats aveg(bytes), max2(bytes)' - ); - expect(fixedQuery).toBe('from logstash-* | stats avg(bytes), max(bytes)'); - }); - - it(`fixes errors correctly for a query with one syntax error for eval`, async () => { - const fixedQuery = await correctQueryWithActions( - 'from logstash-* | stats var0 = max(bytes) | eval ab(var0) | limit 1' - ); - expect(fixedQuery).toBe('from logstash-* | stats var0 = max(bytes) | eval abs(var0) | limit 1'); - }); - - it(`fixes errors correctly for a query with two syntax error for eval`, async () => { - const fixedQuery = await correctQueryWithActions( - 'from logstash-* | stats var0 = max2(bytes) | eval ab(var0) | limit 1' - ); - expect(fixedQuery).toBe('from logstash-* | stats var0 = max(bytes) | eval abs(var0) | limit 1'); - }); - - it(`doesnt complain for @timestamp column`, async () => { - const queryWithTimestamp = `FROM logstash-* - | WHERE @timestamp >= NOW() - 15 minutes - | EVAL bucket = DATE_TRUNC(1 minute, @timestamp) - | STATS avg_cpu = AVG(system.cpu.total.norm.pct) BY service.name, bucket - | SORT avg_cpu DESC - | LIMIT 10`; - const fixedQuery = await correctQueryWithActions(queryWithTimestamp); - expect(fixedQuery).toBe(queryWithTimestamp); - }); -}); diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/query/correct_query_with_actions.ts b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/query/correct_query_with_actions.ts deleted file mode 100644 index 15b050c3a3897..0000000000000 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/server/functions/query/correct_query_with_actions.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { validateQuery, getActions } from '@kbn/esql-validation-autocomplete'; -import { getAstAndSyntaxErrors } from '@kbn/esql-ast'; - -const fixedQueryByOneAction = async (queryString: string) => { - const { errors } = await validateQuery(queryString, getAstAndSyntaxErrors, { - ignoreOnMissingCallbacks: true, - }); - - const actions = await getActions(queryString, errors, getAstAndSyntaxErrors, { - relaxOnMissingCallbacks: true, - }); - - if (actions.length) { - const [firstAction] = actions; - const range = firstAction.edits[0].range; - const correctText = firstAction.edits[0].text; - const problematicString = queryString.substring(range.startColumn - 1, range.endColumn - 1); - const fixedQuery = queryString.replace(problematicString, correctText); - - return { - query: fixedQuery, - shouldRunAgain: Boolean(actions.length), - }; - } - return { - query: queryString, - shouldRunAgain: false, - }; -}; - -/** - * @param queryString - * @returns corrected queryString - * The cases that are handled are: - * - Query stats / eval functions have typos e.g. aveg instead of avg - * - Unquoted fields e.g. keep field-1 instead of keep `field-1` - * - Unquoted fields in stats or eval e.g. stats avg(field-1) instead of stats avg(`field-1`) - * - Combination of the above - */ - -export const correctQueryWithActions = async (queryString: string) => { - let shouldCorrectQuery = true; - let fixedQuery = queryString; - // this is an escape hatch, the loop will end automatically if the ast doesnt return more actions - // in case it goes wrong, we allow it to loop 10 times - let limit = 10; - - while (shouldCorrectQuery && limit >= 0) { - const { query, shouldRunAgain } = await fixedQueryByOneAction(fixedQuery); - shouldCorrectQuery = shouldRunAgain; - fixedQuery = query; - limit--; - } - - return fixedQuery; -};