Skip to content

Commit

Permalink
[ES|QL] Separate GROK and DISSECT autocomplete routines (elastic#…
Browse files Browse the repository at this point in the history
…211101)

## Summary

Part of elastic#195418

Gives `GROK` and `DISSECT` autocomplete logic its own home 🏡

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### Identify risks

- [ ] As with any refactor, there's a possibility this will introduce a
regression in the behavior of commands. However, all automated tests are
passing and I have tested the behavior manually and can detect no
regression.
  • Loading branch information
drewdaemon authored Feb 14, 2025
1 parent c44580f commit 1223926
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 126 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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 { ESQL_STRING_TYPES } from '../../shared/esql_types';
import { attachTriggerCommand, getFieldNamesByType, setup } from './helpers';

describe('autocomplete.suggest', () => {
describe('DISSECT', () => {
it('suggests fields after DISSECT', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions(
'from a | DISSECT /',
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `)
);
await assertSuggestions(
'from a | DISSECT /',
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `),
{ triggerCharacter: ' ' }
);
await assertSuggestions(
'from a | DISSECT key/',
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `)
);
await assertSuggestions(
'from a | DISSECT keywordField/',
['keywordField ', 'textField '].map(attachTriggerCommand)
);
});

const constantPattern = '"%{firstWord}" ';
it('suggests a pattern after a field name', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | DISSECT keywordField /', [constantPattern]);
});

it('suggests an append separator or pipe after a pattern', async () => {
const { assertSuggestions } = await setup();
assertSuggestions(
`from a | DISSECT keywordField ${constantPattern} /`,
['APPEND_SEPARATOR = ', '| '].map(attachTriggerCommand),
{ triggerCharacter: ' ' }
);
assertSuggestions(
`from a | DISSECT keywordField ${constantPattern} /`,
['APPEND_SEPARATOR = ', '| '].map(attachTriggerCommand)
);
});

it('suggests append separators', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions(
`from a | DISSECT keywordField ${constantPattern} append_separator = /`,
['":" ', '";" '].map(attachTriggerCommand)
);
});

it('suggests a pipe after an append separator', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions(
`from a | DISSECT keywordField ${constantPattern} append_separator = ":" /`,
['| ']
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { ESQL_STRING_TYPES } from '../../shared/esql_types';
import { attachTriggerCommand, getFieldNamesByType, setup } from './helpers';

describe('autocomplete.suggest', () => {
describe('GROK', () => {
it('suggests fields after GROK', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions(
'from a | grok /',
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `)
);
await assertSuggestions(
'from a | grok key/',
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `)
);
await assertSuggestions(
'from a | grok keywordField/',
['keywordField ', 'textField '].map(attachTriggerCommand)
);
});

const constantPattern = '"%{WORD:firstWord}"';
it('suggests a pattern after a field name', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | grok keywordField /', [constantPattern + ' ']);
});

it('suggests a pipe after a pattern', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions(`from a | grok keywordField ${constantPattern} /`, ['| ']);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -113,88 +113,6 @@ describe('autocomplete', () => {
testSuggestions('from a metadata _id | eval var0 = a | /', commands);
});

describe('show', () => {
testSuggestions('show /', ['INFO']);
for (const fn of ['info']) {
testSuggestions(`show ${fn} /`, ['| ']);
}
});

describe('grok', () => {
const constantPattern = '"%{WORD:firstWord}"';
const subExpressions = [
'',
`grok keywordField |`,
`grok keywordField ${constantPattern} |`,
`dissect keywordField ${constantPattern} append_separator = ":" |`,
`dissect keywordField ${constantPattern} |`,
];
for (const subExpression of subExpressions) {
testSuggestions(
`from a | ${subExpression} grok /`,
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `)
);
testSuggestions(`from a | ${subExpression} grok keywordField /`, [constantPattern], ' ');
testSuggestions(`from a | ${subExpression} grok keywordField ${constantPattern} /`, ['| ']);
}

testSuggestions(
'from a | grok /',
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `)
);
testSuggestions(
'from a | grok key/',
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `)
);
testSuggestions(
'from a | grok keywordField/',
['keywordField ', 'textField '].map(attachTriggerCommand)
);
});

describe('dissect', () => {
const constantPattern = '"%{firstWord}"';
const subExpressions = [
'',
`dissect keywordField |`,
`dissect keywordField ${constantPattern} |`,
`dissect keywordField ${constantPattern} append_separator = ":" |`,
];
for (const subExpression of subExpressions) {
testSuggestions(
`from a | ${subExpression} dissect /`,
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `)
);
testSuggestions(`from a | ${subExpression} dissect keywordField /`, [constantPattern], ' ');
testSuggestions(
`from a | ${subExpression} dissect keywordField ${constantPattern} /`,
['APPEND_SEPARATOR = $0', '| '],
' '
);
testSuggestions(
`from a | ${subExpression} dissect keywordField ${constantPattern} append_separator = /`,
['":"', '";"']
);
testSuggestions(
`from a | ${subExpression} dissect keywordField ${constantPattern} append_separator = ":" /`,
['| ']
);
}

testSuggestions(
'from a | dissect /',
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `)
);
testSuggestions(
'from a | dissect key/',
getFieldNamesByType(ESQL_STRING_TYPES).map((name) => `${name} `)
);
testSuggestions(
'from a | dissect keywordField/',
['keywordField ', 'textField '].map(attachTriggerCommand)
);
});

describe('limit', () => {
testSuggestions('from a | limit /', ['10 ', '100 ', '1000 ']);
testSuggestions('from a | limit 4 /', ['| ']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,10 @@ import { collectVariables, excludeVariablesFromCurrentCommand } from '../shared/
import type { ESQLPolicy, ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types';
import {
allStarConstant,
colonCompleteItem,
commaCompleteItem,
getAssignmentDefinitionCompletitionItem,
getCommandAutocompleteDefinitions,
pipeCompleteItem,
semiColonCompleteItem,
} from './complete_items';
import {
buildFieldsDefinitions,
Expand Down Expand Up @@ -210,6 +208,7 @@ export async function suggest(
if (
astContext.type === 'expression' ||
(astContext.type === 'option' && astContext.command?.name === 'join') ||
(astContext.type === 'option' && astContext.command?.name === 'dissect') ||
(astContext.type === 'option' && astContext.command?.name === 'from')
) {
return getSuggestionsWithinCommandExpression(
Expand Down Expand Up @@ -285,7 +284,7 @@ export function getFieldsByTypeRetriever(
const supportsControls = resourceRetriever?.canSuggestVariables?.() ?? false;
return {
getFieldsByType: async (
expectedType: string | string[] = 'any',
expectedType: Readonly<string> | Readonly<string[]> = 'any',
ignored: string[] = [],
options
) => {
Expand Down Expand Up @@ -791,7 +790,7 @@ async function getExpressionSuggestionsByType(
// it can be just literal values (i.e. "string")
if (argDef.constantOnly) {
// ... | <COMMAND> ... <suggest>
suggestions.push(...getCompatibleLiterals(command.name, [argDef.type], [argDef.name]));
suggestions.push(...getCompatibleLiterals(command.name, [argDef.type]));
} else {
// or it can be anything else as long as it is of the right type and the end (i.e. column or function)
if (!nodeArg) {
Expand Down Expand Up @@ -1078,7 +1077,6 @@ async function getFunctionArgsSuggestions(
...getCompatibleLiterals(
command.name,
getTypesFromParamDefs(constantOnlyParamDefs) as string[],
undefined,
{
addComma: shouldAddComma,
advanceCursorAndOpenSuggestions: hasMoreMandatoryArgs,
Expand Down Expand Up @@ -1151,7 +1149,7 @@ async function getFunctionArgsSuggestions(
if (isLiteralItem(arg) && isNumericType(arg.literalType)) {
// ... | EVAL fn(2 <suggest>)
suggestions.push(
...getCompatibleLiterals(command.name, ['time_literal_unit'], undefined, {
...getCompatibleLiterals(command.name, ['time_literal_unit'], {
addComma: shouldAddComma,
advanceCursorAndOpenSuggestions: hasMoreMandatoryArgs,
})
Expand Down Expand Up @@ -1403,15 +1401,6 @@ async function getOptionArgsSuggestions(
}
}

if (command.name === 'dissect') {
if (
option.args.filter((arg) => !(isSingleItem(arg) && arg.type === 'unknown')).length < 1 &&
optionDef
) {
suggestions.push(colonCompleteItem, semiColonCompleteItem);
}
}

if (optionDef) {
if (!suggestions.length) {
const argDefIndex = optionDef.signature.multipleParams
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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 { EDITOR_MARKER } from '../../../shared/constants';
import { isSingleItem } from '../../../..';
import { ESQL_STRING_TYPES } from '../../../shared/esql_types';
import { CommandSuggestParams } from '../../../definitions/types';

import type { SuggestionRawDefinition } from '../../types';
import { TRIGGER_SUGGESTION_COMMAND, buildConstantsDefinitions } from '../../factories';
import { colonCompleteItem, pipeCompleteItem, semiColonCompleteItem } from '../../complete_items';

export async function suggest({
command,
innerText,
getColumnsByType,
}: CommandSuggestParams<'dissect'>): Promise<SuggestionRawDefinition[]> {
const commandArgs = command.args.filter(
(arg) => isSingleItem(arg) && arg.text !== EDITOR_MARKER && arg.text !== ''
);

// DISSECT field /
if (commandArgs.length === 1) {
return buildConstantsDefinitions(
['"%{firstWord}"'],
i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.aPatternString', {
defaultMessage: 'A pattern string',
}),
undefined,
{
advanceCursorAndOpenSuggestions: true,
}
);
}
// DISSECT field pattern /
else if (commandArgs.length === 2) {
return [
{ ...pipeCompleteItem, command: TRIGGER_SUGGESTION_COMMAND },
appendSeparatorCompletionItem,
];
}
// DISSECT field APPEND_SEPARATOR = /
else if (/append_separator\s*=\s*$/i.test(innerText)) {
return [colonCompleteItem, semiColonCompleteItem];
}
// DISSECT field APPEND_SEPARATOR = ":" /
else if (commandArgs.some((arg) => isSingleItem(arg) && arg.type === 'option')) {
return [{ ...pipeCompleteItem, command: TRIGGER_SUGGESTION_COMMAND }];
}

// DISSECT /
const fieldSuggestions = await getColumnsByType(ESQL_STRING_TYPES);
return fieldSuggestions.map((sug) => ({
...sug,
text: `${sug.text} `,
command: TRIGGER_SUGGESTION_COMMAND,
}));
}

const appendSeparatorCompletionItem: SuggestionRawDefinition = {
command: TRIGGER_SUGGESTION_COMMAND,
detail: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.appendSeparatorDoc', {
defaultMessage:
'The character(s) that separate the appended fields. Default to empty string ("").',
}),
kind: 'Reference',
label: 'APPEND_SEPARATOR',
sortText: '1',
text: 'APPEND_SEPARATOR = ',
};
Loading

0 comments on commit 1223926

Please sign in to comment.