mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
ca38ad5a31
commit
b091d8e956
10 changed files with 293 additions and 39 deletions
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ( ...)',
|
||||
}),
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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: '=',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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]',
|
||||
]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue