[ES|QL] Added more tests for various scenarios (#176160)

## Summary

More fixes for the AST walker, validation, autocomplete.

A list of areas:
* [x] Add fixes and tests for the `IS NULL` and `IS NOT NULL` which was
broken
* [x] Add fixes and tests for the `+/- value` scenario
* [x] Selecting a command from suggestions will now propose another
suggestion popup with the right list of things ™️
* [x] Add test suite to check who and when we trigger automatically a
new suggestion flow
* [x] Fix validation of no aggregation arguments in `STATS` ( `... |
STATS BY field` )


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

---------

Co-authored-by: Drew Tate <drewctate@gmail.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Liberati 2024-02-05 12:04:05 +01:00 committed by GitHub
parent ca38ad5a31
commit b091d8e956
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 293 additions and 39 deletions

View file

@ -141,8 +141,14 @@ export class AstListener implements ESQLParserListener {
exitStatsCommand(ctx: StatsCommandContext) {
const command = createCommand('stats', ctx);
this.ast.push(command);
const [statsExpr, byExpr] = ctx.fields();
command.args.push(...collectAllFieldsStatements(statsExpr), ...visitByOption(ctx, byExpr));
const fields = ctx.fields();
// STATS expression is optional
if (ctx._stats) {
command.args.push(...collectAllFieldsStatements(fields[0]));
}
if (ctx._grouping) {
command.args.push(...visitByOption(ctx, ctx._stats ? fields[1] : fields[0]));
}
}
/**

View file

@ -277,7 +277,7 @@ function visitOperatorExpression(
if (ctx instanceof ArithmeticUnaryContext) {
const arg = visitOperatorExpression(ctx.operatorExpression());
// this is a number sign thing
const fn = createFunction('multiply', ctx);
const fn = createFunction('*', ctx);
fn.args.push(createFakeMultiplyLiteral(ctx));
if (arg) {
fn.args.push(arg);
@ -443,7 +443,7 @@ function collectIsNullExpression(ctx: BooleanExpressionContext) {
return [];
}
const negate = ctx.NOT();
const fnName = `${negate ? 'not_' : ''}is_null`;
const fnName = `is${negate ? ' not ' : ' '}null`;
const fn = createFunction(fnName, ctx);
const arg = visitValueExpression(ctx.valueExpression());
if (arg) {

View file

@ -17,6 +17,7 @@ import { builtinFunctions } from '../definitions/builtin';
import { statsAggregationFunctionDefinitions } from '../definitions/aggs';
import { chronoLiterals, timeLiterals } from '../definitions/literals';
import { commandDefinitions } from '../definitions/commands';
import { TRIGGER_SUGGESTION_COMMAND } from './factories';
const triggerCharacters = [',', '(', '=', ' '];
@ -130,9 +131,12 @@ function getFunctionSignaturesByReturnType(
}
return true;
})
.map(({ type, name, signatures, ...defRest }) =>
type === 'builtin' ? `${name} $0` : `${name}($0)`
);
.map(({ type, name, signatures }) => {
if (type === 'builtin') {
return signatures.some(({ params }) => params.length > 1) ? `${name} $0` : name;
}
return `${name}($0)`;
});
}
function getFieldNamesByType(requestedType: string) {
@ -287,30 +291,33 @@ describe('autocomplete', () => {
const sourceCommands = ['row', 'from', 'show'];
describe('New command', () => {
testSuggestions(' ', sourceCommands);
testSuggestions(
' ',
sourceCommands.map((name) => name + ' $0')
);
testSuggestions(
'from a | ',
commandDefinitions
.filter(({ name }) => !sourceCommands.includes(name))
.map(({ name }) => name)
.map(({ name }) => name + ' $0')
);
testSuggestions(
'from a [metadata _id] | ',
commandDefinitions
.filter(({ name }) => !sourceCommands.includes(name))
.map(({ name }) => name)
.map(({ name }) => name + ' $0')
);
testSuggestions(
'from a | eval var0 = a | ',
commandDefinitions
.filter(({ name }) => !sourceCommands.includes(name))
.map(({ name }) => name)
.map(({ name }) => name + ' $0')
);
testSuggestions(
'from a [metadata _id] | eval var0 = a | ',
commandDefinitions
.filter(({ name }) => !sourceCommands.includes(name))
.map(({ name }) => name)
.map(({ name }) => name + ' $0')
);
});
@ -319,7 +326,10 @@ describe('autocomplete', () => {
.filter(({ hidden }) => !hidden)
.map(({ name, suggestedAs }) => suggestedAs || name);
// Monaco will filter further down here
testSuggestions('f', sourceCommands);
testSuggestions(
'f',
sourceCommands.map((name) => name + ' $0')
);
testSuggestions('from ', suggestedIndexes);
testSuggestions('from a,', suggestedIndexes);
testSuggestions('from a, b ', ['[metadata $0 ]', '|', ',']);
@ -1038,4 +1048,40 @@ describe('autocomplete', () => {
expect(callbackMocks.getFieldsFor).toHaveBeenCalledWith({ query: 'from a' });
});
});
describe('auto triggers', () => {
function getSuggestionsFor(statement: string) {
const callbackMocks = createCustomCallbackMocks(undefined, undefined, undefined);
const triggerOffset = statement.lastIndexOf(' ') + 1; // drop <here>
const context = createSuggestContext(statement, statement[triggerOffset]);
const { model, position } = createModelAndPosition(statement, triggerOffset + 2);
return suggest(
model,
position,
context,
async (text) => (text ? await getAstAndErrors(text) : { ast: [], errors: [] }),
callbackMocks
);
}
it('should trigger further suggestions for functions', async () => {
const suggestions = await getSuggestionsFor('from a | eval ');
// test that all functions will retrigger suggestions
expect(
suggestions
.filter(({ kind }) => kind === 1)
.every(({ command }) => command === TRIGGER_SUGGESTION_COMMAND)
).toBeTruthy();
// now test that non-function won't retrigger
expect(
suggestions.filter(({ kind }) => kind !== 1).every(({ command }) => command == null)
).toBeTruthy();
});
it('should trigger further suggestions for commands', async () => {
const suggestions = await getSuggestionsFor('from a | ');
// test that all commands will retrigger suggestions
expect(
suggestions.every(({ command }) => command === TRIGGER_SUGGESTION_COMMAND)
).toBeTruthy();
});
});
});

View file

@ -170,7 +170,6 @@ export async function suggest(
context.triggerCharacter === ',' ||
(context.triggerKind === 0 && unclosedRoundBrackets === 0) ||
(context.triggerCharacter === ' ' &&
// make this more robust
(isMathFunction(innerText, offset) || isComma(innerText[offset - 2])))
) {
finalText = `${innerText.substring(0, offset)}${EDITOR_MARKER}${innerText.substring(offset)}`;
@ -324,6 +323,14 @@ function findNewVariable(variables: Map<string, ESQLVariable[]>) {
return name;
}
function workoutBuiltinOptions(
nodeArg: ESQLAstItem,
references: Pick<ReferenceMaps, 'fields' | 'variables'>
): { skipAssign: boolean } {
// skip assign operator if it's a function or an existing field to avoid promoting shadowing
return { skipAssign: Boolean(!isColumnItem(nodeArg) || getColumnHit(nodeArg.name, references)) };
}
function areCurrentArgsValid(
command: ESQLCommand,
node: ESQLAstItem,
@ -617,7 +624,13 @@ async function getExpressionSuggestionsByType(
const nodeArgType = extractFinalTypeFromArg(nodeArg, references);
if (nodeArgType) {
suggestions.push(
...getBuiltinCompatibleFunctionDefinition(command.name, undefined, nodeArgType)
...getBuiltinCompatibleFunctionDefinition(
command.name,
undefined,
nodeArgType,
undefined,
workoutBuiltinOptions(nodeArg, references)
)
);
} else {
suggestions.push(getAssignmentDefinitionCompletitionItem());
@ -663,7 +676,13 @@ async function getExpressionSuggestionsByType(
const [rightArg] = nodeArg.args[1] as [ESQLSingleAstItem];
const nodeArgType = extractFinalTypeFromArg(rightArg, references);
suggestions.push(
...getBuiltinCompatibleFunctionDefinition(command.name, undefined, nodeArgType || 'any')
...getBuiltinCompatibleFunctionDefinition(
command.name,
undefined,
nodeArgType || 'any',
undefined,
workoutBuiltinOptions(rightArg, references)
)
);
if (nodeArgType === 'number' && isLiteralItem(rightArg)) {
// ... EVAL var = 1 <suggest>
@ -798,7 +817,13 @@ async function getExpressionSuggestionsByType(
} else {
// i.e. ... | <COMMAND> field <suggest>
suggestions.push(
...getBuiltinCompatibleFunctionDefinition(command.name, undefined, nodeArgType)
...getBuiltinCompatibleFunctionDefinition(
command.name,
undefined,
nodeArgType,
undefined,
workoutBuiltinOptions(nodeArg, references)
)
);
}
}
@ -874,7 +899,13 @@ async function getBuiltinFunctionNextArgument(
// i.e. ... | <COMMAND> field > 0 <suggest>
// i.e. ... | <COMMAND> field + otherN <suggest>
suggestions.push(
...getBuiltinCompatibleFunctionDefinition(command.name, option?.name, nodeArgType || 'any')
...getBuiltinCompatibleFunctionDefinition(
command.name,
option?.name,
nodeArgType || 'any',
undefined,
workoutBuiltinOptions(nodeArg, references)
)
);
} else {
// i.e. ... | <COMMAND> field >= <suggest>
@ -922,9 +953,13 @@ async function getBuiltinFunctionNextArgument(
// suggest something to complete the builtin function
if (nestedType !== argDef.type) {
suggestions.push(
...getBuiltinCompatibleFunctionDefinition(command.name, undefined, nestedType, [
argDef.type,
])
...getBuiltinCompatibleFunctionDefinition(
command.name,
undefined,
nestedType,
[argDef.type],
workoutBuiltinOptions(nodeArg, references)
)
);
}
}

View file

@ -65,15 +65,18 @@ export const getBuiltinCompatibleFunctionDefinition = (
command: string,
option: string | undefined,
argType: string,
returnTypes?: string[]
returnTypes?: string[],
{ skipAssign }: { skipAssign?: boolean } = {}
): AutocompleteCommandDefinition[] => {
const compatibleFunctions = builtinFunctions.filter(
({ name, supportedCommands, supportedOptions, signatures, ignoreAsSuggestion }) =>
!ignoreAsSuggestion &&
!/not_/.test(name) &&
(!skipAssign || name !== '=') &&
(option ? supportedOptions?.includes(option) : supportedCommands.includes(command)) &&
signatures.some(
({ params }) => !params.length || params.some((pArg) => pArg.type === argType)
({ params }) =>
!params.length || params.some((pArg) => pArg.type === argType || pArg.type === 'any')
)
);
if (!returnTypes) {
@ -100,7 +103,7 @@ function buildCharCompleteItem(
return {
label,
insertText: quoted ? `"${label}"` : label,
kind: 1,
kind: 11,
detail,
sortText,
};
@ -140,7 +143,7 @@ export const listCompleteItem: AutocompleteCommandDefinition = {
label: '( ... )',
insertText: '( $0 )',
insertTextRules: 4,
kind: 1,
kind: 11,
detail: i18n.translate('monaco.esql.autocomplete.listDoc', {
defaultMessage: 'List of items ( ...)',
}),

View file

@ -49,8 +49,10 @@ export function getAutocompleteFunctionDefinition(fn: FunctionDefinition) {
};
}
export function getAutocompleteBuiltinDefinition(fn: FunctionDefinition) {
const hasArgs = fn.signatures.some(({ params }) => params.length);
export function getAutocompleteBuiltinDefinition(
fn: FunctionDefinition
): AutocompleteCommandDefinition {
const hasArgs = fn.signatures.some(({ params }) => params.length > 1);
return {
label: fn.name,
insertText: hasArgs ? `${fn.name} $0` : fn.name,
@ -61,7 +63,7 @@ export function getAutocompleteBuiltinDefinition(fn: FunctionDefinition) {
value: '',
},
sortText: 'D',
command: TRIGGER_SUGGESTION_COMMAND,
command: hasArgs ? TRIGGER_SUGGESTION_COMMAND : undefined,
};
}
@ -95,13 +97,17 @@ export function getAutocompleteCommandDefinition(
const commandSignature = getCommandSignature(commandDefinition);
return {
label: commandDefinition.name,
insertText: commandDefinition.name,
insertText: commandDefinition.signature.params.length
? `${commandDefinition.name} $0`
: commandDefinition.name,
insertTextRules: 4, // monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
kind: 0,
detail: commandDefinition.description,
documentation: {
value: buildDocumentation(commandSignature.declaration, commandSignature.examples),
},
sortText: 'A',
command: TRIGGER_SUGGESTION_COMMAND,
};
}

View file

@ -334,6 +334,31 @@ export const builtinFunctions: FunctionDefinition[] = [
},
],
},
...[
{
name: 'is null',
description: i18n.translate('monaco.esql.definition.isNullDoc', {
defaultMessage: 'Predicate for NULL comparison: returns true if the value is NULL',
}),
},
{
name: 'is not null',
description: i18n.translate('monaco.esql.definition.isNotNullDoc', {
defaultMessage: 'Predicate for NULL comparison: returns true if the value is not NULL',
}),
},
].map<FunctionDefinition>(({ name, description }) => ({
type: 'builtin',
name,
description,
supportedCommands: ['eval', 'where', 'row'],
signatures: [
{
params: [{ name: 'left', type: 'any' }],
returnType: 'boolean',
},
],
})),
{
type: 'builtin' as const,
name: '=',

View file

@ -72,10 +72,24 @@ export const commandDefinitions: CommandDefinition[] = [
examples: ['… | stats avg = avg(a)', '… | stats sum(b) by b', '… | stats sum(b) by b % 2'],
signature: {
multipleParams: true,
params: [{ name: 'expression', type: 'function' }],
params: [{ name: 'expression', type: 'function', optional: true }],
},
options: [byOption],
modes: [],
validate: (command: ESQLCommand) => {
const messages: ESQLMessage[] = [];
if (!command.args.length) {
messages.push({
location: command.location,
text: i18n.translate('monaco.esql.validation.statsNoArguments', {
defaultMessage: 'At least one aggregation or grouping expression required in [STATS]',
}),
type: 'error',
code: 'statsNoArguments',
});
}
return messages;
},
},
{
name: 'eval',

View file

@ -304,10 +304,7 @@ export function getAllArrayValues(arg: ESQLAstItem) {
if (subArg.type === 'literal') {
values.push(String(subArg.value));
}
if (subArg.type === 'column') {
values.push(subArg.name);
}
if (subArg.type === 'timeInterval') {
if (isColumnItem(subArg) || isTimeIntervalItem(subArg)) {
values.push(subArg.name);
}
if (subArg.type === 'function') {

View file

@ -37,7 +37,7 @@ function getCallbackMocks() {
? [{ name: 'unsupported_field', type: 'unsupported' }]
: [
...fieldTypes.map((type) => ({ name: `${camelCase(type)}Field`, type })),
{ name: 'any#Char$ field', type: 'number' },
{ name: 'any#Char$Field', type: 'number' },
{ name: 'kubernetes.something.something', type: 'number' },
{ name: '@timestamp', type: 'date' },
]
@ -311,6 +311,9 @@ describe('validation logic', () => {
testErrorsAndWarnings('row a=1, missing_column', ['Unknown column [missing_column]']);
testErrorsAndWarnings('row a=1, b = average()', ['Unknown function [average]']);
testErrorsAndWarnings('row a = [1, 2, 3]', []);
testErrorsAndWarnings('row a = [true, false]', []);
testErrorsAndWarnings('row a = ["a", "b"]', []);
testErrorsAndWarnings('row a = null', []);
testErrorsAndWarnings('row a = (1)', []);
testErrorsAndWarnings('row a = (1, 2, 3)', [
'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, LP, NOT, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER} but found ","',
@ -321,6 +324,12 @@ describe('validation logic', () => {
testErrorsAndWarnings(`row NOT ${bool}`, []);
}
testErrorsAndWarnings('row var = 1 in ', ['SyntaxError: expected {LP} but found "<EOF>"']);
testErrorsAndWarnings('row var = 1 in (', [
'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, LP, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER} but found "<EOF>"',
'Error building [in]: expects exactly 2 arguments, passed 1 instead.',
]);
testErrorsAndWarnings('row var = 1 not in ', ['SyntaxError: expected {LP} but found "<EOF>"']);
testErrorsAndWarnings('row var = 1 in (1, 2, 3)', []);
testErrorsAndWarnings('row var = 5 in (1, 2, 3)', []);
testErrorsAndWarnings('row var = 5 not in (1, 2, 3)', []);
@ -570,7 +579,7 @@ describe('validation logic', () => {
testErrorsAndWarnings('from index | keep missingField, numberField, dateField', [
'Unknown column [missingField]',
]);
testErrorsAndWarnings('from index | keep `any#Char$ field`', []);
testErrorsAndWarnings('from index | keep `any#Char$Field`', []);
testErrorsAndWarnings(
'from index | project ',
[`SyntaxError: missing {QUOTED_IDENTIFIER, UNQUOTED_ID_PATTERN} at '<EOF>'`],
@ -623,7 +632,7 @@ describe('validation logic', () => {
testErrorsAndWarnings('from index | drop missingField, numberField, dateField', [
'Unknown column [missingField]',
]);
testErrorsAndWarnings('from index | drop `any#Char$ field`', []);
testErrorsAndWarnings('from index | drop `any#Char$Field`', []);
testErrorsAndWarnings('from index | drop s*', []);
testErrorsAndWarnings('from index | drop *Field', []);
testErrorsAndWarnings('from index | drop s*Field', []);
@ -663,6 +672,8 @@ describe('validation logic', () => {
testErrorsAndWarnings('row a = "a" | mv_expand a', []);
testErrorsAndWarnings('row a = [1, 2, 3] | mv_expand a', []);
testErrorsAndWarnings('row a = [true, false] | mv_expand a', []);
testErrorsAndWarnings('row a = ["a", "b"] | mv_expand a', []);
});
describe('rename', () => {
@ -780,7 +791,7 @@ describe('validation logic', () => {
testErrorsAndWarnings(`from a | where ${cond}`, []);
testErrorsAndWarnings(`from a | where NOT ${cond}`, []);
}
for (const nValue of ['1', '+1', '1 * 1', '-1', '1 / 1']) {
for (const nValue of ['1', '+1', '1 * 1', '-1', '1 / 1', '1.0', '1.5']) {
testErrorsAndWarnings(`from a | where ${nValue} > 0`, []);
testErrorsAndWarnings(`from a | where NOT ${nValue} > 0`, []);
}
@ -794,6 +805,34 @@ describe('validation logic', () => {
`Argument of [${op}] must be [number], found value [stringField] type [string]`,
]);
}
for (const nesting of [1, 2, 3, 4]) {
for (const evenOp of ['-', '+']) {
for (const oddOp of ['-', '+']) {
// This builds a combination of +/- operators
// i.e. ---- something, -+-+ something, +-+- something, etc...
const unaryCombination = Array(nesting)
.fill('- ')
.map((_, i) => (i % 2 ? oddOp : evenOp))
.join('');
testErrorsAndWarnings(`from a | where ${unaryCombination} numberField`, []);
testErrorsAndWarnings(`from a | where ${unaryCombination} round(numberField)`, []);
testErrorsAndWarnings(`from a | where 1 + ${unaryCombination} numberField`, []);
// still valid
testErrorsAndWarnings(`from a | where 1 ${unaryCombination} numberField`, []);
}
}
testErrorsAndWarnings(
`from a | where ${Array(nesting).fill('not ').join('')} booleanField`,
[]
);
}
for (const wrongOp of ['*', '/', '%']) {
testErrorsAndWarnings(`from a | where ${wrongOp}+ numberField`, [
`SyntaxError: extraneous input '${wrongOp}' expecting {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, '(', NOT, NULL, '?', TRUE, '+', '-', OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}`,
]);
}
testErrorsAndWarnings(`from a | where numberField =~ 0`, [
'Argument of [=~] must be [string], found value [numberField] type [number]',
'Argument of [=~] must be [string], found value [0] type [number]',
@ -845,6 +884,17 @@ describe('validation logic', () => {
[]
);
for (const field of fieldTypes) {
testErrorsAndWarnings(`from a | where ${camelCase(field)}Field IS NULL`, []);
testErrorsAndWarnings(`from a | where ${camelCase(field)}Field IS null`, []);
testErrorsAndWarnings(`from a | where ${camelCase(field)}Field is null`, []);
testErrorsAndWarnings(`from a | where ${camelCase(field)}Field is NULL`, []);
testErrorsAndWarnings(`from a | where ${camelCase(field)}Field IS NOT NULL`, []);
testErrorsAndWarnings(`from a | where ${camelCase(field)}Field IS NOT null`, []);
testErrorsAndWarnings(`from a | where ${camelCase(field)}Field IS not NULL`, []);
testErrorsAndWarnings(`from a | where ${camelCase(field)}Field Is nOt NuLL`, []);
}
// Test that all functions work in where
const numericOrStringFunctions = evalFunctionsDefinitions.filter(({ name, signatures }) => {
return signatures.some(
@ -951,6 +1001,51 @@ describe('validation logic', () => {
[]
);
testErrorsAndWarnings('from a | eval a=[1, 2, 3]', []);
testErrorsAndWarnings('from a | eval a=[true, false]', []);
testErrorsAndWarnings('from a | eval a=["a", "b"]', []);
testErrorsAndWarnings('from a | eval a=null', []);
for (const field of fieldTypes) {
testErrorsAndWarnings(`from a | eval ${camelCase(field)}Field IS NULL`, []);
testErrorsAndWarnings(`from a | eval ${camelCase(field)}Field IS null`, []);
testErrorsAndWarnings(`from a | eval ${camelCase(field)}Field is null`, []);
testErrorsAndWarnings(`from a | eval ${camelCase(field)}Field is NULL`, []);
testErrorsAndWarnings(`from a | eval ${camelCase(field)}Field IS NOT NULL`, []);
testErrorsAndWarnings(`from a | eval ${camelCase(field)}Field IS NOT null`, []);
testErrorsAndWarnings(`from a | eval ${camelCase(field)}Field IS not NULL`, []);
}
for (const nesting of [1, 2, 3, 4]) {
for (const evenOp of ['-', '+']) {
for (const oddOp of ['-', '+']) {
// This builds a combination of +/- operators
// i.e. ---- something, -+-+ something, +-+- something, etc...
const unaryCombination = Array(nesting)
.fill('- ')
.map((_, i) => (i % 2 ? oddOp : evenOp))
.join('');
testErrorsAndWarnings(`from a | eval ${unaryCombination} numberField`, []);
testErrorsAndWarnings(`from a | eval a=${unaryCombination} numberField`, []);
testErrorsAndWarnings(`from a | eval a=${unaryCombination} round(numberField)`, []);
testErrorsAndWarnings(`from a | eval 1 + ${unaryCombination} numberField`, []);
// still valid
testErrorsAndWarnings(`from a | eval 1 ${unaryCombination} numberField`, []);
}
}
testErrorsAndWarnings(
`from a | eval ${Array(nesting).fill('not ').join('')} booleanField`,
[]
);
}
for (const wrongOp of ['*', '/', '%']) {
testErrorsAndWarnings(`from a | eval ${wrongOp}+ numberField`, [
`SyntaxError: extraneous input '${wrongOp}' expecting {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, '(', NOT, NULL, '?', TRUE, '+', '-', OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}`,
]);
}
for (const { name, alias, signatures, ...defRest } of evalFunctionsDefinitions) {
for (const { params, returnType } of signatures) {
const fieldMapping = getFieldMapping(params);
@ -1128,6 +1223,27 @@ describe('validation logic', () => {
testErrorsAndWarnings('from a | eval avg(numberField)', ['EVAL does not support function avg']);
testErrorsAndWarnings('from a | stats avg(numberField) | eval `avg(numberField)` + 1', []);
testErrorsAndWarnings('from a | eval not', [
'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, LP, NOT, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER} but found "<EOF>"',
'Error building [not]: expects exactly one argument, passed 0 instead.',
]);
testErrorsAndWarnings('from a | eval in', [
'SyntaxError: expected {STRING, INTEGER_LITERAL, DECIMAL_LITERAL, FALSE, LP, NOT, NULL, PARAM, TRUE, PLUS, MINUS, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER} but found "in"',
]);
testErrorsAndWarnings('from a | eval stringField in stringField', [
"SyntaxError: missing '(' at 'stringField'",
'SyntaxError: expected {COMMA, RP} but found "<EOF>"',
]);
testErrorsAndWarnings('from a | eval stringField in stringField)', [
"SyntaxError: missing '(' at 'stringField'",
'Error building [in]: expects exactly 2 arguments, passed 1 instead.',
]);
testErrorsAndWarnings('from a | eval stringField not in stringField', [
"SyntaxError: missing '(' at 'stringField'",
'SyntaxError: expected {COMMA, RP} but found "<EOF>"',
]);
describe('date math', () => {
testErrorsAndWarnings('from a | eval 1 anno', [
@ -1176,7 +1292,13 @@ describe('validation logic', () => {
});
describe('stats', () => {
testErrorsAndWarnings('from a | stats ', []);
testErrorsAndWarnings('from a | stats ', [
'At least one aggregation or grouping expression required in [STATS]',
]);
testErrorsAndWarnings('from a | stats by stringField', []);
testErrorsAndWarnings('from a | stats 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>"',
]);
testErrorsAndWarnings('from a | stats numberField ', [
'STATS expects an aggregate function, found [numberField]',
]);