[ES|QL] Autocomplete for STATS...WHERE (#216379)

## Summary

Resolve https://github.com/elastic/kibana/issues/209359

(No match operator or full-text search functions)

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [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
This commit is contained in:
Drew Tate 2025-04-02 13:37:49 -06:00 committed by GitHub
parent 7bf76b0e7a
commit 2fda88c166
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 479 additions and 223 deletions

View file

@ -37,6 +37,16 @@ describe('STATS', () => {
]);
});
it("doesn't append an undefined arg with a trailing comma", () => {
const src = `
FROM employees
| STATS 123 ,`;
const query = EsqlQuery.fromSrc(src);
expect(query.ast.commands[1].args).toHaveLength(1);
expect(query.ast.commands[1].args.every((arg) => arg)).toBe(true);
});
it('aggregation function with escaped values', () => {
const src = `
FROM employees

View file

@ -48,6 +48,8 @@ export const createStatsCommand = (ctx: StatsCommandContext, src: string): ESQLC
const fields = ctx.aggFields();
for (const fieldCtx of fields.aggField_list()) {
if (fieldCtx.getText() === '') continue;
const node = createAggField(fieldCtx);
command.args.push(node);

View file

@ -129,6 +129,7 @@ export class Visitor<
for (let i = nodes.length - 1; i >= 0; i--) {
const node = nodes[i];
if (!node) continue;
const { location } = node;
if (!location) continue;
const isInside = location.min <= pos && location.max >= pos;
@ -145,6 +146,7 @@ export class Visitor<
const nodes = [...ctx.arguments()];
for (let i = nodes.length - 1; i >= 0; i--) {
const node = nodes[i];
if (!node) continue;
const { location } = node;
if (!location) continue;
const isInside = location.min <= pos && location.max >= pos;
@ -163,6 +165,7 @@ export class Visitor<
const nodes = [...ctx.commands()];
for (let i = nodes.length - 1; i >= 0; i--) {
const node = nodes[i];
if (!node) continue;
const { location } = node;
if (!location) continue;
const isInside = location.min <= pos && location.max >= pos;

View file

@ -732,6 +732,7 @@ const enrichOperators = (
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
]);
}
@ -741,13 +742,20 @@ const enrichOperators = (
Location.EVAL,
Location.WHERE,
Location.ROW,
Location.STATS,
Location.SORT,
Location.STATS,
Location.STATS_WHERE,
Location.STATS_BY,
]);
}
if (isInOperator || isLikeOperator || isNotOperator || arePredicates) {
locationsAvailable = [Location.EVAL, Location.WHERE, Location.SORT, Location.ROW];
locationsAvailable = [
Location.EVAL,
Location.WHERE,
Location.SORT,
Location.ROW,
Location.STATS_WHERE,
];
}
if (isInOperator) {
// Override the signatures to be array types instead of singular

View file

@ -21,7 +21,7 @@ import {
import { ESQL_COMMON_NUMERIC_TYPES } from '../../shared/esql_types';
import { scalarFunctionDefinitions } from '../../definitions/generated/scalar_functions';
import { timeUnitsToSuggest } from '../../definitions/literals';
import { FunctionDefinitionTypes } from '../../definitions/types';
import { FunctionDefinitionTypes, Location } from '../../definitions/types';
import {
getCompatibleTypesToSuggestNext,
getValidFunctionSignaturesForPreviousArgs,
@ -46,7 +46,7 @@ const getTypesFromParamDefs = (paramDefs: FunctionParameter[]): SupportedDataTyp
) as SupportedDataType[];
describe('autocomplete.suggest', () => {
describe('eval', () => {
describe(Location.EVAL, () => {
let assertSuggestions: AssertSuggestionsFn;
beforeEach(async () => {
@ -58,23 +58,23 @@ describe('autocomplete.suggest', () => {
await assertSuggestions('from a | eval /', [
'var0 = ',
...getFieldNamesByType('any').map((v) => `${v} `),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
]);
await assertSuggestions('from a | eval col0 = /', [
...getFieldNamesByType('any').map((v) => `${v} `),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
]);
await assertSuggestions('from a | eval col0 = 1, /', [
'var0 = ',
...getFieldNamesByType('any').map((v) => `${v} `),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
]);
await assertSuggestions('from a | eval col0 = 1, col1 = /', [
...getFieldNamesByType('any').map((v) => `${v} `),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
]);
// Re-enable with https://github.com/elastic/kibana/issues/210639
@ -82,7 +82,7 @@ describe('autocomplete.suggest', () => {
// 'var0 = ',
// ...getFieldNamesByType('any').map((v) => `${v} `),
// 'a',
// ...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
// ...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
// ]);
await assertSuggestions(
@ -90,7 +90,7 @@ describe('autocomplete.suggest', () => {
[
'var0 = ',
'`avg(doubleField)` ',
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
],
{
triggerCharacter: ' ',
@ -108,7 +108,7 @@ describe('autocomplete.suggest', () => {
'var0 = ',
...getFieldNamesByType('any').map((v) => `${v} `),
'`abs(doubleField) + 1` ',
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
],
{
triggerCharacter: ' ',
@ -124,7 +124,7 @@ describe('autocomplete.suggest', () => {
[
'var0 = ',
'`avg(doubleField)` ',
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
],
{
triggerCharacter: ' ',
@ -139,9 +139,12 @@ describe('autocomplete.suggest', () => {
test('after column', async () => {
await assertSuggestions('from a | eval doubleField /', [
...getFunctionSignaturesByReturnType('eval', 'any', { operators: true, skipAssign: true }, [
'double',
]),
...getFunctionSignaturesByReturnType(
Location.EVAL,
'any',
{ operators: true, skipAssign: true },
['double']
),
]);
});
@ -160,7 +163,7 @@ describe('autocomplete.suggest', () => {
await assertSuggestions('from index | EVAL not /', [
...getFieldNamesByType('boolean').map((v) => `${v} `),
...getFunctionSignaturesByReturnType('eval', 'boolean', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'boolean', { scalar: true }),
]);
});
@ -170,7 +173,7 @@ describe('autocomplete.suggest', () => {
'from index | EVAL doubleField in (/)',
[
...getFieldNamesByType('double').filter((name) => name !== 'doubleField'),
...getFunctionSignaturesByReturnType('eval', 'double', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'double', { scalar: true }),
],
{ triggerCharacter: '(' }
);
@ -182,7 +185,7 @@ describe('autocomplete.suggest', () => {
'from a | eval a=/',
[
...getFieldNamesByType('any').map((v) => `${v} `),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
],
{ triggerCharacter: '=' }
);
@ -190,7 +193,7 @@ describe('autocomplete.suggest', () => {
'from a | eval a=abs(doubleField), b= /',
[
...getFieldNamesByType('any').map((v) => `${v} `),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
],
{ triggerCharacter: '=' }
);
@ -202,7 +205,7 @@ describe('autocomplete.suggest', () => {
[
...getFieldNamesByType(roundParameterTypes),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
roundParameterTypes,
{ scalar: true },
undefined,
@ -237,10 +240,12 @@ describe('autocomplete.suggest', () => {
await assertSuggestions('from a | eval a=round(doubleField) /', [
', ',
'| ',
...getFunctionSignaturesByReturnType('eval', 'any', { operators: true, skipAssign: true }, [
'double',
'long',
]),
...getFunctionSignaturesByReturnType(
Location.EVAL,
'any',
{ operators: true, skipAssign: true },
['double', 'long']
),
'IN $0',
'IS NOT NULL',
'IS NULL',
@ -250,7 +255,7 @@ describe('autocomplete.suggest', () => {
[
...getFieldNamesByType(['integer', 'long']),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
['integer', 'long'],
{ scalar: true },
undefined,
@ -264,7 +269,7 @@ describe('autocomplete.suggest', () => {
[
...getFieldNamesByType(['integer', 'long']),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
['integer', 'long'],
{ scalar: true },
undefined,
@ -278,29 +283,29 @@ describe('autocomplete.suggest', () => {
...getFieldNamesByType('any').map((v) => `${v} `),
// Re-enable with https://github.com/elastic/kibana/issues/210639
// 'a',
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
]);
await assertSuggestions('from a | eval a=round(doubleField) + /', [
...getFieldNamesByType(ESQL_COMMON_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_COMMON_NUMERIC_TYPES, {
...getFunctionSignaturesByReturnType(Location.EVAL, ESQL_COMMON_NUMERIC_TYPES, {
scalar: true,
}),
]);
await assertSuggestions('from a | eval a=round(doubleField)+ /', [
...getFieldNamesByType(ESQL_COMMON_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_COMMON_NUMERIC_TYPES, {
...getFunctionSignaturesByReturnType(Location.EVAL, ESQL_COMMON_NUMERIC_TYPES, {
scalar: true,
}),
]);
await assertSuggestions('from a | eval a=doubleField+ /', [
...getFieldNamesByType(ESQL_COMMON_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_COMMON_NUMERIC_TYPES, {
...getFunctionSignaturesByReturnType(Location.EVAL, ESQL_COMMON_NUMERIC_TYPES, {
scalar: true,
}),
]);
await assertSuggestions('from a | eval a=`any#Char$Field`+ /', [
...getFieldNamesByType(ESQL_COMMON_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_COMMON_NUMERIC_TYPES, {
...getFunctionSignaturesByReturnType(Location.EVAL, ESQL_COMMON_NUMERIC_TYPES, {
scalar: true,
}),
]);
@ -310,7 +315,7 @@ describe('autocomplete.suggest', () => {
[
...getFieldNamesByType(roundParameterTypes),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
roundParameterTypes,
{ scalar: true },
undefined,
@ -323,7 +328,7 @@ describe('autocomplete.suggest', () => {
await assertSuggestions('from a | eval a=concat( /', [
...getFieldNamesByType(['text', 'keyword']).map((v) => `${v}, `),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
['text', 'keyword'],
{ scalar: true },
undefined,
@ -335,7 +340,7 @@ describe('autocomplete.suggest', () => {
[
...getFieldNamesByType(['text', 'keyword']),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
['text', 'keyword'],
{ scalar: true },
undefined,
@ -351,7 +356,7 @@ describe('autocomplete.suggest', () => {
// test that comma is correctly added to the suggestions if minParams is not reached yet
await assertSuggestions('from a | eval a=cidr_match(/', [
...getFieldNamesByType('ip').map((v) => `${v}, `),
...getFunctionSignaturesByReturnType('eval', 'ip', { scalar: true }, undefined, [
...getFunctionSignaturesByReturnType(Location.EVAL, 'ip', { scalar: true }, undefined, [
'cidr_match',
]).map((v) => ({ ...v, text: `${v.text},` })),
]);
@ -360,7 +365,7 @@ describe('autocomplete.suggest', () => {
[
...getFieldNamesByType(['text', 'keyword']),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
['text', 'keyword'],
{ scalar: true },
undefined,
@ -378,7 +383,7 @@ describe('autocomplete.suggest', () => {
[
...getFieldNamesByType(roundParameterTypes),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
roundParameterTypes,
{ scalar: true },
undefined,
@ -395,16 +400,19 @@ describe('autocomplete.suggest', () => {
// Smoke testing for suggestions in previous position than the end of the statement
await assertSuggestions('from a | eval var0 = abs(doubleField) / | eval abs(var0)', [
...getFunctionSignaturesByReturnType('eval', 'any', { operators: true, skipAssign: true }, [
'double',
]),
...getFunctionSignaturesByReturnType(
Location.EVAL,
'any',
{ operators: true, skipAssign: true },
['double']
),
', ',
'| ',
]);
await assertSuggestions('from a | eval var0 = abs(b/) | eval abs(var0)', [
...getFieldNamesByType(absParameterTypes),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
absParameterTypes,
{ scalar: true },
undefined,
@ -519,7 +527,7 @@ describe('autocomplete.suggest', () => {
getTypesFromParamDefs(typesToSuggestNext).filter(isFieldType)
),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
getTypesFromParamDefs(typesToSuggestNext).filter(
isReturnType
) as FunctionReturnType[],
@ -581,7 +589,7 @@ describe('autocomplete.suggest', () => {
', ',
'| ',
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
'any',
{ operators: true, skipAssign: true },
['integer']
@ -594,7 +602,7 @@ describe('autocomplete.suggest', () => {
'from a | eval var0=date_trunc(/)',
[
...getLiteralsByType('time_literal').map((t) => `${t}, `),
...getFunctionSignaturesByReturnType('eval', ['time_duration', 'date_period'], {
...getFunctionSignaturesByReturnType(Location.EVAL, ['time_duration', 'date_period'], {
scalar: true,
}).map((t) => `${t.text},`),
],

View file

@ -7,12 +7,13 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Location } from '../../definitions/types';
import { getNewVariableSuggestion } from '../factories';
import { attachTriggerCommand, getFunctionSignaturesByReturnType, setup } from './helpers';
describe('autocomplete.suggest', () => {
describe('ROW column1 = value1[, ..., columnN = valueN]', () => {
const functions = getFunctionSignaturesByReturnType('row', 'any', { scalar: true });
const functions = getFunctionSignaturesByReturnType(Location.ROW, 'any', { scalar: true });
it('suggests functions and an assignment for new expressions', async () => {
const { assertSuggestions } = await setup();
const expectedSuggestions = [getNewVariableSuggestion('var0'), ...functions];

View file

@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Location } from '../../definitions/types';
import {
setup,
getFieldNamesByType,
@ -18,7 +19,7 @@ describe('autocomplete.suggest', () => {
describe('SORT ( <column> [ ASC / DESC ] [ NULLS FIST / NULLS LAST ] )+', () => {
describe('SORT <column> ...', () => {
const expectedFieldSuggestions = getFieldNamesByType('any').map(attachTriggerCommand);
const expectedFunctionSuggestions = getFunctionSignaturesByReturnType('sort', 'any', {
const expectedFunctionSuggestions = getFunctionSignaturesByReturnType(Location.SORT, 'any', {
scalar: true,
}).map(attachTriggerCommand);

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ESQLVariableType } from '@kbn/esql-types';
import { FieldType, FunctionReturnType } from '../../definitions/types';
import { FieldType, FunctionReturnType, Location } from '../../definitions/types';
import { ESQL_COMMON_NUMERIC_TYPES, ESQL_NUMBER_TYPES } from '../../shared/esql_types';
import { getDateHistogramCompletionItem } from '../commands/stats/util';
import { allStarConstant } from '../complete_items';
@ -17,14 +17,16 @@ import {
getFunctionSignaturesByReturnType,
getFieldNamesByType,
getLiteralsByType,
AssertSuggestionsFn,
SuggestFn,
} from './helpers';
const allAggFunctions = getFunctionSignaturesByReturnType('stats', 'any', {
const allAggFunctions = getFunctionSignaturesByReturnType(Location.STATS, 'any', {
agg: true,
});
const allEvaFunctions = getFunctionSignaturesByReturnType(
'stats',
Location.STATS,
'any',
{
scalar: true,
@ -36,7 +38,7 @@ const allEvaFunctions = getFunctionSignaturesByReturnType(
);
const allGroupingFunctions = getFunctionSignaturesByReturnType(
'stats',
Location.STATS,
'any',
{
grouping: true,
@ -51,11 +53,16 @@ const avgTypes: Array<FieldType & FunctionReturnType> = ['double', 'integer', 'l
describe('autocomplete.suggest', () => {
describe('STATS <aggregates> [ BY <grouping> ]', () => {
describe('STATS ...', () => {});
let assertSuggestions: AssertSuggestionsFn;
let suggest: SuggestFn;
beforeEach(async () => {
const res = await setup();
assertSuggestions = res.assertSuggestions;
suggest = res.suggest;
});
describe('... <aggregates> ...', () => {
test('lists possible aggregations on space after command', async () => {
const { assertSuggestions } = await setup();
test('suggestions for a fresh expression', async () => {
const expected = [
'var0 = ',
...allAggFunctions,
@ -65,11 +72,14 @@ describe('autocomplete.suggest', () => {
await assertSuggestions('from a | stats /', expected);
await assertSuggestions('FROM a | STATS /', expected);
await assertSuggestions('from a | stats a=max(b), /', expected);
await assertSuggestions(
'from a | stats a=max(b) WHERE doubleField > longField, /',
expected
);
});
test('on assignment expression, shows all agg and eval functions', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats a=/', [
...allAggFunctions,
...allGroupingFunctions,
@ -78,31 +88,16 @@ describe('autocomplete.suggest', () => {
});
test('on space after aggregate field', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats a=min(b) /', ['BY ', ', ', '| ']);
});
test('on space after aggregate field with comma', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats a=max(b), /', [
'var0 = ',
...allAggFunctions,
...allGroupingFunctions,
...allEvaFunctions,
]);
await assertSuggestions('from a | stats a=min(b) /', ['WHERE ', 'BY ', ', ', '| ']);
});
test('on function left paren', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats by bucket(/', [
...getFieldNamesByType([...ESQL_COMMON_NUMERIC_TYPES, 'date', 'date_nanos']).map(
(field) => `${field}, `
),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
['date', 'date_nanos', ...ESQL_COMMON_NUMERIC_TYPES],
{
scalar: true,
@ -110,12 +105,12 @@ describe('autocomplete.suggest', () => {
).map((s) => ({ ...s, text: `${s.text},` })),
]);
await assertSuggestions('from a | stats round(/', [
...getFunctionSignaturesByReturnType('stats', roundParameterTypes, {
...getFunctionSignaturesByReturnType(Location.STATS, roundParameterTypes, {
agg: true,
}),
...getFieldNamesByType(roundParameterTypes),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
roundParameterTypes,
{ scalar: true },
undefined,
@ -123,10 +118,10 @@ describe('autocomplete.suggest', () => {
),
]);
await assertSuggestions('from a | stats round(round(/', [
...getFunctionSignaturesByReturnType('stats', roundParameterTypes, { agg: true }),
...getFunctionSignaturesByReturnType(Location.STATS, roundParameterTypes, { agg: true }),
...getFieldNamesByType(roundParameterTypes),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
ESQL_NUMBER_TYPES,
{ scalar: true },
undefined,
@ -136,7 +131,7 @@ describe('autocomplete.suggest', () => {
await assertSuggestions('from a | stats avg(round(/', [
...getFieldNamesByType(roundParameterTypes),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
ESQL_NUMBER_TYPES,
{ scalar: true },
undefined,
@ -145,20 +140,23 @@ describe('autocomplete.suggest', () => {
]);
await assertSuggestions('from a | stats avg(/', [
...getFieldNamesByType(avgTypes),
...getFunctionSignaturesByReturnType('eval', avgTypes, {
...getFunctionSignaturesByReturnType(Location.EVAL, avgTypes, {
scalar: true,
}),
]);
await assertSuggestions('from a | stats round(avg(/', [
...getFieldNamesByType(avgTypes),
...getFunctionSignaturesByReturnType('eval', avgTypes, { scalar: true }, undefined, [
'round',
]),
...getFunctionSignaturesByReturnType(
Location.EVAL,
avgTypes,
{ scalar: true },
undefined,
['round']
),
]);
});
test('when typing inside function left paren', async () => {
const { assertSuggestions } = await setup();
const expected = [
...getFieldNamesByType([
...ESQL_COMMON_NUMERIC_TYPES,
@ -171,7 +169,7 @@ describe('autocomplete.suggest', () => {
'keyword',
]),
...getFunctionSignaturesByReturnType(
'stats',
Location.STATS,
[
...ESQL_COMMON_NUMERIC_TYPES,
'date',
@ -194,25 +192,24 @@ describe('autocomplete.suggest', () => {
});
test('inside function argument list', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats avg(b/) by stringField', [
...getFieldNamesByType(avgTypes),
...getFunctionSignaturesByReturnType('eval', avgTypes, {
...getFunctionSignaturesByReturnType(Location.EVAL, avgTypes, {
scalar: true,
}),
]);
});
test('when typing right paren', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats a = min(b)/ | sort b', ['BY ', ', ', '| ']);
await assertSuggestions('from a | stats a = min(b)/ | sort b', [
'WHERE ',
'BY ',
', ',
'| ',
]);
});
test('increments suggested variable name counter', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | eval var0=round(b), var1=round(c) | stats /', [
'var2 = ',
// TODO verify that this change is ok
@ -227,11 +224,108 @@ describe('autocomplete.suggest', () => {
...allGroupingFunctions,
]);
});
describe('...WHERE expression...', () => {
it('suggests fields and functions in empty expression', async () => {
await assertSuggestions('FROM a | STATS MIN(b) WHERE /', [
...getFieldNamesByType('any').map((name) => `${name} `),
...getFunctionSignaturesByReturnType(Location.STATS_WHERE, 'any', { scalar: true }),
]);
});
it('suggests operators after a first operand', async () => {
await assertSuggestions('FROM a | STATS MIN(b) WHERE keywordField /', [
...getFunctionSignaturesByReturnType(Location.STATS_WHERE, 'any', { operators: true }, [
'keyword',
]),
]);
});
it('suggests after operator', async () => {
await assertSuggestions('FROM a | STATS MIN(b) WHERE keywordField != /', [
...getFieldNamesByType(['boolean', 'text', 'keyword']),
...getFunctionSignaturesByReturnType(
Location.STATS_WHERE,
['boolean', 'text', 'keyword'],
{ scalar: true }
),
]);
});
describe('completed expression suggestions', () => {
const completedExpressionSuggestions = ['| ', ', ', 'BY '];
test.each(completedExpressionSuggestions)(
'suggests "%s" after complete boolean expression',
async (suggestion) => {
const suggestions = await suggest(
'FROM a | STATS MIN(b) WHERE keywordField != keywordField /'
);
expect(suggestions.map(({ text }) => text)).toContain(suggestion);
}
);
test.each(completedExpressionSuggestions)(
'does NOT suggest "%s" after complete non-boolean',
async (suggestion) => {
const suggestions = await suggest('FROM a | STATS MIN(b) WHERE longField + 1 /');
expect(suggestions.map(({ text }) => text)).not.toContain(suggestion);
}
);
});
it('suggests after logical operator', async () => {
await assertSuggestions(
`FROM a | STATS AVG(doubleField) WHERE keywordField >= keywordField AND doubleField /`,
[
...getFunctionSignaturesByReturnType(
Location.STATS_WHERE,
'boolean',
{ operators: true },
['double']
),
]
);
});
describe('Parity with WHERE command', () => {
it('matches WHERE suggestions after a keyword expression', async () => {
const expression = 'keywordField';
const suggestions = await suggest(`FROM a | WHERE ${expression} /`);
expect(suggestions).not.toHaveLength(0);
await assertSuggestions(
`FROM a | STATS AVG(longField) WHERE ${expression} /`,
suggestions
.map(({ text }) => text)
// match operator not yet supported, see https://github.com/elastic/elasticsearch/issues/116261
.filter((text) => text !== ': $0')
);
});
it('matches WHERE suggestions after a boolean expression', async () => {
const expression = 'longField > longField';
const suggestions = await suggest(`FROM a | WHERE ${expression} /`);
expect(suggestions).not.toHaveLength(0);
await assertSuggestions(
`FROM a | STATS AVG(longField) WHERE ${expression} /`,
suggestions
.map(({ text }) => text)
// A couple extra goodies in STATS ... WHERE
.concat([', ', 'BY '])
);
});
});
});
});
describe('... BY <grouping>', () => {
test('on space after "BY" keyword', async () => {
const { assertSuggestions } = await setup();
const expected = [
'var0 = ',
getDateHistogramCompletionItem(),
@ -246,14 +340,10 @@ describe('autocomplete.suggest', () => {
});
test('on space after grouping field', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats a=c by d /', [', ', '| ']);
});
test('after comma "," in grouping fields', async () => {
const { assertSuggestions } = await setup();
const fields = getFieldNamesByType('any').map((field) => `${field} `);
await assertSuggestions('from a | stats a=c by d, /', [
'var0 = ',
@ -272,19 +362,17 @@ describe('autocomplete.suggest', () => {
'var0 = ',
getDateHistogramCompletionItem(),
...fields,
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
...allGroupingFunctions,
]);
});
test('on space before expression right hand side operand', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats avg(b) by integerField % /', [
...getFieldNamesByType('integer'),
...getFieldNamesByType('double'),
...getFieldNamesByType('long'),
...getFunctionSignaturesByReturnType('eval', ['integer', 'double', 'long'], {
...getFunctionSignaturesByReturnType(Location.EVAL, ['integer', 'double', 'long'], {
scalar: true,
}),
// categorize is not compatible here
@ -305,8 +393,6 @@ describe('autocomplete.suggest', () => {
});
test('on space after expression right hand side operand', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [', ', '| '], {
triggerCharacter: ' ',
});
@ -319,12 +405,11 @@ describe('autocomplete.suggest', () => {
});
test('on space within bucket()', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats avg(b) by BUCKET(/, 50, ?_tstart, ?_tend)', [
// Note there's no space or comma in the suggested field names
...getFieldNamesByType(['date', 'date_nanos', ...ESQL_COMMON_NUMERIC_TYPES]),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
['date', 'date_nanos', ...ESQL_COMMON_NUMERIC_TYPES],
{
scalar: true,
@ -335,7 +420,7 @@ describe('autocomplete.suggest', () => {
// Note there's no space or comma in the suggested field names
...getFieldNamesByType(['date', 'date_nanos', ...ESQL_COMMON_NUMERIC_TYPES]),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
['date', 'date_nanos', ...ESQL_COMMON_NUMERIC_TYPES],
{
scalar: true,
@ -347,7 +432,7 @@ describe('autocomplete.suggest', () => {
'from a | stats avg(b) by BUCKET(dateField, /50, ?_tstart, ?_tend)',
[
...getLiteralsByType('time_literal'),
...getFunctionSignaturesByReturnType('eval', ['integer', 'date_period'], {
...getFunctionSignaturesByReturnType(Location.EVAL, ['integer', 'date_period'], {
scalar: true,
}),
]
@ -355,14 +440,12 @@ describe('autocomplete.suggest', () => {
});
test('count(/) to suggest * for all', async () => {
const { suggest } = await setup();
const suggestions = await suggest('from a | stats count(/)');
expect(suggestions).toContain(allStarConstant);
});
describe('date histogram snippet', () => {
test('uses histogramBarTarget preference when available', async () => {
const { suggest } = await setup();
const histogramBarTarget = Math.random() * 100;
const expectedCompletionItem = getDateHistogramCompletionItem(histogramBarTarget);
@ -374,7 +457,6 @@ describe('autocomplete.suggest', () => {
});
test('defaults gracefully', async () => {
const { suggest } = await setup();
const expectedCompletionItem = getDateHistogramCompletionItem();
const suggestions = await suggest('FROM a | STATS BY /');
@ -385,8 +467,6 @@ describe('autocomplete.suggest', () => {
describe('create control suggestion', () => {
test('suggests `Create control` option for aggregations', async () => {
const { suggest } = await setup();
const suggestions = await suggest('FROM a | STATS /', {
callbacks: {
canSuggestVariables: () => true,
@ -406,8 +486,6 @@ describe('autocomplete.suggest', () => {
});
test('suggests `??function` option', async () => {
const { suggest } = await setup();
const suggestions = await suggest('FROM a | STATS var0 = /', {
callbacks: {
canSuggestVariables: () => true,
@ -433,8 +511,6 @@ describe('autocomplete.suggest', () => {
});
test('suggests `Create control` option for grouping', async () => {
const { suggest } = await setup();
const suggestions = await suggest('FROM a | STATS BY /', {
callbacks: {
canSuggestVariables: () => true,
@ -454,8 +530,6 @@ describe('autocomplete.suggest', () => {
});
test('suggests `??field` option', async () => {
const { suggest } = await setup();
const suggestions = await suggest('FROM a | STATS BY /', {
callbacks: {
canSuggestVariables: () => true,
@ -481,8 +555,6 @@ describe('autocomplete.suggest', () => {
});
test('suggests `?interval` option', async () => {
const { suggest } = await setup();
const suggestions = await suggest('FROM a | STATS BY BUCKET(@timestamp, /)', {
callbacks: {
canSuggestVariables: () => true,

View file

@ -19,9 +19,10 @@ import {
setup,
} from './helpers';
import { FULL_TEXT_SEARCH_FUNCTIONS } from '../../shared/constants';
import { Location } from '../../definitions/types';
describe('WHERE <expression>', () => {
const allEvalFns = getFunctionSignaturesByReturnType('where', 'any', {
const allEvalFns = getFunctionSignaturesByReturnType(Location.WHERE, 'any', {
scalar: true,
});
test('beginning an expression', async () => {
@ -57,7 +58,7 @@ describe('WHERE <expression>', () => {
await assertSuggestions('from a | where keywordField /', [
// all functions compatible with a keywordField type
...getFunctionSignaturesByReturnType(
'where',
Location.WHERE,
'boolean',
{
operators: true,
@ -75,7 +76,9 @@ describe('WHERE <expression>', () => {
...getDateLiterals(),
...getFieldNamesByType(['date']),
...getFieldNamesByType(['date_nanos']),
...getFunctionSignaturesByReturnType('where', ['date', 'date_nanos'], { scalar: true }),
...getFunctionSignaturesByReturnType(Location.WHERE, ['date', 'date_nanos'], {
scalar: true,
}),
];
await assertSuggestions(
'from a | where dateField == /',
@ -98,7 +101,7 @@ describe('WHERE <expression>', () => {
const expectedComparisonWithTextFieldSuggestions = [
...getFieldNamesByType(['text', 'keyword', 'ip', 'version']),
...getFunctionSignaturesByReturnType('where', ['text', 'keyword', 'ip', 'version'], {
...getFunctionSignaturesByReturnType(Location.WHERE, ['text', 'keyword', 'ip', 'version'], {
scalar: true,
}),
];
@ -119,16 +122,18 @@ describe('WHERE <expression>', () => {
for (const op of ['and', 'or']) {
await assertSuggestions(`from a | where keywordField >= keywordField ${op} /`, [
...getFieldNamesByType('any'),
...getFunctionSignaturesByReturnType('where', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.WHERE, 'any', { scalar: true }),
]);
await assertSuggestions(`from a | where keywordField >= keywordField ${op} doubleField /`, [
...getFunctionSignaturesByReturnType('where', 'boolean', { operators: true }, ['double']),
...getFunctionSignaturesByReturnType(Location.WHERE, 'boolean', { operators: true }, [
'double',
]),
]);
await assertSuggestions(
`from a | where keywordField >= keywordField ${op} doubleField == /`,
[
...getFieldNamesByType(ESQL_COMMON_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('where', ESQL_COMMON_NUMERIC_TYPES, {
...getFunctionSignaturesByReturnType(Location.WHERE, ESQL_COMMON_NUMERIC_TYPES, {
scalar: true,
}),
]
@ -159,7 +164,7 @@ describe('WHERE <expression>', () => {
await assertSuggestions('from a | stats a=avg(doubleField) | where a /', [
...getFunctionSignaturesByReturnType(
'where',
Location.WHERE,
'any',
{ operators: true, skipAssign: true },
['double']
@ -186,7 +191,7 @@ describe('WHERE <expression>', () => {
[
...getFieldNamesByType(log10ParameterTypes),
...getFunctionSignaturesByReturnType(
'where',
Location.WHERE,
log10ParameterTypes,
{ scalar: true },
undefined,
@ -200,7 +205,7 @@ describe('WHERE <expression>', () => {
[
...getFieldNamesByType(powParameterTypes),
...getFunctionSignaturesByReturnType(
'where',
Location.WHERE,
powParameterTypes,
{ scalar: true },
undefined,
@ -215,8 +220,12 @@ describe('WHERE <expression>', () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | where log10(doubleField) /', [
...getFunctionSignaturesByReturnType('where', 'double', { operators: true }, ['double']),
...getFunctionSignaturesByReturnType('where', 'boolean', { operators: true }, ['double']),
...getFunctionSignaturesByReturnType(Location.WHERE, 'double', { operators: true }, [
'double',
]),
...getFunctionSignaturesByReturnType(Location.WHERE, 'boolean', { operators: true }, [
'double',
]),
]);
});
@ -234,13 +243,17 @@ describe('WHERE <expression>', () => {
]);
await assertSuggestions('from index | WHERE not /', [
...getFieldNamesByType('boolean').map((name) => attachTriggerCommand(`${name} `)),
...getFunctionSignaturesByReturnType('where', 'boolean', { scalar: true }, undefined, [
':',
]),
...getFunctionSignaturesByReturnType(
Location.WHERE,
'boolean',
{ scalar: true },
undefined,
[':']
),
]);
await assertSuggestions('FROM index | WHERE NOT ENDS_WITH(keywordField, "foo") /', [
...getFunctionSignaturesByReturnType(
'where',
Location.WHERE,
'boolean',
{ operators: true },
['boolean'],
@ -280,7 +293,7 @@ describe('WHERE <expression>', () => {
'from index | WHERE doubleField not in (/)',
[
...getFieldNamesByType('double').filter((name) => name !== 'doubleField'),
...getFunctionSignaturesByReturnType('where', 'double', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.WHERE, 'double', { scalar: true }),
],
{ triggerCharacter: '(' }
);
@ -288,13 +301,13 @@ describe('WHERE <expression>', () => {
...getFieldNamesByType('double').filter(
(name) => name !== '`any#Char$Field`' && name !== 'doubleField'
),
...getFunctionSignaturesByReturnType('where', 'double', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.WHERE, 'double', { scalar: true }),
]);
await assertSuggestions('from index | WHERE doubleField not in ( `any#Char$Field`, /)', [
...getFieldNamesByType('double').filter(
(name) => name !== '`any#Char$Field`' && name !== 'doubleField'
),
...getFunctionSignaturesByReturnType('where', 'double', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.WHERE, 'double', { scalar: true }),
]);
});
@ -311,7 +324,7 @@ describe('WHERE <expression>', () => {
await assertSuggestions('FROM index | WHERE doubleField + doubleField /', [
...getFunctionSignaturesByReturnType(
'where',
Location.WHERE,
'any',
{ operators: true, skipAssign: true },
['double'],

View file

@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Location } from '../../definitions/types';
import { ESQL_COMMON_NUMERIC_TYPES } from '../../shared/esql_types';
import {
AssertSuggestionsFn,
@ -30,7 +31,9 @@ describe('case', () => {
const allSuggestions = [
// With extra space after field name to open suggestions
...getFieldNamesByType('any').map((field) => `${field} `),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }, undefined, ['case']),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }, undefined, [
'case',
]),
];
test('first position', async () => {
@ -61,7 +64,7 @@ describe('case', () => {
// Notice no extra space after field name
...getFieldNamesByType(['keyword', 'text', 'boolean']).map((field) => `${field}`),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
['keyword', 'text', 'boolean'],
{ scalar: true },
undefined,
@ -77,7 +80,7 @@ describe('case', () => {
// Notice no extra space after field name
...getFieldNamesByType(ESQL_COMMON_NUMERIC_TYPES).map((field) => `${field}`),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
ESQL_COMMON_NUMERIC_TYPES,
{ scalar: true },
undefined,
@ -97,7 +100,9 @@ describe('case', () => {
[
// With extra space after field name to open suggestions
...getFieldNamesByType('any').map((field) => `${field}`),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }, undefined, ['case']),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }, undefined, [
'case',
]),
],
{
triggerCharacter: ' ',
@ -110,7 +115,9 @@ describe('case', () => {
[
// With extra space after field name to open suggestions
...getFieldNamesByType('any').map((field) => `${field} `),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }, undefined, ['case']),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }, undefined, [
'case',
]),
],
{
triggerCharacter: ' ',

View file

@ -16,6 +16,7 @@ import { aggFunctionDefinitions } from '../../definitions/generated/aggregation_
import { timeUnitsToSuggest } from '../../definitions/literals';
import {
FunctionDefinitionTypes,
Location,
getLocationFromCommandOrOptionName,
} from '../../definitions/types';
import { groupingFunctionDefinitions } from '../../definitions/generated/grouping_functions';
@ -135,7 +136,7 @@ export const policies = [
* @returns
*/
export function getFunctionSignaturesByReturnType(
command: string | string[],
location: Location | Location[],
_expectedReturnType: Readonly<FunctionReturnType | 'any' | Array<FunctionReturnType | 'any'>>,
{
agg,
@ -177,7 +178,7 @@ export function getFunctionSignaturesByReturnType(
const deduped = Array.from(new Set(list));
const commands = Array.isArray(command) ? command : [command];
const locations = Array.isArray(location) ? location : [location];
return deduped
.filter(({ signatures, ignoreAsSuggestion, locationsAvailable }) => {
@ -185,8 +186,8 @@ export function getFunctionSignaturesByReturnType(
return false;
}
if (
!(option ? [...commands, option] : commands).some((name) =>
locationsAvailable.includes(getLocationFromCommandOrOptionName(name))
!(option ? [...locations, getLocationFromCommandOrOptionName(option)] : locations).some(
(loc) => locationsAvailable.includes(loc)
)
) {
return false;
@ -332,6 +333,11 @@ export type AssertSuggestionsFn = (
opts?: SuggestOptions
) => Promise<void>;
export type SuggestFn = (
query: string,
opts?: SuggestOptions
) => Promise<SuggestionRawDefinition[]>;
export const setup = async (caret = '/') => {
if (caret.length !== 1) {
throw new Error('Caret must be a single character');
@ -339,7 +345,7 @@ export const setup = async (caret = '/') => {
const callbacks = createCustomCallbackMocks();
const suggest = async (query: string, opts: SuggestOptions = {}) => {
const suggest: SuggestFn = async (query, opts = {}) => {
const pos = query.indexOf(caret);
if (pos < 0) throw new Error(`User cursor/caret "${caret}" not found in query: ${query}`);
const querySansCaret = query.slice(0, pos) + query.slice(pos + 1);
@ -356,11 +362,7 @@ export const setup = async (caret = '/') => {
);
};
const assertSuggestions = async (
query: string,
expected: Array<string | PartialSuggestionWithText>,
opts?: SuggestOptions
) => {
const assertSuggestions: AssertSuggestionsFn = async (query, expected, opts) => {
try {
const result = await suggest(query, opts);
const resultTexts = [...result.map((suggestion) => suggestion.text)].sort();

View file

@ -31,6 +31,7 @@ import { METADATA_FIELDS } from '../shared/constants';
import { ESQL_STRING_TYPES } from '../shared/esql_types';
import { getRecommendedQueries } from './recommended_queries/templates';
import { getDateHistogramCompletionItem } from './commands/stats/util';
import { Location } from '../definitions/types';
const commandDefinitions = unmodifiedCommandDefinitions.filter(({ hidden }) => !hidden);
@ -282,7 +283,7 @@ describe('autocomplete', () => {
testSuggestions('FROM kibana_sample_data_logs | EVAL TRIM(e/)', [
...getFieldNamesByType(['text', 'keyword']),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
['text', 'keyword'],
{ scalar: true },
undefined,
@ -295,12 +296,12 @@ describe('autocomplete', () => {
TIME_PICKER_SUGGESTION,
...TIME_SYSTEM_PARAMS.map((t) => `${t}, `),
...getFieldNamesByType(['date', 'date_nanos']).map((name) => `${name}, `),
...getFunctionSignaturesByReturnType('eval', ['date', 'date_nanos'], { scalar: true }).map(
(s) => ({
...s,
text: `${s.text},`,
})
),
...getFunctionSignaturesByReturnType(Location.EVAL, ['date', 'date_nanos'], {
scalar: true,
}).map((s) => ({
...s,
text: `${s.text},`,
})),
];
testSuggestions('FROM a | EVAL DATE_DIFF("day", /)', expectedDateDiff2ndArgSuggestions);
@ -328,12 +329,12 @@ describe('autocomplete', () => {
testSuggestions('FROM index1 | EVAL b/', [
'var0 = ',
...getFieldNamesByType('any').map((name) => `${name} `),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
]);
testSuggestions('FROM index1 | EVAL var0 = f/', [
...getFieldNamesByType('any').map((name) => `${name} `),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.EVAL, 'any', { scalar: true }),
]);
// DISSECT field
@ -411,7 +412,7 @@ describe('autocomplete', () => {
// STATS argument
testSuggestions('FROM index1 | STATS f/', [
'var0 = ',
...getFunctionSignaturesByReturnType('stats', 'any', {
...getFunctionSignaturesByReturnType(Location.STATS, 'any', {
scalar: true,
agg: true,
grouping: true,
@ -419,27 +420,27 @@ describe('autocomplete', () => {
]);
// STATS argument BY
testSuggestions('FROM index1 | STATS AVG(booleanField) B/', ['BY ', ', ', '| ']);
testSuggestions('FROM index1 | STATS AVG(booleanField) B/', ['WHERE ', 'BY ', ', ', '| ']);
// STATS argument BY expression
testSuggestions('FROM index1 | STATS field BY f/', [
'var0 = ',
getDateHistogramCompletionItem(),
...getFunctionSignaturesByReturnType('stats', 'any', { grouping: true, scalar: true }),
...getFunctionSignaturesByReturnType(Location.STATS, 'any', { grouping: true, scalar: true }),
...getFieldNamesByType('any').map((field) => `${field} `),
]);
// WHERE argument
testSuggestions('FROM index1 | WHERE f/', [
...getFieldNamesByType('any').map((field) => `${field} `),
...getFunctionSignaturesByReturnType('where', 'any', { scalar: true }),
...getFunctionSignaturesByReturnType(Location.WHERE, 'any', { scalar: true }),
]);
// WHERE argument comparison
testSuggestions(
'FROM index1 | WHERE keywordField i/',
getFunctionSignaturesByReturnType(
'where',
Location.WHERE,
'boolean',
{
operators: true,
@ -453,7 +454,7 @@ describe('autocomplete', () => {
testSuggestions(
'FROM index1 | WHERE ABS(integerField) i/',
getFunctionSignaturesByReturnType(
'where',
Location.WHERE,
'any',
{
operators: true,
@ -520,7 +521,7 @@ describe('autocomplete', () => {
.map((field) => `${field}, `)
.map(attachTriggerCommand),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
ESQL_STRING_TYPES,
{ scalar: true },
undefined,
@ -546,7 +547,7 @@ describe('autocomplete', () => {
command: undefined,
})),
...getFunctionSignaturesByReturnType(
'eval',
Location.EVAL,
ESQL_STRING_TYPES,
{ scalar: true },
undefined,
@ -766,7 +767,7 @@ describe('autocomplete', () => {
'FROM a | STATS /',
[
'var0 = ',
...getFunctionSignaturesByReturnType('stats', 'any', {
...getFunctionSignaturesByReturnType(Location.STATS, 'any', {
scalar: true,
agg: true,
grouping: true,
@ -777,13 +778,14 @@ describe('autocomplete', () => {
// STATS argument BY
testSuggestions('FROM a | STATS AVG(numberField) /', [
', ',
attachTriggerCommand('WHERE '),
attachTriggerCommand('BY '),
attachTriggerCommand('| '),
]);
// STATS argument BY field
const allByCompatibleFunctions = getFunctionSignaturesByReturnType(
'stats',
Location.STATS,
'any',
{
scalar: true,
@ -816,14 +818,16 @@ describe('autocomplete', () => {
...getFieldNamesByType('any')
.map((field) => `${field} `)
.map(attachTriggerCommand),
...getFunctionSignaturesByReturnType('where', 'any', { scalar: true }).map(attachAsSnippet),
...getFunctionSignaturesByReturnType(Location.WHERE, 'any', { scalar: true }).map(
attachAsSnippet
),
]);
// WHERE argument comparison
testSuggestions(
'FROM a | WHERE keywordField /',
getFunctionSignaturesByReturnType(
'where',
Location.WHERE,
'boolean',
{
operators: true,

View file

@ -14,10 +14,7 @@ import { CommandSuggestParams, Location } from '../../../definitions/types';
import type { SuggestionRawDefinition } from '../../types';
import { getNewVariableSuggestion } from '../../factories';
import { commaCompleteItem, pipeCompleteItem } from '../../complete_items';
import { buildPartialMatcher, getExpressionPosition, suggestForExpression } from '../../helper';
const isNullMatcher = buildPartialMatcher('is nul');
const isNotNullMatcher = buildPartialMatcher('is not nul');
import { getExpressionPosition, isExpressionComplete, suggestForExpression } from '../../helper';
export async function suggest(
params: CommandSuggestParams<'eval'>
@ -48,15 +45,9 @@ export async function suggest(
suggestions.push(getNewVariableSuggestion(params.getSuggestedVariableName()));
}
const isExpressionComplete =
params.getExpressionType(expressionRoot) !== 'unknown' &&
// see https://github.com/elastic/kibana/issues/199401
// for the reason we need this string check.
!(isNullMatcher.test(params.innerText) || isNotNullMatcher.test(params.innerText));
if (
// don't suggest finishing characters if incomplete expression
isExpressionComplete &&
isExpressionComplete(params.getExpressionType(expressionRoot), params.innerText) &&
// don't suggest finishing characters if the expression is a column
// because "EVAL columnName" is a useless expression
expressionRoot &&

View file

@ -7,6 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ESQLVariableType } from '@kbn/esql-types';
import { ESQLFunction } from '@kbn/esql-ast';
import { isSingleItem } from '../../../..';
import { CommandSuggestParams, Location } from '../../../definitions/types';
import type { SuggestionRawDefinition } from '../../types';
import {
@ -16,8 +18,14 @@ import {
getControlSuggestionIfSupported,
} from '../../factories';
import { commaCompleteItem, pipeCompleteItem } from '../../complete_items';
import { pushItUpInTheList } from '../../helper';
import { byCompleteItem, getDateHistogramCompletionItem, getPosition } from './util';
import { isExpressionComplete, pushItUpInTheList, suggestForExpression } from '../../helper';
import {
byCompleteItem,
getDateHistogramCompletionItem,
getPosition,
whereCompleteItem,
} from './util';
import { isMarkerNode } from '../../../shared/context';
export async function suggest({
innerText,
@ -27,6 +35,7 @@ export async function suggest({
getPreferences,
getVariables,
supportsControls,
getExpressionType,
}: CommandSuggestParams<'stats'>): Promise<SuggestionRawDefinition[]> {
const pos = getPosition(innerText, command);
@ -53,11 +62,38 @@ export async function suggest({
case 'expression_complete':
return [
whereCompleteItem,
byCompleteItem,
pipeCompleteItem,
{ ...commaCompleteItem, command: TRIGGER_SUGGESTION_COMMAND, text: ', ' },
];
case 'after_where':
const whereFn = command.args[command.args.length - 1] as ESQLFunction;
const expressionRoot = isMarkerNode(whereFn.args[1]) ? undefined : whereFn.args[1]!;
if (expressionRoot && !isSingleItem(expressionRoot)) {
return [];
}
const suggestions = await suggestForExpression({
expressionRoot,
getExpressionType,
getColumnsByType,
location: Location.STATS_WHERE,
innerText,
preferredExpressionType: 'boolean',
});
// Is this a complete boolean expression?
// If so, we can call it done and suggest a pipe
const expressionType = getExpressionType(expressionRoot);
if (expressionType === 'boolean' && isExpressionComplete(expressionType, innerText)) {
suggestions.push(pipeCompleteItem, { ...commaCompleteItem, text: ', ' }, byCompleteItem);
}
return suggestions;
case 'grouping_expression_after_assignment':
return [
...getFunctionSuggestions({ location: Location.STATS_BY }),

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ESQLCommand } from '@kbn/esql-ast';
import { ESQLCommand, isFunctionExpression } from '@kbn/esql-ast';
import { i18n } from '@kbn/i18n';
import {
findPreviousWord,
@ -38,7 +38,8 @@ export type CaretPosition =
| 'expression_complete'
| 'grouping_expression_without_assignment'
| 'grouping_expression_after_assignment'
| 'grouping_expression_complete';
| 'grouping_expression_complete'
| 'after_where';
export const getPosition = (innerText: string, command: ESQLCommand): CaretPosition => {
const lastCommandArg = command.args[command.args.length - 1];
@ -65,14 +66,16 @@ export const getPosition = (innerText: string, command: ESQLCommand): CaretPosit
return 'expression_after_assignment';
}
if (
getLastNonWhitespaceChar(innerText) === ',' ||
noCaseCompare(findPreviousWord(innerText), 'stats')
) {
const previousWord = findPreviousWord(innerText);
if (getLastNonWhitespaceChar(innerText) === ',' || noCaseCompare(previousWord, 'stats')) {
return 'expression_without_assignment';
} else {
return 'expression_complete';
}
if (isFunctionExpression(lastCommandArg) && lastCommandArg.name === 'where') {
return 'after_where';
}
return 'expression_complete';
};
export const byCompleteItem: SuggestionRawDefinition = {
@ -84,6 +87,15 @@ export const byCompleteItem: SuggestionRawDefinition = {
command: TRIGGER_SUGGESTION_COMMAND,
};
export const whereCompleteItem: SuggestionRawDefinition = {
label: 'WHERE',
text: 'WHERE ',
kind: 'Reference',
detail: 'Where',
sortText: '1',
command: TRIGGER_SUGGESTION_COMMAND,
};
export const getDateHistogramCompletionItem: (
histogramBarTarget?: number
) => SuggestionRawDefinition = (histogramBarTarget: number = 50) => ({

View file

@ -11,10 +11,7 @@ import { type ESQLSingleAstItem } from '@kbn/esql-ast';
import { CommandSuggestParams, Location } from '../../../definitions/types';
import type { SuggestionRawDefinition } from '../../types';
import { pipeCompleteItem } from '../../complete_items';
import { buildPartialMatcher, suggestForExpression } from '../../helper';
const isNullMatcher = buildPartialMatcher('is nul');
const isNotNullMatcher = buildPartialMatcher('is not nul');
import { isExpressionComplete, suggestForExpression } from '../../helper';
export async function suggest(
params: CommandSuggestParams<'where'>
@ -27,16 +24,10 @@ export async function suggest(
preferredExpressionType: 'boolean',
});
const isExpressionComplete =
expressionRoot &&
params.getExpressionType(expressionRoot) === 'boolean' &&
// see https://github.com/elastic/kibana/issues/199401
// for the reason we need this string check.
!(isNullMatcher.test(params.innerText) || isNotNullMatcher.test(params.innerText));
// Is this a complete boolean expression?
// If so, we can call it done and suggest a pipe
if (isExpressionComplete) {
const expressionType = params.getExpressionType(expressionRoot);
if (expressionType === 'boolean' && isExpressionComplete(expressionType, params.innerText)) {
suggestions.push(pipeCompleteItem);
}

View file

@ -952,3 +952,27 @@ export function buildPartialMatcher(str: string) {
// Return the final regex pattern
return new RegExp(pattern + '$', 'i');
}
const isNullMatcher = buildPartialMatcher('is nul');
const isNotNullMatcher = buildPartialMatcher('is not nul');
/**
* Checks whether an expression is truly complete.
*
* (Encapsulates handling of the "is null" and "is not null"
* checks)
*
* @todo use the simpler "getExpressionType(root) !== 'unknown'"
* as soon as https://github.com/elastic/kibana/issues/199401 is resolved
*/
export function isExpressionComplete(
expressionType: SupportedDataType | 'unknown',
innerText: string
) {
return (
expressionType !== 'unknown' &&
// see https://github.com/elastic/kibana/issues/199401
// for the reason we need this string check.
!(isNullMatcher.test(innerText) || isNotNullMatcher.test(innerText))
);
}

View file

@ -72,6 +72,7 @@ function createComparisonDefinition(
Location.ROW,
Location.SORT,
Location.STATS_BY,
Location.STATS_WHERE,
],
validate,
signatures: [
@ -224,6 +225,7 @@ export const logicalOperators: FunctionDefinition[] = [
Location.ROW,
Location.SORT,
Location.STATS_BY,
Location.STATS_WHERE,
],
signatures: [
{
@ -249,6 +251,7 @@ const otherDefinitions: FunctionDefinition[] = [
Location.ROW,
Location.SORT,
Location.STATS_BY,
Location.STATS_WHERE,
],
signatures: [
{

View file

@ -375,6 +375,7 @@ const addDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: undefined,
@ -549,6 +550,7 @@ const divDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: (fnDef) => {
@ -1070,6 +1072,7 @@ const equalsDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: undefined,
@ -1450,6 +1453,7 @@ const greaterThanDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: undefined,
@ -1833,6 +1837,7 @@ const greaterThanOrEqualDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: undefined,
@ -2075,7 +2080,13 @@ const inDefinition: FunctionDefinition = {
minParams: 2,
},
],
locationsAvailable: [Location.EVAL, Location.WHERE, Location.SORT, Location.ROW],
locationsAvailable: [
Location.EVAL,
Location.WHERE,
Location.SORT,
Location.ROW,
Location.STATS_WHERE,
],
validate: undefined,
examples: ['ROW a = 1, b = 4, c = 3\n| WHERE c-a IN (3, b / 2, a)'],
};
@ -2454,6 +2465,7 @@ const lessThanDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: undefined,
@ -2781,6 +2793,7 @@ const lessThanOrEqualDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: undefined,
@ -2863,7 +2876,13 @@ const likeDefinition: FunctionDefinition = {
minParams: 2,
},
],
locationsAvailable: [Location.EVAL, Location.WHERE, Location.SORT, Location.ROW],
locationsAvailable: [
Location.EVAL,
Location.WHERE,
Location.SORT,
Location.ROW,
Location.STATS_WHERE,
],
validate: undefined,
examples: ['FROM employees\n| WHERE first_name LIKE """?b*"""\n| KEEP first_name, last_name'],
};
@ -3548,6 +3567,7 @@ const modDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: (fnDef) => {
@ -3747,6 +3767,7 @@ const mulDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: undefined,
@ -4056,7 +4077,13 @@ const notInDefinition: FunctionDefinition = {
minParams: 2,
},
],
locationsAvailable: [Location.EVAL, Location.WHERE, Location.SORT, Location.ROW],
locationsAvailable: [
Location.EVAL,
Location.WHERE,
Location.SORT,
Location.ROW,
Location.STATS_WHERE,
],
validate: undefined,
examples: [],
};
@ -4106,7 +4133,13 @@ const notLikeDefinition: FunctionDefinition = {
minParams: 2,
},
],
locationsAvailable: [Location.EVAL, Location.WHERE, Location.SORT, Location.ROW],
locationsAvailable: [
Location.EVAL,
Location.WHERE,
Location.SORT,
Location.ROW,
Location.STATS_WHERE,
],
validate: undefined,
examples: [],
};
@ -4156,7 +4189,13 @@ const notRlikeDefinition: FunctionDefinition = {
minParams: 2,
},
],
locationsAvailable: [Location.EVAL, Location.WHERE, Location.SORT, Location.ROW],
locationsAvailable: [
Location.EVAL,
Location.WHERE,
Location.SORT,
Location.ROW,
Location.STATS_WHERE,
],
validate: undefined,
examples: [],
};
@ -4651,6 +4690,7 @@ const notEqualsDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: undefined,
@ -4818,7 +4858,13 @@ const isNullDefinition: FunctionDefinition = {
returnType: 'boolean',
},
],
locationsAvailable: [Location.EVAL, Location.WHERE, Location.SORT, Location.ROW],
locationsAvailable: [
Location.EVAL,
Location.WHERE,
Location.SORT,
Location.ROW,
Location.STATS_WHERE,
],
validate: undefined,
examples: [
'FROM employees\n| WHERE birth_date IS NULL\n| KEEP first_name, last_name\n| SORT first_name\n| LIMIT 3',
@ -4987,7 +5033,13 @@ const isNotNullDefinition: FunctionDefinition = {
returnType: 'boolean',
},
],
locationsAvailable: [Location.EVAL, Location.WHERE, Location.SORT, Location.ROW],
locationsAvailable: [
Location.EVAL,
Location.WHERE,
Location.SORT,
Location.ROW,
Location.STATS_WHERE,
],
validate: undefined,
examples: [
'FROM employees\n| WHERE birth_date IS NULL\n| KEEP first_name, last_name\n| SORT first_name\n| LIMIT 3',
@ -5071,7 +5123,13 @@ const rlikeDefinition: FunctionDefinition = {
minParams: 2,
},
],
locationsAvailable: [Location.EVAL, Location.WHERE, Location.SORT, Location.ROW],
locationsAvailable: [
Location.EVAL,
Location.WHERE,
Location.SORT,
Location.ROW,
Location.STATS_WHERE,
],
validate: undefined,
examples: [
'FROM employees\n| WHERE first_name RLIKE """.leja.*"""\n| KEEP first_name, last_name',
@ -5392,6 +5450,7 @@ const subDefinition: FunctionDefinition = {
Location.WHERE,
Location.ROW,
Location.SORT,
Location.STATS_WHERE,
Location.STATS_BY,
],
validate: undefined,

View file

@ -10117,7 +10117,6 @@ const termDefinition: FunctionDefinition = {
'Performs a Term query on the specified field. Returns true if the provided term matches the row.',
}),
ignoreAsSuggestion: true,
preview: true,
alias: undefined,
signatures: [

View file

@ -81,7 +81,11 @@ function findCommandSubType<T extends ESQLCommandMode | ESQLCommandOption>(
}
}
export function isMarkerNode(node: ESQLSingleAstItem | undefined): boolean {
export function isMarkerNode(node: ESQLAstItem | undefined): boolean {
if (Array.isArray(node)) {
return false;
}
return Boolean(
node &&
(isColumnItem(node) || isIdentifier(node) || isSourceItem(node)) &&
@ -173,12 +177,22 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number)
};
}
let withinStatsWhereClause = false;
Walker.walk(ast, {
visitFunction: (fn) => {
if (fn.name === 'where' && fn.location.min <= offset) {
withinStatsWhereClause = true;
}
},
});
const { command, option, node, containingFunction } = findAstPosition(ast, offset);
if (node) {
if (node.type === 'literal' && node.literalType === 'keyword') {
// command ... "<here>"
return { type: 'value' as const, command, node, option, containingFunction };
}
if (node.type === 'function') {
if (['in', 'not_in'].includes(node.name) && Array.isArray(node.args[1])) {
// command ... a in ( <here> )
@ -186,11 +200,7 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number)
}
if (
isNotEnrichClauseAssigment(node, command) &&
// Temporarily mangling the logic here to let operators
// be handled as functions for the stats command.
// I expect this to simplify once https://github.com/elastic/kibana/issues/195418
// is complete
!(isOperator(node) && command.name !== 'stats')
(!isOperator(node) || (command.name === 'stats' && !withinStatsWhereClause))
) {
// command ... fn( <here> )
return { type: 'function' as const, command, node, option, containingFunction };