mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
37426f0bde
commit
d9fc2ca1ed
31 changed files with 2031 additions and 1608 deletions
|
@ -24,6 +24,7 @@ export type {
|
|||
ESQLLiteral,
|
||||
AstProviderFn,
|
||||
EditorError,
|
||||
ESQLAstNode,
|
||||
} from './src/types';
|
||||
|
||||
// Low level functions to parse grammar
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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 }],
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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()
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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',
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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}]`,
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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[];
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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',
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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}",
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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] });
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue