mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
7bf76b0e7a
commit
2fda88c166
21 changed files with 479 additions and 223 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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},`),
|
||||
],
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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: ' ',
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue