[ES|QL] New sync with ES changes (#176283)

## Summary

Sync with https://github.com/elastic/elasticsearch/pull/104958 for
support of builtin fn in STATS
  * validation  
  * autocomplete  
  * also fixed `STATS BY <field>` syntax


![new_stats](735f9842-b1d3-4aa0-9d51-4b2f9b136ed3)


Sync with https://github.com/elastic/elasticsearch/pull/104913 for new
`log` function
  * validation   - also warning for negative values
  * autocomplete  

![add_log](146b945d-a23b-45ec-9df2-2d2b291e883b)

Sync with https://github.com/elastic/elasticsearch/pull/105064 for
removal of `PROJECT` command
  * validation   (both new and legacy syntax supported)
  * autocomplete   (will only suggest new syntax)


![remove_project](b6f40afe-a26d-4917-b7a1-d8ae97c5368b)

Sync with https://github.com/elastic/elasticsearch/pull/105221 for
removal of mandatory brackets for `METADATA` command option
* validation  (added warning deprecation message when using brackets)
  * autocomplete  

![fix_metadata](c65db176-dd94-45f3-9524-45453e62f51a)


Sync with https://github.com/elastic/elasticsearch/pull/105224 for
change of syntax for ENRICH ccq mode
  * validation  
* autocomplete  (not directly promoted, the user has to type `_` to
trigger it)
  * hover  
  * code actions  

![fix_ccq_enrich](0900edd9-a0a7-4ac8-bc12-e39a72359984)

![fix_ccq_enrich_2](74b0908f-d385-4723-b3d4-c09108f47a73)


Do not merge until those 5 get merged.

Additional things in this PR:
* Added more tests for `callbacks` not passed scenario
  * covered more cases like those with `dissect`
* Added more tests for signature params number (calling a function with
an extra arg should return an error)
* Cleaned up some more unused code
* Improved messages on too many arguments for functions

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Marco Liberati 2024-02-09 14:25:28 +01:00 committed by GitHub
parent 8d43c0e718
commit 7bbea56c16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 2758 additions and 2441 deletions

View file

@ -19,7 +19,6 @@ INLINESTATS : I N L I N E S T A T S -> pushMode(EXPRESSION_MODE);
KEEP : K E E P -> pushMode(PROJECT_MODE);
LIMIT : L I M I T -> pushMode(EXPRESSION_MODE);
MV_EXPAND : M V UNDERSCORE E X P A N D -> pushMode(MVEXPAND_MODE);
PROJECT : P R O J E C T -> pushMode(PROJECT_MODE);
RENAME : R E N A M E -> pushMode(RENAME_MODE);
ROW : R O W -> pushMode(EXPRESSION_MODE);
SHOW : S H O W -> pushMode(SHOW_MODE);
@ -218,7 +217,7 @@ FROM_WS
: WS -> channel(HIDDEN)
;
//
// DROP, KEEP, PROJECT
// DROP, KEEP
//
mode PROJECT_MODE;
PROJECT_PIPE : PIPE -> type(PIPE), popMode;
@ -299,7 +298,8 @@ fragment ENRICH_POLICY_NAME_BODY
: ~[\\/?"<>| ,#\t\r\n:]
;
ENRICH_POLICY_NAME
: (LETTER | DIGIT) ENRICH_POLICY_NAME_BODY*
// allow prefix for the policy to specify its resolution
: (ENRICH_POLICY_NAME_BODY+ COLON)? ENRICH_POLICY_NAME_BODY+
;
ENRICH_QUOTED_IDENTIFIER

File diff suppressed because one or more lines are too long

View file

@ -9,118 +9,117 @@ INLINESTATS=8
KEEP=9
LIMIT=10
MV_EXPAND=11
PROJECT=12
RENAME=13
ROW=14
SHOW=15
SORT=16
STATS=17
WHERE=18
UNKNOWN_CMD=19
LINE_COMMENT=20
MULTILINE_COMMENT=21
WS=22
EXPLAIN_WS=23
EXPLAIN_LINE_COMMENT=24
EXPLAIN_MULTILINE_COMMENT=25
PIPE=26
STRING=27
INTEGER_LITERAL=28
DECIMAL_LITERAL=29
BY=30
AND=31
ASC=32
ASSIGN=33
COMMA=34
DESC=35
DOT=36
FALSE=37
FIRST=38
LAST=39
LP=40
IN=41
IS=42
LIKE=43
NOT=44
NULL=45
NULLS=46
OR=47
PARAM=48
RLIKE=49
RP=50
TRUE=51
EQ=52
CIEQ=53
NEQ=54
LT=55
LTE=56
GT=57
GTE=58
PLUS=59
MINUS=60
ASTERISK=61
SLASH=62
PERCENT=63
OPENING_BRACKET=64
CLOSING_BRACKET=65
UNQUOTED_IDENTIFIER=66
QUOTED_IDENTIFIER=67
EXPR_LINE_COMMENT=68
EXPR_MULTILINE_COMMENT=69
EXPR_WS=70
METADATA=71
FROM_UNQUOTED_IDENTIFIER=72
FROM_LINE_COMMENT=73
FROM_MULTILINE_COMMENT=74
FROM_WS=75
UNQUOTED_ID_PATTERN=76
PROJECT_LINE_COMMENT=77
PROJECT_MULTILINE_COMMENT=78
PROJECT_WS=79
AS=80
RENAME_LINE_COMMENT=81
RENAME_MULTILINE_COMMENT=82
RENAME_WS=83
ON=84
WITH=85
ENRICH_POLICY_NAME=86
ENRICH_LINE_COMMENT=87
ENRICH_MULTILINE_COMMENT=88
ENRICH_WS=89
ENRICH_FIELD_LINE_COMMENT=90
ENRICH_FIELD_MULTILINE_COMMENT=91
ENRICH_FIELD_WS=92
MVEXPAND_LINE_COMMENT=93
MVEXPAND_MULTILINE_COMMENT=94
MVEXPAND_WS=95
INFO=96
FUNCTIONS=97
SHOW_LINE_COMMENT=98
SHOW_MULTILINE_COMMENT=99
SHOW_WS=100
COLON=101
SETTING=102
SETTING_LINE_COMMENT=103
SETTTING_MULTILINE_COMMENT=104
SETTING_WS=105
'|'=26
'='=33
','=34
'.'=36
'('=40
'?'=48
')'=50
'=='=52
'=~'=53
'!='=54
'<'=55
'<='=56
'>'=57
'>='=58
'+'=59
'-'=60
'*'=61
'/'=62
'%'=63
']'=65
':'=101
RENAME=12
ROW=13
SHOW=14
SORT=15
STATS=16
WHERE=17
UNKNOWN_CMD=18
LINE_COMMENT=19
MULTILINE_COMMENT=20
WS=21
EXPLAIN_WS=22
EXPLAIN_LINE_COMMENT=23
EXPLAIN_MULTILINE_COMMENT=24
PIPE=25
STRING=26
INTEGER_LITERAL=27
DECIMAL_LITERAL=28
BY=29
AND=30
ASC=31
ASSIGN=32
COMMA=33
DESC=34
DOT=35
FALSE=36
FIRST=37
LAST=38
LP=39
IN=40
IS=41
LIKE=42
NOT=43
NULL=44
NULLS=45
OR=46
PARAM=47
RLIKE=48
RP=49
TRUE=50
EQ=51
CIEQ=52
NEQ=53
LT=54
LTE=55
GT=56
GTE=57
PLUS=58
MINUS=59
ASTERISK=60
SLASH=61
PERCENT=62
OPENING_BRACKET=63
CLOSING_BRACKET=64
UNQUOTED_IDENTIFIER=65
QUOTED_IDENTIFIER=66
EXPR_LINE_COMMENT=67
EXPR_MULTILINE_COMMENT=68
EXPR_WS=69
METADATA=70
FROM_UNQUOTED_IDENTIFIER=71
FROM_LINE_COMMENT=72
FROM_MULTILINE_COMMENT=73
FROM_WS=74
UNQUOTED_ID_PATTERN=75
PROJECT_LINE_COMMENT=76
PROJECT_MULTILINE_COMMENT=77
PROJECT_WS=78
AS=79
RENAME_LINE_COMMENT=80
RENAME_MULTILINE_COMMENT=81
RENAME_WS=82
ON=83
WITH=84
ENRICH_POLICY_NAME=85
ENRICH_LINE_COMMENT=86
ENRICH_MULTILINE_COMMENT=87
ENRICH_WS=88
ENRICH_FIELD_LINE_COMMENT=89
ENRICH_FIELD_MULTILINE_COMMENT=90
ENRICH_FIELD_WS=91
MVEXPAND_LINE_COMMENT=92
MVEXPAND_MULTILINE_COMMENT=93
MVEXPAND_WS=94
INFO=95
FUNCTIONS=96
SHOW_LINE_COMMENT=97
SHOW_MULTILINE_COMMENT=98
SHOW_WS=99
COLON=100
SETTING=101
SETTING_LINE_COMMENT=102
SETTTING_MULTILINE_COMMENT=103
SETTING_WS=104
'|'=25
'='=32
','=33
'.'=35
'('=39
'?'=47
')'=49
'=='=51
'=~'=52
'!='=53
'<'=54
'<='=55
'>'=56
'>='=57
'+'=58
'-'=59
'*'=60
'/'=61
'%'=62
']'=64
':'=100

File diff suppressed because it is too large Load diff

View file

@ -102,7 +102,16 @@ fromCommand
;
metadata
: OPENING_BRACKET METADATA fromIdentifier (COMMA fromIdentifier)* CLOSING_BRACKET
: metadataOption
| deprecated_metadata
;
metadataOption
: METADATA fromIdentifier (COMMA fromIdentifier)*
;
deprecated_metadata
: OPENING_BRACKET metadataOption CLOSING_BRACKET
;
@ -168,7 +177,6 @@ orderExpression
keepCommand
: KEEP qualifiedNamePattern (COMMA qualifiedNamePattern)*
| PROJECT qualifiedNamePattern (COMMA qualifiedNamePattern)*
;
dropCommand
@ -242,13 +250,9 @@ showCommand
;
enrichCommand
: ENRICH setting* policyName=ENRICH_POLICY_NAME (ON matchField=qualifiedNamePattern)? (WITH enrichWithClause (COMMA enrichWithClause)*)?
: ENRICH policyName=ENRICH_POLICY_NAME (ON matchField=qualifiedNamePattern)? (WITH enrichWithClause (COMMA enrichWithClause)*)?
;
enrichWithClause
: (newName=qualifiedNamePattern ASSIGN)? enrichField=qualifiedNamePattern
;
setting
: OPENING_BRACKET name=SETTING COLON value=SETTING CLOSING_BRACKET
;

File diff suppressed because one or more lines are too long

View file

@ -9,118 +9,117 @@ INLINESTATS=8
KEEP=9
LIMIT=10
MV_EXPAND=11
PROJECT=12
RENAME=13
ROW=14
SHOW=15
SORT=16
STATS=17
WHERE=18
UNKNOWN_CMD=19
LINE_COMMENT=20
MULTILINE_COMMENT=21
WS=22
EXPLAIN_WS=23
EXPLAIN_LINE_COMMENT=24
EXPLAIN_MULTILINE_COMMENT=25
PIPE=26
STRING=27
INTEGER_LITERAL=28
DECIMAL_LITERAL=29
BY=30
AND=31
ASC=32
ASSIGN=33
COMMA=34
DESC=35
DOT=36
FALSE=37
FIRST=38
LAST=39
LP=40
IN=41
IS=42
LIKE=43
NOT=44
NULL=45
NULLS=46
OR=47
PARAM=48
RLIKE=49
RP=50
TRUE=51
EQ=52
CIEQ=53
NEQ=54
LT=55
LTE=56
GT=57
GTE=58
PLUS=59
MINUS=60
ASTERISK=61
SLASH=62
PERCENT=63
OPENING_BRACKET=64
CLOSING_BRACKET=65
UNQUOTED_IDENTIFIER=66
QUOTED_IDENTIFIER=67
EXPR_LINE_COMMENT=68
EXPR_MULTILINE_COMMENT=69
EXPR_WS=70
METADATA=71
FROM_UNQUOTED_IDENTIFIER=72
FROM_LINE_COMMENT=73
FROM_MULTILINE_COMMENT=74
FROM_WS=75
UNQUOTED_ID_PATTERN=76
PROJECT_LINE_COMMENT=77
PROJECT_MULTILINE_COMMENT=78
PROJECT_WS=79
AS=80
RENAME_LINE_COMMENT=81
RENAME_MULTILINE_COMMENT=82
RENAME_WS=83
ON=84
WITH=85
ENRICH_POLICY_NAME=86
ENRICH_LINE_COMMENT=87
ENRICH_MULTILINE_COMMENT=88
ENRICH_WS=89
ENRICH_FIELD_LINE_COMMENT=90
ENRICH_FIELD_MULTILINE_COMMENT=91
ENRICH_FIELD_WS=92
MVEXPAND_LINE_COMMENT=93
MVEXPAND_MULTILINE_COMMENT=94
MVEXPAND_WS=95
INFO=96
FUNCTIONS=97
SHOW_LINE_COMMENT=98
SHOW_MULTILINE_COMMENT=99
SHOW_WS=100
COLON=101
SETTING=102
SETTING_LINE_COMMENT=103
SETTTING_MULTILINE_COMMENT=104
SETTING_WS=105
'|'=26
'='=33
','=34
'.'=36
'('=40
'?'=48
')'=50
'=='=52
'=~'=53
'!='=54
'<'=55
'<='=56
'>'=57
'>='=58
'+'=59
'-'=60
'*'=61
'/'=62
'%'=63
']'=65
':'=101
RENAME=12
ROW=13
SHOW=14
SORT=15
STATS=16
WHERE=17
UNKNOWN_CMD=18
LINE_COMMENT=19
MULTILINE_COMMENT=20
WS=21
EXPLAIN_WS=22
EXPLAIN_LINE_COMMENT=23
EXPLAIN_MULTILINE_COMMENT=24
PIPE=25
STRING=26
INTEGER_LITERAL=27
DECIMAL_LITERAL=28
BY=29
AND=30
ASC=31
ASSIGN=32
COMMA=33
DESC=34
DOT=35
FALSE=36
FIRST=37
LAST=38
LP=39
IN=40
IS=41
LIKE=42
NOT=43
NULL=44
NULLS=45
OR=46
PARAM=47
RLIKE=48
RP=49
TRUE=50
EQ=51
CIEQ=52
NEQ=53
LT=54
LTE=55
GT=56
GTE=57
PLUS=58
MINUS=59
ASTERISK=60
SLASH=61
PERCENT=62
OPENING_BRACKET=63
CLOSING_BRACKET=64
UNQUOTED_IDENTIFIER=65
QUOTED_IDENTIFIER=66
EXPR_LINE_COMMENT=67
EXPR_MULTILINE_COMMENT=68
EXPR_WS=69
METADATA=70
FROM_UNQUOTED_IDENTIFIER=71
FROM_LINE_COMMENT=72
FROM_MULTILINE_COMMENT=73
FROM_WS=74
UNQUOTED_ID_PATTERN=75
PROJECT_LINE_COMMENT=76
PROJECT_MULTILINE_COMMENT=77
PROJECT_WS=78
AS=79
RENAME_LINE_COMMENT=80
RENAME_MULTILINE_COMMENT=81
RENAME_WS=82
ON=83
WITH=84
ENRICH_POLICY_NAME=85
ENRICH_LINE_COMMENT=86
ENRICH_MULTILINE_COMMENT=87
ENRICH_WS=88
ENRICH_FIELD_LINE_COMMENT=89
ENRICH_FIELD_MULTILINE_COMMENT=90
ENRICH_FIELD_WS=91
MVEXPAND_LINE_COMMENT=92
MVEXPAND_MULTILINE_COMMENT=93
MVEXPAND_WS=94
INFO=95
FUNCTIONS=96
SHOW_LINE_COMMENT=97
SHOW_MULTILINE_COMMENT=98
SHOW_WS=99
COLON=100
SETTING=101
SETTING_LINE_COMMENT=102
SETTTING_MULTILINE_COMMENT=103
SETTING_WS=104
'|'=25
'='=32
','=33
'.'=35
'('=39
'?'=47
')'=49
'=='=51
'=~'=52
'!='=53
'<'=54
'<='=55
'>'=56
'>='=57
'+'=58
'-'=59
'*'=60
'/'=61
'%'=62
']'=64
':'=100

File diff suppressed because it is too large Load diff

View file

@ -49,6 +49,8 @@ import { FieldsContext } from "./esql_parser";
import { FieldContext } from "./esql_parser";
import { FromCommandContext } from "./esql_parser";
import { MetadataContext } from "./esql_parser";
import { MetadataOptionContext } from "./esql_parser";
import { Deprecated_metadataContext } from "./esql_parser";
import { EvalCommandContext } from "./esql_parser";
import { StatsCommandContext } from "./esql_parser";
import { InlinestatsCommandContext } from "./esql_parser";
@ -81,7 +83,6 @@ import { SubqueryExpressionContext } from "./esql_parser";
import { ShowCommandContext } from "./esql_parser";
import { EnrichCommandContext } from "./esql_parser";
import { EnrichWithClauseContext } from "./esql_parser";
import { SettingContext } from "./esql_parser";
/**
@ -642,6 +643,28 @@ export interface esql_parserListener extends ParseTreeListener {
*/
exitMetadata?: (ctx: MetadataContext) => void;
/**
* Enter a parse tree produced by `esql_parser.metadataOption`.
* @param ctx the parse tree
*/
enterMetadataOption?: (ctx: MetadataOptionContext) => void;
/**
* Exit a parse tree produced by `esql_parser.metadataOption`.
* @param ctx the parse tree
*/
exitMetadataOption?: (ctx: MetadataOptionContext) => void;
/**
* Enter a parse tree produced by `esql_parser.deprecated_metadata`.
* @param ctx the parse tree
*/
enterDeprecated_metadata?: (ctx: Deprecated_metadataContext) => void;
/**
* Exit a parse tree produced by `esql_parser.deprecated_metadata`.
* @param ctx the parse tree
*/
exitDeprecated_metadata?: (ctx: Deprecated_metadataContext) => void;
/**
* Enter a parse tree produced by `esql_parser.evalCommand`.
* @param ctx the parse tree
@ -993,16 +1016,5 @@ export interface esql_parserListener extends ParseTreeListener {
* @param ctx the parse tree
*/
exitEnrichWithClause?: (ctx: EnrichWithClauseContext) => void;
/**
* Enter a parse tree produced by `esql_parser.setting`.
* @param ctx the parse tree
*/
enterSetting?: (ctx: SettingContext) => void;
/**
* Exit a parse tree produced by `esql_parser.setting`.
* @param ctx the parse tree
*/
exitSetting?: (ctx: SettingContext) => void;
}

View file

@ -43,7 +43,6 @@ import {
getPolicyName,
getMatchField,
getEnrichClauses,
getPolicySettings,
} from './ast_walker';
import type { ESQLAst } from './types';
@ -117,10 +116,12 @@ export class AstListener implements ESQLParserListener {
this.ast.push(commandAst);
commandAst.args.push(...collectAllSourceIdentifiers(ctx));
const metadataContext = ctx.metadata();
if (metadataContext) {
const option = createOption(metadataContext.METADATA().text.toLowerCase(), metadataContext);
const metadataContent =
metadataContext?.deprecated_metadata()?.metadataOption() || metadataContext?.metadataOption();
if (metadataContent) {
const option = createOption(metadataContent.METADATA().text.toLowerCase(), metadataContent);
commandAst.args.push(option);
option.args.push(...collectAllColumnIdentifiers(metadataContext));
option.args.push(...collectAllColumnIdentifiers(metadataContent));
}
}
@ -251,11 +252,6 @@ export class AstListener implements ESQLParserListener {
exitEnrichCommand(ctx: EnrichCommandContext) {
const command = createCommand('enrich', ctx);
this.ast.push(command);
command.args.push(
...getPolicySettings(ctx),
...getPolicyName(ctx),
...getMatchField(ctx),
...getEnrichClauses(ctx)
);
command.args.push(...getPolicyName(ctx), ...getMatchField(ctx), ...getEnrichClauses(ctx));
}
}

View file

@ -19,7 +19,6 @@ import type {
DecimalValueContext,
IntegerValueContext,
QualifiedIntegerLiteralContext,
SettingContext,
} from '../../antlr/esql_parser';
import { getPosition } from './ast_position_utils';
import type {
@ -198,15 +197,15 @@ export function computeLocationExtends(fn: ESQLFunction) {
/* SCRIPT_MARKER_START */
function getQuotedText(ctx: ParserRuleContext) {
return [67 /* esql_parser.QUOTED_IDENTIFIER */]
return [66 /* esql_parser.QUOTED_IDENTIFIER */]
.map((keyCode) => ctx.tryGetToken(keyCode, 0))
.filter(nonNullable)[0];
}
function getUnquotedText(ctx: ParserRuleContext) {
return [
66 /* esql_parser.UNQUOTED_IDENTIFIER */, 72 /* esql_parser.FROM_UNQUOTED_IDENTIFIER */,
76 /* esql_parser.UNQUOTED_ID_PATTERN */,
65 /* esql_parser.UNQUOTED_IDENTIFIER */, 71 /* esql_parser.FROM_UNQUOTED_IDENTIFIER */,
75 /* esql_parser.UNQUOTED_ID_PATTERN */,
]
.map((keyCode) => ctx.tryGetToken(keyCode, 0))
.filter(nonNullable)[0];
@ -231,16 +230,13 @@ export function wrapIdentifierAsArray<T extends ParserRuleContext>(identifierCtx
return Array.isArray(identifierCtx) ? identifierCtx : [identifierCtx];
}
export function createSettingTuple(ctx: SettingContext): ESQLCommandMode {
export function createSetting(policyName: Token, mode: string): ESQLCommandMode {
return {
type: 'mode',
name: ctx._name?.text || '',
text: ctx.text!,
location: getPosition(ctx.start, ctx.stop),
incomplete:
(ctx._name?.text ? isMissingText(ctx._name.text) : true) ||
(ctx._value?.text ? isMissingText(ctx._value.text) : true),
args: [],
name: mode.replace('_', '').toLowerCase(),
text: mode,
location: getPosition(policyName, { stopIndex: policyName.startIndex + mode.length - 1 }), // unfortunately this is the only location we have
incomplete: false,
};
}
@ -248,13 +244,16 @@ export function createSettingTuple(ctx: SettingContext): ESQLCommandMode {
* In https://github.com/elastic/elasticsearch/pull/103949 the ENRICH policy name
* changed from rule to token type so we need to handle this specifically
*/
export function createPolicy(token: Token): ESQLSource {
export function createPolicy(token: Token, policy: string): ESQLSource {
return {
type: 'source',
name: token.text!,
text: token.text!,
name: policy,
text: policy,
sourceType: 'policy',
location: getPosition(token),
location: getPosition({
startIndex: token.stopIndex - policy.length + 1,
stopIndex: token.stopIndex,
}), // take into account ccq modes
incomplete: false,
};
}

View file

@ -8,7 +8,10 @@
import type { Token } from 'antlr4ts';
export function getPosition(token: Token | undefined, lastToken?: Token | undefined) {
export function getPosition(
token: Pick<Token, 'startIndex' | 'stopIndex'> | undefined,
lastToken?: Pick<Token, 'stopIndex'> | undefined
) {
if (!token || token.startIndex < 0) {
return { min: 0, max: 0 };
}

View file

@ -37,7 +37,7 @@ import {
LogicalBinaryContext,
LogicalInContext,
LogicalNotContext,
MetadataContext,
MetadataOptionContext,
MvExpandCommandContext,
NullLiteralContext,
NumericArrayLiteralContext,
@ -74,9 +74,8 @@ import {
createColumnStar,
wrapIdentifierAsArray,
createPolicy,
createSettingTuple,
createLiteralString,
isMissingText,
createSetting,
} from './ast_helpers';
import { getPosition } from './ast_position_utils';
import type {
@ -92,9 +91,9 @@ export function collectAllSourceIdentifiers(ctx: FromCommandContext): ESQLAstIte
}
function extractIdentifiers(
ctx: KeepCommandContext | DropCommandContext | MvExpandCommandContext | MetadataContext
ctx: KeepCommandContext | DropCommandContext | MvExpandCommandContext | MetadataOptionContext
) {
if (ctx instanceof MetadataContext) {
if (ctx instanceof MetadataOptionContext) {
return wrapIdentifierAsArray(ctx.fromIdentifier());
}
if (ctx instanceof MvExpandCommandContext) {
@ -114,32 +113,22 @@ function makeColumnsOutOfIdentifiers(identifiers: ParserRuleContext[]) {
}
export function collectAllColumnIdentifiers(
ctx: KeepCommandContext | DropCommandContext | MvExpandCommandContext | MetadataContext
ctx: KeepCommandContext | DropCommandContext | MvExpandCommandContext | MetadataOptionContext
): ESQLAstItem[] {
const identifiers = extractIdentifiers(ctx);
return makeColumnsOutOfIdentifiers(identifiers);
}
export function getPolicyName(ctx: EnrichCommandContext) {
if (!ctx._policyName || (ctx._policyName.text && /<missing /.test(ctx._policyName.text))) {
if (!ctx._policyName || !ctx._policyName.text || /<missing /.test(ctx._policyName.text)) {
return [];
}
return [createPolicy(ctx._policyName)];
}
export function getPolicySettings(ctx: EnrichCommandContext) {
if (!ctx.setting() || !ctx.setting().length) {
return [];
const policyComponents = ctx._policyName.text.split(':');
if (policyComponents.length > 1) {
const [setting, policyName] = policyComponents;
return [createSetting(ctx._policyName, setting), createPolicy(ctx._policyName, policyName)];
}
return ctx.setting().map((setting) => {
const node = createSettingTuple(setting);
if (setting._name?.text && setting._value?.text) {
node.args.push(createLiteralString(setting._value)!);
return node;
}
// incomplete setting
return node;
});
return [createPolicy(ctx._policyName, policyComponents[0])];
}
export function getMatchField(ctx: EnrichCommandContext) {

View file

@ -18,6 +18,7 @@ import { statsAggregationFunctionDefinitions } from '../definitions/aggs';
import { chronoLiterals, timeLiterals } from '../definitions/literals';
import { commandDefinitions } from '../definitions/commands';
import { TRIGGER_SUGGESTION_COMMAND } from './factories';
import { camelCase } from 'lodash';
const triggerCharacters = [',', '(', '=', ' '];
@ -332,11 +333,13 @@ describe('autocomplete', () => {
);
testSuggestions('from ', suggestedIndexes);
testSuggestions('from a,', suggestedIndexes);
testSuggestions('from a, b ', ['[metadata $0 ]', '|', ',']);
testSuggestions('from a, b ', ['metadata $0', '|', ',']);
testSuggestions('from *,', suggestedIndexes);
testSuggestions('from index', suggestedIndexes, 6 /* index index in from */);
testSuggestions('from a, b [metadata ]', ['_index', '_score'], 20);
testSuggestions('from a, b metadata ', ['_index', '_score'], 19);
testSuggestions('from a, b [metadata _index, ]', ['_score'], 27);
testSuggestions('from a, b metadata _index, ', ['_score'], 26);
});
describe('show', () => {
@ -542,11 +545,11 @@ describe('autocomplete', () => {
describe('rename', () => {
testSuggestions('from a | rename ', getFieldNamesByType('any'));
testSuggestions('from a | rename stringField ', ['as']);
testSuggestions('from a | rename stringField ', ['as $0']);
testSuggestions('from a | rename stringField as ', ['var0']);
});
for (const command of ['keep', 'drop', 'project']) {
for (const command of ['keep', 'drop']) {
describe(command, () => {
testSuggestions(`from a | ${command} `, getFieldNamesByType('any'));
testSuggestions(
@ -560,40 +563,52 @@ describe('autocomplete', () => {
const allAggFunctions = getFunctionSignaturesByReturnType('stats', 'any', {
agg: true,
});
testSuggestions('from a | stats ', ['var0 =', ...allAggFunctions]);
const allEvaFunctions = getFunctionSignaturesByReturnType('stats', 'any', {
evalMath: true,
});
testSuggestions('from a | stats ', ['var0 =', ...allAggFunctions, ...allEvaFunctions]);
testSuggestions('from a | stats a ', ['= $0']);
testSuggestions('from a | stats a=', [...allAggFunctions]);
testSuggestions('from a | stats a=', [...allAggFunctions, ...allEvaFunctions]);
testSuggestions('from a | stats a=max(b) by ', [
...getFieldNamesByType('any'),
...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }),
...allEvaFunctions,
'var0 =',
]);
testSuggestions('from a | stats a=max(b) BY ', [
...getFieldNamesByType('any'),
...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }),
...allEvaFunctions,
'var0 =',
]);
testSuggestions('from a | stats a=c by d ', ['|', ',']);
testSuggestions('from a | stats a=c by d, ', [
...getFieldNamesByType('any'),
...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }),
...allEvaFunctions,
'var0 =',
]);
testSuggestions('from a | stats a=max(b), ', ['var0 =', ...allAggFunctions]);
testSuggestions('from a | stats a=max(b), ', [
'var0 =',
...allAggFunctions,
...allEvaFunctions,
]);
testSuggestions('from a | stats a=min()', getFieldNamesByType('number'), '(');
testSuggestions('from a | stats a=min(b) ', ['by', '|', ',']);
testSuggestions('from a | stats a=min(b) ', ['by $0', '|', ',']);
testSuggestions('from a | stats a=min(b) by ', [
...getFieldNamesByType('any'),
...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }),
...allEvaFunctions,
'var0 =',
]);
testSuggestions('from a | stats a=min(b),', ['var0 =', ...allAggFunctions]);
testSuggestions('from a | stats var0=min(b),var1=c,', ['var2 =', ...allAggFunctions]);
testSuggestions('from a | stats a=min(b),', ['var0 =', ...allAggFunctions, ...allEvaFunctions]);
testSuggestions('from a | stats var0=min(b),var1=c,', [
'var2 =',
...allAggFunctions,
...allEvaFunctions,
]);
testSuggestions('from a | stats a=min(b), b=max()', getFieldNamesByType('number'));
// @TODO: remove last 2 suggestions if possible
testSuggestions('from a | eval var0=round(b), var1=round(c) | stats ', [
'var2 =',
...allAggFunctions,
...allEvaFunctions,
'var0',
'var1',
]);
@ -601,7 +616,7 @@ describe('autocomplete', () => {
// smoke testing with suggestions not at the end of the string
testSuggestions(
'from a | stats a = min(b) | sort b',
['by', '|', ','],
['by $0', '|', ','],
27 /* " " after min(b) */
);
testSuggestions(
@ -634,29 +649,25 @@ describe('autocomplete', () => {
describe('enrich', () => {
const modes = ['any', 'coordinator', 'remote'];
const policyNames = policies.map(({ name, suggestedAs }) => suggestedAs || name);
for (const prevCommand of [
'',
'| enrich other-policy ',
'| enrich other-policy on b ',
'| enrich other-policy with c ',
// '| enrich other-policy ',
// '| enrich other-policy on b ',
// '| enrich other-policy with c ',
]) {
testSuggestions(`from a ${prevCommand}| enrich `, policyNames);
testSuggestions(
`from a ${prevCommand}| enrich `,
policies.map(({ name, suggestedAs }) => suggestedAs || name)
`from a ${prevCommand}| enrich _`,
modes.map((mode) => `_${mode}:$0`),
'_'
);
testSuggestions(
`from a ${prevCommand}| enrich [`,
modes.map((mode) => `ccq.mode:${mode}`),
'['
);
// Not suggesting duplicate setting
testSuggestions(`from a ${prevCommand}| enrich [ccq.mode:any] [`, [], '[');
testSuggestions(`from a ${prevCommand}| enrich [ccq.mode:`, modes, ':');
testSuggestions(
`from a ${prevCommand}| enrich [ccq.mode:any] `,
policies.map(({ name, suggestedAs }) => suggestedAs || name)
);
testSuggestions(`from a ${prevCommand}| enrich policy `, ['on', 'with', '|']);
for (const mode of modes) {
testSuggestions(`from a ${prevCommand}| enrich _${mode}:`, policyNames, ':');
testSuggestions(`from a ${prevCommand}| enrich _${mode.toUpperCase()}:`, policyNames, ':');
testSuggestions(`from a ${prevCommand}| enrich _${camelCase(mode)}:`, policyNames, ':');
}
testSuggestions(`from a ${prevCommand}| enrich policy `, ['on $0', 'with $0', '|']);
testSuggestions(`from a ${prevCommand}| enrich policy on `, [
'stringField',
'numberField',
@ -666,7 +677,7 @@ describe('autocomplete', () => {
'any#Char$Field',
'kubernetes.something.something',
]);
testSuggestions(`from a ${prevCommand}| enrich policy on b `, ['with', '|', ',']);
testSuggestions(`from a ${prevCommand}| enrich policy on b `, ['with $0', '|', ',']);
testSuggestions(`from a ${prevCommand}| enrich policy on b with `, [
'var0 =',
...getPolicyFields('policy'),
@ -1083,5 +1094,12 @@ describe('autocomplete', () => {
suggestions.every(({ command }) => command === TRIGGER_SUGGESTION_COMMAND)
).toBeTruthy();
});
it('should trigger further suggestions after enrich mode', async () => {
const suggestions = await getSuggestionsFor('from a | enrich _any:');
// test that all commands will retrigger suggestions
expect(
suggestions.every(({ command }) => command === TRIGGER_SUGGESTION_COMMAND)
).toBeTruthy();
});
});
});

View file

@ -14,7 +14,6 @@ import {
columnExists,
getColumnHit,
getCommandDefinition,
getCommandMode,
getCommandOption,
getFunctionDefinition,
getLastCharFromTrimmed,
@ -40,7 +39,6 @@ import type {
AstProviderFn,
ESQLAstItem,
ESQLCommand,
ESQLCommandMode,
ESQLCommandOption,
ESQLFunction,
ESQLSingleAstItem,
@ -70,7 +68,6 @@ import {
buildVariablesDefinitions,
buildOptionDefinition,
buildSettingDefinitions,
buildSettingValueDefinitions,
} from './factories';
import { EDITOR_MARKER } from '../shared/constants';
import { getAstContext, removeMarkerArgFromArgsList } from '../shared/context';
@ -166,9 +163,12 @@ export async function suggest(
const unclosedBrackets = unclosedRoundBrackets + unclosedSquaredBrackets;
// if it's a comma by the user or a forced trigger by a function argument suggestion
// add a marker to make the expression still valid
const charThatNeedMarkers = [',', ':'];
if (
context.triggerCharacter === ',' ||
(context.triggerKind === 0 && unclosedRoundBrackets === 0) ||
(context.triggerCharacter && charThatNeedMarkers.includes(context.triggerCharacter)) ||
(context.triggerKind === 0 &&
unclosedRoundBrackets === 0 &&
getLastCharFromTrimmed(innerText) !== '_') ||
(context.triggerCharacter === ' ' &&
(isMathFunction(innerText, offset) || isComma(innerText[offset - 2])))
) {
@ -225,18 +225,14 @@ export async function suggest(
);
}
if (astContext.type === 'setting') {
// need this wrap/unwrap thing to make TS happy
const { setting, ...rest } = astContext;
if (setting && isSettingItem(setting)) {
return getSettingArgsSuggestions(
innerText,
ast,
{ setting, ...rest },
getFieldsByType,
getFieldsMap,
getPolicyMetadata
);
}
return getSettingArgsSuggestions(
innerText,
ast,
astContext,
getFieldsByType,
getFieldsMap,
getPolicyMetadata
);
}
if (astContext.type === 'option') {
// need this wrap/unwrap thing to make TS happy
@ -1213,10 +1209,8 @@ async function getSettingArgsSuggestions(
{
command,
node,
setting,
}: {
command: ESQLCommand;
setting: ESQLCommandMode;
node: ESQLSingleAstItem | undefined;
},
getFieldsByType: GetFieldsByTypeFn,
@ -1224,25 +1218,15 @@ async function getSettingArgsSuggestions(
getPolicyMetadata: GetPolicyMetadataFn
) {
const suggestions = [];
const existingSettingArgs = new Set(
command.args
.filter((item) => isSettingItem(item) && !item.incomplete)
.map((item) => (isSettingItem(item) ? item.name : undefined))
);
const settingDef =
setting.name && setting.incomplete
? getCommandMode(setting.name)
: getCommandDefinition(command.name).modes.find(({ name }) => !existingSettingArgs.has(name));
const settingDefs = getCommandDefinition(command.name).modes;
if (settingDef) {
if (settingDefs.length) {
const lastChar = getLastCharFromTrimmed(innerText);
if (lastChar === '[') {
// COMMAND [<here>
suggestions.push(...buildSettingDefinitions(settingDef));
} else if (lastChar === ':') {
// COMMAND [setting: <here>
suggestions.push(...buildSettingValueDefinitions(settingDef));
const matchingSettingDefs = settingDefs.filter(({ prefix }) => lastChar === prefix);
if (matchingSettingDefs.length) {
// COMMAND _<here>
suggestions.push(...matchingSettingDefs.flatMap(buildSettingDefinitions));
}
}
return suggestions;

View file

@ -217,12 +217,8 @@ export const buildOptionDefinition = (
detail: option.description,
sortText: 'D',
};
if (option.wrapped) {
completeItem.insertText = `${option.wrapped[0]}${option.name} $0 ${option.wrapped[1]}`;
completeItem.insertTextRules = 4; // monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet;
}
if (isAssignType) {
completeItem.insertText = `${option.name} = $0`;
if (isAssignType || option.signature.params.length) {
completeItem.insertText = isAssignType ? `${option.name} = $0` : `${option.name} $0`;
completeItem.insertTextRules = 4; // monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet;
completeItem.command = TRIGGER_SUGGESTION_COMMAND;
}
@ -233,38 +229,15 @@ export const buildSettingDefinitions = (
setting: CommandModeDefinition
): AutocompleteCommandDefinition[] => {
// for now there's just a single setting with one argument
return setting.signature.params.flatMap(({ values, valueDescriptions }) => {
return values!.map((value, i) => {
const completeItem: AutocompleteCommandDefinition = {
label: `${setting.name}:${value}`,
insertText: `${setting.name}:${value}`,
kind: 21,
detail: valueDescriptions
? `${setting.description} - ${valueDescriptions[i]}`
: setting.description,
sortText: 'D',
};
return completeItem;
});
});
};
export const buildSettingValueDefinitions = (
setting: CommandModeDefinition
): AutocompleteCommandDefinition[] => {
// for now there's just a single setting with one argument
return setting.signature.params.flatMap(({ values, valueDescriptions }) => {
return values!.map((value, i) => {
const completeItem: AutocompleteCommandDefinition = {
label: value,
insertText: value,
kind: 21,
detail: valueDescriptions ? valueDescriptions[i] : setting.description,
sortText: 'D',
};
return completeItem;
});
});
return setting.values.map(({ name, description }) => ({
label: `${setting.prefix || ''}${name}`,
insertText: `${setting.prefix || ''}${name}:$0`,
insertTextRules: 4, // monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
kind: 21,
detail: description ? `${setting.description} - ${description}` : setting.description,
sortText: 'D',
command: TRIGGER_SUGGESTION_COMMAND,
}));
};
export const buildNoPoliciesAvailableDefinition = (): AutocompleteCommandDefinition => ({

View file

@ -130,7 +130,7 @@ function testQuickFixesFn(
});
}
type TestArgs = [string, string[], { equalityCheck?: 'include' | 'equal' }];
type TestArgs = [string, string[], { equalityCheck?: 'include' | 'equal' }?];
// Make only and skip work with our custom wrapper
const testQuickFixes = Object.assign(testQuickFixesFn, {
@ -184,9 +184,14 @@ describe('quick fixes logic', () => {
]);
describe('metafields spellchecks', () => {
testQuickFixes(`FROM index [metadata _i_ndex]`, ['_index']);
testQuickFixes(`FROM index [metadata _id, _i_ndex]`, ['_index']);
testQuickFixes(`FROM index [METADATA _id, _i_ndex]`, ['_index']);
for (const isWrapped of [true, false]) {
function setWrapping(text: string) {
return isWrapped ? `[${text}]` : text;
}
testQuickFixes(`FROM index ${setWrapping('metadata _i_ndex')}`, ['_index']);
testQuickFixes(`FROM index ${setWrapping('metadata _id, _i_ndex')}`, ['_index']);
testQuickFixes(`FROM index ${setWrapping('METADATA _id, _i_ndex')}`, ['_index']);
}
});
});
@ -214,6 +219,15 @@ describe('quick fixes logic', () => {
testQuickFixes(`FROM index | ENRICH poli`, ['policy']);
testQuickFixes(`FROM index | ENRICH mypolicy`, ['policy']);
testQuickFixes(`FROM index | ENRICH policy[`, ['policy', 'policy[]']);
describe('modes', () => {
testQuickFixes(`FROM index | ENRICH _ann:policy`, ['_any']);
const modes = ['_any', '_coordinator', '_remote'];
for (const mode of modes) {
testQuickFixes(`FROM index | ENRICH ${mode.replace('_', '@')}:policy`, [mode]);
}
testQuickFixes(`FROM index | ENRICH unknown:policy`, modes);
});
});
describe('fixing function spellchecks', () => {
@ -281,4 +295,39 @@ describe('quick fixes logic', () => {
testQuickFixes('FROM index | DROP any#Char$Field', ['`any#Char$Field`']);
testQuickFixes('FROM index | DROP numberField, any#Char$Field', ['`any#Char$Field`']);
});
describe('callbacks', () => {
it('should not crash if callback functions are not passed', async () => {
const callbackMocks = getCallbackMocks();
const statement = `from a | eval b = a | enrich policy | dissect stringField "%{firstWord}"`;
const { model, range } = createModelAndRange(statement);
const { errors } = await validateAst(statement, getAstAndErrors, callbackMocks);
const monacoErrors = wrapAsMonacoMessage('error', statement, errors);
const context = createMonacoContext(monacoErrors);
try {
await getActions(model, range, context, getAstAndErrors, {
getFieldsFor: undefined,
getSources: undefined,
getPolicies: undefined,
getMetaFields: undefined,
});
} catch {
fail('Should not throw');
}
});
it('should not crash no callbacks are passed', async () => {
const callbackMocks = getCallbackMocks();
const statement = `from a | eval b = a | enrich policy | dissect stringField "%{firstWord}"`;
const { model, range } = createModelAndRange(statement);
const { errors } = await validateAst(statement, getAstAndErrors, callbackMocks);
const monacoErrors = wrapAsMonacoMessage('error', statement, errors);
const context = createMonacoContext(monacoErrors);
try {
await getActions(model, range, context, getAstAndErrors, undefined);
} catch {
fail('Should not throw');
}
});
});
});

View file

@ -13,7 +13,12 @@ import {
getPolicyHelper,
getSourcesHelper,
} from '../shared/resources_helpers';
import { getAllFunctions, isSourceItem, shouldBeQuotedText } from '../shared/helpers';
import {
getAllFunctions,
getCommandDefinition,
isSourceItem,
shouldBeQuotedText,
} from '../shared/helpers';
import { ESQLCallbacks } from '../shared/types';
import { AstProviderFn, ESQLAst, ESQLCommand } from '../types';
import { buildQueryForFieldsFromSource } from '../validation/helpers';
@ -81,25 +86,13 @@ export function getMetaFieldsRetriever(
export const getCompatibleFunctionDefinitions = (
command: string,
option: string | undefined,
returnTypes?: string[],
ignored: string[] = []
option: string | undefined
): string[] => {
const fnSupportedByCommand = getAllFunctions({ type: ['eval', 'agg'] }).filter(
({ name, supportedCommands, supportedOptions }) =>
(option ? supportedOptions?.includes(option) : supportedCommands.includes(command)) &&
!ignored.includes(name)
option ? supportedOptions?.includes(option) : supportedCommands.includes(command)
);
if (!returnTypes) {
return fnSupportedByCommand.map(({ name }) => name);
}
return fnSupportedByCommand
.filter((mathDefinition) =>
mathDefinition.signatures.some(
(signature) => returnTypes[0] === 'any' || returnTypes.includes(signature.returnType)
)
)
.map(({ name }) => name);
return fnSupportedByCommand.map(({ name }) => name);
};
function createAction(
@ -291,6 +284,32 @@ async function getSpellingActionForMetadata(
return wrapIntoSpellingChangeAction(error, uri, possibleMetafields);
}
async function getSpellingActionForEnrichMode(
error: monaco.editor.IMarkerData,
uri: monaco.Uri,
queryString: string,
ast: ESQLAst,
_callbacks: Callbacks
) {
const errorText = queryString.substring(error.startColumn - 1, error.endColumn - 1);
const commandContext =
ast.find((command) => command.location.max > error.endColumn) || ast[ast.length - 1];
if (!commandContext) {
return [];
}
const commandDef = getCommandDefinition(commandContext.name);
const allModes =
commandDef.modes?.flatMap(({ values, prefix }) =>
values.map(({ name }) => `${prefix || ''}${name}`)
) || [];
const possibleEnrichModes = await getSpellingPossibilities(async () => allModes, errorText);
// if no possible solution is found, push all modes
if (!possibleEnrichModes.length) {
possibleEnrichModes.push(...allModes);
}
return wrapIntoSpellingChangeAction(error, uri, possibleEnrichModes);
}
function wrapIntoSpellingChangeAction(
error: monaco.editor.IMarkerData,
uri: monaco.Uri,
@ -414,6 +433,16 @@ export async function getActions(
)
);
break;
case 'unsupportedSettingCommandValue':
const enrichModeSpellChanges = await getSpellingActionForEnrichMode(
error,
model.uri,
innerText,
ast,
callbacks
);
actions.push(...enrichModeSpellChanges);
break;
default:
break;
}

View file

@ -13,13 +13,13 @@ function createMathDefinition(
name: string,
types: Array<string | string[]>,
description: string,
warning?: FunctionDefinition['warning']
validate?: FunctionDefinition['validate']
): FunctionDefinition {
return {
type: 'builtin',
name,
description,
supportedCommands: ['eval', 'where', 'row'],
supportedCommands: ['eval', 'where', 'row', 'stats'],
supportedOptions: ['by'],
signatures: types.map((type) => {
if (Array.isArray(type)) {
@ -39,7 +39,7 @@ function createMathDefinition(
returnType: type,
};
}),
warning,
validate,
};
}
@ -51,7 +51,7 @@ function createComparisonDefinition(
name: string;
description: string;
},
warning?: FunctionDefinition['warning']
validate?: FunctionDefinition['validate']
): FunctionDefinition {
return {
type: 'builtin' as const,
@ -59,6 +59,7 @@ function createComparisonDefinition(
description,
supportedCommands: ['eval', 'where', 'row'],
supportedOptions: ['by'],
validate,
signatures: [
{
params: [

View file

@ -7,9 +7,15 @@
*/
import { i18n } from '@kbn/i18n';
import { isColumnItem, isSettingItem } from '../shared/helpers';
import { ESQLColumn, ESQLCommand, ESQLCommandMode, ESQLMessage } from '../types';
import { ccqMode } from './settings';
import {
getFunctionDefinition,
isAssignment,
isAssignmentComplete,
isColumnItem,
isFunctionItem,
} from '../shared/helpers';
import type { ESQLColumn, ESQLCommand, ESQLAstItem, ESQLMessage } from '../types';
import { enrichModes } from './settings';
import {
appendSeparatorOption,
asOption,
@ -88,6 +94,64 @@ export const commandDefinitions: CommandDefinition[] = [
code: 'statsNoArguments',
});
}
// now that all functions are supported, there's a specific check to perform
// unfortunately the logic here is a bit complex as it needs to dig deeper into the args
// until an agg function is detected
// in the long run this might be integrated into the validation function
const fnArg = command.args.filter(isFunctionItem);
if (fnArg.length) {
function isAggFunction(arg: ESQLAstItem) {
return isFunctionItem(arg) && getFunctionDefinition(arg.name)?.type === 'agg';
}
function isOtherFunction(arg: ESQLAstItem) {
return isFunctionItem(arg) && getFunctionDefinition(arg.name)?.type !== 'agg';
}
function isOtherFunctionWithAggInside(arg: ESQLAstItem) {
return (
isFunctionItem(arg) &&
isOtherFunction(arg) &&
arg.args.filter(isFunctionItem).some(
// this is recursive as builtin fns can be wrapped one withins another
(subArg): boolean =>
isAggFunction(subArg) ||
(isOtherFunction(subArg) ? isOtherFunctionWithAggInside(subArg) : false)
)
);
}
// which is the presence of at least one agg type function at root level
const hasAggFunction = fnArg.some(isAggFunction);
// or as builtin function arg with an agg function as sub arg
const hasAggFunctionWithinBuiltin = fnArg
.filter((arg) => !isAssignment(arg))
.some(isOtherFunctionWithAggInside);
// assignment requires a special handling
const hasAggFunctionWithinAssignment = fnArg
.filter((arg) => isAssignment(arg) && isAssignmentComplete(arg))
// extract the right hand side of the assignments
.flatMap((arg) => arg.args[1])
.filter(isFunctionItem)
// now check that they are either agg functions
// or builtin functions with an agg function as sub arg
.some((arg) => isAggFunction(arg) || isOtherFunctionWithAggInside(arg));
if (!hasAggFunction && !hasAggFunctionWithinBuiltin && !hasAggFunctionWithinAssignment) {
messages.push({
location: command.location,
text: i18n.translate('monaco.esql.validation.noNestedArgumentSupport', {
defaultMessage:
"Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [{name}] of type [{argType}]",
values: {
name: fnArg[0].name,
argType: getFunctionDefinition(fnArg[0].name)?.signatures[0].returnType,
},
}),
type: 'error',
code: 'noNestedArgumentSupport',
});
}
}
return messages;
},
},
@ -149,22 +213,6 @@ export const commandDefinitions: CommandDefinition[] = [
multipleParams: true,
params: [{ name: 'column', type: 'column', wildcards: true }],
},
validate: (command: ESQLCommand) => {
// the command name is automatically converted into KEEP by the ast_walker
// so validate the actual text
const messages: ESQLMessage[] = [];
if (/^project/.test(command.text.toLowerCase())) {
messages.push({
location: command.location,
text: i18n.translate('monaco.esql.validation.projectCommandDeprecated', {
defaultMessage: 'PROJECT command is no longer supported, please use KEEP instead',
}),
type: 'warning',
code: 'projectCommandDeprecated',
});
}
return messages;
},
},
{
name: 'drop',
@ -304,41 +352,10 @@ export const commandDefinitions: CommandDefinition[] = [
'… | enrich my-policy on pivotField with a = enrichFieldA, b = enrichFieldB',
],
options: [onOption, withOption],
modes: [ccqMode],
modes: [enrichModes],
signature: {
multipleParams: false,
params: [{ name: 'policyName', type: 'source', innerType: 'policy' }],
},
validate: (command: ESQLCommand) => {
const messages: ESQLMessage[] = [];
if (command.args.some(isSettingItem)) {
const settings = command.args.filter(isSettingItem);
const settingCounters: Record<string, number> = {};
const settingLookup: Record<string, ESQLCommandMode> = {};
for (const setting of settings) {
if (!settingCounters[setting.name]) {
settingCounters[setting.name] = 0;
settingLookup[setting.name] = setting;
}
settingCounters[setting.name]++;
}
const duplicateSettings = Object.entries(settingCounters).filter(([_, count]) => count > 1);
messages.push(
...duplicateSettings.map(([name]) => ({
location: settingLookup[name].location,
text: i18n.translate('monaco.esql.validation.duplicateSettingWarning', {
defaultMessage:
'Multiple definition of setting [{name}]. Only last one will be applied.',
values: {
name,
},
}),
type: 'warning' as const,
code: 'duplicateSettingWarning',
}))
);
}
return messages;
},
},
];

View file

@ -7,8 +7,32 @@
*/
import { i18n } from '@kbn/i18n';
import { isLiteralItem } from '../shared/helpers';
import { ESQLFunction } from '../types';
import { FunctionDefinition } from './types';
const validateLogFunctions = (fnDef: ESQLFunction) => {
const messages = [];
// do not really care here about the base and field
// just need to check both values are not negative
for (const arg of fnDef.args) {
if (isLiteralItem(arg) && arg.value < 0) {
messages.push({
type: 'warning' as const,
code: 'logOfNegativeValue',
text: i18n.translate('monaco.esql.divide.warning.logOfNegativeValue', {
defaultMessage: 'Log of a negative number results in null: {value}',
values: {
value: arg.value,
},
}),
location: arg.location,
});
}
}
return messages;
};
export const evalFunctionsDefinitions: FunctionDefinition[] = [
{
name: 'round',
@ -68,6 +92,29 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [
examples: [`from index | eval log10_value = log10(field)`],
},
],
validate: validateLogFunctions,
},
{
name: 'log',
description: i18n.translate('monaco.esql.definitions.logDoc', {
defaultMessage:
'A scalar function log(based, value) returns the logarithm of a value for a particular base, as specified in the argument',
}),
signatures: [
{
params: [
{ name: 'baseOrField', type: 'number' },
{ name: 'field', type: 'number', optional: true },
],
returnType: 'number',
examples: [
`from index | eval log2_value = log(2, field)`,
`from index | eval loge_value = log(field)`,
],
},
],
validate: validateLogFunctions,
},
{
name: 'pow',
@ -1050,7 +1097,7 @@ export const evalFunctionsDefinitions: FunctionDefinition[] = [
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
.map((def) => ({
...def,
supportedCommands: ['eval', 'where', 'row'],
supportedCommands: ['stats', 'eval', 'where', 'row'],
supportedOptions: ['by'],
type: 'eval',
}));

View file

@ -33,10 +33,20 @@ export const metadataOption: CommandOptionsDefinition = {
params: [{ name: 'column', type: 'column' }],
},
optional: true,
wrapped: ['[', ']'],
skipCommonValidation: true,
validate: (option, command, references) => {
const messages: ESQLMessage[] = [];
// need to test the parent command here
if (/\[metadata/i.test(command.text)) {
messages.push({
location: option.location,
text: i18n.translate('monaco.esql.validation.metadataBracketsDeprecation', {
defaultMessage: "Square brackets '[]' need to be removed from FROM METADATA declaration",
}),
type: 'warning',
code: 'metadataBracketsDeprecation',
});
}
const fields = option.args.filter(isColumnItem);
const metadataFieldsAvailable = references as unknown as Set<string>;
if (metadataFieldsAvailable.size > 0) {

View file

@ -9,30 +9,30 @@
import { i18n } from '@kbn/i18n';
import { CommandModeDefinition } from './types';
export const ccqMode: CommandModeDefinition = {
export const enrichModes: CommandModeDefinition = {
name: 'ccq.mode',
description: i18n.translate('monaco.esql.definitions.ccqModeDoc', {
defaultMessage: 'Cross-clusters query mode',
}),
signature: {
multipleParams: false,
params: [
{
name: 'mode',
type: 'string',
values: ['any', 'coordinator', 'remote'],
valueDescriptions: [
i18n.translate('monaco.esql.definitions.ccqAnyDoc', {
defaultMessage: 'Enrich takes place on any cluster',
}),
i18n.translate('monaco.esql.definitions.ccqCoordinatorDoc', {
defaultMessage: 'Enrich takes place on the coordinating cluster receiving an ES|QL',
}),
i18n.translate('monaco.esql.definitions.ccqRemoteDoc', {
defaultMessage: 'Enrich takes place on the cluster hosting the target index.',
}),
],
},
],
},
prefix: '_',
values: [
{
name: 'any',
description: i18n.translate('monaco.esql.definitions.ccqAnyDoc', {
defaultMessage: 'Enrich takes place on any cluster',
}),
},
{
name: 'coordinator',
description: i18n.translate('monaco.esql.definitions.ccqCoordinatorDoc', {
defaultMessage: 'Enrich takes place on the coordinating cluster receiving an ES|QL',
}),
},
{
name: 'remote',
description: i18n.translate('monaco.esql.definitions.ccqRemoteDoc', {
defaultMessage: 'Enrich takes place on the cluster hosting the target index.',
}),
},
],
};

View file

@ -29,7 +29,7 @@ export interface FunctionDefinition {
returnType: string;
examples?: string[];
}>;
warning?: (fnDef: ESQLFunction) => ESQLMessage[];
validate?: (fnDef: ESQLFunction) => ESQLMessage[];
}
export interface CommandBaseDefinition {
@ -64,9 +64,11 @@ export interface CommandOptionsDefinition extends CommandBaseDefinition {
) => ESQLMessage[];
}
export interface CommandModeDefinition extends CommandBaseDefinition {
export interface CommandModeDefinition {
name: string;
description: string;
values: Array<{ name: string; description: string }>;
prefix?: string;
}
export interface CommandDefinition extends CommandBaseDefinition {

View file

@ -14,6 +14,7 @@ import { AstListener } from '../ast_factory';
import { getHoverItem } from './hover';
import { getFunctionDefinition } from '../shared/helpers';
import { getFunctionSignatures } from '../definitions/helpers';
import { enrichModes } from '../definitions/settings';
const fields: Array<{ name: string; type: string; suggestedAs?: string }> = [
...['string', 'number', 'date', 'boolean', 'ip'].map((type) => ({
@ -187,6 +188,16 @@ describe('hover', () => {
testSuggestions(`from a | enrich policy`, 'policy', createPolicyContent);
testSuggestions(`from a | enrich policy on b `, 'policy', createPolicyContent);
testSuggestions(`from a | enrich policy on b `, 'non-policy', createPolicyContent);
describe('ccq mode', () => {
for (const mode of enrichModes.values) {
testSuggestions(
`from a | enrich ${enrichModes.prefix || ''}${mode.name}:policy`,
`${enrichModes.prefix || ''}${mode.name}`,
() => [enrichModes.description, `**${mode.name}**: ${mode.description}`]
);
}
});
});
describe('functions', () => {
function createFunctionContent(fn: string) {

View file

@ -10,7 +10,13 @@ import { i18n } from '@kbn/i18n';
import type { monaco } from '../../../../monaco_imports';
import { getFunctionSignatures } from '../definitions/helpers';
import { getAstContext } from '../shared/context';
import { monacoPositionToOffset, getFunctionDefinition, isSourceItem } from '../shared/helpers';
import {
monacoPositionToOffset,
getFunctionDefinition,
isSourceItem,
isSettingItem,
getCommandDefinition,
} from '../shared/helpers';
import { getPolicyHelper } from '../shared/resources_helpers';
import { ESQLCallbacks } from '../shared/types';
import type { AstProviderFn } from '../types';
@ -47,32 +53,47 @@ export async function getHoverItem(
}
if (astContext.type === 'expression') {
if (
astContext.node &&
isSourceItem(astContext.node) &&
astContext.node.sourceType === 'policy'
) {
const policyMetadata = await getPolicyMetadata(astContext.node.name);
if (policyMetadata) {
return {
contents: [
{
value: `${i18n.translate('monaco.esql.hover.policyIndexes', {
defaultMessage: '**Indexes**',
})}: ${policyMetadata.sourceIndices.join(', ')}`,
},
{
value: `${i18n.translate('monaco.esql.hover.policyMatchingField', {
defaultMessage: '**Matching field**',
})}: ${policyMetadata.matchField}`,
},
{
value: `${i18n.translate('monaco.esql.hover.policyEnrichedFields', {
defaultMessage: '**Fields**',
})}: ${policyMetadata.enrichFields.join(', ')}`,
},
],
};
if (astContext.node) {
if (isSourceItem(astContext.node) && astContext.node.sourceType === 'policy') {
const policyMetadata = await getPolicyMetadata(astContext.node.name);
if (policyMetadata) {
return {
contents: [
{
value: `${i18n.translate('monaco.esql.hover.policyIndexes', {
defaultMessage: '**Indexes**',
})}: ${policyMetadata.sourceIndices.join(', ')}`,
},
{
value: `${i18n.translate('monaco.esql.hover.policyMatchingField', {
defaultMessage: '**Matching field**',
})}: ${policyMetadata.matchField}`,
},
{
value: `${i18n.translate('monaco.esql.hover.policyEnrichedFields', {
defaultMessage: '**Fields**',
})}: ${policyMetadata.enrichFields.join(', ')}`,
},
],
};
}
}
if (isSettingItem(astContext.node)) {
const commandDef = getCommandDefinition(astContext.command.name);
const settingDef = commandDef?.modes.find(({ values }) =>
values.some(({ name }) => name === astContext.node!.name)
);
if (settingDef) {
const mode = settingDef.values.find(({ name }) => name === astContext.node!.name)!;
return {
contents: [
{ value: settingDef.description },
{
value: `**${mode.name}**: ${mode.description}`,
},
],
};
}
}
}
}

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { enrichModes } from '../definitions/settings';
import type {
ESQLAstItem,
ESQLSingleAstItem,
@ -152,8 +153,9 @@ export function getAstContext(innerText: string, ast: ESQLAst, offset: number) {
// command ... by <here>
return { type: 'option' as const, command, node, option, setting };
}
if (node.type === 'mode' || option) {
// command [<here>
// for now it's only an enrich thing
if (node.type === 'source' && node.text === enrichModes.prefix) {
// command _<here>
return { type: 'setting' as const, command, node, option, setting };
}
}
@ -184,9 +186,6 @@ export function getAstContext(innerText: string, ast: ESQLAst, offset: number) {
if (option) {
return { type: 'option' as const, command, node, option, setting };
}
if (setting?.incomplete) {
return { type: 'setting' as const, command, node, option, setting };
}
}
// command a ... <here> OR command a = ... <here>

View file

@ -22,10 +22,8 @@ import {
withOption,
appendSeparatorOption,
} from '../definitions/options';
import { ccqMode } from '../definitions/settings';
import {
CommandDefinition,
CommandModeDefinition,
CommandOptionsDefinition,
FunctionDefinition,
SignatureArgType,
@ -219,10 +217,6 @@ export function getCommandOption(optionName: CommandOptionsDefinition['name']) {
);
}
export function getCommandMode(settingName: CommandModeDefinition['name']) {
return [ccqMode].find(({ name }) => name === settingName);
}
function compareLiteralType(argTypes: string, item: ESQLLiteral) {
if (item.literalType !== 'string') {
return argTypes === item.literalType;

View file

@ -46,7 +46,6 @@ export interface ESQLCommandOption extends ESQLAstBaseItem {
export interface ESQLCommandMode extends ESQLAstBaseItem {
type: 'mode';
args: ESQLAstItem[];
}
export interface ESQLFunction extends ESQLAstBaseItem {

View file

@ -59,8 +59,13 @@ function getMessageAndTypeFromId<K extends ErrorTypes>({
return {
message: i18n.translate('monaco.esql.validation.wrongArgumentNumber', {
defaultMessage:
'Error building [{fn}]: expects exactly {numArgs, plural, one {one argument} other {{numArgs} arguments}}, passed {passedArgs} instead.',
values: { fn: out.fn, numArgs: out.numArgs, passedArgs: out.passedArgs },
'Error building [{fn}]: expects {canHaveMoreArgs, plural, =0 {exactly } other {}}{numArgs, plural, one {one argument} other {{numArgs} arguments}}, passed {passedArgs} instead.',
values: {
fn: out.fn,
numArgs: out.numArgs,
passedArgs: out.passedArgs,
canHaveMoreArgs: out.exactly,
},
}),
};
case 'noNestedArgumentSupport':
@ -209,9 +214,8 @@ function getMessageAndTypeFromId<K extends ErrorTypes>({
return {
message: i18n.translate('monaco.esql.validation.unsupportedSettingValue', {
defaultMessage:
'Unrecognized value [{value}], {command} [{setting}] needs to be one of [{expected}]',
'Unrecognized value [{value}] for {command}, mode needs to be one of [{expected}]',
values: {
setting: out.setting,
expected: out.expected,
value: out.value,
command: out.command,

View file

@ -101,7 +101,7 @@ export async function retrieveFieldsFromStringSources(
commands: ESQLCommand[],
callbacks?: ESQLCallbacks
): Promise<Map<string, ESQLRealField>> {
if (!callbacks) {
if (!callbacks || !callbacks?.getMetaFields) {
return new Map();
}
const customQuery = buildQueryForFieldsForStringSources(queryString, commands);

View file

@ -47,7 +47,7 @@ export interface ValidationErrors {
};
wrongArgumentNumber: {
message: string;
type: { fn: string; numArgs: number; passedArgs: number };
type: { fn: string; numArgs: number; passedArgs: number; exactly: number };
};
unknownColumn: {
message: string;
@ -123,7 +123,7 @@ export interface ValidationErrors {
};
unsupportedSettingCommandValue: {
message: string;
type: { command: string; setting: string; value: string; expected: string };
type: { command: string; value: string; expected: string };
};
}

View file

@ -27,21 +27,26 @@ const fieldTypes = ['number', 'date', 'boolean', 'ip', 'string', 'cartesian_poin
function getCallbackMocks() {
return {
getFieldsFor: jest.fn(async ({ query }) =>
/enrich/.test(query)
? [
{ name: 'otherField', type: 'string' },
{ name: 'yetAnotherField', type: 'number' },
]
: /unsupported_index/.test(query)
? [{ name: 'unsupported_field', type: 'unsupported' }]
: [
...fieldTypes.map((type) => ({ name: `${camelCase(type)}Field`, type })),
{ name: 'any#Char$Field', type: 'number' },
{ name: 'kubernetes.something.something', type: 'number' },
{ name: '@timestamp', type: 'date' },
]
),
getFieldsFor: jest.fn(async ({ query }) => {
if (/enrich/.test(query)) {
return [
{ name: 'otherField', type: 'string' },
{ name: 'yetAnotherField', type: 'number' },
];
}
if (/unsupported_index/.test(query)) {
return [{ name: 'unsupported_field', type: 'unsupported' }];
}
if (/dissect|grok/.test(query)) {
return [{ name: 'firstWord', type: 'string' }];
}
return [
...fieldTypes.map((type) => ({ name: `${camelCase(type)}Field`, type })),
{ name: 'any#Char$Field', type: 'number' },
{ name: 'kubernetes.something.something', type: 'number' },
{ name: '@timestamp', type: 'date' },
];
}),
getSources: jest.fn(async () =>
['a', 'index', 'otherIndex', '.secretIndex', 'my-index', 'unsupported_index'].map((name) => ({
name,
@ -250,7 +255,7 @@ describe('validation logic', () => {
"SyntaxError: missing {QUOTED_IDENTIFIER, FROM_UNQUOTED_IDENTIFIER} at '<EOF>'",
]);
testErrorsAndWarnings(`from assignment = 1`, [
'SyntaxError: expected {<EOF>, PIPE, COMMA, OPENING_BRACKET} but found "="',
'SyntaxError: expected {<EOF>, PIPE, COMMA, OPENING_BRACKET, METADATA} but found "="',
'Unknown index [assignment]',
]);
testErrorsAndWarnings(`from index`, []);
@ -262,21 +267,53 @@ describe('validation logic', () => {
testErrorsAndWarnings(`from index, missingIndex`, ['Unknown index [missingIndex]']);
testErrorsAndWarnings(`from fn()`, ['Unknown index [fn()]']);
testErrorsAndWarnings(`from average()`, ['Unknown index [average()]']);
testErrorsAndWarnings(`from index [METADATA _id]`, []);
testErrorsAndWarnings(`from index [metadata _id]`, []);
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 [METADATA _id, _source]`, []);
testErrorsAndWarnings(`from index [METADATA _id, _source2]`, [
'Metadata field [_source2] is not available. Available metadata fields are: [_id, _source]',
]);
testErrorsAndWarnings(`from index [metadata _id, _source] [METADATA _id2]`, [
'SyntaxError: expected {<EOF>, PIPE} but found "["',
]);
testErrorsAndWarnings(`from index metadata _id`, [
'SyntaxError: expected {<EOF>, PIPE, COMMA, OPENING_BRACKET} but found "metadata"',
]);
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: [_id, _source]',
],
addBracketsWarning()
);
testErrorsAndWarnings(
`from index ${setWrapping('metadata _id, _source')} ${setWrapping('METADATA _id2')}`,
[
isWrapped
? 'SyntaxError: expected {COMMA, CLOSING_BRACKET} but found "["'
: 'SyntaxError: expected {<EOF>, PIPE, COMMA} but found "METADATA"',
],
addBracketsWarning()
);
testErrorsAndWarnings(
`from remote-ccs:indexes ${setWrapping('METADATA _id')}`,
[],
addBracketsWarning()
);
testErrorsAndWarnings(
`from *:indexes ${setWrapping('METADATA _id')}`,
[],
addBracketsWarning()
);
}
testErrorsAndWarnings(`from index (metadata _id)`, [
'SyntaxError: expected {<EOF>, PIPE, COMMA, OPENING_BRACKET} but found "(metadata"',
'SyntaxError: expected {<EOF>, PIPE, COMMA, OPENING_BRACKET, METADATA} but found "(metadata"',
]);
testErrorsAndWarnings(`from ind*, other*`, []);
testErrorsAndWarnings(`from index*`, []);
@ -289,8 +326,6 @@ describe('validation logic', () => {
testErrorsAndWarnings(`from remote-*:indexes`, []);
testErrorsAndWarnings(`from remote-ccs:indexes`, []);
testErrorsAndWarnings(`from a, remote-ccs:indexes`, []);
testErrorsAndWarnings(`from remote-ccs:indexes [METADATA _id]`, []);
testErrorsAndWarnings(`from *:indexes [METADATA _id]`, []);
testErrorsAndWarnings('from .secretIndex', []);
testErrorsAndWarnings('from my-index', []);
testErrorsAndWarnings('from numberField', ['Unknown index [numberField]']);
@ -374,7 +409,7 @@ describe('validation logic', () => {
);
testErrorsAndWarnings(`row var = ${signatureStringCorrect}`, []);
testErrorsAndWarnings(`row ${signatureStringCorrect}`);
testErrorsAndWarnings(`row ${signatureStringCorrect}`, []);
if (alias) {
for (const otherName of alias) {
@ -412,7 +447,7 @@ describe('validation logic', () => {
)[0].declaration
);
testErrorsAndWarnings(`row var = ${signatureString}`);
testErrorsAndWarnings(`row var = ${signatureString}`, []);
const wrongFieldMapping = params.map(({ name: _name, type, ...rest }) => {
const typeString = type;
@ -580,26 +615,18 @@ describe('validation logic', () => {
'Unknown column [missingField]',
]);
testErrorsAndWarnings('from index | keep `any#Char$Field`', []);
testErrorsAndWarnings(
'from index | project ',
[`SyntaxError: missing {QUOTED_IDENTIFIER, UNQUOTED_ID_PATTERN} at '<EOF>'`],
['PROJECT command is no longer supported, please use KEEP instead']
);
testErrorsAndWarnings(
'from index | project stringField, numberField, dateField',
[],
['PROJECT command is no longer supported, please use KEEP instead']
);
testErrorsAndWarnings(
'from index | PROJECT stringField, numberField, dateField',
[],
['PROJECT command is no longer supported, please use KEEP instead']
);
testErrorsAndWarnings(
'from index | project missingField, numberField, dateField',
['Unknown column [missingField]'],
['PROJECT command is no longer supported, please use KEEP instead']
);
testErrorsAndWarnings('from index | project ', [
`SyntaxError: expected {DISSECT, DROP, ENRICH, EVAL, GROK, INLINESTATS, KEEP, LIMIT, MV_EXPAND, RENAME, SORT, STATS, WHERE} but found \"project\"`,
]);
testErrorsAndWarnings('from index | project stringField, numberField, dateField', [
`SyntaxError: expected {DISSECT, DROP, ENRICH, EVAL, GROK, INLINESTATS, KEEP, LIMIT, MV_EXPAND, RENAME, SORT, STATS, WHERE} but found \"project\"`,
]);
testErrorsAndWarnings('from index | PROJECT stringField, numberField, dateField', [
`SyntaxError: expected {DISSECT, DROP, ENRICH, EVAL, GROK, INLINESTATS, KEEP, LIMIT, MV_EXPAND, RENAME, SORT, STATS, WHERE} but found \"PROJECT\"`,
]);
testErrorsAndWarnings('from index | project missingField, numberField, dateField', [
`SyntaxError: expected {DISSECT, DROP, ENRICH, EVAL, GROK, INLINESTATS, KEEP, LIMIT, MV_EXPAND, RENAME, SORT, STATS, WHERE} but found \"project\"`,
]);
testErrorsAndWarnings('from index | keep s*', []);
testErrorsAndWarnings('from index | keep *Field', []);
testErrorsAndWarnings('from index | keep s*Field', []);
@ -736,27 +763,28 @@ describe('validation logic', () => {
"SyntaxError: missing STRING at '%'",
]);
// Do not try to validate the dissect pattern string
testErrorsAndWarnings('from a | dissect stringField "%{a}"', []);
testErrorsAndWarnings('from a | dissect numberField "%{a}"', [
testErrorsAndWarnings('from a | dissect stringField "%{firstWord}"', []);
testErrorsAndWarnings('from a | dissect numberField "%{firstWord}"', [
'DISSECT only supports string type values, found [numberField] of type number',
]);
testErrorsAndWarnings('from a | dissect stringField "%{a}" option ', [
testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" option ', [
'SyntaxError: expected {ASSIGN} but found "<EOF>"',
]);
testErrorsAndWarnings('from a | dissect stringField "%{a}" option = ', [
testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" option = ', [
'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET} but found "<EOF>"',
'Invalid option for DISSECT: [option]',
]);
testErrorsAndWarnings('from a | dissect stringField "%{a}" option = 1', [
testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" option = 1', [
'Invalid option for DISSECT: [option]',
]);
testErrorsAndWarnings('from a | dissect stringField "%{a}" append_separator = "-"', []);
testErrorsAndWarnings('from a | dissect stringField "%{a}" ignore_missing = true', [
testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" append_separator = "-"', []);
testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" ignore_missing = true', [
'Invalid option for DISSECT: [ignore_missing]',
]);
testErrorsAndWarnings('from a | dissect stringField "%{a}" append_separator = true', [
testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" append_separator = true', [
'Invalid value for DISSECT append_separator: expected a string, but was [true]',
]);
testErrorsAndWarnings('from a | dissect stringField "%{firstWord}" | keep firstWord', []);
// testErrorsAndWarnings('from a | dissect s* "%{a}"', [
// 'Using wildcards (*) in dissect is not allowed [s*]',
// ]);
@ -776,10 +804,11 @@ describe('validation logic', () => {
]);
testErrorsAndWarnings('from a | grok stringField %a', ["SyntaxError: missing STRING at '%'"]);
// Do not try to validate the grok pattern string
testErrorsAndWarnings('from a | grok stringField "%{a}"', []);
testErrorsAndWarnings('from a | grok numberField "%{a}"', [
testErrorsAndWarnings('from a | grok stringField "%{firstWord}"', []);
testErrorsAndWarnings('from a | grok numberField "%{firstWord}"', [
'GROK only supports string type values, found [numberField] of type number',
]);
testErrorsAndWarnings('from a | grok stringField "%{firstWord}" | keep firstWord', []);
// testErrorsAndWarnings('from a | grok s* "%{a}"', [
// 'Using wildcards (*) in grok is not allowed [s*]',
// ]);
@ -1047,7 +1076,7 @@ describe('validation logic', () => {
}
for (const { name, alias, signatures, ...defRest } of evalFunctionsDefinitions) {
for (const { params, returnType } of signatures) {
for (const { params, returnType, infiniteParams, minParams } of signatures) {
const fieldMapping = getFieldMapping(params);
testErrorsAndWarnings(
`from a | eval var = ${
@ -1128,6 +1157,40 @@ describe('validation logic', () => {
}`,
expectedErrors
);
if (!infiniteParams && !minParams) {
// test that additional args are spotted
const fieldMappingWithOneExtraArg = getFieldMapping(params).concat({
name: 'extraArg',
type: 'number',
});
// get the expected args from the first signature in case of errors
const expectedArgs = signatures[0].params.filter(({ optional }) => !optional).length;
const shouldBeExactly = signatures[0].params.length;
testErrorsAndWarnings(
`from a | eval ${
getFunctionSignatures(
{
name,
...defRest,
signatures: [{ params: fieldMappingWithOneExtraArg, returnType }],
},
{ withTypes: false }
)[0].declaration
}`,
[
`Error building [${name}]: expects ${
shouldBeExactly - expectedArgs === 0 ? 'exactly ' : ''
}${
expectedArgs === 1
? 'one argument'
: expectedArgs === 0
? '0 arguments'
: `${expectedArgs} arguments`
}, passed ${fieldMappingWithOneExtraArg.length} instead.`,
]
);
}
}
// test that wildcard won't work as arg
@ -1151,6 +1214,37 @@ describe('validation logic', () => {
}
}
}
testErrorsAndWarnings(
'from a | eval log10(-1)',
[],
['Log of a negative number results in null: -1']
);
testErrorsAndWarnings(
'from a | eval log(-1)',
[],
['Log of a negative number results in null: -1']
);
testErrorsAndWarnings(
'from a | eval log(-1, 20)',
[],
['Log of a negative number results in null: -1']
);
testErrorsAndWarnings(
'from a | eval log(-1, -20)',
[],
[
'Log of a negative number results in null: -1',
'Log of a negative number results in null: -20',
]
);
testErrorsAndWarnings(
'from a | eval var0 = log(-1, -20)',
[],
[
'Log of a negative number results in null: -1',
'Log of a negative number results in null: -20',
]
);
for (const op of ['>', '>=', '<', '<=', '==']) {
testErrorsAndWarnings(`from a | eval numberField ${op} 0`, []);
testErrorsAndWarnings(`from a | eval NOT numberField ${op} 0`, []);
@ -1304,9 +1398,11 @@ describe('validation logic', () => {
]);
testErrorsAndWarnings('from a | stats numberField=', [
'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, LP, NOT, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER} but found "<EOF>"',
"Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [=] of type [void]",
]);
testErrorsAndWarnings('from a | stats numberField=5 by ', [
'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, LP, NOT, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER} but found "<EOF>"',
"Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [=] of type [void]",
]);
testErrorsAndWarnings('from a | stats avg(numberField) by wrongField', [
'Unknown column [wrongField]',
@ -1347,6 +1443,12 @@ describe('validation logic', () => {
'from a | stats avg(numberField), percentile(numberField, 50) BY ipField',
[]
);
for (const op of ['+', '-', '*', '/', '%']) {
testErrorsAndWarnings(
`from a | stats avg(numberField) ${op} percentile(numberField, 50) BY ipField`,
[]
);
}
testErrorsAndWarnings('from a | stats count(* + 1) BY ipField', [
'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, LP, NOT, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER} but found "+"',
]);
@ -1359,15 +1461,33 @@ describe('validation logic', () => {
testErrorsAndWarnings('from a | 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 | stats numberField + 1', ['STATS does not support function +']);
testErrorsAndWarnings('from a | stats numberField + 1', [
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [+] of type [number]`,
]);
for (const nesting of [1, 2, 3, 4]) {
const moreBuiltinWrapping = Array(nesting).fill('+ 1').join('');
testErrorsAndWarnings(`from a | stats 5 + avg(numberField) ${moreBuiltinWrapping}`, []);
testErrorsAndWarnings(`from a | stats 5 ${moreBuiltinWrapping} + avg(numberField)`, []);
testErrorsAndWarnings(`from a | stats 5 ${moreBuiltinWrapping} + numberField`, [
"Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [+] of type [number]",
]);
testErrorsAndWarnings(`from a | stats 5 + numberField ${moreBuiltinWrapping}`, [
"Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [+] of type [number]",
]);
}
testErrorsAndWarnings('from a | stats 5 + numberField + 1', [
"Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [+] of type [number]",
]);
testErrorsAndWarnings('from a | stats numberField + 1 by ipField', [
'STATS does not support function +',
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [+] of type [number]`,
]);
testErrorsAndWarnings(
'from a | stats avg(numberField), percentile(numberField, 50) + 1 by ipField',
['STATS does not support function +']
[]
);
testErrorsAndWarnings('from a | stats count(*)', []);
@ -1400,6 +1520,52 @@ describe('validation logic', () => {
}`,
[]
);
testErrorsAndWarnings(
`from a | stats var = round(${
getFunctionSignatures(
{ name, ...defRest, signatures: [{ params: fieldMapping, returnType }] },
{ withTypes: false }
)[0].declaration
})`,
[]
);
testErrorsAndWarnings(
`from a | stats round(${
getFunctionSignatures(
{ name, ...defRest, signatures: [{ params: fieldMapping, returnType }] },
{ withTypes: false }
)[0].declaration
})`,
[]
);
testErrorsAndWarnings(
`from a | stats var = round(${
getFunctionSignatures(
{ name, ...defRest, signatures: [{ params: fieldMapping, returnType }] },
{ withTypes: false }
)[0].declaration
}) + ${
getFunctionSignatures(
{ name, ...defRest, signatures: [{ params: fieldMapping, returnType }] },
{ withTypes: false }
)[0].declaration
}`,
[]
);
testErrorsAndWarnings(
`from a | stats round(${
getFunctionSignatures(
{ name, ...defRest, signatures: [{ params: fieldMapping, returnType }] },
{ withTypes: false }
)[0].declaration
}) + ${
getFunctionSignatures(
{ name, ...defRest, signatures: [{ params: fieldMapping, returnType }] },
{ withTypes: false }
)[0].declaration
}`,
[]
);
if (alias) {
for (const otherName of alias) {
@ -1623,55 +1789,50 @@ describe('validation logic', () => {
describe('enrich', () => {
testErrorsAndWarnings(`from a | enrich`, [
'SyntaxError: expected {OPENING_BRACKET, ENRICH_POLICY_NAME} but found "<EOF>"',
"SyntaxError: missing ENRICH_POLICY_NAME at '<EOF>'",
]);
testErrorsAndWarnings(`from a | enrich [`, [
'SyntaxError: expected {SETTING} but found "<EOF>"',
testErrorsAndWarnings(`from a | enrich _`, ['Unknown policy [_]']);
testErrorsAndWarnings(`from a | enrich _:`, [
"SyntaxError: token recognition error at: ':'",
'Unknown policy [_]',
]);
testErrorsAndWarnings(`from a | enrich [ccq.mode`, [
'SyntaxError: expected {COLON} but found "<EOF>"',
testErrorsAndWarnings(`from a | enrich _:policy`, [
'Unrecognized value [_] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]',
]);
testErrorsAndWarnings(`from a | enrich [ccq.mode:`, [
'SyntaxError: expected {SETTING} but found "<EOF>"',
testErrorsAndWarnings(`from a | enrich :policy`, [
"SyntaxError: token recognition error at: ':'",
]);
testErrorsAndWarnings(`from a | enrich [ccq.mode:any`, [
'SyntaxError: expected {CLOSING_BRACKET} but found "<EOF>"',
testErrorsAndWarnings(`from a | enrich any:`, [
"SyntaxError: token recognition error at: ':'",
'Unknown policy [any]',
]);
testErrorsAndWarnings(`from a | enrich [ccq.mode:any] `, [
"SyntaxError: extraneous input '<EOF>' expecting {OPENING_BRACKET, ENRICH_POLICY_NAME}",
testErrorsAndWarnings(`from a | enrich _any:`, [
"SyntaxError: token recognition error at: ':'",
'Unknown policy [_any]',
]);
testErrorsAndWarnings(`from a | enrich any:policy`, [
'Unrecognized value [any] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]',
]);
testErrorsAndWarnings(`from a | enrich policy `, []);
testErrorsAndWarnings(`from a | enrich [ccq.mode:value] policy `, [
'Unrecognized value [value], ENRICH [ccq.mode] needs to be one of [ANY, COORDINATOR, REMOTE]',
]);
for (const value of ['any', 'coordinator', 'remote']) {
testErrorsAndWarnings(`from a | enrich [ccq.mode:${value}] policy `, []);
testErrorsAndWarnings(`from a | enrich [ccq.mode:${value.toUpperCase()}] policy `, []);
testErrorsAndWarnings(`from a | enrich _${value}:policy `, []);
testErrorsAndWarnings(`from a | enrich _${value} : policy `, [
"SyntaxError: token recognition error at: ':'",
"SyntaxError: extraneous input 'policy' expecting <EOF>",
`Unknown policy [_${value}]`,
]);
testErrorsAndWarnings(`from a | enrich _${value}: policy `, [
"SyntaxError: token recognition error at: ':'",
"SyntaxError: extraneous input 'policy' expecting <EOF>",
`Unknown policy [_${value}]`,
]);
testErrorsAndWarnings(`from a | enrich _${camelCase(value)}:policy `, []);
testErrorsAndWarnings(`from a | enrich _${value.toUpperCase()}:policy `, []);
}
testErrorsAndWarnings(`from a | enrich [setting:value policy`, [
'SyntaxError: expected {CLOSING_BRACKET} but found "policy"',
'Unsupported setting [setting], expected [ccq.mode]',
testErrorsAndWarnings(`from a | enrich _unknown:policy`, [
'Unrecognized value [_unknown] for ENRICH, mode needs to be one of [_ANY, _COORDINATOR, _REMOTE]',
]);
testErrorsAndWarnings(`from a | enrich [ccq.mode:any policy`, [
'SyntaxError: expected {CLOSING_BRACKET} but found "policy"',
]);
testErrorsAndWarnings(`from a | enrich [ccq.mode:any policy`, [
'SyntaxError: expected {CLOSING_BRACKET} but found "policy"',
]);
testErrorsAndWarnings(`from a | enrich [setting:value] policy`, [
'Unsupported setting [setting], expected [ccq.mode]',
]);
testErrorsAndWarnings(`from a | enrich [ccq.mode:any] policy[]`, []);
testErrorsAndWarnings(
`from a | enrich [ccq.mode:any][ccq.mode:coordinator] policy[]`,
[],
['Multiple definition of setting [ccq.mode]. Only last one will be applied.']
);
testErrorsAndWarnings(`from a | enrich missing-policy `, ['Unknown policy [missing-policy]']);
testErrorsAndWarnings(`from a | enrich policy on `, [
"SyntaxError: missing {QUOTED_IDENTIFIER, UNQUOTED_ID_PATTERN} at '<EOF>'",
@ -1749,7 +1910,7 @@ describe('validation logic', () => {
expect(callbackMocks.getSources).not.toHaveBeenCalled();
});
it(`should fetch policies if no enrich command is found`, async () => {
it(`should not fetch policies if no enrich command is found`, async () => {
const callbackMocks = getCallbackMocks();
await validateAst(`row a = 1 | eval a`, getAstAndErrors, callbackMocks);
expect(callbackMocks.getPolicies).not.toHaveBeenCalled();
@ -1794,5 +1955,33 @@ describe('validation logic', () => {
query: `from enrichIndex1 | keep otherField, yetAnotherField`,
});
});
it(`should not crash if no callbacks are available`, async () => {
try {
await validateAst(
`from a | eval b = a | enrich policy | dissect stringField "%{firstWord}"`,
getAstAndErrors,
{
getFieldsFor: undefined,
getSources: undefined,
getPolicies: undefined,
getMetaFields: undefined,
}
);
} catch {
fail('Should not throw');
}
});
it(`should not crash if no callbacks are passed`, async () => {
try {
await validateAst(
`from a | eval b = a | enrich policy | dissect stringField "%{firstWord}"`,
getAstAndErrors
);
} catch {
fail('Should not throw');
}
});
});
});

View file

@ -10,6 +10,7 @@ import uniqBy from 'lodash/uniqBy';
import {
CommandModeDefinition,
CommandOptionsDefinition,
FunctionDefinition,
SignatureArgType,
} from '../definitions/types';
import {
@ -134,8 +135,9 @@ function validateNestedFunctionArg(
) {
// The isSupported check ensure the definition exists
const argFn = getFunctionDefinition(actualArg.name)!;
if ('noNestingFunctions' in argDef && argDef.noNestingFunctions) {
const fnDef = getFunctionDefinition(astFunction.name)!;
// no nestying criteria should be enforced only for same type function
if ('noNestingFunctions' in argDef && argDef.noNestingFunctions && fnDef.type === argFn.type) {
messages.push(
getMessageFromId({
messageId: 'noNestedArgumentSupport',
@ -170,51 +172,53 @@ function validateFunctionColumnArg(
parentCommand: string
) {
const messages: ESQLMessage[] = [];
if (isColumnItem(actualArg) && actualArg.name) {
const { hit: columnCheck, nameHit } = columnExists(actualArg, references);
if (!columnCheck) {
messages.push(
getMessageFromId({
messageId: 'unknownColumn',
values: {
name: actualArg.name,
},
locations: actualArg.location,
})
);
} else {
if (actualArg.name === '*') {
// if function does not support wildcards return a specific error
if (!('supportsWildcard' in argDef) || !argDef.supportsWildcard) {
messages.push(
getMessageFromId({
messageId: 'noWildcardSupportAsArg',
values: {
name: astFunction.name,
},
locations: actualArg.location,
})
);
}
// do not validate any further for now, only count() accepts wildcard as args...
if (isColumnItem(actualArg)) {
if (actualArg.name) {
const { hit: columnCheck, nameHit } = columnExists(actualArg, references);
if (!columnCheck) {
messages.push(
getMessageFromId({
messageId: 'unknownColumn',
values: {
name: actualArg.name,
},
locations: actualArg.location,
})
);
} else {
// guaranteed by the check above
const columnHit = getColumnHit(nameHit!, references);
// check the type of the column hit
const typeHit = columnHit!.type;
if (!isEqualType(actualArg, argDef, references, parentCommand)) {
messages.push(
getMessageFromId({
messageId: 'wrongArgumentType',
values: {
name: astFunction.name,
argType: argDef.type,
value: actualArg.name,
givenType: typeHit,
},
locations: actualArg.location,
})
);
if (actualArg.name === '*') {
// if function does not support wildcards return a specific error
if (!('supportsWildcard' in argDef) || !argDef.supportsWildcard) {
messages.push(
getMessageFromId({
messageId: 'noWildcardSupportAsArg',
values: {
name: astFunction.name,
},
locations: actualArg.location,
})
);
}
// do not validate any further for now, only count() accepts wildcard as args...
} else {
// guaranteed by the check above
const columnHit = getColumnHit(nameHit!, references);
// check the type of the column hit
const typeHit = columnHit!.type;
if (!isEqualType(actualArg, argDef, references, parentCommand)) {
messages.push(
getMessageFromId({
messageId: 'wrongArgumentType',
values: {
name: astFunction.name,
argType: argDef.type,
value: actualArg.name,
givenType: typeHit,
},
locations: actualArg.location,
})
);
}
}
}
}
@ -222,6 +226,24 @@ function validateFunctionColumnArg(
return messages;
}
function extractCompatibleSignaturesForFunction(
fnDef: FunctionDefinition,
astFunction: ESQLFunction
) {
return fnDef.signatures.filter((def) => {
if (def.infiniteParams && astFunction.args.length > 0) {
return true;
}
if (def.minParams && astFunction.args.length >= def.minParams) {
return true;
}
if (astFunction.args.length === def.params.length) {
return true;
}
return astFunction.args.length === def.params.filter(({ optional }) => !optional).length;
});
}
function validateFunction(
astFunction: ESQLFunction,
parentCommand: string,
@ -235,16 +257,8 @@ function validateFunction(
return messages;
}
const fnDefinition = getFunctionDefinition(astFunction.name)!;
const supportNestedFunctions =
fnDefinition?.signatures.some(({ params }) =>
params.some(({ noNestingFunctions }) => !noNestingFunctions)
) || true;
const isFnSupported = isSupportedFunction(
astFunction.name,
isNested && !supportNestedFunctions ? 'eval' : parentCommand,
parentOption
);
const isFnSupported = isSupportedFunction(astFunction.name, parentCommand, parentOption);
if (!isFnSupported.supported) {
if (isFnSupported.reason === 'unknownFunction') {
@ -282,18 +296,7 @@ function validateFunction(
return messages;
}
}
const matchingSignatures = fnDefinition.signatures.filter((def) => {
if (def.infiniteParams && astFunction.args.length > 0) {
return true;
}
if (def.minParams && astFunction.args.length >= def.minParams) {
return true;
}
if (astFunction.args.length === def.params.length) {
return true;
}
return astFunction.args.length >= def.params.filter(({ optional }) => !optional).length;
});
const matchingSignatures = extractCompatibleSignaturesForFunction(fnDefinition, astFunction);
if (!matchingSignatures.length) {
const numArgs = fnDefinition.signatures[0].params.filter(({ optional }) => !optional).length;
messages.push(
@ -303,6 +306,7 @@ function validateFunction(
fn: astFunction.name,
numArgs,
passedArgs: astFunction.args.length,
exactly: fnDefinition.signatures[0].params.length - numArgs,
},
locations: astFunction.location,
})
@ -325,9 +329,9 @@ function validateFunction(
}
}
}
// check if the definition has some warning to show:
if (fnDefinition.warning) {
const payloads = fnDefinition.warning(astFunction);
// check if the definition has some specific validation to apply:
if (fnDefinition.validate) {
const payloads = fnDefinition.validate(astFunction);
if (payloads.length) {
messages.push(...payloads);
}
@ -399,6 +403,7 @@ function validateFunction(
failingSignatures.push(failingSignature);
}
}
if (failingSignatures.length && failingSignatures.length === matchingSignatures.length) {
const failingSignatureOrderedByErrorCount = failingSignatures
.map((arr, index) => ({ index, count: arr.length }))
@ -435,27 +440,27 @@ function validateSetting(
);
return messages;
}
setting.args.forEach((arg, index) => {
if (!Array.isArray(arg)) {
const argDef = settingDef.signature.params[index];
const value = 'value' in arg ? arg.value : arg.name;
if (argDef.values && !argDef.values?.includes(String(value).toLowerCase())) {
messages.push(
getMessageFromId({
messageId: 'unsupportedSettingCommandValue',
values: {
setting: setting.name,
command: command.name.toUpperCase(),
value: String(value),
// for some reason all this enums are uppercase in ES
expected: (argDef.values?.join(', ') || argDef.type).toUpperCase(),
},
locations: arg.location,
})
);
}
}
});
if (
settingDef.values.every(({ name }) => name !== setting.name) ||
// enforce the check on the prefix if present
(settingDef.prefix && !setting.text.startsWith(settingDef.prefix))
) {
messages.push(
getMessageFromId({
messageId: 'unsupportedSettingCommandValue',
values: {
command: command.name.toUpperCase(),
value: setting.text,
// for some reason all this enums are uppercase in ES
expected: settingDef.values
.map(({ name }) => `${settingDef.prefix || ''}${name}`)
.join(', ')
.toUpperCase(),
},
locations: setting.location,
})
);
}
return messages;
}
@ -657,14 +662,7 @@ function validateCommand(command: ESQLCommand, references: ReferenceMaps): ESQLM
}
if (isSettingItem(arg)) {
messages.push(
...validateSetting(
arg,
commandDef.modes?.find(({ name }) => name === arg.name),
command,
references
)
);
messages.push(...validateSetting(arg, commandDef.modes[0], command, references));
}
if (isOptionItem(arg)) {
@ -790,7 +788,7 @@ export async function validateAst(
retrieveMetadataFields(callbacks),
]);
if (availablePolicies.size && ast.filter(({ name }) => name === 'enrich')) {
if (availablePolicies.size) {
const fieldsFromPoliciesMap = await retrievePoliciesFields(ast, availablePolicies, callbacks);
fieldsFromPoliciesMap.forEach((value, key) => availableFields.set(key, value));
}