[ES|QL] Improved support for Elasticsearch sub-types in AST for both validation and autocomplete (#189689)

## Summary

Fixed version of https://github.com/elastic/kibana/pull/188600 that
updates the failed tests [caused by clash with the visitor API
tests](https://github.com/elastic/kibana/pull/189516).

### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

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

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen (Quinn) 2024-08-01 11:04:21 -05:00 committed by GitHub
parent 788e9b34dd
commit 7dca2aa712
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 30749 additions and 14414 deletions

View file

@ -17,7 +17,7 @@ describe('literal expression', () => {
expect(literal).toMatchObject({
type: 'literal',
literalType: 'number',
literalType: 'integer',
name: '1',
value: 1,
});

View file

@ -205,7 +205,7 @@ export class AstListener implements ESQLParserListener {
const command = createCommand('limit', ctx);
this.ast.push(command);
if (ctx.getToken(esql_parser.INTEGER_LITERAL, 0)) {
const literal = createLiteral('number', ctx.INTEGER_LITERAL());
const literal = createLiteral('integer', ctx.INTEGER_LITERAL());
if (literal) {
command.args.push(literal);
}

View file

@ -35,7 +35,9 @@ import type {
ESQLCommandMode,
ESQLInlineCast,
ESQLUnknownItem,
ESQLNumericLiteralType,
FunctionSubtype,
ESQLNumericLiteral,
} from './types';
export function nonNullable<T>(v: T): v is NonNullable<T> {
@ -87,11 +89,14 @@ export function createList(ctx: ParserRuleContext, values: ESQLLiteral[]): ESQLL
};
}
export function createNumericLiteral(ctx: DecimalValueContext | IntegerValueContext): ESQLLiteral {
export function createNumericLiteral(
ctx: DecimalValueContext | IntegerValueContext,
literalType: ESQLNumericLiteralType
): ESQLLiteral {
const text = ctx.getText();
return {
type: 'literal',
literalType: 'number',
literalType,
text,
name: text,
value: Number(text),
@ -100,10 +105,13 @@ export function createNumericLiteral(ctx: DecimalValueContext | IntegerValueCont
};
}
export function createFakeMultiplyLiteral(ctx: ArithmeticUnaryContext): ESQLLiteral {
export function createFakeMultiplyLiteral(
ctx: ArithmeticUnaryContext,
literalType: ESQLNumericLiteralType
): ESQLLiteral {
return {
type: 'literal',
literalType: 'number',
literalType,
text: ctx.getText(),
name: ctx.getText(),
value: ctx.PLUS() ? 1 : -1,
@ -158,12 +166,13 @@ export function createLiteral(
location: getPosition(node.symbol),
incomplete: isMissingText(text),
};
if (type === 'number') {
if (type === 'decimal' || type === 'integer') {
return {
...partialLiteral,
literalType: type,
value: Number(text),
};
paramType: 'number',
} as ESQLNumericLiteral<'decimal'> | ESQLNumericLiteral<'integer'>;
} else if (type === 'param') {
throw new Error('Should never happen');
}
@ -171,7 +180,7 @@ export function createLiteral(
...partialLiteral,
literalType: type,
value: text,
};
} as ESQLLiteral;
}
export function createTimeUnit(ctx: QualifiedIntegerLiteralContext): ESQLTimeInterval {

View file

@ -84,7 +84,7 @@ import {
createUnknownItem,
} from './ast_helpers';
import { getPosition } from './ast_position_utils';
import type {
import {
ESQLLiteral,
ESQLColumn,
ESQLFunction,
@ -289,7 +289,7 @@ function visitOperatorExpression(
const arg = visitOperatorExpression(ctx.operatorExpression());
// this is a number sign thing
const fn = createFunction('*', ctx, undefined, 'binary-expression');
fn.args.push(createFakeMultiplyLiteral(ctx));
fn.args.push(createFakeMultiplyLiteral(ctx, 'integer'));
if (arg) {
fn.args.push(arg);
}
@ -328,16 +328,21 @@ function getConstant(ctx: ConstantContext): ESQLAstItem {
// e.g. 1 year, 15 months
return createTimeUnit(ctx);
}
// Decimal type covers multiple ES|QL types: long, double, etc.
if (ctx instanceof DecimalLiteralContext) {
return createNumericLiteral(ctx.decimalValue());
return createNumericLiteral(ctx.decimalValue(), 'decimal');
}
// Integer type encompasses integer
if (ctx instanceof IntegerLiteralContext) {
return createNumericLiteral(ctx.integerValue());
return createNumericLiteral(ctx.integerValue(), 'integer');
}
if (ctx instanceof BooleanLiteralContext) {
return getBooleanValue(ctx);
}
if (ctx instanceof StringLiteralContext) {
// String literal covers multiple ES|QL types: text and keyword types
return createLiteral('string', ctx.string_().QUOTED_STRING());
}
if (
@ -346,14 +351,18 @@ function getConstant(ctx: ConstantContext): ESQLAstItem {
ctx instanceof StringArrayLiteralContext
) {
const values: ESQLLiteral[] = [];
for (const numericValue of ctx.getTypedRuleContexts(NumericValueContext)) {
const isDecimal =
numericValue.decimalValue() !== null && numericValue.decimalValue() !== undefined;
const value = numericValue.decimalValue() || numericValue.integerValue();
values.push(createNumericLiteral(value!));
values.push(createNumericLiteral(value!, isDecimal ? 'decimal' : 'integer'));
}
for (const booleanValue of ctx.getTypedRuleContexts(BooleanValueContext)) {
values.push(getBooleanValue(booleanValue)!);
}
for (const string of ctx.getTypedRuleContexts(StringContext)) {
// String literal covers multiple ES|QL types: text and keyword types
const literal = createLiteral('string', string.QUOTED_STRING());
if (literal) {
values.push(literal);

View file

@ -13,7 +13,7 @@ test('can mint a numeric literal', () => {
expect(node).toMatchObject({
type: 'literal',
literalType: 'number',
literalType: 'integer',
name: '42',
value: 42,
});

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { ESQLNumberLiteral } from '../types';
import { ESQLDecimalLiteral, ESQLIntegerLiteral, ESQLNumericLiteralType } from '../types';
import { AstNodeParserFields, AstNodeTemplate } from './types';
export class Builder {
@ -25,16 +25,20 @@ export class Builder {
});
/**
* Constructs a number literal node.
* Constructs a integer literal node.
*/
public static readonly numericLiteral = (
template: Omit<AstNodeTemplate<ESQLNumberLiteral>, 'literalType' | 'name'>
): ESQLNumberLiteral => {
const node: ESQLNumberLiteral = {
template: Omit<
AstNodeTemplate<ESQLIntegerLiteral | ESQLDecimalLiteral>,
'literalType' | 'name'
>,
type: ESQLNumericLiteralType = 'integer'
): ESQLIntegerLiteral | ESQLDecimalLiteral => {
const node: ESQLIntegerLiteral | ESQLDecimalLiteral = {
...template,
...Builder.parserFields(template),
type: 'literal',
literalType: 'number',
literalType: type,
name: template.value.toString(),
};

View file

@ -179,19 +179,30 @@ export interface ESQLList extends ESQLAstBaseItem {
values: ESQLLiteral[];
}
export type ESQLNumericLiteralType = 'decimal' | 'integer';
export type ESQLLiteral =
| ESQLNumberLiteral
| ESQLDecimalLiteral
| ESQLIntegerLiteral
| ESQLBooleanLiteral
| ESQLNullLiteral
| ESQLStringLiteral
| ESQLParamLiteral<string>;
// Exporting here to prevent TypeScript error TS4058
// Return type of exported function has or is using name 'ESQLNumericLiteral' from external module
// @internal
export interface ESQLNumberLiteral extends ESQLAstBaseItem {
export interface ESQLNumericLiteral<T extends ESQLNumericLiteralType> extends ESQLAstBaseItem {
type: 'literal';
literalType: 'number';
literalType: T;
value: number;
}
// We cast anything as decimal (e.g. 32.12) as generic decimal numeric type here
// @internal
export type ESQLDecimalLiteral = ESQLNumericLiteral<'decimal'>;
// @internal
export type ESQLIntegerLiteral = ESQLNumericLiteral<'integer'>;
// @internal
export interface ESQLBooleanLiteral extends ESQLAstBaseItem {

View file

@ -18,11 +18,12 @@ import type {
ESQLAstNodeWithArgs,
ESQLColumn,
ESQLCommandOption,
ESQLDecimalLiteral,
ESQLFunction,
ESQLInlineCast,
ESQLIntegerLiteral,
ESQLList,
ESQLLiteral,
ESQLNumberLiteral,
ESQLSource,
ESQLTimeInterval,
} from '../types';
@ -260,10 +261,14 @@ export class LimitCommandVisitorContext<
/**
* @returns The first numeric literal argument of the command.
*/
public numericLiteral(): ESQLNumberLiteral | undefined {
public numericLiteral(): ESQLIntegerLiteral | ESQLDecimalLiteral | undefined {
const arg = firstItem(this.node.args);
if (arg && arg.type === 'literal' && arg.literalType === 'number') {
if (
arg &&
arg.type === 'literal' &&
(arg.literalType === 'integer' || arg.literalType === 'decimal')
) {
return arg;
}
}

View file

@ -211,7 +211,7 @@ describe('structurally can walk all nodes', () => {
expect(columns).toMatchObject([
{
type: 'literal',
literalType: 'number',
literalType: 'integer',
name: '123',
},
{
@ -244,7 +244,7 @@ describe('structurally can walk all nodes', () => {
expect(columns).toMatchObject([
{
type: 'literal',
literalType: 'number',
literalType: 'integer',
name: '1',
},
{
@ -264,7 +264,7 @@ describe('structurally can walk all nodes', () => {
},
{
type: 'literal',
literalType: 'number',
literalType: 'decimal',
name: '3.14',
},
]);
@ -288,12 +288,12 @@ describe('structurally can walk all nodes', () => {
values: [
{
type: 'literal',
literalType: 'number',
literalType: 'integer',
name: '1',
},
{
type: 'literal',
literalType: 'number',
literalType: 'integer',
name: '2',
},
],
@ -318,12 +318,12 @@ describe('structurally can walk all nodes', () => {
values: [
{
type: 'literal',
literalType: 'number',
literalType: 'integer',
name: '1',
},
{
type: 'literal',
literalType: 'number',
literalType: 'integer',
name: '2',
},
],
@ -333,7 +333,7 @@ describe('structurally can walk all nodes', () => {
values: [
{
type: 'literal',
literalType: 'number',
literalType: 'decimal',
name: '3.3',
},
],
@ -342,17 +342,17 @@ describe('structurally can walk all nodes', () => {
expect(literals).toMatchObject([
{
type: 'literal',
literalType: 'number',
literalType: 'integer',
name: '1',
},
{
type: 'literal',
literalType: 'number',
literalType: 'integer',
name: '2',
},
{
type: 'literal',
literalType: 'number',
literalType: 'decimal',
name: '3.3',
},
]);
@ -511,7 +511,7 @@ describe('structurally can walk all nodes', () => {
describe('cast expression', () => {
test('can visit cast expression', () => {
const query = 'FROM index | STATS a = 123::number';
const query = 'FROM index | STATS a = 123::integer';
const { ast } = getAstAndSyntaxErrors(query);
const casts: ESQLInlineCast[] = [];
@ -523,10 +523,10 @@ describe('structurally can walk all nodes', () => {
expect(casts).toMatchObject([
{
type: 'inlineCast',
castType: 'number',
castType: 'integer',
value: {
type: 'literal',
literalType: 'number',
literalType: 'integer',
value: 123,
},
},

View file

@ -12,7 +12,6 @@ import { join } from 'path';
import _ from 'lodash';
import type { RecursivePartial } from '@kbn/utility-types';
import { FunctionDefinition } from '../src/definitions/types';
import { esqlToKibanaType } from '../src/shared/esql_to_kibana_type';
const aliasTable: Record<string, string[]> = {
to_version: ['to_ver'],
@ -240,10 +239,10 @@ function getFunctionDefinition(ESFunctionDefinition: Record<string, any>): Funct
...signature,
params: signature.params.map((param: any) => ({
...param,
type: esqlToKibanaType(param.type),
type: param.type,
description: undefined,
})),
returnType: esqlToKibanaType(signature.returnType),
returnType: signature.returnType,
variadic: undefined, // we don't support variadic property
minParams: signature.variadic
? signature.params.filter((param: any) => !param.optional).length

View file

@ -25,6 +25,7 @@ import {
} from '../src/definitions/types';
import { FUNCTION_DESCRIBE_BLOCK_NAME } from '../src/validation/function_describe_block_name';
import { getMaxMinNumberOfParams } from '../src/validation/helpers';
import { ESQL_NUMBER_TYPES, isNumericType, isStringType } from '../src/shared/esql_types';
export const fieldNameFromType = (type: SupportedFieldType) => `${camelCase(type)}Field`;
@ -141,8 +142,8 @@ function generateImplicitDateCastingTestsForFunction(
const allSignaturesWithDateParams = definition.signatures.filter((signature) =>
signature.params.some(
(param, i) =>
param.type === 'date' &&
!definition.signatures.some((def) => getParamAtPosition(def, i)?.type === 'string') // don't count parameters that already accept a string
(param.type === 'date' || param.type === 'date_period') &&
!definition.signatures.some((def) => isStringType(getParamAtPosition(def, i)?.type)) // don't count parameters that already accept a string
)
);
@ -300,8 +301,8 @@ function generateWhereCommandTestsForEvalFunction(
// TODO: not sure why there's this constraint...
const supportedFunction = signatures.some(
({ returnType, params }) =>
['number', 'string'].includes(returnType) &&
params.every(({ type }) => ['number', 'string'].includes(type))
[...ESQL_NUMBER_TYPES, 'string'].includes(returnType) &&
params.every(({ type }) => [...ESQL_NUMBER_TYPES, 'string'].includes(type))
);
if (!supportedFunction) {
@ -311,12 +312,12 @@ function generateWhereCommandTestsForEvalFunction(
const supportedSignatures = signatures.filter(({ returnType }) =>
// TODO — not sure why the tests have this limitation... seems like any type
// that can be part of a boolean expression should be allowed in a where clause
['number', 'string'].includes(returnType)
[...ESQL_NUMBER_TYPES, 'string'].includes(returnType)
);
for (const { params, returnType, ...restSign } of supportedSignatures) {
const correctMapping = getFieldMapping(params);
testCases.set(
`from a_index | where ${returnType !== 'number' ? 'length(' : ''}${
`from a_index | where ${!isNumericType(returnType) ? 'length(' : ''}${
// hijacking a bit this function to produce a function call
getFunctionSignatures(
{
@ -326,7 +327,7 @@ function generateWhereCommandTestsForEvalFunction(
},
{ withTypes: false }
)[0].declaration
}${returnType !== 'number' ? ')' : ''} > 0`,
}${!isNumericType(returnType) ? ')' : ''} > 0`,
[]
);
@ -337,7 +338,7 @@ function generateWhereCommandTestsForEvalFunction(
supportedTypesAndFieldNames
);
testCases.set(
`from a_index | where ${returnType !== 'number' ? 'length(' : ''}${
`from a_index | where ${!isNumericType(returnType) ? 'length(' : ''}${
// hijacking a bit this function to produce a function call
getFunctionSignatures(
{
@ -347,7 +348,7 @@ function generateWhereCommandTestsForEvalFunction(
},
{ withTypes: false }
)[0].declaration
}${returnType !== 'number' ? ')' : ''} > 0`,
}${!isNumericType(returnType) ? ')' : ''} > 0`,
expectedErrors
);
}
@ -357,7 +358,7 @@ function generateWhereCommandTestsForAggFunction(
{ name, alias, signatures, ...defRest }: FunctionDefinition,
testCases: Map<string, string[]>
) {
// statsSignatures.some(({ returnType, params }) => ['number'].includes(returnType))
// statsSignatures.some(({ returnType, params }) => [...ESQL_NUMBER_TYPES].includes(returnType))
for (const { params, ...signRest } of signatures) {
const fieldMapping = getFieldMapping(params);
@ -542,7 +543,7 @@ function generateEvalCommandTestsForEvalFunction(
signatureWithGreatestNumberOfParams.params
).concat({
name: 'extraArg',
type: 'number',
type: 'integer',
});
// get the expected args from the first signature in case of errors
@ -660,7 +661,7 @@ function generateStatsCommandTestsForAggFunction(
testCases.set(`from a_index | stats var = ${correctSignature}`, []);
testCases.set(`from a_index | stats ${correctSignature}`, []);
if (signRest.returnType === 'number') {
if (isNumericType(signRest.returnType)) {
testCases.set(`from a_index | stats var = round(${correctSignature})`, []);
testCases.set(`from a_index | stats round(${correctSignature})`, []);
testCases.set(
@ -713,8 +714,8 @@ function generateStatsCommandTestsForAggFunction(
}
// test only numeric functions for now
if (params[0].type === 'number') {
const nestedBuiltin = 'numberField / 2';
if (isNumericType(params[0].type)) {
const nestedBuiltin = 'doubleField / 2';
const fieldMappingWithNestedBuiltinFunctions = getFieldMapping(params);
fieldMappingWithNestedBuiltinFunctions[0].name = nestedBuiltin;
@ -726,16 +727,16 @@ function generateStatsCommandTestsForAggFunction(
},
{ withTypes: false }
)[0].declaration;
// from a_index | STATS aggFn( numberField / 2 )
// from a_index | STATS aggFn( doubleField / 2 )
testCases.set(`from a_index | stats ${fnSignatureWithBuiltinString}`, []);
testCases.set(`from a_index | stats var0 = ${fnSignatureWithBuiltinString}`, []);
testCases.set(`from a_index | stats avg(numberField), ${fnSignatureWithBuiltinString}`, []);
testCases.set(`from a_index | stats avg(doubleField), ${fnSignatureWithBuiltinString}`, []);
testCases.set(
`from a_index | stats avg(numberField), var0 = ${fnSignatureWithBuiltinString}`,
`from a_index | stats avg(doubleField), var0 = ${fnSignatureWithBuiltinString}`,
[]
);
const nestedEvalAndBuiltin = 'round(numberField / 2)';
const nestedEvalAndBuiltin = 'round(doubleField / 2)';
const fieldMappingWithNestedEvalAndBuiltinFunctions = getFieldMapping(params);
fieldMappingWithNestedBuiltinFunctions[0].name = nestedEvalAndBuiltin;
@ -747,18 +748,18 @@ function generateStatsCommandTestsForAggFunction(
},
{ withTypes: false }
)[0].declaration;
// from a_index | STATS aggFn( round(numberField / 2) )
// from a_index | STATS aggFn( round(doubleField / 2) )
testCases.set(`from a_index | stats ${fnSignatureWithEvalAndBuiltinString}`, []);
testCases.set(`from a_index | stats var0 = ${fnSignatureWithEvalAndBuiltinString}`, []);
testCases.set(
`from a_index | stats avg(numberField), ${fnSignatureWithEvalAndBuiltinString}`,
`from a_index | stats avg(doubleField), ${fnSignatureWithEvalAndBuiltinString}`,
[]
);
testCases.set(
`from a_index | stats avg(numberField), var0 = ${fnSignatureWithEvalAndBuiltinString}`,
`from a_index | stats avg(doubleField), var0 = ${fnSignatureWithEvalAndBuiltinString}`,
[]
);
// from a_index | STATS aggFn(round(numberField / 2) ) BY round(numberField / 2)
// from a_index | STATS aggFn(round(doubleField / 2) ) BY round(doubleField / 2)
testCases.set(
`from a_index | stats ${fnSignatureWithEvalAndBuiltinString} by ${nestedEvalAndBuiltin}`,
[]
@ -768,19 +769,19 @@ function generateStatsCommandTestsForAggFunction(
[]
);
testCases.set(
`from a_index | stats avg(numberField), ${fnSignatureWithEvalAndBuiltinString} by ${nestedEvalAndBuiltin}, ipField`,
`from a_index | stats avg(doubleField), ${fnSignatureWithEvalAndBuiltinString} by ${nestedEvalAndBuiltin}, ipField`,
[]
);
testCases.set(
`from a_index | stats avg(numberField), var0 = ${fnSignatureWithEvalAndBuiltinString} by var1 = ${nestedEvalAndBuiltin}, ipField`,
`from a_index | stats avg(doubleField), var0 = ${fnSignatureWithEvalAndBuiltinString} by var1 = ${nestedEvalAndBuiltin}, ipField`,
[]
);
testCases.set(
`from a_index | stats avg(numberField), ${fnSignatureWithEvalAndBuiltinString} by ${nestedEvalAndBuiltin}, ${nestedBuiltin}`,
`from a_index | stats avg(doubleField), ${fnSignatureWithEvalAndBuiltinString} by ${nestedEvalAndBuiltin}, ${nestedBuiltin}`,
[]
);
testCases.set(
`from a_index | stats avg(numberField), var0 = ${fnSignatureWithEvalAndBuiltinString} by var1 = ${nestedEvalAndBuiltin}, ${nestedBuiltin}`,
`from a_index | stats avg(doubleField), var0 = ${fnSignatureWithEvalAndBuiltinString} by var1 = ${nestedEvalAndBuiltin}, ${nestedBuiltin}`,
[]
);
}
@ -798,7 +799,7 @@ function generateStatsCommandTestsForAggFunction(
.filter(({ constantOnly }) => !constantOnly)
.map(
(_) =>
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [avg(numberField)] of type [number]`
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [avg(doubleField)] of type [double]`
);
testCases.set(
`from a_index | stats var = ${
@ -965,9 +966,17 @@ function generateSortCommandTestsForAggFunction(
const generateSortCommandTestsForGroupingFunction = generateSortCommandTestsForAggFunction;
const fieldTypesToConstants: Record<SupportedFieldType, string> = {
string: '"a"',
number: '5',
date: 'now()',
text: '"a"',
keyword: '"a"',
double: '5.5',
integer: '5',
long: '5',
unsigned_long: '5',
counter_integer: '5',
counter_long: '5',
counter_double: '5.5',
date: 'to_datetime("2021-01-01T00:00:00Z")',
date_period: 'to_date_period("2021-01-01/2021-01-02")',
boolean: 'true',
version: 'to_version("1.0.0")',
ip: 'to_ip("127.0.0.1")',
@ -1003,8 +1012,8 @@ function prepareNestedFunction(fnSignature: FunctionDefinition): string {
}
const toAvgSignature = statsAggregationFunctionDefinitions.find(({ name }) => name === 'avg')!;
const toInteger = evalFunctionDefinitions.find(({ name }) => name === 'to_integer')!;
const toDoubleSignature = evalFunctionDefinitions.find(({ name }) => name === 'to_double')!;
const toStringSignature = evalFunctionDefinitions.find(({ name }) => name === 'to_string')!;
const toDateSignature = evalFunctionDefinitions.find(({ name }) => name === 'to_datetime')!;
const toBooleanSignature = evalFunctionDefinitions.find(({ name }) => name === 'to_boolean')!;
@ -1019,10 +1028,12 @@ const toCartesianShapeSignature = evalFunctionDefinitions.find(
)!;
const toVersionSignature = evalFunctionDefinitions.find(({ name }) => name === 'to_version')!;
// We don't have full list for long, unsigned_long, etc.
const nestedFunctions: Record<SupportedFieldType, string> = {
number: prepareNestedFunction(toInteger),
string: prepareNestedFunction(toStringSignature),
date: prepareNestedFunction(toDateSignature),
double: prepareNestedFunction(toDoubleSignature),
integer: prepareNestedFunction(toInteger),
text: prepareNestedFunction(toStringSignature),
keyword: prepareNestedFunction(toStringSignature),
boolean: prepareNestedFunction(toBooleanSignature),
ip: prepareNestedFunction(toIpSignature),
version: prepareNestedFunction(toVersionSignature),
@ -1030,6 +1041,8 @@ const nestedFunctions: Record<SupportedFieldType, string> = {
geo_shape: prepareNestedFunction(toGeoShapeSignature),
cartesian_point: prepareNestedFunction(toCartesianPointSignature),
cartesian_shape: prepareNestedFunction(toCartesianShapeSignature),
// @ts-expect-error
datetime: prepareNestedFunction(toDateSignature),
};
function getFieldName(
@ -1086,6 +1099,7 @@ function getFieldMapping(
number: '5',
date: 'now()',
};
return params.map(({ name: _name, type, constantOnly, literalOptions, ...rest }) => {
const typeString: string = type;
if (isSupportedFieldType(typeString)) {
@ -1124,7 +1138,7 @@ function getFieldMapping(
...rest,
};
}
return { name: 'stringField', type, ...rest };
return { name: 'textField', type, ...rest };
});
}
@ -1225,8 +1239,12 @@ function generateIncorrectlyTypedParameters(
}
const fieldName = wrongFieldMapping[i].name;
if (
fieldName === 'numberField' &&
signatures.every((signature) => getParamAtPosition(signature, i)?.type !== 'string')
fieldName === 'doubleField' &&
signatures.every(
(signature) =>
getParamAtPosition(signature, i)?.type !== 'keyword' ||
getParamAtPosition(signature, i)?.type !== 'text'
)
) {
return;
}

View file

@ -11,14 +11,14 @@ import { supportedFieldTypes } from '../definitions/types';
export const fields = [
...supportedFieldTypes.map((type) => ({ name: `${camelCase(type)}Field`, type })),
{ name: 'any#Char$Field', type: 'number' },
{ name: 'kubernetes.something.something', type: 'number' },
{ name: 'any#Char$Field', type: 'double' },
{ name: 'kubernetes.something.something', type: 'double' },
{ name: '@timestamp', type: 'date' },
];
export const enrichFields = [
{ name: 'otherField', type: 'string' },
{ name: 'yetAnotherField', type: 'number' },
{ name: 'otherField', type: 'text' },
{ name: 'yetAnotherField', type: 'double' },
];
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -58,7 +58,7 @@ export function getCallbackMocks() {
return unsupported_field;
}
if (/dissect|grok/.test(query)) {
return [{ name: 'firstWord', type: 'string' }];
return [{ name: 'firstWord', type: 'text' }];
}
return fields;
}),

View file

@ -6,8 +6,11 @@
* Side Public License, v 1.
*/
import { ESQL_COMMON_NUMERIC_TYPES, ESQL_NUMBER_TYPES } from '../../shared/esql_types';
import { setup, getFunctionSignaturesByReturnType, getFieldNamesByType } from './helpers';
const ESQL_NUMERIC_TYPES = ESQL_NUMBER_TYPES as unknown as string[];
const allAggFunctions = getFunctionSignaturesByReturnType('stats', 'any', {
agg: true,
});
@ -74,51 +77,76 @@ describe('autocomplete.suggest', () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats by bucket(/', [
...getFieldNamesByType(['number', 'date']).map((field) => `${field},`),
...getFunctionSignaturesByReturnType('eval', ['date', 'number'], { scalar: true }).map(
(s) => ({ ...s, text: `${s.text},` })
...getFieldNamesByType([...ESQL_COMMON_NUMERIC_TYPES, 'date']).map(
(field) => `${field},`
),
...getFunctionSignaturesByReturnType('eval', ['date', ...ESQL_COMMON_NUMERIC_TYPES], {
scalar: true,
}).map((s) => ({ ...s, text: `${s.text},` })),
]);
await assertSuggestions('from a | stats round(/', [
...getFunctionSignaturesByReturnType('stats', 'number', { agg: true, grouping: true }),
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }, undefined, [
'round',
]),
...getFunctionSignaturesByReturnType('stats', ESQL_NUMERIC_TYPES, {
agg: true,
grouping: true,
}),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
{ scalar: true },
undefined,
['round']
),
]);
await assertSuggestions('from a | stats round(round(/', [
...getFunctionSignaturesByReturnType('stats', 'number', { agg: true }),
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }, undefined, [
'round',
]),
...getFunctionSignaturesByReturnType('stats', ESQL_NUMERIC_TYPES, { agg: true }),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
{ scalar: true },
undefined,
['round']
),
]);
await assertSuggestions('from a | stats avg(round(/', [
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }, undefined, [
'round',
]),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
{ scalar: true },
undefined,
['round']
),
]);
await assertSuggestions('from a | stats avg(/', [
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_NUMERIC_TYPES, { scalar: true }),
]);
await assertSuggestions('from a | stats round(avg(/', [
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }, undefined, [
'round',
]),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
{ scalar: true },
undefined,
['round']
),
]);
});
test('when typing inside function left paren', async () => {
const { assertSuggestions } = await setup();
const expected = [
...getFieldNamesByType(['number', 'date', 'boolean', 'ip']),
...getFunctionSignaturesByReturnType('stats', ['number', 'date', 'boolean', 'ip'], {
scalar: true,
}),
...getFieldNamesByType([...ESQL_COMMON_NUMERIC_TYPES, 'date', 'boolean', 'ip']),
...getFunctionSignaturesByReturnType(
'stats',
[...ESQL_COMMON_NUMERIC_TYPES, 'date', 'boolean', 'ip'],
{
scalar: true,
}
),
];
await assertSuggestions('from a | stats a=min(/)', expected);
@ -130,8 +158,14 @@ describe('autocomplete.suggest', () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats avg(b/) by stringField', [
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }),
...getFieldNamesByType('double'),
...getFunctionSignaturesByReturnType(
'eval',
['double', 'integer', 'long', 'unsigned_long'],
{
scalar: true,
}
),
]);
});
@ -205,10 +239,15 @@ describe('autocomplete.suggest', () => {
test('on space before expression right hand side operand', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats avg(b) by numberField % /', [
...getFieldNamesByType('number'),
await assertSuggestions('from a | stats avg(b) by integerField % /', [
...getFieldNamesByType('integer'),
...getFieldNamesByType('double'),
...getFieldNamesByType('long'),
'`avg(b)`',
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }),
...getFunctionSignaturesByReturnType('eval', ['integer', 'double', 'long'], {
scalar: true,
}),
...allGroupingFunctions,
]);
await assertSuggestions('from a | stats avg(b) by var0 = /', [
@ -226,10 +265,10 @@ describe('autocomplete.suggest', () => {
test('on space after expression right hand side operand', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats avg(b) by numberField % 2 /', [',', '|']);
await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [',', '|']);
await assertSuggestions(
'from a | stats var0 = AVG(products.base_price) BY var1 = BUCKET(order_date, 1 day)/',
'from a | stats var0 = AVG(doubleField) BY var1 = BUCKET(dateField, 1 day)/',
[',', '|', '+ $0', '- $0']
);
});

View file

@ -41,7 +41,7 @@ export const triggerCharacters = [',', '(', '=', ' '];
export const fields: Array<{ name: string; type: string; suggestedAs?: string }> = [
...[
'string',
'number',
'double',
'date',
'boolean',
'ip',
@ -53,8 +53,8 @@ export const fields: Array<{ name: string; type: string; suggestedAs?: string }>
name: `${camelCase(type)}Field`,
type,
})),
{ name: 'any#Char$Field', type: 'number', suggestedAs: '`any#Char$Field`' },
{ name: 'kubernetes.something.something', type: 'number' },
{ name: 'any#Char$Field', type: 'double', suggestedAs: '`any#Char$Field`' },
{ name: 'kubernetes.something.something', type: 'double' },
];
export const indexes = (

View file

@ -10,15 +10,10 @@ import { suggest } from './autocomplete';
import { evalFunctionDefinitions } from '../definitions/functions';
import { timeUnitsToSuggest } from '../definitions/literals';
import { commandDefinitions } from '../definitions/commands';
import {
getSafeInsertText,
getUnitDuration,
TRIGGER_SUGGESTION_COMMAND,
TIME_SYSTEM_PARAMS,
} from './factories';
import { getSafeInsertText, getUnitDuration, TRIGGER_SUGGESTION_COMMAND } from './factories';
import { camelCase, partition } from 'lodash';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { FunctionParameter } from '../definitions/types';
import { FunctionParameter, FunctionReturnType } from '../definitions/types';
import { getParamAtPosition } from './helper';
import { nonNullable } from '../shared/helpers';
import {
@ -31,9 +26,16 @@ import {
createCompletionContext,
getPolicyFields,
PartialSuggestionWithText,
TIME_PICKER_SUGGESTION,
} from './__tests__/helpers';
import { METADATA_FIELDS } from '../shared/constants';
import {
ESQL_COMMON_NUMERIC_TYPES as UNCASTED_ESQL_COMMON_NUMERIC_TYPES,
ESQL_NUMBER_TYPES,
} from '../shared/esql_types';
const ESQL_NUMERIC_TYPES = ESQL_NUMBER_TYPES as unknown as string[];
const ESQL_COMMON_NUMERIC_TYPES =
UNCASTED_ESQL_COMMON_NUMERIC_TYPES as unknown as FunctionReturnType[];
describe('autocomplete', () => {
type TestArgs = [
@ -166,25 +168,18 @@ describe('autocomplete', () => {
['string']
),
]);
testSuggestions('from a | where stringField >= ', [
...getFieldNamesByType('string'),
...getFunctionSignaturesByReturnType('where', 'string', { scalar: true }),
testSuggestions('from a | where textField >= ', [
...getFieldNamesByType('any'),
...getFunctionSignaturesByReturnType('where', ['any'], { scalar: true }),
]);
// Skip these tests until the insensitive case equality gets restored back
testSuggestions.skip('from a | where stringField =~ ', [
...getFieldNamesByType('string'),
...getFunctionSignaturesByReturnType('where', 'string', { scalar: true }),
]);
testSuggestions('from a | where stringField >= stringField ', [
'|',
...getFunctionSignaturesByReturnType(
'where',
'boolean',
{
builtin: true,
},
['boolean']
),
testSuggestions('from a | where textField >= textField', [
...getFieldNamesByType('any'),
...getFunctionSignaturesByReturnType('where', 'any', { scalar: true }),
]);
testSuggestions.skip('from a | where stringField =~ stringField ', [
'|',
@ -202,52 +197,60 @@ describe('autocomplete', () => {
...getFieldNamesByType('any'),
...getFunctionSignaturesByReturnType('where', 'any', { scalar: true }),
]);
testSuggestions(`from a | where stringField >= stringField ${op} numberField `, [
...getFunctionSignaturesByReturnType('where', 'boolean', { builtin: true }, ['number']),
testSuggestions(`from a | where stringField >= stringField ${op} doubleField `, [
...getFunctionSignaturesByReturnType('where', 'boolean', { builtin: true }, ['double']),
]);
testSuggestions(`from a | where stringField >= stringField ${op} numberField == `, [
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('where', 'number', { scalar: true }),
testSuggestions(`from a | where stringField >= stringField ${op} doubleField == `, [
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('where', ESQL_COMMON_NUMERIC_TYPES, { scalar: true }),
]);
}
testSuggestions('from a | stats a=avg(numberField) | where a ', [
testSuggestions('from a | stats a=avg(doubleField) | where a ', [
...getFunctionSignaturesByReturnType('where', 'any', { builtin: true, skipAssign: true }, [
'number',
'double',
]),
]);
// Mind this test: suggestion is aware of previous commands when checking for fields
// in this case the numberField has been wiped by the STATS command and suggest cannot find it's type
// in this case the doubleField has been wiped by the STATS command and suggest cannot find it's type
// @TODO: verify this is the correct behaviour in this case or if we want a "generic" suggestion anyway
testSuggestions(
'from a | stats a=avg(numberField) | where numberField ',
'from a | stats a=avg(doubleField) | where doubleField ',
[],
undefined,
undefined,
// make the fields suggest aware of the previous STATS, leave the other callbacks untouched
[[{ name: 'a', type: 'number' }], undefined, undefined]
[[{ name: 'a', type: 'double' }], undefined, undefined]
);
// The editor automatically inject the final bracket, so it is not useful to test with just open bracket
testSuggestions(
'from a | where log10()',
[
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('where', 'number', { scalar: true }, undefined, [
'log10',
]),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType(
'where',
ESQL_NUMERIC_TYPES,
{ scalar: true },
undefined,
['log10']
),
],
'('
);
testSuggestions('from a | where log10(numberField) ', [
...getFunctionSignaturesByReturnType('where', 'number', { builtin: true }, ['number']),
...getFunctionSignaturesByReturnType('where', 'boolean', { builtin: true }, ['number']),
testSuggestions('from a | where log10(doubleField) ', [
...getFunctionSignaturesByReturnType('where', 'double', { builtin: true }, ['double']),
...getFunctionSignaturesByReturnType('where', 'boolean', { builtin: true }, ['double']),
]);
testSuggestions(
'from a | WHERE pow(numberField, )',
'from a | WHERE pow(doubleField, )',
[
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('where', 'number', { scalar: true }, undefined, [
'pow',
]),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType(
'where',
ESQL_NUMERIC_TYPES,
{ scalar: true },
undefined,
['pow']
),
],
','
);
@ -258,34 +261,34 @@ describe('autocomplete', () => {
...getFieldNamesByType('boolean'),
...getFunctionSignaturesByReturnType('eval', 'boolean', { scalar: true }),
]);
testSuggestions('from index | WHERE numberField in ', ['( $0 )']);
testSuggestions('from index | WHERE numberField not in ', ['( $0 )']);
testSuggestions('from index | WHERE doubleField in ', ['( $0 )']);
testSuggestions('from index | WHERE doubleField not in ', ['( $0 )']);
testSuggestions(
'from index | WHERE numberField not in ( )',
'from index | WHERE doubleField not in ( )',
[
...getFieldNamesByType('number').filter((name) => name !== 'numberField'),
...getFunctionSignaturesByReturnType('where', 'number', { scalar: true }),
...getFieldNamesByType('double').filter((name) => name !== 'doubleField'),
...getFunctionSignaturesByReturnType('where', 'double', { scalar: true }),
],
'('
);
testSuggestions(
'from index | WHERE numberField in ( `any#Char$Field`, )',
'from index | WHERE doubleField in ( `any#Char$Field`, )',
[
...getFieldNamesByType('number').filter(
(name) => name !== '`any#Char$Field`' && name !== 'numberField'
...getFieldNamesByType('double').filter(
(name) => name !== '`any#Char$Field`' && name !== 'doubleField'
),
...getFunctionSignaturesByReturnType('where', 'number', { scalar: true }),
...getFunctionSignaturesByReturnType('where', 'double', { scalar: true }),
],
undefined,
54 // after the first suggestions
);
testSuggestions(
'from index | WHERE numberField not in ( `any#Char$Field`, )',
'from index | WHERE doubleField not in ( `any#Char$Field`, )',
[
...getFieldNamesByType('number').filter(
(name) => name !== '`any#Char$Field`' && name !== 'numberField'
...getFieldNamesByType('double').filter(
(name) => name !== '`any#Char$Field`' && name !== 'doubleField'
),
...getFunctionSignaturesByReturnType('where', 'number', { scalar: true }),
...getFunctionSignaturesByReturnType('where', 'double', { scalar: true }),
],
undefined,
58 // after the first suggestions
@ -377,14 +380,14 @@ describe('autocomplete', () => {
);
testSuggestions(
`from a_index | eval round(numberField) + 1 | eval \`round(numberField) + 1\` + 1 | eval \`\`\`round(numberField) + 1\`\` + 1\` + 1 | eval \`\`\`\`\`\`\`round(numberField) + 1\`\`\`\` + 1\`\` + 1\` + 1 | eval \`\`\`\`\`\`\`\`\`\`\`\`\`\`\`round(numberField) + 1\`\`\`\`\`\`\`\` + 1\`\`\`\` + 1\`\` + 1\` + 1 | ${command} `,
`from a_index | eval round(doubleField) + 1 | eval \`round(doubleField) + 1\` + 1 | eval \`\`\`round(doubleField) + 1\`\` + 1\` + 1 | eval \`\`\`\`\`\`\`round(doubleField) + 1\`\`\`\` + 1\`\` + 1\` + 1 | eval \`\`\`\`\`\`\`\`\`\`\`\`\`\`\`round(doubleField) + 1\`\`\`\`\`\`\`\` + 1\`\`\`\` + 1\`\` + 1\` + 1 | ${command} `,
[
...getFieldNamesByType('any'),
'`round(numberField) + 1`',
'```round(numberField) + 1`` + 1`',
'```````round(numberField) + 1```` + 1`` + 1`',
'```````````````round(numberField) + 1```````` + 1```` + 1`` + 1`',
'```````````````````````````````round(numberField) + 1```````````````` + 1```````` + 1```` + 1`` + 1`',
'`round(doubleField) + 1`',
'```round(doubleField) + 1`` + 1`',
'```````round(doubleField) + 1```` + 1`` + 1`',
'```````````````round(doubleField) + 1```````` + 1```` + 1`` + 1`',
'```````````````````````````````round(doubleField) + 1```````````````` + 1```````` + 1```` + 1`` + 1`',
]
);
});
@ -413,7 +416,7 @@ describe('autocomplete', () => {
testSuggestions(`from a ${prevCommand}| enrich policy `, ['ON $0', 'WITH $0', '|']);
testSuggestions(`from a ${prevCommand}| enrich policy on `, [
'stringField',
'numberField',
'doubleField',
'dateField',
'booleanField',
'ipField',
@ -466,25 +469,25 @@ describe('autocomplete', () => {
...getFieldNamesByType('any'),
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
]);
testSuggestions('from a | eval numberField ', [
testSuggestions('from a | eval doubleField ', [
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
'number',
'double',
]),
',',
'|',
]);
testSuggestions('from index | EVAL stringField not ', ['LIKE $0', 'RLIKE $0', 'IN $0']);
testSuggestions('from index | EVAL stringField NOT ', ['LIKE $0', 'RLIKE $0', 'IN $0']);
testSuggestions('from index | EVAL numberField in ', ['( $0 )']);
testSuggestions('from index | EVAL doubleField in ', ['( $0 )']);
testSuggestions(
'from index | EVAL numberField in ( )',
'from index | EVAL doubleField in ( )',
[
...getFieldNamesByType('number').filter((name) => name !== 'numberField'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }),
...getFieldNamesByType('double').filter((name) => name !== 'doubleField'),
...getFunctionSignaturesByReturnType('eval', 'double', { scalar: true }),
],
'('
);
testSuggestions('from index | EVAL numberField not in ', ['( $0 )']);
testSuggestions('from index | EVAL doubleField not in ', ['( $0 )']);
testSuggestions('from index | EVAL not ', [
...getFieldNamesByType('boolean'),
...getFunctionSignaturesByReturnType('eval', 'boolean', { scalar: true }),
@ -492,10 +495,10 @@ describe('autocomplete', () => {
testSuggestions('from a | eval a=', [
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
]);
testSuggestions('from a | eval a=abs(numberField), b= ', [
testSuggestions('from a | eval a=abs(doubleField), b= ', [
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
]);
testSuggestions('from a | eval a=numberField, ', [
testSuggestions('from a | eval a=doubleField, ', [
'var0 =',
...getFieldNamesByType('any'),
'a',
@ -509,10 +512,14 @@ describe('autocomplete', () => {
testSuggestions(
'from a | eval a=round()',
[
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }, undefined, [
'round',
]),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
{ scalar: true },
undefined,
['round']
),
],
'('
);
@ -539,64 +546,59 @@ describe('autocomplete', () => {
[],
' '
);
testSuggestions('from a | eval a=round(numberField) ', [
testSuggestions('from a | eval a=round(doubleField) ', [
',',
'|',
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
'number',
'double',
]),
]);
testSuggestions(
'from a | eval a=round(numberField, ',
'from a | eval a=round(doubleField, ',
[
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }, undefined, [
...getFieldNamesByType('integer'),
...getFunctionSignaturesByReturnType('eval', 'integer', { scalar: true }, undefined, [
'round',
]),
],
' '
);
testSuggestions(
'from a | eval round(numberField, ',
'from a | eval round(doubleField, ',
[
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }, undefined, [
...getFunctionSignaturesByReturnType('eval', 'integer', { scalar: true }, undefined, [
'round',
]),
],
' '
);
testSuggestions('from a | eval a=round(numberField),', [
testSuggestions('from a | eval a=round(doubleField),', [
'var0 =',
...getFieldNamesByType('any'),
'a',
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
]);
testSuggestions('from a | eval a=round(numberField) + ', [
...getFieldNamesByType('number'),
'a', // @TODO remove this
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }),
testSuggestions('from a | eval a=round(doubleField) + ', [
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_COMMON_NUMERIC_TYPES, { scalar: true }),
]);
testSuggestions('from a | eval a=round(numberField)+ ', [
...getFieldNamesByType('number'),
'a', // @TODO remove this
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }),
testSuggestions('from a | eval a=round(doubleField)+ ', [
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_COMMON_NUMERIC_TYPES, { scalar: true }),
]);
testSuggestions('from a | eval a=numberField+ ', [
...getFieldNamesByType('number'),
'a', // @TODO remove this
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }),
testSuggestions('from a | eval a=doubleField+ ', [
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_COMMON_NUMERIC_TYPES, { scalar: true }),
]);
testSuggestions('from a | eval a=`any#Char$Field`+ ', [
...getFieldNamesByType('number'),
'a', // @TODO remove this
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_COMMON_NUMERIC_TYPES, { scalar: true }),
]);
testSuggestions(
'from a | stats avg(numberField) by stringField | eval ',
'from a | stats avg(doubleField) by stringField | eval ',
[
'var0 =',
'`avg(numberField)`',
'`avg(doubleField)`',
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
],
' ',
@ -605,32 +607,32 @@ describe('autocomplete', () => {
[[], undefined, undefined]
);
testSuggestions(
'from a | eval abs(numberField) + 1 | eval ',
'from a | eval abs(doubleField) + 1 | eval ',
[
'var0 =',
...getFieldNamesByType('any'),
'`abs(numberField) + 1`',
'`abs(doubleField) + 1`',
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
],
' '
);
testSuggestions(
'from a | stats avg(numberField) by stringField | eval ',
'from a | stats avg(doubleField) by stringField | eval ',
[
'var0 =',
'`avg(numberField)`',
'`avg(doubleField)`',
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
],
' ',
undefined,
// make aware EVAL of the previous STATS command with the buggy field name from expression
[[{ name: 'avg_numberField_', type: 'number' }], undefined, undefined]
[[{ name: 'avg_doubleField_', type: 'double' }], undefined, undefined]
);
testSuggestions(
'from a | stats avg(numberField), avg(kubernetes.something.something) by stringField | eval ',
'from a | stats avg(doubleField), avg(kubernetes.something.something) by stringField | eval ',
[
'var0 =',
'`avg(numberField)`',
'`avg(doubleField)`',
'`avg(kubernetes.something.something)`',
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
],
@ -639,48 +641,64 @@ describe('autocomplete', () => {
// make aware EVAL of the previous STATS command with the buggy field name from expression
[
[
{ name: 'avg_numberField_', type: 'number' },
{ name: 'avg_kubernetes.something.something_', type: 'number' },
{ name: 'avg_doubleField_', type: 'double' },
{ name: 'avg_kubernetes.something.something_', type: 'double' },
],
undefined,
undefined,
]
);
testSuggestions(
'from a | eval a=round(numberField), b=round()',
'from a | eval a=round(doubleField), b=round()',
[
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }, undefined, [
'round',
]),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
{ scalar: true },
undefined,
['round']
),
],
'('
);
// test that comma is correctly added to the suggestions if minParams is not reached yet
testSuggestions('from a | eval a=concat( ', [
...getFieldNamesByType('string').map((v) => `${v},`),
...getFunctionSignaturesByReturnType('eval', 'string', { scalar: true }, undefined, [
'concat',
]).map((v) => ({ ...v, text: `${v.text},` })),
...getFieldNamesByType(['text', 'keyword']).map((v) => `${v},`),
...getFunctionSignaturesByReturnType(
'eval',
['text', 'keyword'],
{ scalar: true },
undefined,
['concat']
).map((v) => ({ ...v, text: `${v.text},` })),
]);
testSuggestions(
'from a | eval a=concat(stringField, ',
'from a | eval a=concat(textField, ',
[
...getFieldNamesByType('string'),
...getFunctionSignaturesByReturnType('eval', 'string', { scalar: true }, undefined, [
'concat',
]),
...getFieldNamesByType(['text', 'keyword']),
...getFunctionSignaturesByReturnType(
'eval',
['text', 'keyword'],
{ scalar: true },
undefined,
['concat']
),
],
' '
);
// test that the arg type is correct after minParams
testSuggestions(
'from a | eval a=cidr_match(ipField, stringField, ',
'from a | eval a=cidr_match(ipField, textField, ',
[
...getFieldNamesByType('string'),
...getFunctionSignaturesByReturnType('eval', 'string', { scalar: true }, undefined, [
'cidr_match',
]),
...getFieldNamesByType('text'),
...getFunctionSignaturesByReturnType(
'eval',
['text', 'keyword'],
{ scalar: true },
undefined,
['cidr_match']
),
],
' '
);
@ -694,10 +712,14 @@ describe('autocomplete', () => {
testSuggestions(
'from a | eval a=cidr_match(ipField, ',
[
...getFieldNamesByType('string'),
...getFunctionSignaturesByReturnType('eval', 'string', { scalar: true }, undefined, [
'cidr_match',
]),
...getFieldNamesByType(['text', 'keyword']),
...getFunctionSignaturesByReturnType(
'eval',
['text', 'keyword'],
{ scalar: true },
undefined,
['cidr_match']
),
],
' '
);
@ -709,10 +731,14 @@ describe('autocomplete', () => {
testSuggestions(
`from a | eval a=${Array(nesting).fill('round(').join('')}`,
[
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }, undefined, [
'round',
]),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
{ scalar: true },
undefined,
['round']
),
],
'('
);
@ -720,12 +746,12 @@ describe('autocomplete', () => {
// Smoke testing for suggestions in previous position than the end of the statement
testSuggestions(
'from a | eval var0 = abs(numberField) | eval abs(var0)',
'from a | eval var0 = abs(doubleField) | eval abs(var0)',
[
',',
'|',
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
'number',
'double',
]),
],
undefined,
@ -734,10 +760,14 @@ describe('autocomplete', () => {
testSuggestions(
'from a | eval var0 = abs(b) | eval abs(var0)',
[
...getFieldNamesByType('number'),
...getFunctionSignaturesByReturnType('eval', 'number', { scalar: true }, undefined, [
'abs',
]),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
{ scalar: true },
undefined,
['abs']
),
],
undefined,
26 /* b column in abs */
@ -746,7 +776,7 @@ describe('autocomplete', () => {
// Test suggestions for each possible param, within each signature variation, for each function
for (const fn of evalFunctionDefinitions) {
// skip this fn for the moment as it's quite hard to test
if (fn.name !== 'bucket') {
if (!['bucket', 'date_extract', 'date_diff'].includes(fn.name)) {
for (const signature of fn.signatures) {
signature.params.forEach((param, i) => {
if (i < signature.params.length) {
@ -822,6 +852,23 @@ describe('autocomplete', () => {
});
}
}
// The above test fails cause it expects nested functions like
// DATE_EXTRACT(concat("aligned_day_","of_week_in_month"), date) to also be suggested
// which is actually valid according to func signature
// but currently, our autocomplete only suggests the literal suggestions
if (['date_extract', 'date_diff'].includes(fn.name)) {
const firstParam = fn.signatures[0].params[0];
const suggestedConstants = firstParam?.literalSuggestions || firstParam?.literalOptions;
const requiresMoreArgs = true;
testSuggestions(
`from a | eval ${fn.name}(`,
suggestedConstants?.length
? [...suggestedConstants.map((option) => `"${option}"${requiresMoreArgs ? ',' : ''}`)]
: []
);
}
}
testSuggestions('from a | eval var0 = bucket(@timestamp, ', getUnitDuration(1), ' ');
@ -836,7 +883,7 @@ describe('autocomplete', () => {
',',
'|',
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
'number',
'integer',
]),
],
' '
@ -848,39 +895,20 @@ describe('autocomplete', () => {
'time_interval',
]),
]);
testSuggestions(
'from a | eval a = 1 day + 2 ',
[
...dateSuggestions,
',',
'|',
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
'number',
]),
],
' '
);
testSuggestions('from a | eval a = 1 day + 2 ', [',', '|']);
testSuggestions(
'from a | eval 1 day + 2 ',
[
...dateSuggestions,
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
'number',
'integer',
]),
],
' '
);
testSuggestions(
'from a | eval var0=date_trunc()',
[
...[...TIME_SYSTEM_PARAMS].map((t) => `${t},`),
...getLiteralsByType('time_literal').map((t) => `${t},`),
...getFunctionSignaturesByReturnType('eval', 'date', { scalar: true }, undefined, [
'date_trunc',
]).map((t) => ({ ...t, text: `${t.text},` })),
...getFieldNamesByType('date').map((t) => `${t},`),
TIME_PICKER_SUGGESTION,
],
[...getLiteralsByType('time_literal').map((t) => `${t},`)],
'('
);
testSuggestions(
@ -917,7 +945,7 @@ describe('autocomplete', () => {
describe('callbacks', () => {
it('should send the fields query without the last command', async () => {
const callbackMocks = createCustomCallbackMocks(undefined, undefined, undefined);
const statement = 'from a | drop stringField | eval var0 = abs(numberField) ';
const statement = 'from a | drop stringField | eval var0 = abs(doubleField) ';
const triggerOffset = statement.lastIndexOf(' ');
const context = createCompletionContext(statement[triggerOffset]);
await suggest(
@ -933,7 +961,7 @@ describe('autocomplete', () => {
});
it('should send the fields query aware of the location', async () => {
const callbackMocks = createCustomCallbackMocks(undefined, undefined, undefined);
const statement = 'from a | drop | eval var0 = abs(numberField) ';
const statement = 'from a | drop | eval var0 = abs(doubleField) ';
const triggerOffset = statement.lastIndexOf('p') + 1; // drop <here>
const context = createCompletionContext(statement[triggerOffset]);
await suggest(
@ -1025,10 +1053,13 @@ describe('autocomplete', () => {
testSuggestions(
'FROM kibana_sample_data_logs | EVAL TRIM(e)',
[
...getFieldNamesByType('string'),
...getFunctionSignaturesByReturnType('eval', 'string', { scalar: true }, undefined, [
'trim',
]),
...getFunctionSignaturesByReturnType(
'eval',
['text', 'keyword'],
{ scalar: true },
undefined,
['trim']
),
],
undefined,
42

View file

@ -16,6 +16,7 @@ import type {
ESQLSingleAstItem,
} from '@kbn/esql-ast';
import { partition } from 'lodash';
import { ESQL_NUMBER_TYPES, isNumericType } from '../shared/esql_types';
import type { EditorContext, SuggestionRawDefinition } from './types';
import {
lookupColumn,
@ -88,6 +89,7 @@ import {
getParamAtPosition,
getQueryForFields,
getSourcesFromCommands,
getSupportedTypesForBinaryOperators,
isAggFunctionUsedAlready,
removeQuoteForSuggestedSources,
} from './helper';
@ -124,7 +126,7 @@ function appendEnrichFields(
// @TODO: improve this
const newMap: Map<string, ESQLRealField> = new Map(fieldsMap);
for (const field of policyMetadata.enrichFields) {
newMap.set(field, { name: field, type: 'number' });
newMap.set(field, { name: field, type: 'double' });
}
return newMap;
}
@ -732,7 +734,7 @@ async function getExpressionSuggestionsByType(
workoutBuiltinOptions(rightArg, references)
)
);
if (nodeArgType === 'number' && isLiteralItem(rightArg)) {
if (isNumericType(nodeArgType) && isLiteralItem(rightArg)) {
// ... EVAL var = 1 <suggest>
suggestions.push(...getCompatibleLiterals(command.name, ['time_literal_unit']));
}
@ -740,7 +742,7 @@ async function getExpressionSuggestionsByType(
if (rightArg.args.some(isTimeIntervalItem)) {
const lastFnArg = rightArg.args[rightArg.args.length - 1];
const lastFnArgType = extractFinalTypeFromArg(lastFnArg, references);
if (lastFnArgType === 'number' && isLiteralItem(lastFnArg))
if (isNumericType(lastFnArgType) && isLiteralItem(lastFnArg))
// ... EVAL var = 1 year + 2 <suggest>
suggestions.push(...getCompatibleLiterals(command.name, ['time_literal_unit']));
}
@ -777,7 +779,7 @@ async function getExpressionSuggestionsByType(
if (nodeArg.args.some(isTimeIntervalItem)) {
const lastFnArg = nodeArg.args[nodeArg.args.length - 1];
const lastFnArgType = extractFinalTypeFromArg(lastFnArg, references);
if (lastFnArgType === 'number' && isLiteralItem(lastFnArg))
if (isNumericType(lastFnArgType) && isLiteralItem(lastFnArg))
// ... EVAL var = 1 year + 2 <suggest>
suggestions.push(...getCompatibleLiterals(command.name, ['time_literal_unit']));
}
@ -793,7 +795,10 @@ async function getExpressionSuggestionsByType(
suggestions.push(...buildConstantsDefinitions(argDef.values));
}
// If the type is specified try to dig deeper in the definition to suggest the best candidate
if (['string', 'number', 'boolean'].includes(argDef.type) && !argDef.values) {
if (
['string', 'text', 'keyword', 'boolean', ...ESQL_NUMBER_TYPES].includes(argDef.type) &&
!argDef.values
) {
// it can be just literal values (i.e. "string")
if (argDef.constantOnly) {
// ... | <COMMAND> ... <suggest>
@ -971,6 +976,7 @@ async function getBuiltinFunctionNextArgument(
) {
const suggestions = [];
const isFnComplete = isFunctionArgComplete(nodeArg, references);
if (isFnComplete.complete) {
// i.e. ... | <COMMAND> field > 0 <suggest>
// i.e. ... | <COMMAND> field + otherN <suggest>
@ -1001,17 +1007,16 @@ async function getBuiltinFunctionNextArgument(
suggestions.push(listCompleteItem);
} else {
const finalType = nestedType || nodeArgType || 'any';
const supportedTypes = getSupportedTypesForBinaryOperators(fnDef, finalType);
suggestions.push(
...(await getFieldsOrFunctionsSuggestions(
// this is a special case with AND/OR
// <COMMAND> expression AND/OR <suggest>
// technically another boolean value should be suggested, but it is a better experience
// to actually suggest a wider set of fields/functions
[
finalType === 'boolean' && getFunctionDefinition(nodeArg.name)?.type === 'builtin'
? 'any'
: finalType,
],
finalType === 'boolean' && getFunctionDefinition(nodeArg.name)?.type === 'builtin'
? ['any']
: supportedTypes,
command.name,
option?.name,
getFieldsByType,
@ -1321,7 +1326,7 @@ async function getFunctionArgsSuggestions(
// for eval and row commands try also to complete numeric literals with time intervals where possible
if (arg) {
if (command.name !== 'stats') {
if (isLiteralItem(arg) && arg.literalType === 'number') {
if (isLiteralItem(arg) && isNumericType(arg.literalType)) {
// ... | EVAL fn(2 <suggest>)
suggestions.push(
...(await getFieldsOrFunctionsSuggestions(

View file

@ -22,7 +22,8 @@ import {
import { shouldBeQuotedSource, getCommandDefinition, shouldBeQuotedText } from '../shared/helpers';
import { buildDocumentation, buildFunctionDocumentation } from './documentation_util';
import { DOUBLE_BACKTICK, SINGLE_TICK_REGEX } from '../shared/constants';
import type { ESQLRealField } from '../validation/types';
import { ESQLRealField } from '../validation/types';
import { isNumericType } from '../shared/esql_types';
const allFunctions = statsAggregationFunctionDefinitions
.concat(evalFunctionDefinitions)
@ -359,7 +360,7 @@ export function getUnitDuration(unit: number = 1) {
*/
export function getCompatibleLiterals(commandName: string, types: string[], names?: string[]) {
const suggestions: SuggestionRawDefinition[] = [];
if (types.includes('number')) {
if (types.some(isNumericType)) {
if (commandName === 'limit') {
// suggest 10/100/1000 for limit
suggestions.push(...buildConstantsDefinitions(['10', '100', '1000'], ''));

View file

@ -80,3 +80,15 @@ export function removeQuoteForSuggestedSources(suggestions: SuggestionRawDefinit
text: d.text.startsWith('"') && d.text.endsWith('"') ? d.text.slice(1, -1) : d.text,
}));
}
export function getSupportedTypesForBinaryOperators(
fnDef: FunctionDefinition | undefined,
previousType: string
) {
// Retrieve list of all 'right' supported types that match the left hand side of the function
return fnDef && Array.isArray(fnDef?.signatures)
? fnDef.signatures
.filter(({ params }) => params.find((p) => p.name === 'left' && p.type === previousType))
.map(({ params }) => params[1].type)
: [previousType];
}

View file

@ -7,15 +7,18 @@
*/
import { i18n } from '@kbn/i18n';
import type { FunctionDefinition, FunctionParameterType } from './types';
import { ESQL_COMMON_NUMERIC_TYPES, ESQL_NUMBER_TYPES } from '../shared/esql_types';
import type { FunctionDefinition, FunctionParameterType, FunctionReturnType } from './types';
function createNumericAggDefinition({
name,
description,
returnType,
args = [],
}: {
name: string;
description: string;
returnType?: (numericType: FunctionParameterType) => FunctionReturnType;
args?: Array<{
name: string;
type: FunctionParameterType;
@ -30,9 +33,9 @@ function createNumericAggDefinition({
description,
supportedCommands: ['stats', 'metrics'],
signatures: [
{
...ESQL_NUMBER_TYPES.map((numericType) => ({
params: [
{ name: 'column', type: 'number', noNestingFunctions: true },
{ name: 'column', type: numericType, noNestingFunctions: true },
...args.map(({ name: paramName, type, constantOnly }) => ({
name: paramName,
type,
@ -40,8 +43,8 @@ function createNumericAggDefinition({
constantOnly,
})),
],
returnType: 'number',
},
returnType: returnType ? returnType(numericType) : numericType,
})),
],
examples: [
`from index | stats result = ${name}(field${extraParamsExample})`,
@ -56,18 +59,28 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.avgDoc', {
defaultMessage: 'Returns the average of the values in a field',
}),
returnType: () => 'double' as FunctionReturnType,
},
{
name: 'sum',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.sumDoc', {
defaultMessage: 'Returns the sum of the values in a field.',
}),
returnType: (numericType: FunctionParameterType): FunctionReturnType => {
switch (numericType) {
case 'double':
return 'double';
default:
return 'long';
}
},
},
{
name: 'median',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.medianDoc', {
defaultMessage: 'Returns the 50% percentile.',
}),
returnType: () => 'double' as FunctionReturnType,
},
{
name: 'median_absolute_deviation',
@ -78,20 +91,42 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
'Returns the median of each data points deviation from the median of the entire sample.',
}
),
},
{
name: 'percentile',
description: i18n.translate(
'kbn-esql-validation-autocomplete.esql.definitions.percentiletDoc',
{
defaultMessage: 'Returns the n percentile of a field.',
}
),
args: [{ name: 'percentile', type: 'number' as const, value: '90', constantOnly: true }],
returnType: () => 'double' as FunctionReturnType,
},
]
.map(createNumericAggDefinition)
.concat([
{
name: 'percentile',
description: i18n.translate(
'kbn-esql-validation-autocomplete.esql.definitions.percentiletDoc',
{
defaultMessage: 'Returns the n percentile of a field.',
}
),
type: 'agg',
supportedCommands: ['stats', 'metrics'],
signatures: [
...ESQL_COMMON_NUMERIC_TYPES.map((numericType: FunctionParameterType) => {
return ESQL_COMMON_NUMERIC_TYPES.map((weightType: FunctionParameterType) => ({
params: [
{
name: 'column',
type: numericType,
noNestingFunctions: true,
},
{
name: 'percentile',
type: weightType,
noNestingFunctions: true,
constantOnly: true,
},
],
returnType: 'double' as FunctionReturnType,
}));
}).flat(),
],
},
{
name: 'max',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.maxDoc', {
@ -100,13 +135,17 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
type: 'agg',
supportedCommands: ['stats', 'metrics'],
signatures: [
{
params: [{ name: 'column', type: 'number', noNestingFunctions: true }],
returnType: 'number',
},
...ESQL_COMMON_NUMERIC_TYPES.map((type) => ({
params: [{ name: 'column', type, noNestingFunctions: true }],
returnType: type,
})),
{
params: [{ name: 'column', type: 'date', noNestingFunctions: true }],
returnType: 'number',
returnType: 'date',
},
{
params: [{ name: 'column', type: 'date_period', noNestingFunctions: true }],
returnType: 'date_period',
},
{
params: [{ name: 'column', type: 'boolean', noNestingFunctions: true }],
@ -127,13 +166,17 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
type: 'agg',
supportedCommands: ['stats', 'metrics'],
signatures: [
{
params: [{ name: 'column', type: 'number', noNestingFunctions: true }],
returnType: 'number',
},
...ESQL_COMMON_NUMERIC_TYPES.map((type) => ({
params: [{ name: 'column', type, noNestingFunctions: true }],
returnType: type,
})),
{
params: [{ name: 'column', type: 'date', noNestingFunctions: true }],
returnType: 'number',
returnType: 'date',
},
{
params: [{ name: 'column', type: 'date_period', noNestingFunctions: true }],
returnType: 'date_period',
},
{
params: [{ name: 'column', type: 'boolean', noNestingFunctions: true }],
@ -166,7 +209,7 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
optional: true,
},
],
returnType: 'number',
returnType: 'long',
},
],
examples: [`from index | stats result = count(field)`, `from index | stats count(field)`],
@ -185,9 +228,14 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
{
params: [
{ name: 'column', type: 'any', noNestingFunctions: true },
{ name: 'precision', type: 'number', noNestingFunctions: true, optional: true },
...ESQL_NUMBER_TYPES.map((type) => ({
name: 'precision',
type,
noNestingFunctions: true,
optional: true,
})),
],
returnType: 'number',
returnType: 'long',
},
],
examples: [
@ -258,14 +306,14 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
},
{
name: 'limit',
type: 'number',
type: 'integer',
noNestingFunctions: true,
optional: false,
constantOnly: true,
},
{
name: 'order',
type: 'string',
type: 'keyword',
noNestingFunctions: true,
optional: false,
constantOnly: true,
@ -292,23 +340,25 @@ export const statsAggregationFunctionDefinitions: FunctionDefinition[] = [
),
supportedCommands: ['stats', 'metrics'],
signatures: [
{
params: [
{
name: 'number',
type: 'number',
noNestingFunctions: true,
optional: false,
},
{
name: 'weight',
type: 'number',
noNestingFunctions: true,
optional: false,
},
],
returnType: 'number',
},
...ESQL_COMMON_NUMERIC_TYPES.map((numericType: FunctionParameterType) => {
return ESQL_COMMON_NUMERIC_TYPES.map((weightType: FunctionParameterType) => ({
params: [
{
name: 'number',
type: numericType,
noNestingFunctions: true,
optional: false,
},
{
name: 'weight',
type: weightType,
noNestingFunctions: true,
optional: false,
},
],
returnType: 'double' as FunctionReturnType,
}));
}).flat(),
],
examples: [
`from employees | stats w_avg = weighted_avg(salary, height) by languages | eval w_avg = round(w_avg)`,

View file

@ -7,14 +7,14 @@
*/
import { i18n } from '@kbn/i18n';
import { ESQL_NUMBER_TYPES, isNumericType } from '../shared/esql_types';
import type { FunctionDefinition, FunctionParameterType, FunctionReturnType } from './types';
type MathFunctionSignature = [FunctionParameterType, FunctionParameterType, FunctionReturnType];
function createMathDefinition(
name: string,
types: Array<
| (FunctionParameterType & FunctionReturnType)
| [FunctionParameterType, FunctionParameterType, FunctionReturnType]
>,
functionSignatures: MathFunctionSignature[],
description: string,
validate?: FunctionDefinition['validate']
): FunctionDefinition {
@ -24,28 +24,41 @@ function createMathDefinition(
description,
supportedCommands: ['eval', 'where', 'row', 'stats', 'metrics', 'sort'],
supportedOptions: ['by'],
signatures: types.map((type) => {
if (Array.isArray(type)) {
return {
params: [
{ name: 'left', type: type[0] },
{ name: 'right', type: type[1] },
],
returnType: type[2],
};
}
signatures: functionSignatures.map((functionSignature) => {
const [lhs, rhs, result] = functionSignature;
return {
params: [
{ name: 'left', type },
{ name: 'right', type },
{ name: 'left', type: lhs },
{ name: 'right', type: rhs },
],
returnType: type,
returnType: result,
};
}),
validate,
};
}
// https://www.elastic.co/guide/en/elasticsearch/reference/master/esql-functions-operators.html#_less_than
const baseComparisonTypeTable: MathFunctionSignature[] = [
['date', 'date', 'boolean'],
['double', 'double', 'boolean'],
['double', 'integer', 'boolean'],
['double', 'long', 'boolean'],
['integer', 'double', 'boolean'],
['integer', 'integer', 'boolean'],
['integer', 'long', 'boolean'],
['ip', 'ip', 'boolean'],
['keyword', 'keyword', 'boolean'],
['keyword', 'text', 'boolean'],
['long', 'double', 'boolean'],
['long', 'integer', 'boolean'],
['long', 'long', 'boolean'],
['text', 'keyword', 'boolean'],
['text', 'text', 'boolean'],
['unsigned_long', 'unsigned_long', 'boolean'],
['version', 'version', 'boolean'],
];
function createComparisonDefinition(
{
name,
@ -58,6 +71,17 @@ function createComparisonDefinition(
},
validate?: FunctionDefinition['validate']
): FunctionDefinition {
const commonSignatures = baseComparisonTypeTable.map((functionSignature) => {
const [lhs, rhs, result] = functionSignature;
return {
params: [
{ name: 'left', type: lhs },
{ name: 'right', type: rhs },
],
returnType: result,
};
});
return {
type: 'builtin' as const,
name,
@ -66,41 +90,7 @@ function createComparisonDefinition(
supportedOptions: ['by'],
validate,
signatures: [
{
params: [
{ name: 'left', type: 'number' },
{ name: 'right', type: 'number' },
],
returnType: 'boolean',
},
{
params: [
{ name: 'left', type: 'string' },
{ name: 'right', type: 'string' },
],
returnType: 'boolean',
},
{
params: [
{ name: 'left', type: 'date' },
{ name: 'right', type: 'date' },
],
returnType: 'boolean',
},
{
params: [
{ name: 'left', type: 'ip' },
{ name: 'right', type: 'ip' },
],
returnType: 'boolean',
},
{
params: [
{ name: 'left', type: 'version' },
{ name: 'right', type: 'version' },
],
returnType: 'boolean',
},
...commonSignatures,
// constant strings okay because of implicit casting for
// string to version and ip
//
@ -113,13 +103,13 @@ function createComparisonDefinition(
{
params: [
{ name: 'left', type },
{ name: 'right', type: 'string' as const, constantOnly: true },
{ name: 'right', type: 'text' as const, constantOnly: true },
],
returnType: 'boolean' as const,
},
{
params: [
{ name: 'right', type: 'string' as const, constantOnly: true },
{ name: 'left', type: 'text' as const, constantOnly: true },
{ name: 'right', type },
],
returnType: 'boolean' as const,
@ -130,31 +120,111 @@ function createComparisonDefinition(
};
}
const addTypeTable: MathFunctionSignature[] = [
['date_period', 'date_period', 'date_period'],
['date_period', 'date', 'date'],
['date', 'date_period', 'date'],
['date', 'time_duration', 'date'],
['date', 'time_literal', 'date'],
['double', 'double', 'double'],
['double', 'integer', 'double'],
['double', 'long', 'double'],
['integer', 'double', 'double'],
['integer', 'integer', 'integer'],
['integer', 'long', 'long'],
['long', 'double', 'double'],
['long', 'integer', 'long'],
['long', 'long', 'long'],
['time_duration', 'date', 'date'],
['time_duration', 'time_duration', 'time_duration'],
['unsigned_long', 'unsigned_long', 'unsigned_long'],
['time_literal', 'date', 'date'],
];
const subtractTypeTable: MathFunctionSignature[] = [
['date_period', 'date_period', 'date_period'],
['date', 'date_period', 'date'],
['date', 'time_duration', 'date'],
['date', 'time_literal', 'date'],
['double', 'double', 'double'],
['double', 'integer', 'double'],
['double', 'long', 'double'],
['integer', 'double', 'double'],
['integer', 'integer', 'integer'],
['integer', 'long', 'long'],
['long', 'double', 'double'],
['long', 'integer', 'long'],
['long', 'long', 'long'],
['time_duration', 'date', 'date'],
['time_duration', 'time_duration', 'time_duration'],
['unsigned_long', 'unsigned_long', 'unsigned_long'],
['time_literal', 'date', 'date'],
];
const multiplyTypeTable: MathFunctionSignature[] = [
['double', 'double', 'double'],
['double', 'integer', 'double'],
['double', 'long', 'double'],
['integer', 'double', 'double'],
['integer', 'integer', 'integer'],
['integer', 'long', 'long'],
['long', 'double', 'double'],
['long', 'integer', 'long'],
['long', 'long', 'long'],
['unsigned_long', 'unsigned_long', 'unsigned_long'],
];
const divideTypeTable: MathFunctionSignature[] = [
['double', 'double', 'double'],
['double', 'integer', 'double'],
['double', 'long', 'double'],
['integer', 'double', 'double'],
['integer', 'integer', 'integer'],
['integer', 'long', 'long'],
['long', 'double', 'double'],
['long', 'integer', 'long'],
['long', 'long', 'long'],
['unsigned_long', 'unsigned_long', 'unsigned_long'],
];
const modulusTypeTable: MathFunctionSignature[] = [
['double', 'double', 'double'],
['double', 'integer', 'double'],
['double', 'long', 'double'],
['integer', 'double', 'double'],
['integer', 'integer', 'integer'],
['integer', 'long', 'long'],
['long', 'double', 'double'],
['long', 'integer', 'long'],
['long', 'long', 'long'],
['unsigned_long', 'unsigned_long', 'unsigned_long'],
];
export const mathFunctions: FunctionDefinition[] = [
createMathDefinition(
'+',
['number', ['date', 'time_literal', 'date'], ['time_literal', 'date', 'date']],
addTypeTable,
i18n.translate('kbn-esql-validation-autocomplete.esql.definition.addDoc', {
defaultMessage: 'Add (+)',
})
),
createMathDefinition(
'-',
['number', ['date', 'time_literal', 'date'], ['time_literal', 'date', 'date']],
subtractTypeTable,
i18n.translate('kbn-esql-validation-autocomplete.esql.definition.subtractDoc', {
defaultMessage: 'Subtract (-)',
})
),
createMathDefinition(
'*',
['number'],
multiplyTypeTable,
i18n.translate('kbn-esql-validation-autocomplete.esql.definition.multiplyDoc', {
defaultMessage: 'Multiply (*)',
})
),
createMathDefinition(
'/',
['number'],
divideTypeTable,
i18n.translate('kbn-esql-validation-autocomplete.esql.definition.divideDoc', {
defaultMessage: 'Divide (/)',
}),
@ -162,7 +232,7 @@ export const mathFunctions: FunctionDefinition[] = [
const [left, right] = fnDef.args;
const messages = [];
if (!Array.isArray(left) && !Array.isArray(right)) {
if (right.type === 'literal' && right.literalType === 'number') {
if (right.type === 'literal' && isNumericType(right.literalType)) {
if (right.value === 0) {
messages.push({
type: 'warning' as const,
@ -187,7 +257,7 @@ export const mathFunctions: FunctionDefinition[] = [
),
createMathDefinition(
'%',
['number'],
modulusTypeTable,
i18n.translate('kbn-esql-validation-autocomplete.esql.definition.moduleDoc', {
defaultMessage: 'Module (%)',
}),
@ -195,7 +265,7 @@ export const mathFunctions: FunctionDefinition[] = [
const [left, right] = fnDef.args;
const messages = [];
if (!Array.isArray(left) && !Array.isArray(right)) {
if (right.type === 'literal' && right.literalType === 'number') {
if (right.type === 'literal' && isNumericType(right.literalType)) {
if (right.value === 0) {
messages.push({
type: 'warning' as const,
@ -244,7 +314,7 @@ const comparisonFunctions: FunctionDefinition[] = [
},
{
params: [
{ name: 'right', type: 'string' as const, constantOnly: true },
{ name: 'left', type: 'string' as const, constantOnly: true },
{ name: 'right', type: 'boolean' as const },
],
returnType: 'boolean' as const,
@ -274,7 +344,7 @@ const comparisonFunctions: FunctionDefinition[] = [
},
{
params: [
{ name: 'right', type: 'string' as const, constantOnly: true },
{ name: 'left', type: 'string' as const, constantOnly: true },
{ name: 'right', type: 'boolean' as const },
],
returnType: 'boolean' as const,
@ -347,8 +417,15 @@ const likeFunctions: FunctionDefinition[] = [
signatures: [
{
params: [
{ name: 'left', type: 'string' as const },
{ name: 'right', type: 'string' as const },
{ name: 'left', type: 'text' as const },
{ name: 'right', type: 'text' as const },
],
returnType: 'boolean',
},
{
params: [
{ name: 'left', type: 'keyword' as const },
{ name: 'right', type: 'keyword' as const },
],
returnType: 'boolean',
},
@ -383,17 +460,24 @@ const inFunctions: FunctionDefinition[] = [
description,
supportedCommands: ['eval', 'where', 'row', 'sort'],
signatures: [
...ESQL_NUMBER_TYPES.map((type) => ({
params: [
{ name: 'left', type: type as FunctionParameterType },
{ name: 'right', type: 'any[]' as FunctionParameterType },
],
returnType: 'boolean' as FunctionReturnType,
})),
{
params: [
{ name: 'left', type: 'number' },
{ name: 'left', type: 'keyword' },
{ name: 'right', type: 'any[]' },
],
returnType: 'boolean',
},
{
params: [
{ name: 'left', type: 'string' },
{ name: 'left', type: 'text' },
{ name: 'right', type: 'any[]' },
],
returnType: 'boolean',

View file

@ -273,7 +273,7 @@ export const commandDefinitions: CommandDefinition[] = [
examples: ['… | limit 100', '… | limit 0'],
signature: {
multipleParams: false,
params: [{ name: 'size', type: 'number', constantOnly: true }],
params: [{ name: 'size', type: 'integer', constantOnly: true }],
},
options: [],
modes: [],
@ -390,6 +390,7 @@ export const commandDefinitions: CommandDefinition[] = [
signature: {
multipleParams: false,
params: [
// innerType: 'string' is interpreted as keyword and text (see columnParamsWithInnerTypes)
{ name: 'column', type: 'column', innerType: 'string' },
{ name: 'pattern', type: 'string', constantOnly: true },
],
@ -407,6 +408,7 @@ export const commandDefinitions: CommandDefinition[] = [
signature: {
multipleParams: false,
params: [
// innerType: 'string' is interpreted as keyword and text (see columnParamsWithInnerTypes)
{ name: 'column', type: 'column', innerType: 'string' },
{ name: 'pattern', type: 'string', constantOnly: true },
],

View file

@ -7,8 +7,53 @@
*/
import { i18n } from '@kbn/i18n';
import { FunctionDefinition } from './types';
import { FunctionDefinition, FunctionParameterType, FunctionReturnType } from './types';
const groupingTypeTable: Array<
[
FunctionParameterType,
FunctionParameterType,
FunctionParameterType | null,
FunctionParameterType | null,
FunctionReturnType
]
> = [
// field // bucket //from // to //result
['date', 'date_period', null, null, 'date'],
['date', 'integer', 'date', 'date', 'date'],
// Modified time_duration to time_literal
['date', 'time_literal', null, null, 'date'],
['double', 'double', null, null, 'double'],
['double', 'integer', 'double', 'double', 'double'],
['double', 'integer', 'double', 'integer', 'double'],
['double', 'integer', 'double', 'long', 'double'],
['double', 'integer', 'integer', 'double', 'double'],
['double', 'integer', 'integer', 'integer', 'double'],
['double', 'integer', 'integer', 'long', 'double'],
['double', 'integer', 'long', 'double', 'double'],
['double', 'integer', 'long', 'integer', 'double'],
['double', 'integer', 'long', 'long', 'double'],
['integer', 'double', null, null, 'double'],
['integer', 'integer', 'double', 'double', 'double'],
['integer', 'integer', 'double', 'integer', 'double'],
['integer', 'integer', 'double', 'long', 'double'],
['integer', 'integer', 'integer', 'double', 'double'],
['integer', 'integer', 'integer', 'integer', 'double'],
['integer', 'integer', 'integer', 'long', 'double'],
['integer', 'integer', 'long', 'double', 'double'],
['integer', 'integer', 'long', 'integer', 'double'],
['integer', 'integer', 'long', 'long', 'double'],
['long', 'double', null, null, 'double'],
['long', 'integer', 'double', 'double', 'double'],
['long', 'integer', 'double', 'integer', 'double'],
['long', 'integer', 'double', 'long', 'double'],
['long', 'integer', 'integer', 'double', 'double'],
['long', 'integer', 'integer', 'integer', 'double'],
['long', 'integer', 'integer', 'long', 'double'],
['long', 'integer', 'long', 'double', 'double'],
['long', 'integer', 'long', 'integer', 'double'],
['long', 'integer', 'long', 'long', 'double'],
];
export const groupingFunctionDefinitions: FunctionDefinition[] = [
{
name: 'bucket',
@ -21,65 +66,18 @@ export const groupingFunctionDefinitions: FunctionDefinition[] = [
supportedCommands: ['stats'],
supportedOptions: ['by'],
signatures: [
{
params: [
{ name: 'field', type: 'date' },
{ name: 'buckets', type: 'time_literal', constantOnly: true },
],
returnType: 'date',
},
{
params: [
{ name: 'field', type: 'number' },
{ name: 'buckets', type: 'number', constantOnly: true },
],
returnType: 'number',
},
{
params: [
{ name: 'field', type: 'date' },
{ name: 'buckets', type: 'number', constantOnly: true },
{ name: 'startDate', type: 'string', constantOnly: true },
{ name: 'endDate', type: 'string', constantOnly: true },
],
returnType: 'date',
},
{
params: [
{ name: 'field', type: 'date' },
{ name: 'buckets', type: 'number', constantOnly: true },
{ name: 'startDate', type: 'date', constantOnly: true },
{ name: 'endDate', type: 'date', constantOnly: true },
],
returnType: 'date',
},
{
params: [
{ name: 'field', type: 'date' },
{ name: 'buckets', type: 'number', constantOnly: true },
{ name: 'startDate', type: 'string', constantOnly: true },
{ name: 'endDate', type: 'date', constantOnly: true },
],
returnType: 'date',
},
{
params: [
{ name: 'field', type: 'date' },
{ name: 'buckets', type: 'number', constantOnly: true },
{ name: 'startDate', type: 'date', constantOnly: true },
{ name: 'endDate', type: 'string', constantOnly: true },
],
returnType: 'date',
},
{
params: [
{ name: 'field', type: 'number' },
{ name: 'buckets', type: 'number', constantOnly: true },
{ name: 'startValue', type: 'number', constantOnly: true },
{ name: 'endValue', type: 'number', constantOnly: true },
],
returnType: 'number',
},
...groupingTypeTable.map((signature) => {
const [fieldType, bucketType, fromType, toType, resultType] = signature;
return {
params: [
{ name: 'field', type: fieldType },
{ name: 'buckets', type: bucketType, constantOnly: true },
...(fromType ? [{ name: 'startDate', type: fromType, constantOnly: true }] : []),
...(toType ? [{ name: 'endDate', type: toType, constantOnly: true }] : []),
],
returnType: resultType,
};
}),
],
examples: [
'from index | eval hd = bucket(bytes, 1 hour)',

View file

@ -8,10 +8,20 @@
import type { ESQLCommand, ESQLCommandOption, ESQLFunction, ESQLMessage } from '@kbn/esql-ast';
// Currently, partial of the full list
// https://github.com/elastic/elasticsearch/blob/main/x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/DataType.java
export const supportedFieldTypes = [
'number',
'double',
'unsigned_long',
'long',
'integer',
'counter_integer',
'counter_long',
'counter_double',
'date',
'string',
'date_period',
'text',
'keyword',
'boolean',
'ip',
'cartesian_point',
@ -28,21 +38,43 @@ export type SupportedFieldType = (typeof supportedFieldTypes)[number];
export type FunctionParameterType =
| SupportedFieldType
| 'string'
| 'null'
| 'any'
| 'chrono_literal'
| 'time_literal'
| 'number[]'
| 'time_duration'
| 'double[]'
| 'unsigned_long[]'
| 'long[]'
| 'integer[]'
| 'counter_integer[]'
| 'counter_long[]'
| 'counter_double[]'
| 'string[]'
| 'keyword[]'
| 'text[]'
| 'boolean[]'
| 'any[]'
| 'date[]';
| 'datetime[]'
| 'date_period[]';
export type FunctionReturnType =
| 'number'
| 'double'
| 'unsigned_long'
| 'long'
| 'integer'
| 'int'
| 'counter_integer'
| 'counter_long'
| 'counter_double'
| 'date'
| 'date_period'
| 'time_duration'
| 'any'
| 'boolean'
| 'text'
| 'keyword'
| 'string'
| 'cartesian_point'
| 'cartesian_shape'

View file

@ -1,44 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
const ESQL_NUMBER_TYPES = [
'double',
'unsigned_long',
'long',
'integer',
'int',
'counter_integer',
'counter_long',
'counter_double',
];
const ESQL_TEXT_TYPES = ['text', 'keyword', 'string'];
export const esqlToKibanaType = (elasticsearchType: string) => {
if (ESQL_NUMBER_TYPES.includes(elasticsearchType)) {
return 'number';
}
if (ESQL_TEXT_TYPES.includes(elasticsearchType)) {
return 'string';
}
if (['datetime', 'time_duration'].includes(elasticsearchType)) {
return 'date';
}
if (elasticsearchType === 'bool') {
return 'boolean';
}
if (elasticsearchType === 'date_period') {
return 'time_literal'; // TODO - consider aligning with Elasticsearch
}
return elasticsearchType;
};

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ESQLDecimalLiteral, ESQLNumericLiteralType } from '@kbn/esql-ast/src/types';
export const ESQL_COMMON_NUMERIC_TYPES = ['double', 'long', 'integer'] as const;
export const ESQL_NUMERIC_DECIMAL_TYPES = [
'double',
'unsigned_long',
'long',
'counter_long',
'counter_double',
] as const;
export const ESQL_NUMBER_TYPES = [
'integer',
'counter_integer',
...ESQL_NUMERIC_DECIMAL_TYPES,
] as const;
export const ESQL_STRING_TYPES = ['keyword', 'text'] as const;
export const ESQL_DATE_TYPES = ['datetime', 'date_period'] as const;
/**
*
* @param type
* @returns
*/
export function isStringType(type: unknown) {
return typeof type === 'string' && ['keyword', 'text'].includes(type);
}
export function isNumericType(type: unknown): type is ESQLNumericLiteralType {
return (
typeof type === 'string' &&
[...ESQL_NUMBER_TYPES, 'decimal'].includes(type as (typeof ESQL_NUMBER_TYPES)[number])
);
}
export function isNumericDecimalType(type: unknown): type is ESQLDecimalLiteral {
return (
typeof type === 'string' &&
ESQL_NUMERIC_DECIMAL_TYPES.includes(type as (typeof ESQL_NUMERIC_DECIMAL_TYPES)[number])
);
}

View file

@ -33,7 +33,7 @@ import {
withOption,
appendSeparatorOption,
} from '../definitions/options';
import type {
import {
CommandDefinition,
CommandOptionsDefinition,
FunctionParameter,
@ -43,7 +43,7 @@ import type {
} from '../definitions/types';
import type { ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types';
import { removeMarkerArgFromArgsList } from './context';
import { esqlToKibanaType } from './esql_to_kibana_type';
import { isNumericDecimalType } from './esql_types';
import type { ReasonTypes } from './types';
export function nonNullable<T>(v: T): v is NonNullable<T> {
@ -226,6 +226,14 @@ function compareLiteralType(argType: string, item: ESQLLiteral) {
return true;
}
if (item.literalType === 'decimal' && isNumericDecimalType(argType)) {
return true;
}
if (item.literalType === 'string' && (argType === 'text' || argType === 'keyword')) {
return true;
}
if (item.literalType !== 'string') {
if (argType === item.literalType) {
return true;
@ -234,7 +242,7 @@ function compareLiteralType(argType: string, item: ESQLLiteral) {
}
// date-type parameters accept string literals because of ES auto-casting
return ['string', 'date'].includes(argType);
return ['string', 'date', 'date', 'date_period'].includes(argType);
}
/**
@ -245,7 +253,14 @@ export function lookupColumn(
{ fields, variables }: Pick<ReferenceMaps, 'fields' | 'variables'>
): ESQLRealField | ESQLVariable | undefined {
const columnName = getQuotedColumnName(column);
return fields.get(columnName) || variables.get(columnName)?.[0];
return (
fields.get(columnName) ||
variables.get(columnName)?.[0] ||
// It's possible columnName has backticks "`fieldName`"
// so we need to access the original name as well
fields.get(column.name) ||
variables.get(column.name)?.[0]
);
}
const ARRAY_REGEXP = /\[\]$/;
@ -255,10 +270,19 @@ export function isArrayType(type: string) {
}
const arrayToSingularMap: Map<FunctionParameterType, FunctionParameterType> = new Map([
['number[]', 'number'],
['date[]', 'date'],
['boolean[]', 'boolean'],
['double[]', 'double'],
['unsigned_long[]', 'unsigned_long'],
['long[]', 'long'],
['integer[]', 'integer'],
['counter_integer[]', 'counter_integer'],
['counter_long[]', 'counter_long'],
['counter_double[]', 'counter_double'],
['string[]', 'string'],
['keyword[]', 'keyword'],
['text[]', 'text'],
['datetime[]', 'date'],
['date_period[]', 'date_period'],
['boolean[]', 'boolean'],
['any[]', 'any'],
]);
@ -407,7 +431,8 @@ export function checkFunctionArgMatchesDefinition(
return true;
}
if (arg.type === 'literal') {
return compareLiteralType(argType, arg);
const matched = compareLiteralType(argType, arg);
return matched;
}
if (arg.type === 'function') {
if (isSupportedFunction(arg.name, parentCommand).supported) {
@ -428,11 +453,21 @@ export function checkFunctionArgMatchesDefinition(
}
const wrappedTypes = Array.isArray(validHit.type) ? validHit.type : [validHit.type];
// if final type is of type any make it pass for now
return wrappedTypes.some((ct) => ['any', 'null'].includes(ct) || argType === ct);
return wrappedTypes.some(
(ct) =>
['any', 'null'].includes(ct) ||
argType === ct ||
(ct === 'string' && ['text', 'keyword'].includes(argType))
);
}
if (arg.type === 'inlineCast') {
// TODO - remove with https://github.com/elastic/kibana/issues/174710
return argType === esqlToKibanaType(arg.castType);
const lowerArgType = argType?.toLowerCase();
const lowerArgCastType = arg.castType?.toLowerCase();
return (
lowerArgType === lowerArgCastType ||
// for valid shorthand casts like 321.12::int or "false"::bool
(['int', 'bool'].includes(lowerArgCastType) && argType.startsWith(lowerArgCastType))
);
}
}

View file

@ -35,7 +35,7 @@ function addToVariables(
if (isColumnItem(oldArg) && isColumnItem(newArg)) {
const newVariable: ESQLVariable = {
name: newArg.name,
type: 'number' /* fallback to number */,
type: 'double' /* fallback to number */,
location: newArg.location,
};
// Now workout the exact type
@ -107,7 +107,7 @@ function addVariableFromAssignment(
const rightHandSideArgType = getAssignRightHandSideType(assignOperation.args[1], fields);
addToVariableOccurrencies(variables, {
name: assignOperation.args[0].name,
type: rightHandSideArgType || 'number' /* fallback to number */,
type: rightHandSideArgType || 'double' /* fallback to number */,
location: assignOperation.args[0].location,
});
}
@ -125,7 +125,7 @@ function addVariableFromExpression(
queryString,
expressionOperation.location
);
const expressionType = 'number';
const expressionType = 'double';
addToVariableOccurrencies(variables, {
name: forwardThinkingVariableName,
type: expressionType,

View file

@ -31,7 +31,7 @@ export const setup = async () => {
return await validateQuery(query, getAstAndSyntaxErrors, opts, cb);
};
const assertErrors = (errors: unknown[], expectedErrors: string[]) => {
const assertErrors = (errors: unknown[], expectedErrors: string[], query?: string) => {
const errorMessages: string[] = [];
for (const error of errors) {
if (error && typeof error === 'object') {
@ -46,7 +46,16 @@ export const setup = async () => {
errorMessages.push(String(error));
}
}
expect(errorMessages.sort()).toStrictEqual(expectedErrors.sort());
try {
expect(errorMessages.sort()).toStrictEqual(expectedErrors.sort());
} catch (error) {
throw Error(`${query}\n
Received:
'${errorMessages.sort()}'
Expected:
${expectedErrors.sort()}`);
}
};
const expectErrors = async (
@ -57,9 +66,9 @@ export const setup = async () => {
cb: ESQLCallbacks = callbacks
) => {
const { errors, warnings } = await validateQuery(query, getAstAndSyntaxErrors, opts, cb);
assertErrors(errors, expectedErrors);
assertErrors(errors, expectedErrors, query);
if (expectedWarnings) {
assertErrors(warnings, expectedWarnings);
assertErrors(warnings, expectedWarnings, query);
}
};

View file

@ -85,7 +85,7 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
await expectErrors(`METRICS average()`, ['Unknown index [average()]']);
await expectErrors(`metrics custom_function()`, ['Unknown index [custom_function()]']);
await expectErrors(`metrics indexes*`, ['Unknown index [indexes*]']);
await expectErrors('metrics numberField', ['Unknown index [numberField]']);
await expectErrors('metrics doubleField', ['Unknown index [doubleField]']);
await expectErrors('metrics policy', ['Unknown index [policy]']);
});
});
@ -95,26 +95,26 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
const { expectErrors } = await setup();
await expectErrors('METRICS a_index count()', []);
await expectErrors('metrics a_index avg(numberField) by 1', []);
await expectErrors('metrics a_index count(`numberField`)', []);
await expectErrors('metrics a_index avg(doubleField) by 1', []);
await expectErrors('metrics a_index count(`doubleField`)', []);
await expectErrors('metrics a_index count(*)', []);
await expectErrors('metrics index var0 = count(*)', []);
await expectErrors('metrics a_index var0 = count()', []);
await expectErrors('metrics a_index var0 = avg(numberField), count(*)', []);
await expectErrors('metrics a_index var0 = avg(doubleField), count(*)', []);
await expectErrors(`metrics a_index sum(case(false, 0, 1))`, []);
await expectErrors(`metrics a_index var0 = sum( case(false, 0, 1))`, []);
await expectErrors('metrics a_index count(stringField == "a" or null)', []);
await expectErrors('metrics other_index max(numberField) by stringField', []);
await expectErrors('metrics a_index count(textField == "a" or null)', []);
await expectErrors('metrics other_index max(doubleField) by textField', []);
});
test('syntax errors', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index numberField=', [
await expectErrors('metrics a_index doubleField=', [
expect.any(String),
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
await expectErrors('metrics a_index numberField=5 by ', [
await expectErrors('metrics a_index doubleField=5 by ', [
expect.any(String),
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
@ -131,29 +131,29 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
test('errors when no aggregation function specified', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index numberField + 1', [
'At least one aggregation function required in [METRICS], found [numberField+1]',
await expectErrors('metrics a_index doubleField + 1', [
'At least one aggregation function required in [METRICS], found [doubleField+1]',
]);
await expectErrors('metrics a_index a = numberField + 1', [
'At least one aggregation function required in [METRICS], found [a=numberField+1]',
await expectErrors('metrics a_index a = doubleField + 1', [
'At least one aggregation function required in [METRICS], found [a=doubleField+1]',
]);
await expectErrors('metrics a_index a = numberField + 1, stringField', [
'At least one aggregation function required in [METRICS], found [a=numberField+1]',
'Expected an aggregate function or group but got [stringField] of type [FieldAttribute]',
await expectErrors('metrics a_index a = doubleField + 1, textField', [
'At least one aggregation function required in [METRICS], found [a=doubleField+1]',
'Expected an aggregate function or group but got [textField] of type [FieldAttribute]',
]);
await expectErrors('metrics a_index numberField + 1 by ipField', [
'At least one aggregation function required in [METRICS], found [numberField+1]',
await expectErrors('metrics a_index doubleField + 1 by ipField', [
'At least one aggregation function required in [METRICS], found [doubleField+1]',
]);
});
test('errors on agg and non-agg mix', async () => {
const { expectErrors } = await setup();
await expectErrors('METRICS a_index sum( numberField ) + abs( numberField ) ', [
'Cannot combine aggregation and non-aggregation values in [METRICS], found [sum(numberField)+abs(numberField)]',
await expectErrors('METRICS a_index sum( doubleField ) + abs( doubleField ) ', [
'Cannot combine aggregation and non-aggregation values in [METRICS], found [sum(doubleField)+abs(doubleField)]',
]);
await expectErrors('METRICS a_index abs( numberField + sum( numberField )) ', [
'Cannot combine aggregation and non-aggregation values in [METRICS], found [abs(numberField+sum(numberField))]',
await expectErrors('METRICS a_index abs( doubleField + sum( doubleField )) ', [
'Cannot combine aggregation and non-aggregation values in [METRICS], found [abs(doubleField+sum(doubleField))]',
]);
});
@ -169,8 +169,8 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
test('errors when input is not an aggregate function', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index numberField ', [
'Expected an aggregate function or group but got [numberField] of type [FieldAttribute]',
await expectErrors('metrics a_index doubleField ', [
'Expected an aggregate function or group but got [doubleField] of type [FieldAttribute]',
]);
});
@ -179,9 +179,9 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
for (const subCommand of ['keep', 'drop', 'eval']) {
await expectErrors(
'metrics a_index count(`numberField`) | ' +
'metrics a_index count(`doubleField`) | ' +
subCommand +
' `count(``numberField``)` ',
' `count(``doubleField``)` ',
[]
);
}
@ -194,7 +194,7 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
'Using wildcards (*) in round is not allowed',
]);
await expectErrors('metrics a_index count(count(*))', [
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [number]`,
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [long]`,
]);
});
});
@ -204,21 +204,21 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
const { expectErrors } = await setup();
await expectErrors(
'metrics a_index avg(numberField), percentile(numberField, 50) by ipField',
'metrics a_index avg(doubleField), percentile(doubleField, 50) by ipField',
[]
);
await expectErrors(
'metrics a_index avg(numberField), percentile(numberField, 50) BY ipField',
'metrics a_index avg(doubleField), percentile(doubleField, 50) BY ipField',
[]
);
await expectErrors(
'metrics a_index avg(numberField), percentile(numberField, 50) + 1 by ipField',
'metrics a_index avg(doubleField), percentile(doubleField, 50) + 1 by ipField',
[]
);
await expectErrors('metrics a_index avg(numberField) by stringField | limit 100', []);
await expectErrors('metrics a_index avg(doubleField) by textField | limit 100', []);
for (const op of ['+', '-', '*', '/', '%']) {
await expectErrors(
`metrics a_index avg(numberField) ${op} percentile(numberField, 50) BY ipField`,
`metrics a_index avg(doubleField) ${op} percentile(doubleField, 50) BY ipField`,
[]
);
}
@ -227,9 +227,9 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
test('syntax does not allow <grouping> clause without <aggregates>', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index BY stringField', [
await expectErrors('metrics a_index BY textField', [
'Expected an aggregate function or group but got [BY] of type [FieldAttribute]',
"SyntaxError: extraneous input 'stringField' expecting <EOF>",
"SyntaxError: extraneous input 'textField' expecting <EOF>",
]);
});
@ -239,7 +239,7 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
await expectErrors('metrics a_index count(* + 1) BY ipField', [
"SyntaxError: no viable alternative at input 'count(* +'",
]);
await expectErrors('metrics a_index \n count(* + round(numberField)) BY ipField', [
await expectErrors('metrics a_index \n count(* + round(doubleField)) BY ipField', [
"SyntaxError: no viable alternative at input 'count(* +'",
]);
});
@ -251,20 +251,20 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
'Using wildcards (*) in round is not allowed',
]);
await expectErrors('metrics a_index count(count(*)) BY ipField', [
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [number]`,
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [long]`,
]);
});
test('errors on unknown field', async () => {
const { expectErrors } = await setup();
await expectErrors('metrics a_index avg(numberField) by wrongField', [
await expectErrors('metrics a_index avg(doubleField) by wrongField', [
'Unknown column [wrongField]',
]);
await expectErrors('metrics a_index avg(numberField) by wrongField + 1', [
await expectErrors('metrics a_index avg(doubleField) by wrongField + 1', [
'Unknown column [wrongField]',
]);
await expectErrors('metrics a_index avg(numberField) by var0 = wrongField + 1', [
await expectErrors('metrics a_index avg(doubleField) by var0 = wrongField + 1', [
'Unknown column [wrongField]',
]);
});
@ -272,11 +272,11 @@ export const validationMetricsCommandTestSuite = (setup: helpers.Setup) => {
test('various errors', async () => {
const { expectErrors } = await setup();
await expectErrors('METRICS a_index avg(numberField) by percentile(numberField)', [
await expectErrors('METRICS a_index avg(doubleField) by percentile(doubleField)', [
'METRICS BY does not support function percentile',
]);
await expectErrors(
'METRICS a_index avg(numberField) by stringField, percentile(numberField) by ipField',
'METRICS a_index avg(doubleField) by textField, percentile(doubleField) by ipField',
[
"SyntaxError: mismatched input 'by' expecting <EOF>",
'METRICS BY does not support function percentile',

View file

@ -15,15 +15,15 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
test('no errors on correct usage', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats by stringField', []);
await expectErrors('from a_index | stats by textField', []);
await expectErrors(
`FROM index
| EVAL numberField * 3.281
| STATS avg_numberField = AVG(\`numberField * 3.281\`)`,
| EVAL doubleField * 3.281
| STATS avg_doubleField = AVG(\`doubleField * 3.281\`)`,
[]
);
await expectErrors(
`FROM index | STATS AVG(numberField) by round(numberField) + 1 | EVAL \`round(numberField) + 1\` / 2`,
`FROM index | STATS AVG(doubleField) by round(doubleField) + 1 | EVAL \`round(doubleField) + 1\` / 2`,
[]
);
});
@ -40,18 +40,18 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
test('no errors on correct usage', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats avg(numberField) by 1', []);
await expectErrors('from a_index | stats count(`numberField`)', []);
await expectErrors('from a_index | stats avg(doubleField) by 1', []);
await expectErrors('from a_index | stats count(`doubleField`)', []);
await expectErrors('from a_index | stats count(*)', []);
await expectErrors('from a_index | stats count()', []);
await expectErrors('from a_index | stats var0 = count(*)', []);
await expectErrors('from a_index | stats var0 = count()', []);
await expectErrors('from a_index | stats var0 = avg(numberField), count(*)', []);
await expectErrors('from a_index | stats var0 = avg(doubleField), count(*)', []);
await expectErrors(`from a_index | stats sum(case(false, 0, 1))`, []);
await expectErrors(`from a_index | stats var0 = sum( case(false, 0, 1))`, []);
// "or" must accept "null"
await expectErrors('from a_index | stats count(stringField == "a" or null)', []);
await expectErrors('from a_index | stats count(textField == "a" or null)', []);
});
test('sub-command can reference aggregated field', async () => {
@ -59,9 +59,9 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
for (const subCommand of ['keep', 'drop', 'eval']) {
await expectErrors(
'from a_index | stats count(`numberField`) | ' +
'from a_index | stats count(`doubleField`) | ' +
subCommand +
' `count(``numberField``)` ',
' `count(``doubleField``)` ',
[]
);
}
@ -70,64 +70,64 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
test('errors on agg and non-agg mix', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | STATS sum( numberField ) + abs( numberField ) ', [
'Cannot combine aggregation and non-aggregation values in [STATS], found [sum(numberField)+abs(numberField)]',
await expectErrors('from a_index | STATS sum( doubleField ) + abs( doubleField ) ', [
'Cannot combine aggregation and non-aggregation values in [STATS], found [sum(doubleField)+abs(doubleField)]',
]);
await expectErrors('from a_index | STATS abs( numberField + sum( numberField )) ', [
'Cannot combine aggregation and non-aggregation values in [STATS], found [abs(numberField+sum(numberField))]',
await expectErrors('from a_index | STATS abs( doubleField + sum( doubleField )) ', [
'Cannot combine aggregation and non-aggregation values in [STATS], found [abs(doubleField+sum(doubleField))]',
]);
});
test('errors on each aggregation field, which does not contain at least one agg function', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats numberField + 1', [
'At least one aggregation function required in [STATS], found [numberField+1]',
await expectErrors('from a_index | stats doubleField + 1', [
'At least one aggregation function required in [STATS], found [doubleField+1]',
]);
await expectErrors('from a_index | stats numberField + 1, stringField', [
'At least one aggregation function required in [STATS], found [numberField+1]',
'Expected an aggregate function or group but got [stringField] of type [FieldAttribute]',
await expectErrors('from a_index | stats doubleField + 1, textField', [
'At least one aggregation function required in [STATS], found [doubleField+1]',
'Expected an aggregate function or group but got [textField] of type [FieldAttribute]',
]);
await expectErrors('from a_index | stats numberField + 1, numberField + 2, count()', [
'At least one aggregation function required in [STATS], found [numberField+1]',
'At least one aggregation function required in [STATS], found [numberField+2]',
await expectErrors('from a_index | stats doubleField + 1, doubleField + 2, count()', [
'At least one aggregation function required in [STATS], found [doubleField+1]',
'At least one aggregation function required in [STATS], found [doubleField+2]',
]);
await expectErrors(
'from a_index | stats numberField + 1, numberField + count(), count()',
['At least one aggregation function required in [STATS], found [numberField+1]']
'from a_index | stats doubleField + 1, doubleField + count(), count()',
['At least one aggregation function required in [STATS], found [doubleField+1]']
);
await expectErrors('from a_index | stats 5 + numberField + 1', [
'At least one aggregation function required in [STATS], found [5+numberField+1]',
await expectErrors('from a_index | stats 5 + doubleField + 1', [
'At least one aggregation function required in [STATS], found [5+doubleField+1]',
]);
await expectErrors('from a_index | stats numberField + 1 by ipField', [
'At least one aggregation function required in [STATS], found [numberField+1]',
await expectErrors('from a_index | stats doubleField + 1 by ipField', [
'At least one aggregation function required in [STATS], found [doubleField+1]',
]);
});
test('errors when input is not an aggregate function', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats numberField ', [
'Expected an aggregate function or group but got [numberField] of type [FieldAttribute]',
await expectErrors('from a_index | stats doubleField ', [
'Expected an aggregate function or group but got [doubleField] of type [FieldAttribute]',
]);
});
test('various errors', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats numberField=', [
await expectErrors('from a_index | stats doubleField=', [
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
await expectErrors('from a_index | stats numberField=5 by ', [
await expectErrors('from a_index | stats doubleField=5 by ', [
"SyntaxError: mismatched input '<EOF>' expecting {QUOTED_STRING, INTEGER_LITERAL, DECIMAL_LITERAL, 'false', '(', 'not', 'null', '?', 'true', '+', '-', NAMED_OR_POSITIONAL_PARAM, OPENING_BRACKET, UNQUOTED_IDENTIFIER, QUOTED_IDENTIFIER}",
]);
await expectErrors('from a_index | stats avg(numberField) by wrongField', [
await expectErrors('from a_index | stats avg(doubleField) by wrongField', [
'Unknown column [wrongField]',
]);
await expectErrors('from a_index | stats avg(numberField) by wrongField + 1', [
await expectErrors('from a_index | stats avg(doubleField) by wrongField + 1', [
'Unknown column [wrongField]',
]);
await expectErrors('from a_index | stats avg(numberField) by var0 = wrongField + 1', [
await expectErrors('from a_index | stats avg(doubleField) by var0 = wrongField + 1', [
'Unknown column [wrongField]',
]);
await expectErrors('from a_index | stats var0 = avg(fn(number)), count(*)', [
@ -142,7 +142,7 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
'Using wildcards (*) in round is not allowed',
]);
await expectErrors('from a_index | stats count(count(*))', [
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [number]`,
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [long]`,
]);
});
});
@ -152,20 +152,20 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
const { expectErrors } = await setup();
await expectErrors(
'from a_index | stats avg(numberField), percentile(numberField, 50) by ipField',
'from a_index | stats avg(doubleField), percentile(doubleField, 50) by ipField',
[]
);
await expectErrors(
'from a_index | stats avg(numberField), percentile(numberField, 50) BY ipField',
'from a_index | stats avg(doubleField), percentile(doubleField, 50) BY ipField',
[]
);
await expectErrors(
'from a_index | stats avg(numberField), percentile(numberField, 50) + 1 by ipField',
'from a_index | stats avg(doubleField), percentile(doubleField, 50) + 1 by ipField',
[]
);
for (const op of ['+', '-', '*', '/', '%']) {
await expectErrors(
`from a_index | stats avg(numberField) ${op} percentile(numberField, 50) BY ipField`,
`from a_index | stats avg(doubleField) ${op} percentile(doubleField, 50) BY ipField`,
[]
);
}
@ -185,7 +185,7 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
await expectErrors('from a_index | stats count(* + 1) BY ipField', [
"SyntaxError: no viable alternative at input 'count(* +'",
]);
await expectErrors('from a_index | stats count(* + round(numberField)) BY ipField', [
await expectErrors('from a_index | stats count(* + round(doubleField)) BY ipField', [
"SyntaxError: no viable alternative at input 'count(* +'",
]);
});
@ -197,18 +197,18 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
'Using wildcards (*) in round is not allowed',
]);
await expectErrors('from a_index | stats count(count(*)) BY ipField', [
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [number]`,
`Aggregate function's parameters must be an attribute, literal or a non-aggregation function; found [count(*)] of type [long]`,
]);
});
test('various errors', async () => {
const { expectErrors } = await setup();
await expectErrors('from a_index | stats avg(numberField) by percentile(numberField)', [
await expectErrors('from a_index | stats avg(doubleField) by percentile(doubleField)', [
'STATS BY does not support function percentile',
]);
await expectErrors(
'from a_index | stats avg(numberField) by stringField, percentile(numberField) by ipField',
'from a_index | stats avg(doubleField) by textField, percentile(doubleField) by ipField',
[
"SyntaxError: mismatched input 'by' expecting <EOF>",
'STATS BY does not support function percentile',
@ -220,34 +220,37 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
test('no errors', async () => {
const { expectErrors } = await setup();
await expectErrors('from index | stats by bucket(dateField, pi(), "", "")', []);
await expectErrors(
'from index | stats by bucket(dateField, 1 + 30 / 10, "", "")',
[]
);
await expectErrors(
'from index | stats by bucket(dateField, 1 + 30 / 10, concat("", ""), "")',
[]
['Argument of [bucket] must be [date], found value [concat("","")] type [keyword]']
);
});
test('errors', async () => {
const { expectErrors } = await setup();
await expectErrors('from index | stats by bucket(dateField, pi(), "", "")', [
'Argument of [bucket] must be [integer], found value [pi()] type [double]',
]);
await expectErrors(
'from index | stats by bucket(dateField, abs(numberField), "", "")',
['Argument of [bucket] must be a constant, received [abs(numberField)]']
'from index | stats by bucket(dateField, abs(doubleField), "", "")',
['Argument of [bucket] must be a constant, received [abs(doubleField)]']
);
await expectErrors(
'from index | stats by bucket(dateField, abs(length(numberField)), "", "")',
['Argument of [bucket] must be a constant, received [abs(length(numberField))]']
'from index | stats by bucket(dateField, abs(length(doubleField)), "", "")',
['Argument of [bucket] must be a constant, received [abs(length(doubleField))]']
);
await expectErrors(
'from index | stats by bucket(dateField, numberField, stringField, stringField)',
'from index | stats by bucket(dateField, doubleField, textField, textField)',
[
'Argument of [bucket] must be a constant, received [numberField]',
'Argument of [bucket] must be a constant, received [stringField]',
'Argument of [bucket] must be a constant, received [stringField]',
'Argument of [bucket] must be a constant, received [doubleField]',
'Argument of [bucket] must be a constant, received [textField]',
'Argument of [bucket] must be a constant, received [textField]',
]
);
});
@ -269,11 +272,11 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
const { expectErrors } = await setup();
await expectErrors(
`from a_index | stats 5 + avg(numberField) ${builtinWrapping}`,
`from a_index | stats 5 + avg(doubleField) ${builtinWrapping}`,
[]
);
await expectErrors(
`from a_index | stats 5 ${builtinWrapping} + avg(numberField)`,
`from a_index | stats 5 ${builtinWrapping} + avg(doubleField)`,
[]
);
});
@ -281,16 +284,16 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
test('errors', async () => {
const { expectErrors } = await setup();
await expectErrors(`from a_index | stats 5 ${builtinWrapping} + numberField`, [
`At least one aggregation function required in [STATS], found [5${builtinWrapping}+numberField]`,
await expectErrors(`from a_index | stats 5 ${builtinWrapping} + doubleField`, [
`At least one aggregation function required in [STATS], found [5${builtinWrapping}+doubleField]`,
]);
await expectErrors(`from a_index | stats 5 + numberField ${builtinWrapping}`, [
`At least one aggregation function required in [STATS], found [5+numberField${builtinWrapping}]`,
await expectErrors(`from a_index | stats 5 + doubleField ${builtinWrapping}`, [
`At least one aggregation function required in [STATS], found [5+doubleField${builtinWrapping}]`,
]);
await expectErrors(
`from a_index | stats 5 + numberField ${builtinWrapping}, var0 = sum(numberField)`,
`from a_index | stats 5 + doubleField ${builtinWrapping}, var0 = sum(doubleField)`,
[
`At least one aggregation function required in [STATS], found [5+numberField${builtinWrapping}]`,
`At least one aggregation function required in [STATS], found [5+doubleField${builtinWrapping}]`,
]
);
});
@ -304,31 +307,31 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
const { expectErrors } = await setup();
await expectErrors(
`from a_index | stats ${evalWrapping} sum(numberField) ${closingWrapping}`,
`from a_index | stats ${evalWrapping} sum(doubleField) ${closingWrapping}`,
[]
);
await expectErrors(
`from a_index | stats ${evalWrapping} sum(numberField) ${closingWrapping} + ${evalWrapping} sum(numberField) ${closingWrapping}`,
`from a_index | stats ${evalWrapping} sum(doubleField) ${closingWrapping} + ${evalWrapping} sum(doubleField) ${closingWrapping}`,
[]
);
await expectErrors(
`from a_index | stats ${evalWrapping} sum(numberField + numberField) ${closingWrapping}`,
`from a_index | stats ${evalWrapping} sum(doubleField + doubleField) ${closingWrapping}`,
[]
);
await expectErrors(
`from a_index | stats ${evalWrapping} sum(numberField + round(numberField)) ${closingWrapping}`,
`from a_index | stats ${evalWrapping} sum(doubleField + round(doubleField)) ${closingWrapping}`,
[]
);
await expectErrors(
`from a_index | stats ${evalWrapping} sum(numberField + round(numberField)) ${closingWrapping} + ${evalWrapping} sum(numberField + round(numberField)) ${closingWrapping}`,
`from a_index | stats ${evalWrapping} sum(doubleField + round(doubleField)) ${closingWrapping} + ${evalWrapping} sum(doubleField + round(doubleField)) ${closingWrapping}`,
[]
);
await expectErrors(
`from a_index | stats sum(${evalWrapping} numberField ${closingWrapping} )`,
`from a_index | stats sum(${evalWrapping} doubleField ${closingWrapping} )`,
[]
);
await expectErrors(
`from a_index | stats sum(${evalWrapping} numberField ${closingWrapping} ) + sum(${evalWrapping} numberField ${closingWrapping} )`,
`from a_index | stats sum(${evalWrapping} doubleField ${closingWrapping} ) + sum(${evalWrapping} doubleField ${closingWrapping} )`,
[]
);
});
@ -337,21 +340,21 @@ export const validationStatsCommandTestSuite = (setup: helpers.Setup) => {
const { expectErrors } = await setup();
await expectErrors(
`from a_index | stats ${evalWrapping} numberField + sum(numberField) ${closingWrapping}`,
`from a_index | stats ${evalWrapping} doubleField + sum(doubleField) ${closingWrapping}`,
[
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalWrapping}numberField+sum(numberField)${closingWrapping}]`,
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalWrapping}doubleField+sum(doubleField)${closingWrapping}]`,
]
);
await expectErrors(
`from a_index | stats ${evalWrapping} numberField + sum(numberField) ${closingWrapping}, var0 = sum(numberField)`,
`from a_index | stats ${evalWrapping} doubleField + sum(doubleField) ${closingWrapping}, var0 = sum(doubleField)`,
[
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalWrapping}numberField+sum(numberField)${closingWrapping}]`,
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalWrapping}doubleField+sum(doubleField)${closingWrapping}]`,
]
);
await expectErrors(
`from a_index | stats var0 = ${evalWrapping} numberField + sum(numberField) ${closingWrapping}, var1 = sum(numberField)`,
`from a_index | stats var0 = ${evalWrapping} doubleField + sum(doubleField) ${closingWrapping}, var1 = sum(doubleField)`,
[
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalWrapping}numberField+sum(numberField)${closingWrapping}]`,
`Cannot combine aggregation and non-aggregation values in [STATS], found [${evalWrapping}doubleField+sum(doubleField)${closingWrapping}]`,
]
);
});

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { setup } from './helpers';
describe('validation', () => {
describe('command', () => {
test('date_diff', async () => {
const { expectErrors } = await setup();
await expectErrors(
'row var = date_diff("month", "2023-12-02T11:00:00.000Z", "2023-12-02T11:00:00.000Z")',
[]
);
await expectErrors(
'row var = date_diff("mm", "2023-12-02T11:00:00.000Z", "2023-12-02T11:00:00.000Z")',
[]
);
await expectErrors(
'row var = date_diff("bogus", "2023-12-02T11:00:00.000Z", "2023-12-02T11:00:00.000Z")',
[]
);
await expectErrors(
'from a_index | eval date_diff(textField, "2023-12-02T11:00:00.000Z", "2023-12-02T11:00:00.000Z")',
[]
);
await expectErrors(
'from a_index | eval date_diff("month", dateField, "2023-12-02T11:00:00.000Z")',
[]
);
await expectErrors(
'from a_index | eval date_diff("month", "2023-12-02T11:00:00.000Z", dateField)',
[]
);
await expectErrors('from a_index | eval date_diff("month", textField, dateField)', [
'Argument of [date_diff] must be [date], found value [textField] type [text]',
]);
await expectErrors('from a_index | eval date_diff("month", dateField, textField)', [
'Argument of [date_diff] must be [date], found value [textField] type [text]',
]);
await expectErrors(
'from a_index | eval var = date_diff("year", to_datetime(textField), to_datetime(textField))',
[]
);
await expectErrors('from a_index | eval date_diff(doubleField, textField, textField)', [
'Argument of [date_diff] must be [date], found value [textField] type [text]',
'Argument of [date_diff] must be [date], found value [textField] type [text]',
'Argument of [date_diff] must be [keyword], found value [doubleField] type [double]',
]);
});
});
});

View file

@ -23,18 +23,18 @@ test('should allow param inside agg function argument', async () => {
test('allow params in WHERE command expressions', async () => {
const { validate } = await setup();
const res1 = await validate('FROM index | WHERE stringField >= ?start');
const res1 = await validate('FROM index | WHERE textField >= ?start');
const res2 = await validate(`
FROM index
| WHERE stringField >= ?start
| WHERE stringField <= ?0
| WHERE stringField == ?
| WHERE textField >= ?start
| WHERE textField <= ?0
| WHERE textField == ?
`);
const res3 = await validate(`
FROM index
| WHERE stringField >= ?start
AND stringField <= ?0
AND stringField == ?
| WHERE textField >= ?start
AND textField <= ?0
AND textField == ?
`);
expect(res1).toMatchObject({ errors: [], warnings: [] });

View file

@ -76,6 +76,7 @@ import {
import { collapseWrongArgumentTypeMessages, getMaxMinNumberOfParams } from './helpers';
import { getParamAtPosition } from '../autocomplete/helper';
import { METADATA_FIELDS } from '../shared/constants';
import { isStringType } from '../shared/esql_types';
function validateFunctionLiteralArg(
astFunction: ESQLFunction,
@ -879,6 +880,7 @@ function validateColumnForCommand(
if (columnParamsWithInnerTypes.length) {
const hasSomeWrongInnerTypes = columnParamsWithInnerTypes.every(({ innerType }) => {
if (innerType === 'string' && isStringType(columnRef.type)) return false;
return innerType !== 'any' && innerType !== columnRef.type;
});
if (hasSomeWrongInnerTypes) {

View file

@ -13,9 +13,9 @@ import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/publ
describe('getColumnsWithMetadata', () => {
it('should return original columns if fieldsMetadata is not provided', async () => {
const columns = [
{ name: 'ecs.version', type: 'string' as DatatableColumnType },
{ name: 'field1', type: 'string' as DatatableColumnType },
{ name: 'field2', type: 'number' as DatatableColumnType },
{ name: 'ecs.version', type: 'keyword' as DatatableColumnType },
{ name: 'field1', type: 'text' as DatatableColumnType },
{ name: 'field2', type: 'double' as DatatableColumnType },
];
const result = await getColumnsWithMetadata(columns);
@ -24,16 +24,16 @@ describe('getColumnsWithMetadata', () => {
it('should return columns with metadata if both name and type match with ECS fields', async () => {
const columns = [
{ name: 'ecs.field', type: 'string' as DatatableColumnType },
{ name: 'ecs.field', type: 'text' as DatatableColumnType },
{ name: 'ecs.fakeBooleanField', type: 'boolean' as DatatableColumnType },
{ name: 'field2', type: 'number' as DatatableColumnType },
{ name: 'field2', type: 'double' as DatatableColumnType },
];
const fieldsMetadata = {
getClient: jest.fn().mockResolvedValue({
find: jest.fn().mockResolvedValue({
fields: {
'ecs.version': { description: 'ECS version field', type: 'keyword' },
'ecs.field': { description: 'ECS field description', type: 'keyword' },
'ecs.field': { description: 'ECS field description', type: 'text' },
'ecs.fakeBooleanField': {
description: 'ECS fake boolean field description',
type: 'keyword',
@ -48,19 +48,19 @@ describe('getColumnsWithMetadata', () => {
expect(result).toEqual([
{
name: 'ecs.field',
type: 'string',
type: 'text',
metadata: { description: 'ECS field description' },
},
{ name: 'ecs.fakeBooleanField', type: 'boolean' },
{ name: 'field2', type: 'number' },
{ name: 'field2', type: 'double' },
]);
});
it('should handle keyword suffix correctly', async () => {
const columns = [
{ name: 'ecs.version', type: 'string' as DatatableColumnType },
{ name: 'ecs.version.keyword', type: 'string' as DatatableColumnType },
{ name: 'field2', type: 'number' as DatatableColumnType },
{ name: 'ecs.version', type: 'keyword' as DatatableColumnType },
{ name: 'ecs.version.keyword', type: 'keyword' as DatatableColumnType },
{ name: 'field2', type: 'double' as DatatableColumnType },
];
const fieldsMetadata = {
getClient: jest.fn().mockResolvedValue({
@ -75,13 +75,13 @@ describe('getColumnsWithMetadata', () => {
const result = await getColumnsWithMetadata(columns, fieldsMetadata);
expect(result).toEqual([
{ name: 'ecs.version', type: 'string', metadata: { description: 'ECS version field' } },
{ name: 'ecs.version', type: 'keyword', metadata: { description: 'ECS version field' } },
{
name: 'ecs.version.keyword',
type: 'string',
type: 'keyword',
metadata: { description: 'ECS version field' },
},
{ name: 'field2', type: 'number' },
{ name: 'field2', type: 'double' },
]);
});
});

View file

@ -7,7 +7,6 @@
*/
import type { ESQLRealField } from '@kbn/esql-validation-autocomplete';
import { esFieldTypeToKibanaFieldType } from '@kbn/field-types';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { chunk } from 'lodash';
@ -45,11 +44,7 @@ export async function getColumnsWithMetadata(
const metadata = fields.fields[removeKeywordSuffix(c.name)];
// Need to convert metadata's type (e.g. keyword) to ES|QL type (e.g. string) to check if they are the same
if (
!metadata ||
(metadata?.type && esFieldTypeToKibanaFieldType(metadata.type) !== c.type)
)
return c;
if (!metadata || (metadata?.type && metadata.type !== c.type)) return c;
return {
...c,
metadata: { description: metadata.description },

View file

@ -468,7 +468,14 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
undefined,
abortController
).result;
const columns = table?.columns.map((c) => ({ name: c.name, type: c.meta.type })) || [];
const columns =
table?.columns.map((c) => {
// Casting unsupported as unknown to avoid plethora of warnings
// Remove when addressed https://github.com/elastic/kibana/issues/189666
if (!c.meta.esType || c.meta.esType === 'unsupported')
return { name: c.name, type: 'unknown' };
return { name: c.name, type: c.meta.esType };
}) || [];
return await getRateLimitedColumnsWithMetadata(columns, fieldsMetadata);
} catch (e) {
// no action yet

View file

@ -28,7 +28,6 @@
"@kbn/shared-ux-markdown",
"@kbn/fields-metadata-plugin",
"@kbn/esql-validation-autocomplete",
"@kbn/field-types"
],
"exclude": [
"target/**/*",

View file

@ -137,6 +137,7 @@ export default function ({ getService }: FtrProviderContext) {
describe('error messages', () => {
const config = readSetupFromESQLPackage();
const { queryToErrors, indexes, policies } = parseConfig(config);
const missmatches: Array<{ query: string; error: string }> = [];
// Swap these for DEBUG/further investigation on ES bugs
const stringVariants = ['text', 'keyword'] as const;
@ -182,11 +183,18 @@ export default function ({ getService }: FtrProviderContext) {
for (const index of indexes) {
// setup all indexes, mappings and policies here
log.info(`creating a index "${index}" with mapping...`);
log.info(
`creating a index "${index}" with mapping...\n${JSON.stringify(config.fields)}`
);
const fieldsExcludingCounterType = config.fields.filter(
// ES|QL supports counter_integer, counter_long, counter_double, date_period, etc.
// but they are not types suitable for Elasticsearch indices
(c: { type: string }) => !c.type.startsWith('counter_') && c.type !== 'date_period'
);
await es.indices.create(
createIndexRequest(
index,
/unsupported/.test(index) ? config.unsupported_field : config.fields,
/unsupported/.test(index) ? config.unsupported_field : fieldsExcludingCounterType,
stringFieldType,
numberFieldType
),