Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ES|QL] Improve ES|QL autocomplete suggestions for case() expressions #192135

Merged
merged 10 commits into from
Sep 9, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -549,5 +549,87 @@ describe('autocomplete.suggest', () => {
{ triggerCharacter: ' ' }
);
});

test('case', async () => {
const { assertSuggestions } = await setup();
const comparisonOperators = ['==', '!=', '>', '<', '>=', '<=']
.map((op) => `${op} `)
.concat(',');

// case( / ) suggest any field/eval function in this position as first argument
await assertSuggestions(
'from a | eval case(/)',
[
// With extra space after field name to open suggestions
...getFieldNamesByType('any').map((field) => `${field} `),
...getFunctionSignaturesByReturnType('eval', 'boolean', { scalar: true }, undefined, []),
],
{
triggerCharacter: ' ',
}
);

// case( field /) suggest comparison operators at this point to converge to a boolean
await assertSuggestions('from a | eval case( textField /)', comparisonOperators, {
triggerCharacter: ' ',
});
await assertSuggestions('from a | eval case( doubleField /)', comparisonOperators, {
triggerCharacter: ' ',
});
await assertSuggestions('from a | eval case( booleanField /)', comparisonOperators, {
triggerCharacter: ' ',
});

// case( field > /) suggest field/function of the same type of the right hand side to complete the boolean expression
await assertSuggestions(
'from a | eval case( keywordField != /)',
[
// Notice no extra space after field name
...getFieldNamesByType(['keyword', 'text', 'boolean']).map((field) => `${field}`),
...getFunctionSignaturesByReturnType(
'eval',
['keyword', 'text', 'boolean'],
{ scalar: true },
undefined,
[]
),
],
{
triggerCharacter: ' ',
}
);
await assertSuggestions(
'from a | eval case( integerField != /)',
[
// Notice no extra space after field name
...getFieldNamesByType(ESQL_COMMON_NUMERIC_TYPES).map((field) => `${field}`),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_COMMON_NUMERIC_TYPES,
{ scalar: true },
undefined,
[]
),
],
{
triggerCharacter: ' ',
}
);
drewdaemon marked this conversation as resolved.
Show resolved Hide resolved

// case( field > 0, >) suggests fields like normal
await assertSuggestions(
'from a | eval case( integerField != doubleField, /)',
[
// With extra space after field name to open suggestions
...getFieldNamesByType('any').map((field) => `${field}`),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }, undefined, [
'case',
]),
],
{
triggerCharacter: ' ',
}
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type {
} from '@kbn/esql-ast';
import { i18n } from '@kbn/i18n';
import { ESQL_NUMBER_TYPES, isNumericType } from '../shared/esql_types';
import type { EditorContext, SuggestionRawDefinition } from './types';
import type { EditorContext, ItemKind, SuggestionRawDefinition } from './types';
import {
getColumnForASTNode,
getCommandDefinition,
Expand Down Expand Up @@ -109,6 +109,7 @@ import {
isParameterType,
isReturnType,
} from '../definitions/types';
import { comparisonFunctions } from '../definitions/builtin';

type GetSourceFn = () => Promise<SuggestionRawDefinition[]>;
type GetDataStreamsForIntegrationFn = (
Expand Down Expand Up @@ -1300,6 +1301,7 @@ async function getFunctionArgsSuggestions(
fieldsMap,
innerText
);

// pick the type of the next arg
const shouldGetNextArgument = node.text.includes(EDITOR_MARKER);
let argIndex = Math.max(node.args.length, 0);
Expand Down Expand Up @@ -1327,8 +1329,16 @@ async function getFunctionArgsSuggestions(
// Whether to prepend comma to suggestion string
// E.g. if true, "fieldName" -> "fieldName, "
const alreadyHasComma = fullText ? fullText[offset] === ',' : false;
const shouldBeBooleanCondition = typesToSuggestNext.some(
(t) => t && t.type === 'boolean' && t.name === 'condition'
);

const shouldAddComma =
hasMoreMandatoryArgs && fnDefinition.type !== 'builtin' && !alreadyHasComma;
hasMoreMandatoryArgs &&
fnDefinition.type !== 'builtin' &&
!alreadyHasComma &&
!shouldBeBooleanCondition;
const shouldAdvanceCursor = hasMoreMandatoryArgs && fnDefinition.type !== 'builtin';

const suggestedConstants = uniq(
typesToSuggestNext
Expand Down Expand Up @@ -1413,21 +1423,29 @@ async function getFunctionArgsSuggestions(
);

// Fields

suggestions.push(
...pushItUpInTheList(
await getFieldsByType(
// @TODO: have a way to better suggest constant only params
getTypesFromParamDefs(typesToSuggestNext.filter((d) => !d.constantOnly)) as string[],
// For example, in case() where we are expecting a boolean condition
// we can accept any field types (field1 !== field2)
shouldBeBooleanCondition
? ['any']
: // @TODO: have a way to better suggest constant only params
(getTypesFromParamDefs(
typesToSuggestNext.filter((d) => !d.constantOnly)
) as string[]),
[],
{
addComma: shouldAddComma,
advanceCursor: shouldAddComma,
openSuggestions: shouldAddComma,
advanceCursor: shouldAdvanceCursor,
openSuggestions: shouldAdvanceCursor,
}
),
true
)
);

// Functions
suggestions.push(
...getCompatibleFunctionDefinition(
Expand Down Expand Up @@ -1467,9 +1485,20 @@ async function getFunctionArgsSuggestions(
);
}
}

// Suggest comparison functions for boolean conditions
if (shouldBeBooleanCondition && isColumnItem(arg)) {
suggestions.push(
...comparisonFunctions.map<SuggestionRawDefinition>(({ name, description }) => ({
label: name,
text: name + ' ',
kind: 'Function' as ItemKind,
detail: description,
command: TRIGGER_SUGGESTION_COMMAND,
}))
);
}
if (hasMoreMandatoryArgs) {
// suggest a comma if there's another argument for the function
// Suggest a comma if there's another argument for the function
suggestions.push(commaCompleteItem);
}
}
Expand Down Expand Up @@ -1650,6 +1679,7 @@ async function getOptionArgsSuggestions(
suggestions.push(...buildFieldsDefinitions(policyMetadata.enrichFields));
}
}

if (
assignFn &&
hasSameArgBothSides(assignFn) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export const mathFunctions: FunctionDefinition[] = [
),
];

const comparisonFunctions: FunctionDefinition[] = [
export const comparisonFunctions: FunctionDefinition[] = [
{
name: '==',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definition.equalToDoc', {
Expand Down