[ES|QL] METRICS command definition and validation (#184905)

## Summary

Partially addresses https://github.com/elastic/kibana/issues/184498

The main contribution of this PR is the `METRICS` command validation
cases:

<img width="778" alt="image"
src="3d768952-3fa3-4928-b251-204c30d20c4b">

See own-review below for more comments.


### Checklist

Delete any items that are not applicable to this PR.

- [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

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Vadim Kibana 2024-06-20 09:22:58 +02:00 committed by GitHub
parent 37426f0bde
commit d9fc2ca1ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 2031 additions and 1608 deletions

View file

@ -24,6 +24,7 @@ export type {
ESQLLiteral,
AstProviderFn,
EditorError,
ESQLAstNode,
} from './src/types';
// Low level functions to parse grammar

View file

@ -19,7 +19,7 @@ describe('METRICS', () => {
{
type: 'command',
name: 'metrics',
indices: [
sources: [
{
type: 'source',
name: 'foo',
@ -30,7 +30,7 @@ describe('METRICS', () => {
]);
});
it('can parse multiple "indices"', () => {
it('can parse multiple "sources"', () => {
const text = 'METRICS foo ,\nbar\t,\t\nbaz \n';
const { ast, errors } = parse(text);
@ -39,7 +39,7 @@ describe('METRICS', () => {
{
type: 'command',
name: 'metrics',
indices: [
sources: [
{
type: 'source',
name: 'foo',
@ -69,7 +69,7 @@ describe('METRICS', () => {
{
type: 'command',
name: 'metrics',
indices: [
sources: [
{
type: 'source',
name: 'foo',
@ -99,7 +99,7 @@ describe('METRICS', () => {
{
type: 'command',
name: 'metrics',
indices: [
sources: [
{
type: 'source',
name: 'foo',

View file

@ -44,7 +44,7 @@ import {
import { getPosition } from './ast_position_utils';
import {
collectAllSourceIdentifiers,
collectAllFieldsStatements,
collectAllFields,
visitByOption,
collectAllColumnIdentifiers,
visitRenameClauses,
@ -120,7 +120,7 @@ export class AstListener implements ESQLParserListener {
exitRowCommand(ctx: RowCommandContext) {
const command = createCommand('row', ctx);
this.ast.push(command);
command.args.push(...collectAllFieldsStatements(ctx.fields()));
command.args.push(...collectAllFields(ctx.fields()));
}
/**
@ -153,20 +153,20 @@ export class AstListener implements ESQLParserListener {
...createAstBaseItem('metrics', ctx),
type: 'command',
args: [],
indices: ctx
sources: ctx
.getTypedRuleContexts(IndexIdentifierContext)
.map((sourceCtx) => createSource(sourceCtx)),
};
this.ast.push(node);
const aggregates = collectAllFieldsStatements(ctx.fields(0));
const grouping = collectAllFieldsStatements(ctx.fields(1));
const aggregates = collectAllFields(ctx.fields(0));
const grouping = collectAllFields(ctx.fields(1));
if (aggregates && aggregates.length) {
node.aggregates = aggregates;
}
if (grouping && grouping.length) {
node.grouping = grouping;
}
node.args.push(...node.indices, ...aggregates, ...grouping);
node.args.push(...node.sources, ...aggregates, ...grouping);
}
/**
@ -176,7 +176,7 @@ export class AstListener implements ESQLParserListener {
exitEvalCommand(ctx: EvalCommandContext) {
const commandAst = createCommand('eval', ctx);
this.ast.push(commandAst);
commandAst.args.push(...collectAllFieldsStatements(ctx.fields()));
commandAst.args.push(...collectAllFields(ctx.fields()));
}
/**
@ -189,7 +189,7 @@ export class AstListener implements ESQLParserListener {
// STATS expression is optional
if (ctx._stats) {
command.args.push(...collectAllFieldsStatements(ctx.fields(0)));
command.args.push(...collectAllFields(ctx.fields(0)));
}
if (ctx._grouping) {
command.args.push(...visitByOption(ctx, ctx._stats ? ctx.fields(1) : ctx.fields(0)));

View file

@ -90,6 +90,7 @@ import type {
ESQLFunction,
ESQLCommandOption,
ESQLAstItem,
ESQLAstField,
ESQLInlineCast,
ESQLUnnamedParamLiteral,
ESQLPositionalParamLiteral,
@ -547,14 +548,14 @@ export function visitField(ctx: FieldContext) {
return collectBooleanExpression(ctx.booleanExpression());
}
export function collectAllFieldsStatements(ctx: FieldsContext | undefined): ESQLAstItem[] {
const ast: ESQLAstItem[] = [];
export function collectAllFields(ctx: FieldsContext | undefined): ESQLAstField[] {
const ast: ESQLAstField[] = [];
if (!ctx) {
return ast;
}
try {
for (const field of ctx.field_list()) {
ast.push(...visitField(field));
ast.push(...(visitField(field) as ESQLAstField[]));
}
} catch (e) {
// do nothing
@ -567,7 +568,7 @@ export function visitByOption(ctx: StatsCommandContext, expr: FieldsContext | un
return [];
}
const option = createOption(ctx.BY()!.getText().toLowerCase(), ctx);
option.args.push(...collectAllFieldsStatements(expr));
option.args.push(...collectAllFields(expr));
return [option];
}

View file

@ -46,9 +46,9 @@ export interface ESQLCommand<Name = string> extends ESQLAstBaseItem<Name> {
}
export interface ESQLAstMetricsCommand extends ESQLCommand<'metrics'> {
indices: ESQLSource[];
aggregates?: ESQLAstItem[];
grouping?: ESQLAstItem[];
sources: ESQLSource[];
aggregates?: ESQLAstField[];
grouping?: ESQLAstField[];
}
export interface ESQLCommandOption extends ESQLAstBaseItem {

View file

@ -39,7 +39,7 @@ const myCallbacks = {
const { errors, warnings } = await validateQuery("from index | stats 1 + avg(myColumn)", getAstAndSyntaxErrors, undefined, myCallbacks);
```
If not all callbacks are available it is possible to gracefully degradate the validation experience with the `ignoreOnMissingCallbacks` option:
If not all callbacks are available it is possible to gracefully degrade the validation experience with the `ignoreOnMissingCallbacks` option:
```js
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
@ -61,7 +61,7 @@ const { errors, warnings } = await validateQuery(
#### Autocomplete
This is the complete logic for the ES|QL autocomplete language, it is completely indepedent from the actual editor (i.e. Monaco) and the suggestions reported need to be wrapped against the specific editor shape.
This is the complete logic for the ES|QL autocomplete language, it is completely independent from the actual editor (i.e. Monaco) and the suggestions reported need to be wrapped against the specific editor shape.
```js
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
@ -207,13 +207,13 @@ The autocomplete/suggest task takes a query as input together with the current c
Note that autocomplete works most of the time with incomplete/invalid queries, so some logic to manipulate the query into something valid (see the `EDITOR_MARKER` or the `countBracketsUnclosed` functions for more).
Once the AST is produced there's a `getAstContext` function that finds the cursor position node (and its parent command), together with some hint like the type of current context: `expression`, `function`, `newCommand`, `option`.
The most complex case is the `expression` as it can cover a moltitude of cases. The function is highly commented in order to identify the specific cases, but there's probably some obscure area still to comment/clarify.
The most complex case is the `expression` as it can cover a multitude of cases. The function is highly commented in order to identify the specific cases, but there's probably some obscure area still to comment/clarify.
### Adding new commands/options/functions/erc...
### Adding new commands/options/functions/etc...
To update the definitions:
1. open either approriate definition file within the `definitions` folder and add a new entry to the relative array
1. open either appropriate definition file within the `definitions` folder and add a new entry to the relative array
2. if you are adding a function, run `yarn maketests` to add a set of fundamental validation tests for the new definition. If any of the suggested tests are wrong, feel free to correct them by hand. If it seems like a general problem, open an issue with the details so that we can update the generator code.
3. write new tests for validation and autocomplete

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test/jest_integration_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-esql-validation-autocomplete'],
openHandlesTimeout: 0,
forceExit: true,
};

View file

@ -26,7 +26,7 @@ const aliasTable: Record<string, string[]> = {
const aliases = new Set(Object.values(aliasTable).flat());
const evalSupportedCommandsAndOptions = {
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
};
@ -288,8 +288,24 @@ function printGeneratedFunctionsFile(functionDefinitions: FunctionDefinition[])
}`;
};
const fileHeader = `// NOTE: This file is generated by the generate_function_definitions.ts script
// Do not edit it manually
const fileHeader = `/**
* __AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.__
*
* @note This file is generated by the \`generate_function_definitions.ts\`
* script. Do not edit it manually.
*
*
*
*
*
*
*
*
*
*
*
*
*/
import type { ESQLFunction } from '@kbn/esql-ast';
import { i18n } from '@kbn/i18n';

View file

@ -28,7 +28,7 @@ function createNumericAggDefinition({
name,
type: 'agg',
description,
supportedCommands: ['stats'],
supportedCommands: ['stats', 'metrics'],
signatures: [
{
params: [
@ -98,7 +98,7 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
defaultMessage: 'Returns the maximum value in a field.',
}),
type: 'agg',
supportedCommands: ['stats'],
supportedCommands: ['stats', 'metrics'],
signatures: [
{
params: [{ name: 'column', type: 'number', noNestingFunctions: true }],
@ -117,7 +117,7 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
defaultMessage: 'Returns the minimum value in a field.',
}),
type: 'agg',
supportedCommands: ['stats'],
supportedCommands: ['stats', 'metrics'],
signatures: [
{
params: [{ name: 'column', type: 'number', noNestingFunctions: true }],
@ -138,7 +138,7 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.countDoc', {
defaultMessage: 'Returns the count of the values in a field.',
}),
supportedCommands: ['stats'],
supportedCommands: ['stats', 'metrics'],
signatures: [
{
params: [
@ -164,7 +164,7 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
defaultMessage: 'Returns the count of distinct values in a field.',
}
),
supportedCommands: ['stats'],
supportedCommands: ['stats', 'metrics'],
signatures: [
{
params: [
@ -188,7 +188,7 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
defaultMessage: 'Returns the count of distinct values in a field.',
}
),
supportedCommands: ['stats'],
supportedCommands: ['stats', 'metrics'],
signatures: [
{
params: [{ name: 'column', type: 'cartesian_point', noNestingFunctions: true }],
@ -212,7 +212,7 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.values', {
defaultMessage: 'Returns all values in a group as an array.',
}),
supportedCommands: ['stats'],
supportedCommands: ['stats', 'metrics'],
signatures: [
{
params: [{ name: 'expression', type: 'any', noNestingFunctions: true }],

View file

@ -22,7 +22,7 @@ function createMathDefinition(
type: 'builtin',
name,
description,
supportedCommands: ['eval', 'where', 'row', 'stats', 'sort'],
supportedCommands: ['eval', 'where', 'row', 'stats', 'metrics', 'sort'],
supportedOptions: ['by'],
signatures: types.map((type) => {
if (Array.isArray(type)) {
@ -507,7 +507,7 @@ const otherDefinitions: FunctionDefinition[] = [
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definition.assignDoc', {
defaultMessage: 'Assign (=)',
}),
supportedCommands: ['eval', 'stats', 'row', 'dissect', 'where', 'enrich'],
supportedCommands: ['eval', 'stats', 'metrics', 'row', 'dissect', 'where', 'enrich'],
supportedOptions: ['by', 'with'],
signatures: [
{

View file

@ -88,6 +88,36 @@ export const commandDefinitions: CommandDefinition[] = [
params: [{ name: 'functions', type: 'function' }],
},
},
{
name: 'metrics',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.metricsDoc', {
defaultMessage:
'A metrics-specific source command, use this command to load data from TSDB indices. ' +
'Similar to STATS command on can calculate aggregate statistics, such as average, count, and sum, over the incoming search results set. ' +
'When used without a BY clause, only one row is returned, which is the aggregation over the entire incoming search results set. ' +
'When you use a BY clause, one row is returned for each distinct value in the field specified in the BY clause. ' +
'The command returns only the fields in the aggregation, and you can use a wide range of statistical functions with the stats command. ' +
'When you perform more than one aggregation, separate each aggregation with a comma.',
}),
examples: [
'metrics index',
'metrics index, index2',
'metrics index avg = avg(a)',
'metrics index sum(b) by b',
'metrics index, index2 sum(b) by b % 2',
'metrics <sources> [ <aggregates> [ by <grouping> ]]',
'metrics src1, src2 agg1, agg2 by field1, field2',
],
options: [],
modes: [],
signature: {
multipleParams: true,
params: [
{ name: 'index', type: 'source', wildcards: true },
{ name: 'expression', type: 'function', optional: true },
],
},
},
{
name: 'stats',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.statsDoc', {

View file

@ -34,7 +34,7 @@ const absDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -63,7 +63,7 @@ const acosDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=.9\n| EVAL acos=ACOS(a)'],
@ -90,7 +90,7 @@ const asinDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=.9\n| EVAL asin=ASIN(a)'],
@ -117,7 +117,7 @@ const atanDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=12.9\n| EVAL atan=ATAN(a)'],
@ -149,7 +149,7 @@ const atan2Definition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW y=12.9, x=.6\n| EVAL atan2=ATAN2(y, x)'],
@ -176,7 +176,7 @@ const cbrtDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW d = 1000.0\n| EVAL c = cbrt(d)'],
@ -202,7 +202,7 @@ const ceilDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=1.8\n| EVAL a=CEIL(a)'],
@ -235,7 +235,7 @@ const cidrMatchDefinition: FunctionDefinition = {
minParams: 2,
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -479,7 +479,7 @@ const concatDefinition: FunctionDefinition = {
minParams: 2,
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -507,7 +507,7 @@ const cosDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=1.8 \n| EVAL cos=COS(a)'],
@ -533,7 +533,7 @@ const coshDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=1.8 \n| EVAL cosh=COSH(a)'],
@ -631,7 +631,7 @@ const dateDiffDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -664,7 +664,7 @@ const dateExtractDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -698,7 +698,7 @@ const dateFormatDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -732,7 +732,7 @@ const dateParseDefinition: FunctionDefinition = {
returnType: 'date',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW date_string = "2022-05-06"\n| EVAL date = DATE_PARSE("yyyy-MM-dd", date_string)'],
@ -778,7 +778,7 @@ const dateTruncDefinition: FunctionDefinition = {
returnType: 'date',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -802,7 +802,7 @@ const eDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW E()'],
@ -834,7 +834,7 @@ const endsWithDefinition: FunctionDefinition = {
returnType: 'boolean',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['FROM employees\n| KEEP last_name\n| EVAL ln_E = ENDS_WITH(last_name, "d")'],
@ -860,7 +860,7 @@ const floorDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=1.8\n| EVAL a=FLOOR(a)'],
@ -886,7 +886,7 @@ const fromBase64Definition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['row a = "ZWxhc3RpYw==" \n| eval d = from_base64(a)'],
@ -1016,7 +1016,7 @@ const greatestDefinition: FunctionDefinition = {
minParams: 1,
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a = 10, b = 20\n| EVAL g = GREATEST(a, b)'],
@ -1184,7 +1184,7 @@ const leastDefinition: FunctionDefinition = {
minParams: 1,
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a = 10, b = 20\n| EVAL l = LEAST(a, b)'],
@ -1216,7 +1216,7 @@ const leftDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -1244,7 +1244,7 @@ const lengthDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['FROM employees\n| KEEP first_name, last_name\n| EVAL fn_length = LENGTH(first_name)'],
@ -1296,7 +1296,7 @@ const locateDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['row a = "hello"\n| eval a_ll = locate(a, "ll")'],
@ -1338,7 +1338,7 @@ const logDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: (fnDef: ESQLFunction) => {
const messages = [];
@ -1391,7 +1391,7 @@ const log10Definition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: (fnDef: ESQLFunction) => {
const messages = [];
@ -1440,7 +1440,7 @@ const ltrimDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -1635,7 +1635,7 @@ const mvAvgDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=[3, 5, 1, 6]\n| EVAL avg_a = MV_AVG(a)'],
@ -1667,7 +1667,7 @@ const mvConcatDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -1787,7 +1787,7 @@ const mvCountDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=["foo", "zoo", "bar"]\n| EVAL count_a = MV_COUNT(a)'],
@ -1903,7 +1903,7 @@ const mvDedupeDefinition: FunctionDefinition = {
returnType: 'version',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=["foo", "foo", "bar", "foo"]\n| EVAL dedupe_a = MV_DEDUPE(a)'],
@ -2020,7 +2020,7 @@ const mvFirstDefinition: FunctionDefinition = {
returnType: 'version',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a="foo;bar;baz"\n| EVAL first_a = MV_FIRST(SPLIT(a, ";"))'],
@ -2137,7 +2137,7 @@ const mvLastDefinition: FunctionDefinition = {
returnType: 'version',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a="foo;bar;baz"\n| EVAL last_a = MV_LAST(SPLIT(a, ";"))'],
@ -2214,7 +2214,7 @@ const mvMaxDefinition: FunctionDefinition = {
returnType: 'version',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -2244,7 +2244,7 @@ const mvMedianDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -2324,7 +2324,7 @@ const mvMinDefinition: FunctionDefinition = {
returnType: 'version',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -2544,7 +2544,7 @@ const mvSliceDefinition: FunctionDefinition = {
returnType: 'version',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -2659,7 +2659,7 @@ const mvSortDefinition: FunctionDefinition = {
returnType: 'version',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a = [4, 2, -3, 2]\n| EVAL sa = mv_sort(a), sd = mv_sort(a, "DESC")'],
@ -2686,7 +2686,7 @@ const mvSumDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=[3, 5, 6]\n| EVAL sum_a = MV_SUM(a)'],
@ -2738,7 +2738,7 @@ const mvZipDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -2760,7 +2760,7 @@ const nowDefinition: FunctionDefinition = {
returnType: 'date',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW current_date = NOW()', 'FROM sample_data\n| WHERE @timestamp > NOW() - 1 hour'],
@ -2780,7 +2780,7 @@ const piDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW PI()'],
@ -2811,7 +2811,7 @@ const powDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -2883,7 +2883,7 @@ const replaceDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW str = "Hello World"\n| EVAL str = REPLACE(str, "World", "Universe")\n| KEEP str'],
@ -2915,7 +2915,7 @@ const rightDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -2959,7 +2959,7 @@ const roundDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -2987,7 +2987,7 @@ const rtrimDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -3016,7 +3016,7 @@ const signumDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW d = 100.0\n| EVAL s = SIGNUM(d)'],
@ -3042,7 +3042,7 @@ const sinDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=1.8 \n| EVAL sin=SIN(a)'],
@ -3068,7 +3068,7 @@ const sinhDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=1.8 \n| EVAL sinh=SINH(a)'],
@ -3099,7 +3099,7 @@ const splitDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW words="foo;bar;baz;qux;quux;corge"\n| EVAL word = SPLIT(words, ";")'],
@ -3126,7 +3126,7 @@ const sqrtDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW d = 100.0\n| EVAL s = SQRT(d)'],
@ -3263,7 +3263,7 @@ const stContainsDefinition: FunctionDefinition = {
returnType: 'boolean',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -3402,7 +3402,7 @@ const stDisjointDefinition: FunctionDefinition = {
returnType: 'boolean',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -3541,7 +3541,7 @@ const stIntersectsDefinition: FunctionDefinition = {
returnType: 'boolean',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -3680,7 +3680,7 @@ const stWithinDefinition: FunctionDefinition = {
returnType: 'boolean',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -3719,7 +3719,7 @@ const stXDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -3758,7 +3758,7 @@ const stYDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -3792,7 +3792,7 @@ const startsWithDefinition: FunctionDefinition = {
returnType: 'boolean',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['FROM employees\n| KEEP last_name\n| EVAL ln_S = STARTS_WITH(last_name, "B")'],
@ -3829,7 +3829,7 @@ const substringDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -3859,7 +3859,7 @@ const tanDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=1.8 \n| EVAL tan=TAN(a)'],
@ -3885,7 +3885,7 @@ const tanhDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=1.8 \n| EVAL tanh=TANH(a)'],
@ -3905,7 +3905,7 @@ const tauDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW TAU()'],
@ -3931,7 +3931,7 @@ const toBase64Definition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['row a = "elastic" \n| eval e = to_base64(a)'],
@ -3978,7 +3978,7 @@ const toBooleanDefinition: FunctionDefinition = {
returnType: 'boolean',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW str = ["true", "TRuE", "false", "", "yes", "1"]\n| EVAL bool = TO_BOOLEAN(str)'],
@ -4018,7 +4018,7 @@ const toCartesianpointDefinition: FunctionDefinition = {
returnType: 'cartesian_point',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -4070,7 +4070,7 @@ const toCartesianshapeDefinition: FunctionDefinition = {
returnType: 'cartesian_shape',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -4119,7 +4119,7 @@ const toDatetimeDefinition: FunctionDefinition = {
returnType: 'date',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -4148,7 +4148,7 @@ const toDegreesDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW rad = [1.57, 3.14, 4.71]\n| EVAL deg = TO_DEGREES(rad)'],
@ -4205,7 +4205,7 @@ const toDoubleDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -4244,7 +4244,7 @@ const toGeopointDefinition: FunctionDefinition = {
returnType: 'geo_point',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW wkt = "POINT(42.97109630194 14.7552534413725)"\n| EVAL pt = TO_GEOPOINT(wkt)'],
@ -4291,7 +4291,7 @@ const toGeoshapeDefinition: FunctionDefinition = {
returnType: 'geo_shape',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -4350,7 +4350,7 @@ const toIntegerDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW long = [5013792, 2147483647, 501379200000]\n| EVAL int = TO_INTEGER(long)'],
@ -4386,7 +4386,7 @@ const toIpDefinition: FunctionDefinition = {
returnType: 'ip',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -4445,7 +4445,7 @@ const toLongDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -4473,7 +4473,7 @@ const toLowerDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW message = "Some Text"\n| EVAL message_lower = TO_LOWER(message)'],
@ -4499,7 +4499,7 @@ const toRadiansDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW deg = [90.0, 180.0, 270.0]\n| EVAL rad = TO_RADIANS(deg)'],
@ -4615,7 +4615,7 @@ const toStringDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW a=10\n| EVAL j = TO_STRING(a)', 'ROW a=[10, 9, 8]\n| EVAL j = TO_STRING(a)'],
@ -4675,7 +4675,7 @@ const toUnsignedLongDefinition: FunctionDefinition = {
returnType: 'number',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -4703,7 +4703,7 @@ const toUpperDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW message = "Some Text"\n| EVAL message_upper = TO_UPPER(message)'],
@ -4739,7 +4739,7 @@ const toVersionDefinition: FunctionDefinition = {
returnType: 'version',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: ['ROW v = TO_VERSION("1.2.3")'],
@ -4765,7 +4765,7 @@ const trimDefinition: FunctionDefinition = {
returnType: 'string',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [
@ -4798,7 +4798,7 @@ const caseDefinition: FunctionDefinition = {
returnType: 'any',
},
],
supportedCommands: ['stats', 'eval', 'where', 'row', 'sort'],
supportedCommands: ['stats', 'metrics', 'eval', 'where', 'row', 'sort'],
supportedOptions: ['by'],
validate: undefined,
examples: [

View file

@ -571,3 +571,6 @@ export function shouldBeQuotedText(
) {
return dashSupported ? /[^a-zA-Z\d_\.@-]/.test(text) : /[^a-zA-Z\d_\.@]/.test(text);
}
export const isAggFunction = (arg: ESQLFunction): boolean =>
getFunctionDefinition(arg.name)?.type === 'agg';

View file

@ -134,6 +134,21 @@ function addVariableFromExpression(
}
}
export const collectVariablesFromList = (
list: ESQLAstItem[],
fields: Map<string, ESQLRealField>,
queryString: string,
variables: Map<string, ESQLVariable[]>
) => {
for (const arg of list) {
if (isAssignment(arg)) {
addVariableFromAssignment(arg, variables, fields);
} else if (isExpression(arg)) {
addVariableFromExpression(arg, queryString, variables);
}
}
};
export function collectVariables(
commands: ESQLCommand[],
fields: Map<string, ESQLRealField>,
@ -141,28 +156,14 @@ export function collectVariables(
): Map<string, ESQLVariable[]> {
const variables = new Map<string, ESQLVariable[]>();
for (const command of commands) {
if (['row', 'eval', 'stats'].includes(command.name)) {
for (const arg of command.args) {
if (isAssignment(arg)) {
addVariableFromAssignment(arg, variables, fields);
}
if (isExpression(arg)) {
addVariableFromExpression(arg, queryString, variables);
}
}
if (['row', 'eval', 'stats', 'metrics'].includes(command.name)) {
collectVariablesFromList(command.args, fields, queryString, variables);
if (command.name === 'stats') {
const commandOptionsWithAssignment = command.args.filter(
(arg) => isOptionItem(arg) && arg.name === 'by'
) as ESQLCommandOption[];
for (const commandOption of commandOptionsWithAssignment) {
for (const optArg of commandOption.args) {
if (isAssignment(optArg)) {
addVariableFromAssignment(optArg, variables, fields);
}
if (isExpression(optArg)) {
addVariableFromExpression(optArg, queryString, variables);
}
}
collectVariablesFromList(commandOption.args, fields, queryString, variables);
}
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { setup } from './helpers';
describe('FROM', () => {
test('does not load fields when validating only a single FROM, SHOW, ROW command', async () => {
const { validate, callbacks } = await setup();
await validate('FROM kib');
await validate('FROM kibana_ecommerce METADATA _i');
await validate('FROM kibana_ecommerce METADATA _id | ');
await validate('SHOW');
await validate('ROW \t');
expect(callbacks.getFieldsFor.mock.calls.length).toBe(0);
});
test('loads fields with FROM source when commands after pipe present', async () => {
const { validate, callbacks } = await setup();
await validate('FROM kibana_ecommerce METADATA _id | eval');
expect(callbacks.getFieldsFor.mock.calls.length).toBe(1);
});
});

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EditorError, ESQLMessage, getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { ESQLCallbacks } from '../../shared/types';
import { getCallbackMocks } from '../../__tests__/helpers';
import { ValidationOptions } from '../types';
import { validateQuery } from '../validation';
/** Validation test API factory, can be called at the start of each unit test. */
export type Setup = typeof setup;
/**
* Sets up an API for ES|QL query validation testing.
*
* @returns API for testing validation logic.
*/
export const setup = async () => {
const callbacks = getCallbackMocks();
const validate = async (
query: string,
opts: ValidationOptions = {},
cb: ESQLCallbacks = callbacks
) => {
return await validateQuery(query, getAstAndSyntaxErrors, opts, cb);
};
const assertErrors = (errors: unknown[], expectedErrors: string[]) => {
const errorMessages: string[] = [];
for (const error of errors) {
if (error && typeof error === 'object') {
const message =
typeof (error as ESQLMessage).text === 'string'
? (error as ESQLMessage).text
: typeof (error as EditorError).message === 'string'
? (error as EditorError).message
: String(error);
errorMessages.push(message);
} else {
errorMessages.push(String(error));
}
}
expect(errorMessages.sort()).toStrictEqual(expectedErrors.sort());
};
const expectErrors = async (
query: string,
expectedErrors: string[],
expectedWarnings?: string[],
opts: ValidationOptions = {},
cb: ESQLCallbacks = callbacks
) => {
const { errors, warnings } = await validateQuery(query, getAstAndSyntaxErrors, opts, cb);
assertErrors(errors, expectedErrors);
if (expectedWarnings) {
assertErrors(warnings, expectedWarnings);
}
};
return {
callbacks,
validate,
expectErrors,
};
};

View file

@ -0,0 +1,194 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { METADATA_FIELDS } from '../../../shared/constants';
import * as helpers from '../helpers';
export const validationFromCommandTestSuite = (setup: helpers.Setup) => {
describe('validation', () => {
describe('command', () => {
describe('FROM <sources> [ METADATA <indices> ]', () => {
test('errors on invalid command start', async () => {
const { expectErrors } = await setup();
await expectErrors('f', [
"SyntaxError: mismatched input 'f' expecting {'explain', 'from', 'meta', 'metrics', 'row', 'show'}",
]);
await expectErrors('from ', [
"SyntaxError: missing INDEX_UNQUOTED_IDENTIFIER at '<EOF>'",
]);
});
describe('... <sources> ...', () => {
test('no errors on correct indices usage', async () => {
const { expectErrors } = await setup();
await expectErrors('from index', []);
await expectErrors('FROM index', []);
await expectErrors('FrOm index', []);
await expectErrors('from index, other_index', []);
await expectErrors('from index, other_index,.secret_index', []);
await expectErrors('from .secret_index', []);
await expectErrors('from .secret_index', []);
await expectErrors('from .secret_index', []);
await expectErrors('from ind*, other*', []);
await expectErrors('from index*', []);
await expectErrors('FROM *a_i*dex*', []);
await expectErrors('FROM in*ex*', []);
await expectErrors('FROM *n*ex', []);
await expectErrors('FROM *n*ex*', []);
await expectErrors('FROM i*d*x*', []);
await expectErrors('FROM i*d*x', []);
await expectErrors('FROM i***x*', []);
await expectErrors('FROM i****', []);
await expectErrors('FROM i**', []);
await expectErrors('fRoM index**', []);
await expectErrors('fRoM *ex', []);
await expectErrors('fRoM *ex*', []);
await expectErrors('fRoM in*ex', []);
await expectErrors('fRoM ind*ex', []);
await expectErrors('fRoM *,-.*', []);
await expectErrors('fRoM remote-*:indexes*', []);
await expectErrors('fRoM remote-*:indexes', []);
await expectErrors('fRoM remote-ccs:indexes', []);
await expectErrors('fRoM a_index, remote-ccs:indexes', []);
await expectErrors('fRoM .secret_index', []);
await expectErrors('from my-index', []);
});
test('errors on trailing comma', async () => {
const { expectErrors } = await setup();
await expectErrors('from index,', [
"SyntaxError: missing INDEX_UNQUOTED_IDENTIFIER at '<EOF>'",
]);
await expectErrors(`FROM index\n, \tother_index\t,\n \t `, [
"SyntaxError: missing INDEX_UNQUOTED_IDENTIFIER at '<EOF>'",
]);
await expectErrors(`from assignment = 1`, [
"SyntaxError: mismatched input '=' expecting <EOF>",
'Unknown index [assignment]',
]);
});
test('errors on invalid syntax', async () => {
const { expectErrors } = await setup();
await expectErrors('FROM `index`', [
"SyntaxError: token recognition error at: '`'",
"SyntaxError: token recognition error at: '`'",
]);
await expectErrors(`from assignment = 1`, [
"SyntaxError: mismatched input '=' expecting <EOF>",
'Unknown index [assignment]',
]);
});
test('errors on unknown index', async () => {
const { expectErrors } = await setup();
await expectErrors(`FROM index, missingIndex`, ['Unknown index [missingIndex]']);
await expectErrors(`from average()`, ['Unknown index [average()]']);
await expectErrors(`fRom custom_function()`, ['Unknown index [custom_function()]']);
await expectErrors(`FROM indexes*`, ['Unknown index [indexes*]']);
await expectErrors('from numberField', ['Unknown index [numberField]']);
await expectErrors('FROM policy', ['Unknown index [policy]']);
});
});
describe('... METADATA <indices>', () => {
test('no errors on correct METADATA ... usage', async () => {
const { expectErrors } = await setup();
await expectErrors('from index metadata _id', []);
await expectErrors('from index metadata _id, \t\n _index\n ', []);
});
test('errors when wrapped in brackets', async () => {
const { expectErrors } = await setup();
await expectErrors(`from index (metadata _id)`, [
"SyntaxError: mismatched input '(metadata' expecting <EOF>",
]);
});
for (const isWrapped of [true, false]) {
function setWrapping(option: string) {
return isWrapped ? `[${option}]` : option;
}
function addBracketsWarning() {
return isWrapped
? ["Square brackets '[]' need to be removed from FROM METADATA declaration"]
: [];
}
describe(`wrapped = ${isWrapped}`, () => {
test('no errors on correct usage, waning on square brackets', async () => {
const { expectErrors } = await setup();
await expectErrors(`from index ${setWrapping('METADATA _id')}`, []);
await expectErrors(
`from index ${setWrapping('METADATA _id')}`,
[],
addBracketsWarning()
);
await expectErrors(
`from index ${setWrapping('metadata _id')}`,
[],
addBracketsWarning()
);
await expectErrors(
`from index ${setWrapping('METADATA _id, _source')}`,
[],
addBracketsWarning()
);
await expectErrors(
`from remote-ccs:indexes ${setWrapping('METADATA _id')}`,
[],
addBracketsWarning()
);
await expectErrors(
`from *:indexes ${setWrapping('METADATA _id')}`,
[],
addBracketsWarning()
);
});
test('validates fields', async () => {
const { expectErrors } = await setup();
await expectErrors(
`from index ${setWrapping('METADATA _id, _source2')}`,
[
`Metadata field [_source2] is not available. Available metadata fields are: [${METADATA_FIELDS.join(
', '
)}]`,
],
addBracketsWarning()
);
await expectErrors(
`from index ${setWrapping('metadata _id, _source')} ${setWrapping(
'METADATA _id2'
)}`,
[
isWrapped
? "SyntaxError: mismatched input '[' expecting <EOF>"
: "SyntaxError: mismatched input 'METADATA' expecting <EOF>",
],
addBracketsWarning()
);
});
});
}
});
});
});
});
};

View file

@ -0,0 +1,293 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as helpers from '../helpers';
export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
describe('validation', () => {
describe('command', () => {
describe('METRICS <sources> [ <aggregates> [ BY <grouping> ]]', () => {
test('errors on invalid command start', async () => {
const { expectErrors } = await setup();
await expectErrors('m', [
"SyntaxError: mismatched input 'm' expecting {'explain', 'from', 'meta', 'metrics', 'row', 'show'}",
]);
await expectErrors('metrics ', [
"SyntaxError: missing INDEX_UNQUOTED_IDENTIFIER at '<EOF>'",
]);
});
describe('... <sources> ...', () => {
test('no errors on correct indices usage', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics index', []);
await expectErrors('metrics index, other_index', []);
await expectErrors('metrics index, other_index,.secret_index', []);
await expectErrors('metrics .secret_index', []);
await expectErrors('METRICS .secret_index', []);
await expectErrors('mEtRiCs .secret_index', []);
await expectErrors('metrics ind*, other*', []);
await expectErrors('metrics index*', []);
await expectErrors('metrics *a_i*dex*', []);
await expectErrors('metrics in*ex*', []);
await expectErrors('metrics *n*ex', []);
await expectErrors('metrics *n*ex*', []);
await expectErrors('metrics i*d*x*', []);
await expectErrors('metrics i*d*x', []);
await expectErrors('metrics i***x*', []);
await expectErrors('metrics i****', []);
await expectErrors('metrics i**', []);
await expectErrors('metrics index**', []);
await expectErrors('metrics *ex', []);
await expectErrors('metrics *ex*', []);
await expectErrors('metrics in*ex', []);
await expectErrors('metrics ind*ex', []);
await expectErrors('metrics *,-.*', []);
await expectErrors('metrics remote-*:indexes*', []);
await expectErrors('metrics remote-*:indexes', []);
await expectErrors('metrics remote-ccs:indexes', []);
await expectErrors('metrics a_index, remote-ccs:indexes', []);
await expectErrors('metrics .secret_index', []);
});
test('errors on trailing comma', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics index,', [
"SyntaxError: missing INDEX_UNQUOTED_IDENTIFIER at '<EOF>'",
]);
await expectErrors(`metrics index\n, \tother_index\t,\n \t `, [
"SyntaxError: missing INDEX_UNQUOTED_IDENTIFIER at '<EOF>'",
]);
});
test('errors on invalid syntax', async () => {
const { expectErrors } = await setup();
await expectErrors(`metrics index = 1`, [
"SyntaxError: token recognition error at: '='",
"SyntaxError: token recognition error at: '1'",
]);
await expectErrors('metrics `index`', [
"SyntaxError: token recognition error at: '`'",
"SyntaxError: token recognition error at: '`'",
]);
});
test('errors on unknown index', async () => {
const { expectErrors } = await setup();
await expectErrors(`METRICS index, missingIndex`, ['Unknown index [missingIndex]']);
await expectErrors(`METRICS average()`, ['Unknown index [average()]']);
await expectErrors(`metrics custom_function()`, ['Unknown index [custom_function()]']);
await expectErrors(`metrics indexes*`, ['Unknown index [indexes*]']);
await expectErrors('metrics numberField', ['Unknown index [numberField]']);
await expectErrors('metrics policy', ['Unknown index [policy]']);
});
});
describe('... <aggregates> ...', () => {
test('no errors on correct usage', async () => {
const { expectErrors } = await setup();
await expectErrors('METRICS a_index count()', []);
await expectErrors('metrics a_index avg(numberField) by 1', []);
await expectErrors('metrics a_index count(`numberField`)', []);
await expectErrors('metrics a_index count(*)', []);
await expectErrors('metrics index var0 = count(*)', []);
await expectErrors('metrics a_index var0 = count()', []);
await expectErrors('metrics a_index var0 = avg(numberField), count(*)', []);
await expectErrors(`metrics a_index sum(case(false, 0, 1))`, []);
await expectErrors(`metrics a_index var0 = sum( case(false, 0, 1))`, []);
await expectErrors('metrics a_index count(stringField == "a" or null)', []);
await expectErrors('metrics other_index max(numberField) by stringField', []);
});
test('syntax errors', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index numberField=', [
expect.any(String),
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
await expectErrors('metrics a_index numberField=5 by ', [
expect.any(String),
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
});
test('errors on unknown function', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index var0 = avg(fn(number)), count(*)', [
'Unknown function [fn]',
]);
});
test('errors when no aggregation function specified', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index numberField + 1', [
'At least one aggregation function required in [METRICS], found [numberField+1]',
]);
await expectErrors('metrics a_index a = numberField + 1', [
'At least one aggregation function required in [METRICS], found [a=numberField+1]',
]);
await expectErrors('metrics a_index a = numberField + 1, stringField', [
'At least one aggregation function required in [METRICS], found [a=numberField+1]',
'Expected an aggregate function or group but got [stringField] of type [FieldAttribute]',
]);
await expectErrors('metrics a_index numberField + 1 by ipField', [
'At least one aggregation function required in [METRICS], found [numberField+1]',
]);
});
test('errors on agg and non-agg mix', async () => {
const { expectErrors } = await setup();
await expectErrors('METRICS a_index sum( numberField ) + abs( numberField ) ', [
'Cannot combine aggregation and non-aggregation values in [METRICS], found [sum(numberField)+abs(numberField)]',
]);
await expectErrors('METRICS a_index abs( numberField + sum( numberField )) ', [
'Cannot combine aggregation and non-aggregation values in [METRICS], found [abs(numberField+sum(numberField))]',
]);
});
test('errors when aggregation functions are nested', async () => {
const { expectErrors } = await setup();
// avg() inside avg()
await expectErrors('METRICS a_index avg(to_long(avg(2)))', [
'The aggregation function [avg] cannot be used as an argument in another aggregation function',
]);
});
test('errors when input is not an aggregate function', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index numberField ', [
'Expected an aggregate function or group but got [numberField] of type [FieldAttribute]',
]);
});
test('sub-command can reference aggregated field', async () => {
const { expectErrors } = await setup();
for (const subCommand of ['keep', 'drop', 'eval']) {
await expectErrors(
'metrics a_index count(`numberField`) | ' +
subCommand +
' `count(``numberField``)` ',
[]
);
}
});
test('semantic function validation errors', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index count(round(*))', [
'Using wildcards (*) in round is not allowed',
]);
await expectErrors('metrics a_index count(count(*))', [
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [number]`,
]);
});
});
describe('... BY <grouping>', () => {
test('no errors on correct usage', async () => {
const { expectErrors } = await setup();
await expectErrors(
'metrics a_index avg(numberField), percentile(numberField, 50) by ipField',
[]
);
await expectErrors(
'metrics a_index avg(numberField), percentile(numberField, 50) BY ipField',
[]
);
await expectErrors(
'metrics a_index avg(numberField), percentile(numberField, 50) + 1 by ipField',
[]
);
await expectErrors('metrics a_index avg(numberField) by stringField | limit 100', []);
for (const op of ['+', '-', '*', '/', '%']) {
await expectErrors(
`metrics a_index avg(numberField) ${op} percentile(numberField, 50) BY ipField`,
[]
);
}
});
test('syntax does not allow <grouping> clause without <aggregates>', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index BY stringField', [
'Expected an aggregate function or group but got [BY] of type [FieldAttribute]',
"SyntaxError: extraneous input 'stringField' expecting <EOF>",
]);
});
test('syntax errors in <aggregates>', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index count(* + 1) BY ipField', [
"SyntaxError: no viable alternative at input 'count(* +'",
]);
await expectErrors('metrics a_index \n count(* + round(numberField)) BY ipField', [
"SyntaxError: no viable alternative at input 'count(* +'",
]);
});
test('semantic errors in <aggregates>', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index count(round(*)) BY ipField', [
'Using wildcards (*) in round is not allowed',
]);
await expectErrors('metrics a_index count(count(*)) BY ipField', [
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [number]`,
]);
});
test('errors on unknown field', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index avg(numberField) by wrongField', [
'Unknown column [wrongField]',
]);
await expectErrors('metrics a_index avg(numberField) by wrongField + 1', [
'Unknown column [wrongField]',
]);
await expectErrors('metrics a_index avg(numberField) by var0 = wrongField + 1', [
'Unknown column [wrongField]',
]);
});
test('various errors', async () => {
const { expectErrors } = await setup();
await expectErrors('METRICS a_index avg(numberField) by percentile(numberField)', [
'METRICS BY does not support function percentile',
]);
await expectErrors(
'METRICS a_index avg(numberField) by stringField, percentile(numberField) by ipField',
[
"SyntaxError: mismatched input 'by' expecting <EOF>",
'METRICS BY does not support function percentile',
]
);
});
});
});
});
});
};

View file

@ -0,0 +1,365 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as helpers from '../helpers';
export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
describe('validation', () => {
describe('command', () => {
describe('STATS <aggregates> [ BY <grouping> ]', () => {
test('no errors on correct usage', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats by stringField', []);
await expectErrors(
`FROM index
| EVAL numberField * 3.281
| STATS avg_numberField = AVG(\`numberField * 3.281\`)`,
[]
);
await expectErrors(
`FROM index | STATS AVG(numberField) by round(numberField) + 1 | EVAL \`round(numberField) + 1\` / 2`,
[]
);
});
test('errors on invalid command start', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats ', [
'At least one aggregation or grouping expression required in [STATS]',
]);
});
describe('... <aggregates> ...', () => {
test('no errors on correct usage', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats avg(numberField) by 1', []);
await expectErrors('from a_index | stats count(`numberField`)', []);
await expectErrors('from a_index | stats count(*)', []);
await expectErrors('from a_index | stats count()', []);
await expectErrors('from a_index | stats var0 = count(*)', []);
await expectErrors('from a_index | stats var0 = count()', []);
await expectErrors('from a_index | stats var0 = avg(numberField), count(*)', []);
await expectErrors(`from a_index | stats sum(case(false, 0, 1))`, []);
await expectErrors(`from a_index | stats var0 = sum( case(false, 0, 1))`, []);
// "or" must accept "null"
await expectErrors('from a_index | stats count(stringField == "a" or null)', []);
});
test('sub-command can reference aggregated field', async () => {
const { expectErrors } = await setup();
for (const subCommand of ['keep', 'drop', 'eval']) {
await expectErrors(
'from a_index | stats count(`numberField`) | ' +
subCommand +
' `count(``numberField``)` ',
[]
);
}
});
test('errors on agg and non-agg mix', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | STATS sum( numberField ) + abs( numberField ) ', [
'Cannot combine aggregation and non-aggregation values in [STATS], found [sum(numberField)+abs(numberField)]',
]);
await expectErrors('from a_index | STATS abs( numberField + sum( numberField )) ', [
'Cannot combine aggregation and non-aggregation values in [STATS], found [abs(numberField+sum(numberField))]',
]);
});
test('errors on each aggregation field, which does not contain at least one agg function', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats numberField + 1', [
'At least one aggregation function required in [STATS], found [numberField+1]',
]);
await expectErrors('from a_index | stats numberField + 1, stringField', [
'At least one aggregation function required in [STATS], found [numberField+1]',
'Expected an aggregate function or group but got [stringField] of type [FieldAttribute]',
]);
await expectErrors('from a_index | stats numberField + 1, numberField + 2, count()', [
'At least one aggregation function required in [STATS], found [numberField+1]',
'At least one aggregation function required in [STATS], found [numberField+2]',
]);
await expectErrors(
'from a_index | stats numberField + 1, numberField + count(), count()',
['At least one aggregation function required in [STATS], found [numberField+1]']
);
await expectErrors('from a_index | stats 5 + numberField + 1', [
'At least one aggregation function required in [STATS], found [5+numberField+1]',
]);
await expectErrors('from a_index | stats numberField + 1 by ipField', [
'At least one aggregation function required in [STATS], found [numberField+1]',
]);
});
test('errors when input is not an aggregate function', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats numberField ', [
'Expected an aggregate function or group but got [numberField] of type [FieldAttribute]',
]);
});
test('various errors', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats numberField=', [
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
await expectErrors('from a_index | stats numberField=5 by ', [
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
await expectErrors('from a_index | stats avg(numberField) by wrongField', [
'Unknown column [wrongField]',
]);
await expectErrors('from a_index | stats avg(numberField) by wrongField + 1', [
'Unknown column [wrongField]',
]);
await expectErrors('from a_index | stats avg(numberField) by var0 = wrongField + 1', [
'Unknown column [wrongField]',
]);
await expectErrors('from a_index | stats var0 = avg(fn(number)), count(*)', [
'Unknown function [fn]',
]);
});
test('semantic errors', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats count(round(*))', [
'Using wildcards (*) in round is not allowed',
]);
await expectErrors('from a_index | stats count(count(*))', [
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [number]`,
]);
});
});
describe('... BY <grouping>', () => {
test('no errors on correct usage', async () => {
const { expectErrors } = await setup();
await expectErrors(
'from a_index | stats avg(numberField), percentile(numberField, 50) by ipField',
[]
);
await expectErrors(
'from a_index | stats avg(numberField), percentile(numberField, 50) BY ipField',
[]
);
await expectErrors(
'from a_index | stats avg(numberField), percentile(numberField, 50) + 1 by ipField',
[]
);
for (const op of ['+', '-', '*', '/', '%']) {
await expectErrors(
`from a_index | stats avg(numberField) ${op} percentile(numberField, 50) BY ipField`,
[]
);
}
});
test('cannot specify <grouping> without <aggregates>', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats by ', [
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
});
test('syntax errors in <aggregates>', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats count(* + 1) BY ipField', [
"SyntaxError: no viable alternative at input 'count(* +'",
]);
await expectErrors('from a_index | stats count(* + round(numberField)) BY ipField', [
"SyntaxError: no viable alternative at input 'count(* +'",
]);
});
test('semantic errors in <aggregates>', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats count(round(*)) BY ipField', [
'Using wildcards (*) in round is not allowed',
]);
await expectErrors('from a_index | stats count(count(*)) BY ipField', [
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [number]`,
]);
});
test('various errors', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats avg(numberField) by percentile(numberField)', [
'STATS BY does not support function percentile',
]);
await expectErrors(
'from a_index | stats avg(numberField) by stringField, percentile(numberField) by ipField',
[
"SyntaxError: mismatched input 'by' expecting <EOF>",
'STATS BY does not support function percentile',
]
);
});
describe('constant-only parameters', () => {
test('no errors', async () => {
const { expectErrors } = await setup();
await expectErrors('from index | stats by bucket(dateField, pi(), "", "")', []);
await expectErrors(
'from index | stats by bucket(dateField, 1 + 30 / 10, "", "")',
[]
);
await expectErrors(
'from index | stats by bucket(dateField, 1 + 30 / 10, concat("", ""), "")',
[]
);
});
test('errors', async () => {
const { expectErrors } = await setup();
await expectErrors(
'from index | stats by bucket(dateField, abs(numberField), "", "")',
['Argument of [bucket] must be a constant, received [abs(numberField)]']
);
await expectErrors(
'from index | stats by bucket(dateField, abs(length(numberField)), "", "")',
['Argument of [bucket] must be a constant, received [abs(length(numberField))]']
);
await expectErrors(
'from index | stats by bucket(dateField, numberField, stringField, stringField)',
[
'Argument of [bucket] must be a constant, received [numberField]',
'Argument of [bucket] must be a constant, received [stringField]',
'Argument of [bucket] must be a constant, received [stringField]',
]
);
});
});
});
describe('nesting', () => {
const NESTING_LEVELS = 4;
const NESTED_DEPTHS = Array(NESTING_LEVELS)
.fill(0)
.map((_, i) => i + 1);
for (const nesting of NESTED_DEPTHS) {
describe(`depth = ${nesting}`, () => {
describe('builtin', () => {
const builtinWrapping = Array(nesting).fill('+1').join('');
test('no errors', async () => {
const { expectErrors } = await setup();
await expectErrors(
`from a_index | stats 5 + avg(numberField) ${builtinWrapping}`,
[]
);
await expectErrors(
`from a_index | stats 5 ${builtinWrapping} + avg(numberField)`,
[]
);
});
test('errors', async () => {
const { expectErrors } = await setup();
await expectErrors(`from a_index | stats 5 ${builtinWrapping} + numberField`, [
`At least one aggregation function required in [STATS], found [5${builtinWrapping}+numberField]`,
]);
await expectErrors(`from a_index | stats 5 + numberField ${builtinWrapping}`, [
`At least one aggregation function required in [STATS], found [5+numberField${builtinWrapping}]`,
]);
await expectErrors(
`from a_index | stats 5 + numberField ${builtinWrapping}, var0 = sum(numberField)`,
[
`At least one aggregation function required in [STATS], found [5+numberField${builtinWrapping}]`,
]
);
});
});
describe('EVAL', () => {
const evalWrapping = Array(nesting).fill('round(').join('');
const closingWrapping = Array(nesting).fill(')').join('');
test('no errors', async () => {
const { expectErrors } = await setup();
await expectErrors(
`from a_index | stats ${evalWrapping} sum(numberField) ${closingWrapping}`,
[]
);
await expectErrors(
`from a_index | stats ${evalWrapping} sum(numberField) ${closingWrapping} + ${evalWrapping} sum(numberField) ${closingWrapping}`,
[]
);
await expectErrors(
`from a_index | stats ${evalWrapping} sum(numberField + numberField) ${closingWrapping}`,
[]
);
await expectErrors(
`from a_index | stats ${evalWrapping} sum(numberField + round(numberField)) ${closingWrapping}`,
[]
);
await expectErrors(
`from a_index | stats ${evalWrapping} sum(numberField + round(numberField)) ${closingWrapping} + ${evalWrapping} sum(numberField + round(numberField)) ${closingWrapping}`,
[]
);
await expectErrors(
`from a_index | stats sum(${evalWrapping} numberField ${closingWrapping} )`,
[]
);
await expectErrors(
`from a_index | stats sum(${evalWrapping} numberField ${closingWrapping} ) + sum(${evalWrapping} numberField ${closingWrapping} )`,
[]
);
});
test('errors', async () => {
const { expectErrors } = await setup();
await expectErrors(
`from a_index | stats ${evalWrapping} numberField + sum(numberField) ${closingWrapping}`,
[
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalWrapping}numberField+sum(numberField)${closingWrapping}]`,
]
);
await expectErrors(
`from a_index | stats ${evalWrapping} numberField + sum(numberField) ${closingWrapping}, var0 = sum(numberField)`,
[
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalWrapping}numberField+sum(numberField)${closingWrapping}]`,
]
);
await expectErrors(
`from a_index | stats var0 = ${evalWrapping} numberField + sum(numberField) ${closingWrapping}, var1 = sum(numberField)`,
[
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalWrapping}numberField+sum(numberField)${closingWrapping}]`,
]
);
});
});
});
}
});
});
});
});
};

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export interface ValidationTestCase {
query: string;
error: string[];
warning: string[];
}
export interface ValidationTestSuite {
testCases: ValidationTestCase[];
}

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as helpers from './helpers';
import { validationFromCommandTestSuite } from './test_suites/validation.command.from';
validationFromCommandTestSuite(helpers.setup);

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as helpers from './helpers';
import { validationMetricsCommandTestSuite } from './test_suites/validation.command.metrics';
validationMetricsCommandTestSuite(helpers.setup);

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import * as helpers from './helpers';
import { validationStatsCommandTestSuite } from './test_suites/validation.command.stats';
validationStatsCommandTestSuite(helpers.setup);

View file

@ -7,7 +7,13 @@
*/
import { i18n } from '@kbn/i18n';
import type { ESQLLocation, ESQLMessage } from '@kbn/esql-ast';
import type {
ESQLColumn,
ESQLCommand,
ESQLFunction,
ESQLLocation,
ESQLMessage,
} from '@kbn/esql-ast';
import type { ErrorTypes, ErrorValues } from './types';
function getMessageAndTypeFromId<K extends ErrorTypes>({
@ -370,6 +376,46 @@ function getMessageAndTypeFromId<K extends ErrorTypes>({
),
type: 'error',
};
case 'noAggFunction':
return {
message: i18n.translate('kbn-esql-validation-autocomplete.esql.validation.noAggFunction', {
defaultMessage:
'At least one aggregation function required in [{command}], found [{expression}]',
values: {
command: out.commandName.toUpperCase(),
expression: out.expression,
},
}),
type: 'error',
};
case 'expressionNotAggClosed':
return {
message: i18n.translate(
'kbn-esql-validation-autocomplete.esql.validation.expressionNotAggClosed',
{
defaultMessage:
'Cannot combine aggregation and non-aggregation values in [{command}], found [{expression}]',
values: {
command: out.commandName.toUpperCase(),
expression: out.expression,
},
}
),
type: 'error',
};
case 'aggInAggFunction':
return {
message: i18n.translate(
'kbn-esql-validation-autocomplete.esql.validation.aggInAggFunction',
{
defaultMessage:
'The aggregation function [{nestedAgg}] cannot be used as an argument in another aggregation function',
values: {
nestedAgg: out.nestedAgg,
},
}
),
};
}
return { message: '' };
}
@ -391,7 +437,7 @@ export function createMessage(
message: string,
location: ESQLLocation,
messageId: string
) {
): ESQLMessage {
return {
type,
text: message,
@ -400,6 +446,65 @@ export function createMessage(
};
}
const createError = (messageId: string, location: ESQLLocation, message: string = '') =>
createMessage('error', message, location, messageId);
export const errors = {
unexpected: (
location: ESQLLocation,
message: string = i18n.translate(
'kbn-esql-validation-autocomplete.esql.validation.errors.unexpected.message',
{
defaultMessage: 'Unexpected error, this should never happen.',
}
)
): ESQLMessage => {
return createError('unexpected', location, message);
},
byId: <K extends ErrorTypes>(
id: K,
location: ESQLLocation,
values: ErrorValues<K>
): ESQLMessage =>
getMessageFromId({
messageId: id,
values,
locations: location,
}),
unknownFunction: (fn: ESQLFunction): ESQLMessage =>
errors.byId('unknownFunction', fn.location, fn),
unknownColumn: (column: ESQLColumn): ESQLMessage =>
errors.byId('unknownColumn', column.location, {
name: column.name,
}),
noAggFunction: (cmd: ESQLCommand, fn: ESQLFunction): ESQLMessage =>
errors.byId('noAggFunction', fn.location, {
commandName: cmd.name,
expression: fn.text,
}),
expressionNotAggClosed: (cmd: ESQLCommand, fn: ESQLFunction): ESQLMessage =>
errors.byId('expressionNotAggClosed', fn.location, {
commandName: cmd.name,
expression: fn.text,
}),
unknownAggFunction: (col: ESQLColumn, type: string = 'FieldAttribute'): ESQLMessage =>
errors.byId('unknownAggregateFunction', col.location, {
value: col.name,
type,
}),
aggInAggFunction: (fn: ESQLFunction): ESQLMessage =>
errors.byId('aggInAggFunction', fn.location, {
nestedAgg: fn.name,
}),
};
export function getUnknownTypeLabel() {
return i18n.translate('kbn-esql-validation-autocomplete.esql.validation.unknownColumnType', {
defaultMessage: 'Unknown type',

View file

@ -6,7 +6,13 @@
* Side Public License, v 1.
*/
import type { ESQLAst, ESQLAstItem, ESQLMessage, ESQLSingleAstItem } from '@kbn/esql-ast';
import type {
ESQLAst,
ESQLAstItem,
ESQLAstMetricsCommand,
ESQLMessage,
ESQLSingleAstItem,
} from '@kbn/esql-ast';
import { FunctionDefinition } from '../definitions/types';
import { getAllArrayTypes, getAllArrayValues } from '../shared/helpers';
import { getMessageFromId } from './errors';
@ -14,8 +20,10 @@ import type { ESQLPolicy, ReferenceMaps } from './types';
export function buildQueryForFieldsFromSource(queryString: string, ast: ESQLAst) {
const firstCommand = ast[0];
if (firstCommand == null) {
return '';
if (!firstCommand) return '';
if (firstCommand.name === 'metrics') {
const metrics = firstCommand as ESQLAstMetricsCommand;
return `FROM ${metrics.sources.map((source) => source.name).join(', ')}`;
}
return queryString.substring(0, firstCommand.location.max + 1);
}

View file

@ -167,6 +167,26 @@ export interface ValidationErrors {
message: string;
type: { value: string | number };
};
noAggFunction: {
message: string;
type: {
commandName: string;
expression: string;
};
};
expressionNotAggClosed: {
message: string;
type: {
commandName: string;
expression: string;
};
};
aggInAggFunction: {
message: string;
type: {
nestedAgg: string;
};
};
}
export type ErrorTypes = keyof ValidationErrors;

View file

@ -1,49 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { ESQLCallbacks } from '../shared/types';
import { ValidationOptions } from './types';
import { validateQuery } from './validation';
import { getCallbackMocks } from '../__tests__/helpers';
const setup = async () => {
const callbacks = getCallbackMocks();
const validate = async (
query: string,
opts: ValidationOptions = {},
cb: ESQLCallbacks = callbacks
) => {
return await validateQuery(query, getAstAndSyntaxErrors, opts, cb);
};
return {
callbacks,
validate,
};
};
test('does not load fields when validating only a single FROM, SHOW, ROW command', async () => {
const { validate, callbacks } = await setup();
await validate('FROM kib');
await validate('FROM kibana_ecommerce METADATA _i');
await validate('FROM kibana_ecommerce METADATA _id | ');
await validate('SHOW');
await validate('ROW \t');
expect(callbacks.getFieldsFor.mock.calls.length).toBe(0);
});
test('loads fields with FROM source when commands after pipe present', async () => {
const { validate, callbacks } = await setup();
await validate('FROM kibana_ecommerce METADATA _id | eval');
expect(callbacks.getFieldsFor.mock.calls.length).toBe(1);
});

View file

@ -18,7 +18,6 @@ import capitalize from 'lodash/capitalize';
import { camelCase } from 'lodash';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { nonNullable } from '../shared/helpers';
import { METADATA_FIELDS } from '../shared/constants';
import { FUNCTION_DESCRIBE_BLOCK_NAME } from './function_describe_block_name';
import {
fields,
@ -28,6 +27,8 @@ import {
policies,
unsupported_field,
} from '../__tests__/helpers';
import { validationFromCommandTestSuite as runFromTestSuite } from './__tests__/test_suites/validation.command.from';
import { Setup, setup } from './__tests__/helpers';
const NESTING_LEVELS = 4;
const NESTED_DEPTHS = Array(NESTING_LEVELS)
@ -262,116 +263,28 @@ describe('validation logic', () => {
);
});
describe('from', () => {
testErrorsAndWarnings('f', [
`SyntaxError: mismatched input 'f' expecting {'explain', 'from', 'meta', 'metrics', 'row', 'show'}`,
]);
testErrorsAndWarnings(`from `, ["SyntaxError: missing INDEX_UNQUOTED_IDENTIFIER at '<EOF>'"]);
testErrorsAndWarnings(`from index,`, [
"SyntaxError: missing INDEX_UNQUOTED_IDENTIFIER at '<EOF>'",
]);
testErrorsAndWarnings(`from assignment = 1`, [
"SyntaxError: mismatched input '=' expecting <EOF>",
'Unknown index [assignment]',
]);
testErrorsAndWarnings(`from index`, []);
testErrorsAndWarnings(`FROM index`, []);
testErrorsAndWarnings(`FrOm index`, []);
testErrorsAndWarnings('from `index`', [
"SyntaxError: token recognition error at: '`'",
"SyntaxError: token recognition error at: '`'",
]);
const collectFixturesSetup: Setup = async (...args) => {
const api = await setup(...args);
type ExpectErrors = Awaited<ReturnType<Setup>>['expectErrors'];
return {
...api,
expectErrors: async (...params: Parameters<ExpectErrors>) => {
const [query, error = [], warning = []] = params;
const allStrings =
error.every((e) => typeof e === 'string') &&
warning.every((w) => typeof w === 'string');
if (allStrings) {
testCases.push({
query,
error,
warning,
});
}
},
};
};
testErrorsAndWarnings(`from index, other_index`, []);
testErrorsAndWarnings(`from index, missingIndex`, ['Unknown index [missingIndex]']);
testErrorsAndWarnings(`from fn()`, ['Unknown index [fn()]']);
testErrorsAndWarnings(`from average()`, ['Unknown index [average()]']);
for (const isWrapped of [true, false]) {
function setWrapping(option: string) {
return isWrapped ? `[${option}]` : option;
}
function addBracketsWarning() {
return isWrapped
? ["Square brackets '[]' need to be removed from FROM METADATA declaration"]
: [];
}
testErrorsAndWarnings(
`from index ${setWrapping('METADATA _id')}`,
[],
addBracketsWarning()
);
testErrorsAndWarnings(
`from index ${setWrapping('metadata _id')}`,
[],
addBracketsWarning()
);
testErrorsAndWarnings(
`from index ${setWrapping('METADATA _id, _source')}`,
[],
addBracketsWarning()
);
testErrorsAndWarnings(
`from index ${setWrapping('METADATA _id, _source2')}`,
[
`Metadata field [_source2] is not available. Available metadata fields are: [${METADATA_FIELDS.join(
', '
)}]`,
],
addBracketsWarning()
);
testErrorsAndWarnings(
`from index ${setWrapping('metadata _id, _source')} ${setWrapping('METADATA _id2')}`,
[
isWrapped
? "SyntaxError: mismatched input '[' expecting <EOF>"
: "SyntaxError: mismatched input 'METADATA' expecting <EOF>",
],
addBracketsWarning()
);
testErrorsAndWarnings(
`from remote-ccs:indexes ${setWrapping('METADATA _id')}`,
[],
addBracketsWarning()
);
testErrorsAndWarnings(
`from *:indexes ${setWrapping('METADATA _id')}`,
[],
addBracketsWarning()
);
}
testErrorsAndWarnings(`from index (metadata _id)`, [
"SyntaxError: mismatched input '(metadata' expecting <EOF>",
]);
testErrorsAndWarnings(`from ind*, other*`, []);
testErrorsAndWarnings(`from index*`, []);
testErrorsAndWarnings(`from *a_i*dex*`, []);
testErrorsAndWarnings(`from in*ex*`, []);
testErrorsAndWarnings(`from *n*ex`, []);
testErrorsAndWarnings(`from *n*ex*`, []);
testErrorsAndWarnings(`from i*d*x*`, []);
testErrorsAndWarnings(`from i*d*x`, []);
testErrorsAndWarnings(`from i***x*`, []);
testErrorsAndWarnings(`from i****`, []);
testErrorsAndWarnings(`from i**`, []);
testErrorsAndWarnings(`from index**`, []);
testErrorsAndWarnings(`from *ex`, []);
testErrorsAndWarnings(`from *ex*`, []);
testErrorsAndWarnings(`from in*ex`, []);
testErrorsAndWarnings(`from ind*ex`, []);
testErrorsAndWarnings(`from *,-.*`, []);
testErrorsAndWarnings(`from indexes*`, ['Unknown index [indexes*]']);
testErrorsAndWarnings(`from remote-*:indexes*`, []);
testErrorsAndWarnings(`from remote-*:indexes`, []);
testErrorsAndWarnings(`from remote-ccs:indexes`, []);
testErrorsAndWarnings(`from a_index, remote-ccs:indexes`, []);
testErrorsAndWarnings('from .secret_index', []);
testErrorsAndWarnings('from my-index', []);
testErrorsAndWarnings('from numberField', ['Unknown index [numberField]']);
testErrorsAndWarnings('from policy', ['Unknown index [policy]']);
});
runFromTestSuite(collectFixturesSetup);
describe('row', () => {
testErrorsAndWarnings('row', [
@ -1370,230 +1283,6 @@ describe('validation logic', () => {
});
});
describe('stats', () => {
testErrorsAndWarnings('from a_index | stats ', [
'At least one aggregation or grouping expression required in [STATS]',
]);
testErrorsAndWarnings('from a_index | stats by stringField', []);
testErrorsAndWarnings('from a_index | stats by ', [
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
testErrorsAndWarnings('from a_index | stats numberField ', [
'Expected an aggregate function or group but got [numberField] of type [FieldAttribute]',
]);
testErrorsAndWarnings('from a_index | stats numberField=', [
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
testErrorsAndWarnings('from a_index | stats numberField=5 by ', [
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
testErrorsAndWarnings('from a_index | stats avg(numberField) by wrongField', [
'Unknown column [wrongField]',
]);
testErrorsAndWarnings('from a_index | stats avg(numberField) by wrongField + 1', [
'Unknown column [wrongField]',
]);
testErrorsAndWarnings('from a_index | stats avg(numberField) by var0 = wrongField + 1', [
'Unknown column [wrongField]',
]);
testErrorsAndWarnings('from a_index | stats avg(numberField) by 1', []);
testErrorsAndWarnings('from a_index | stats avg(numberField) by percentile(numberField)', [
'STATS BY does not support function percentile',
]);
testErrorsAndWarnings('from a_index | stats count(`numberField`)', []);
// this is a scenario that was failing because "or" didn't accept "null"
testErrorsAndWarnings('from a_index | stats count(stringField == "a" or null)', []);
for (const subCommand of ['keep', 'drop', 'eval']) {
testErrorsAndWarnings(
`from a_index | stats count(\`numberField\`) | ${subCommand} \`count(\`\`numberField\`\`)\` `,
[]
);
}
testErrorsAndWarnings(
'from a_index | stats avg(numberField) by stringField, percentile(numberField) by ipField',
[
"SyntaxError: mismatched input 'by' expecting <EOF>",
'STATS BY does not support function percentile',
]
);
testErrorsAndWarnings(
'from a_index | stats avg(numberField), percentile(numberField, 50) by ipField',
[]
);
testErrorsAndWarnings(
'from a_index | stats avg(numberField), percentile(numberField, 50) BY ipField',
[]
);
for (const op of ['+', '-', '*', '/', '%']) {
testErrorsAndWarnings(
`from a_index | stats avg(numberField) ${op} percentile(numberField, 50) BY ipField`,
[]
);
}
testErrorsAndWarnings('from a_index | stats count(* + 1) BY ipField', [
"SyntaxError: no viable alternative at input 'count(* +'",
]);
testErrorsAndWarnings('from a_index | stats count(* + round(numberField)) BY ipField', [
"SyntaxError: no viable alternative at input 'count(* +'",
]);
testErrorsAndWarnings('from a_index | stats count(round(*)) BY ipField', [
'Using wildcards (*) in round is not allowed',
]);
testErrorsAndWarnings('from a_index | stats count(count(*)) BY ipField', [
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [number]`,
]);
testErrorsAndWarnings('from a_index | stats numberField + 1', [
'At least one aggregation function required in [STATS], found [numberField+1]',
]);
for (const nesting of NESTED_DEPTHS) {
const moreBuiltinWrapping = Array(nesting).fill('+1').join('');
testErrorsAndWarnings(
`from a_index | stats 5 + avg(numberField) ${moreBuiltinWrapping}`,
[]
);
testErrorsAndWarnings(
`from a_index | stats 5 ${moreBuiltinWrapping} + avg(numberField)`,
[]
);
testErrorsAndWarnings(`from a_index | stats 5 ${moreBuiltinWrapping} + numberField`, [
`At least one aggregation function required in [STATS], found [5${moreBuiltinWrapping}+numberField]`,
]);
testErrorsAndWarnings(`from a_index | stats 5 + numberField ${moreBuiltinWrapping}`, [
`At least one aggregation function required in [STATS], found [5+numberField${moreBuiltinWrapping}]`,
]);
testErrorsAndWarnings(
`from a_index | stats 5 + numberField ${moreBuiltinWrapping}, var0 = sum(numberField)`,
[
`At least one aggregation function required in [STATS], found [5+numberField${moreBuiltinWrapping}]`,
]
);
const evalFnWrapping = Array(nesting).fill('round(').join('');
const closingWrapping = Array(nesting).fill(')').join('');
// stress test the validation of the nesting check here
testErrorsAndWarnings(
`from a_index | stats ${evalFnWrapping} sum(numberField) ${closingWrapping}`,
[]
);
testErrorsAndWarnings(
`from a_index | stats ${evalFnWrapping} sum(numberField) ${closingWrapping} + ${evalFnWrapping} sum(numberField) ${closingWrapping}`,
[]
);
testErrorsAndWarnings(
`from a_index | stats ${evalFnWrapping} numberField + sum(numberField) ${closingWrapping}`,
[
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalFnWrapping}numberField+sum(numberField)${closingWrapping}]`,
]
);
testErrorsAndWarnings(
`from a_index | stats ${evalFnWrapping} numberField + sum(numberField) ${closingWrapping}, var0 = sum(numberField)`,
[
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalFnWrapping}numberField+sum(numberField)${closingWrapping}]`,
]
);
testErrorsAndWarnings(
`from a_index | stats var0 = ${evalFnWrapping} numberField + sum(numberField) ${closingWrapping}, var1 = sum(numberField)`,
[
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalFnWrapping}numberField+sum(numberField)${closingWrapping}]`,
]
);
testErrorsAndWarnings(
`from a_index | stats ${evalFnWrapping} sum(numberField + numberField) ${closingWrapping}`,
[]
);
testErrorsAndWarnings(
`from a_index | stats ${evalFnWrapping} sum(numberField + round(numberField)) ${closingWrapping}`,
[]
);
testErrorsAndWarnings(
`from a_index | stats ${evalFnWrapping} sum(numberField + round(numberField)) ${closingWrapping} + ${evalFnWrapping} sum(numberField + round(numberField)) ${closingWrapping}`,
[]
);
testErrorsAndWarnings(
`from a_index | stats sum(${evalFnWrapping} numberField ${closingWrapping} )`,
[]
);
testErrorsAndWarnings(
`from a_index | stats sum(${evalFnWrapping} numberField ${closingWrapping} ) + sum(${evalFnWrapping} numberField ${closingWrapping} )`,
[]
);
}
testErrorsAndWarnings('from a_index | stats 5 + numberField + 1', [
'At least one aggregation function required in [STATS], found [5+numberField+1]',
]);
testErrorsAndWarnings('from a_index | stats numberField + 1 by ipField', [
'At least one aggregation function required in [STATS], found [numberField+1]',
]);
testErrorsAndWarnings(
'from a_index | stats avg(numberField), percentile(numberField, 50) + 1 by ipField',
[]
);
testErrorsAndWarnings('from a_index | stats count(*)', []);
testErrorsAndWarnings('from a_index | stats count()', []);
testErrorsAndWarnings('from a_index | stats var0 = count(*)', []);
testErrorsAndWarnings('from a_index | stats var0 = count()', []);
testErrorsAndWarnings('from a_index | stats var0 = avg(numberField), count(*)', []);
testErrorsAndWarnings('from a_index | stats var0 = avg(fn(number)), count(*)', [
'Unknown function [fn]',
]);
// test all not allowed combinations
testErrorsAndWarnings('from a_index | STATS sum( numberField ) + abs( numberField ) ', [
'Cannot combine aggregation and non-aggregation values in [STATS], found [sum(numberField)+abs(numberField)]',
]);
testErrorsAndWarnings('from a_index | STATS abs( numberField + sum( numberField )) ', [
'Cannot combine aggregation and non-aggregation values in [STATS], found [abs(numberField+sum(numberField))]',
]);
testErrorsAndWarnings(
`FROM index
| EVAL numberField * 3.281
| STATS avg_numberField = AVG(\`numberField * 3.281\`)`,
[]
);
testErrorsAndWarnings(
`FROM index | STATS AVG(numberField) by round(numberField) + 1 | EVAL \`round(numberField) + 1\` / 2`,
[]
);
testErrorsAndWarnings(`from a_index | stats sum(case(false, 0, 1))`, []);
testErrorsAndWarnings(`from a_index | stats var0 = sum( case(false, 0, 1))`, []);
describe('constant-only parameters', () => {
testErrorsAndWarnings('from index | stats by bucket(dateField, abs(numberField), "", "")', [
'Argument of [bucket] must be a constant, received [abs(numberField)]',
]);
testErrorsAndWarnings(
'from index | stats by bucket(dateField, abs(length(numberField)), "", "")',
['Argument of [bucket] must be a constant, received [abs(length(numberField))]']
);
testErrorsAndWarnings('from index | stats by bucket(dateField, pi(), "", "")', []);
testErrorsAndWarnings('from index | stats by bucket(dateField, 1 + 30 / 10, "", "")', []);
testErrorsAndWarnings(
'from index | stats by bucket(dateField, 1 + 30 / 10, concat("", ""), "")',
[]
);
testErrorsAndWarnings(
'from index | stats by bucket(dateField, numberField, stringField, stringField)',
[
'Argument of [bucket] must be a constant, received [numberField]',
'Argument of [bucket] must be a constant, received [stringField]',
'Argument of [bucket] must be a constant, received [stringField]',
]
);
});
});
describe('sort', () => {
testErrorsAndWarnings('from a_index | sort ', [
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",

View file

@ -7,18 +7,20 @@
*/
import uniqBy from 'lodash/uniqBy';
import type {
import {
AstProviderFn,
ESQLAstItem,
ESQLAstMetricsCommand,
ESQLColumn,
ESQLCommand,
ESQLCommandMode,
ESQLCommandOption,
ESQLFunction,
ESQLMessage,
ESQLSingleAstItem,
ESQLSource,
walk,
} from '@kbn/esql-ast';
import type { ESQLAstField } from '@kbn/esql-ast/src/types';
import {
CommandModeDefinition,
CommandOptionsDefinition,
@ -50,11 +52,12 @@ import {
isAssignment,
isVariable,
isValidLiteralOption,
isAggFunction,
getQuotedColumnName,
isInlineCastItem,
} from '../shared/helpers';
import { collectVariables } from '../shared/variables';
import { getMessageFromId } from './errors';
import { getMessageFromId, errors } from './errors';
import type {
ErrorTypes,
ESQLRealField,
@ -351,15 +354,7 @@ function validateFunction(
if (!isFnSupported.supported) {
if (isFnSupported.reason === 'unknownFunction') {
messages.push(
getMessageFromId({
messageId: 'unknownFunction',
values: {
name: astFunction.name,
},
locations: astFunction.location,
})
);
messages.push(errors.unknownFunction(astFunction));
}
// for nested functions skip this check and make the nested check fail later on
if (isFnSupported.reason === 'unsupportedFunction' && !isNested) {
@ -467,9 +462,11 @@ function validateFunction(
* and each should be validated as if each were constantOnly.
*/
allMatchingArgDefinitionsAreConstantOnly || forceConstantOnly,
// use the nesting flag for now just for stats
// use the nesting flag for now just for stats and metrics
// TODO: revisit this part later on to make it more generic
parentCommand === 'stats' ? isNested || !isAssignment(astFunction) : false
parentCommand === 'stats' || parentCommand === 'metrics'
? isNested || !isAssignment(astFunction)
: false
);
if (messagesFromArg.some(({ code }) => code === 'expectedConstant')) {
@ -616,6 +613,153 @@ function validateSetting(
return messages;
}
/**
* Validate that a function is an aggregate function or that all children
* recursively terminate at either a literal or an aggregate function.
*/
const isFunctionAggClosed = (fn: ESQLFunction): boolean =>
isAggFunction(fn) || areFunctionArgsAggClosed(fn);
const areFunctionArgsAggClosed = (fn: ESQLFunction): boolean =>
fn.args.every((arg) => isLiteralItem(arg) || (isFunctionItem(arg) && isFunctionAggClosed(arg)));
/**
* Looks for first nested aggregate function in an aggregate function, recursively.
*/
const findNestedAggFunctionInAggFunction = (agg: ESQLFunction): ESQLFunction | undefined => {
for (const arg of agg.args) {
if (isFunctionItem(arg)) {
return isAggFunction(arg) ? arg : findNestedAggFunctionInAggFunction(arg);
}
}
};
/**
* Looks for first nested aggregate function in another aggregate a function,
* recursively.
*
* @param fn Function to check for nested aggregate functions.
* @param parentIsAgg Whether the parent function of `fn` is an aggregate function.
* @returns The first nested aggregate function in `fn`, or `undefined` if none is found.
*/
const findNestedAggFunction = (
fn: ESQLFunction,
parentIsAgg: boolean = false
): ESQLFunction | undefined => {
if (isAggFunction(fn)) {
return parentIsAgg ? fn : findNestedAggFunctionInAggFunction(fn);
}
for (const arg of fn.args) {
if (isFunctionItem(arg)) {
const nestedAgg = findNestedAggFunction(arg, parentIsAgg || isAggFunction(fn));
if (nestedAgg) return nestedAgg;
}
}
};
/**
* Validates aggregates fields: `... <aggregates> ...`.
*/
const validateAggregates = (
command: ESQLCommand,
aggregates: ESQLAstField[],
references: ReferenceMaps
) => {
const messages: ESQLMessage[] = [];
// Should never happen.
if (!aggregates.length) {
messages.push(errors.unexpected(command.location));
return messages;
}
let hasMissingAggregationFunctionError = false;
for (const aggregate of aggregates) {
if (isFunctionItem(aggregate)) {
messages.push(...validateFunction(aggregate, command.name, undefined, references));
let hasAggregationFunction = false;
walk(aggregate, {
visitFunction: (fn) => {
const definition = getFunctionDefinition(fn.name);
if (!definition) return;
if (definition.type === 'agg') hasAggregationFunction = true;
},
});
if (!hasAggregationFunction) {
hasMissingAggregationFunctionError = true;
messages.push(errors.noAggFunction(command, aggregate));
}
} else if (isColumnItem(aggregate)) {
messages.push(errors.unknownAggFunction(aggregate));
} else {
// Should never happen.
}
}
if (hasMissingAggregationFunctionError) {
return messages;
}
for (const aggregate of aggregates) {
if (isFunctionItem(aggregate)) {
const fn = isAssignment(aggregate) ? aggregate.args[1] : aggregate;
if (isFunctionItem(fn) && !isFunctionAggClosed(fn)) {
messages.push(errors.expressionNotAggClosed(command, fn));
}
}
}
if (messages.length) {
return messages;
}
for (const aggregate of aggregates) {
if (isFunctionItem(aggregate)) {
const aggInAggFunction = findNestedAggFunction(aggregate);
if (aggInAggFunction) {
messages.push(errors.aggInAggFunction(aggInAggFunction));
break;
}
}
}
return messages;
};
/**
* Validates grouping fields of the BY clause: `... BY <grouping>`.
*/
const validateByGrouping = (
fields: ESQLAstItem[],
commandName: string,
referenceMaps: ReferenceMaps,
multipleParams: boolean
): ESQLMessage[] => {
const messages: ESQLMessage[] = [];
for (const field of fields) {
if (!Array.isArray(field)) {
if (!multipleParams) {
if (isColumnItem(field)) {
messages.push(...validateColumnForCommand(field, commandName, referenceMaps));
}
} else {
if (isColumnItem(field)) {
messages.push(...validateColumnForCommand(field, commandName, referenceMaps));
}
if (isFunctionItem(field)) {
messages.push(...validateFunction(field, commandName, 'by', referenceMaps));
}
}
}
}
return messages;
};
function validateOption(
option: ESQLCommandOption,
optionDef: CommandOptionsDefinition | undefined,
@ -673,38 +817,40 @@ function validateSource(
if (source.incomplete) {
return messages;
}
const commandDef = getCommandDefinition(commandName);
// give up on validate if CCS for now
const hasCCS = hasCCSSource(source.name);
if (!hasCCS) {
const isWildcardAndNotSupported =
hasWildcard(source.name) && !commandDef.signature.params.some(({ wildcards }) => wildcards);
if (isWildcardAndNotSupported) {
if (hasCCS) {
return messages;
}
const commandDef = getCommandDefinition(commandName);
const isWildcardAndNotSupported =
hasWildcard(source.name) && !commandDef.signature.params.some(({ wildcards }) => wildcards);
if (isWildcardAndNotSupported) {
messages.push(
getMessageFromId({
messageId: 'wildcardNotSupportedForCommand',
values: { command: commandName.toUpperCase(), value: source.name },
locations: source.location,
})
);
} else {
if (source.sourceType === 'index' && !sourceExists(source.name, sources)) {
messages.push(
getMessageFromId({
messageId: 'wildcardNotSupportedForCommand',
values: { command: commandName.toUpperCase(), value: source.name },
messageId: 'unknownIndex',
values: { name: source.name },
locations: source.location,
})
);
} else if (source.sourceType === 'policy' && !policies.has(source.name)) {
messages.push(
getMessageFromId({
messageId: 'unknownPolicy',
values: { name: source.name },
locations: source.location,
})
);
} else {
if (source.sourceType === 'index' && !sourceExists(source.name, sources)) {
messages.push(
getMessageFromId({
messageId: 'unknownIndex',
values: { name: source.name },
locations: source.location,
})
);
} else if (source.sourceType === 'policy' && !policies.has(source.name)) {
messages.push(
getMessageFromId({
messageId: 'unknownPolicy',
values: { name: source.name },
locations: source.location,
})
);
}
}
}
@ -720,15 +866,7 @@ function validateColumnForCommand(
if (commandName === 'row') {
if (!references.variables.has(column.name)) {
messages.push(
getMessageFromId({
messageId: 'unknownColumn',
values: {
name: column.name,
},
locations: column.location,
})
);
messages.push(errors.unknownColumn(column));
}
} else {
const columnName = getQuotedColumnName(column);
@ -780,21 +918,55 @@ function validateColumnForCommand(
}
} else {
if (column.name) {
messages.push(
getMessageFromId({
messageId: 'unknownColumn',
values: {
name: column.name,
},
locations: column.location,
})
);
messages.push(errors.unknownColumn(column));
}
}
}
return messages;
}
export function validateSources(
command: ESQLCommand,
sources: ESQLSource[],
references: ReferenceMaps
): ESQLMessage[] {
const messages: ESQLMessage[] = [];
for (const source of sources) {
messages.push(...validateSource(source, command.name, references));
}
return messages;
}
/**
* Validates the METRICS source command:
*
* METRICS <sources> [ <aggregates> [ BY <grouping> ]]
*/
const validateMetricsCommand = (
command: ESQLAstMetricsCommand,
references: ReferenceMaps
): ESQLMessage[] => {
const messages: ESQLMessage[] = [];
const { sources, aggregates, grouping } = command;
// METRICS <sources> ...
messages.push(...validateSources(command, sources, references));
// ... <aggregates> ...
if (aggregates && aggregates.length) {
messages.push(...validateAggregates(command, aggregates, references));
// ... BY <grouping>
if (grouping && grouping.length) {
messages.push(...validateByGrouping(grouping, 'metrics', references, true));
}
}
return messages;
};
function validateCommand(command: ESQLCommand, references: ReferenceMaps): ESQLMessage[] {
const messages: ESQLMessage[] = [];
if (command.incomplete) {
@ -807,62 +979,63 @@ function validateCommand(command: ESQLCommand, references: ReferenceMaps): ESQLM
messages.push(...commandDef.validate(command));
}
// Now validate arguments
for (const commandArg of command.args) {
const wrappedArg = Array.isArray(commandArg) ? commandArg : [commandArg];
for (const arg of wrappedArg) {
if (isFunctionItem(arg)) {
messages.push(...validateFunction(arg, command.name, undefined, references));
}
switch (commandDef.name) {
case 'metrics': {
const metrics = command as ESQLAstMetricsCommand;
messages.push(...validateMetricsCommand(metrics, references));
break;
}
default: {
// Now validate arguments
for (const commandArg of command.args) {
const wrappedArg = Array.isArray(commandArg) ? commandArg : [commandArg];
for (const arg of wrappedArg) {
if (isFunctionItem(arg)) {
messages.push(...validateFunction(arg, command.name, undefined, references));
}
if (isSettingItem(arg)) {
messages.push(...validateSetting(arg, commandDef.modes[0], command, references));
}
if (isSettingItem(arg)) {
messages.push(...validateSetting(arg, commandDef.modes[0], command, references));
}
if (isOptionItem(arg)) {
messages.push(
...validateOption(
arg,
commandDef.options.find(({ name }) => name === arg.name),
command,
references
)
);
}
if (isColumnItem(arg)) {
if (command.name === 'stats') {
messages.push(
getMessageFromId({
messageId: 'unknownAggregateFunction',
values: {
value: (arg as ESQLSingleAstItem).name,
type: 'FieldAttribute',
},
locations: (arg as ESQLSingleAstItem).location,
})
);
} else {
messages.push(...validateColumnForCommand(arg, command.name, references));
if (isOptionItem(arg)) {
messages.push(
...validateOption(
arg,
commandDef.options.find(({ name }) => name === arg.name),
command,
references
)
);
}
if (isColumnItem(arg)) {
if (command.name === 'stats') {
messages.push(errors.unknownAggFunction(arg));
} else {
messages.push(...validateColumnForCommand(arg, command.name, references));
}
}
if (isTimeIntervalItem(arg)) {
messages.push(
getMessageFromId({
messageId: 'unsupportedTypeForCommand',
values: {
command: command.name.toUpperCase(),
type: 'date_period',
value: arg.name,
},
locations: arg.location,
})
);
}
if (isSourceItem(arg)) {
messages.push(...validateSource(arg, command.name, references));
}
}
}
if (isTimeIntervalItem(arg)) {
messages.push(
getMessageFromId({
messageId: 'unsupportedTypeForCommand',
values: {
command: command.name.toUpperCase(),
type: 'date_period',
value: arg.name,
},
locations: arg.location,
})
);
}
if (isSourceItem(arg)) {
messages.push(...validateSource(arg, command.name, references));
}
}
}
// no need to check for mandatory options passed
// as they are already validated at syntax level
return messages;
@ -940,7 +1113,6 @@ export async function validateQuery(
if (!options.ignoreOnMissingCallbacks) {
return result;
}
const { errors, warnings } = result;
const finalCallbacks = callbacks || {};
const errorTypoesToIgnore = Object.entries(ignoreErrorsMap).reduce((acc, [key, errorCodes]) => {
if (
@ -953,7 +1125,7 @@ export async function validateQuery(
}
return acc;
}, {} as Partial<Record<ErrorTypes, boolean>>);
const filteredErrors = errors
const filteredErrors = result.errors
.filter((error) => {
if ('severity' in error) {
return true;
@ -970,7 +1142,7 @@ export async function validateQuery(
}
: error
);
return { errors: filteredErrors, warnings };
return { errors: filteredErrors, warnings: result.warnings };
}
/**
@ -986,7 +1158,8 @@ async function validateAst(
): Promise<ValidationResult> {
const messages: ESQLMessage[] = [];
const { ast, errors } = await astProvider(queryString);
const parsingResult = await astProvider(queryString);
const { ast } = parsingResult;
const [sources, availableFields, availablePolicies] = await Promise.all([
// retrieve the list of available sources
@ -1023,18 +1196,19 @@ async function validateAst(
messages.push(...validateUnsupportedTypeFields(availableFields));
for (const command of ast) {
const commandMessages = validateCommand(command, {
const references: ReferenceMaps = {
sources,
fields: availableFields,
policies: availablePolicies,
variables,
query: queryString,
});
};
const commandMessages = validateCommand(command, references);
messages.push(...commandMessages);
}
return {
errors: [...errors, ...messages.filter(({ type }) => type === 'error')],
errors: [...parsingResult.errors, ...messages.filter(({ type }) => type === 'error')],
warnings: messages.filter(({ type }) => type === 'warning'),
};
}

View file

@ -152,6 +152,7 @@ export default function ({ getService }: FtrProviderContext) {
);
for (const policy of policies) {
log.info(`deleting policy "${policy}"...`);
// TODO: Maybe `policy` -> `policy.name`?
await es.enrich.deletePolicy({ name: policy }, { ignore: [404] });
}
}