[8.16] [ES|QL] Create expression type evaluator (#195989) (#196921)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[ES|QL] Create expression type evaluator
(#195989)](https://github.com/elastic/kibana/pull/195989)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Drew
Tate","email":"drew.tate@elastic.co"},"sourceCommit":{"committedDate":"2024-10-18T16:15:11Z","message":"[ES|QL]
Create expression type evaluator (#195989)\n\n## Summary\r\n\r\nClose
https://github.com/elastic/kibana/issues/195682\r\nClose
https://github.com/elastic/kibana/issues/195430\r\n\r\nIntroduces
`getExpressionType`, the ES|QL expression type evaluator to\r\nrule them
all!\r\n\r\nAlso, fixes several validation bugs related to the faulty
logic that\r\nexisted before with variable type detection (some noted
in\r\nhttps://github.com/elastic/kibana/issues/192255#issuecomment-2394613881).\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"2173af79fde374008b181ca42cf98a7137a7bb24","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","v9.0.0","Feature:ES|QL","Team:ESQL","v8.16.0","backport:version","v8.17.0"],"title":"[ES|QL]
Create expression type
evaluator","number":195989,"url":"https://github.com/elastic/kibana/pull/195989","mergeCommit":{"message":"[ES|QL]
Create expression type evaluator (#195989)\n\n## Summary\r\n\r\nClose
https://github.com/elastic/kibana/issues/195682\r\nClose
https://github.com/elastic/kibana/issues/195430\r\n\r\nIntroduces
`getExpressionType`, the ES|QL expression type evaluator to\r\nrule them
all!\r\n\r\nAlso, fixes several validation bugs related to the faulty
logic that\r\nexisted before with variable type detection (some noted
in\r\nhttps://github.com/elastic/kibana/issues/192255#issuecomment-2394613881).\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"2173af79fde374008b181ca42cf98a7137a7bb24"}},"sourceBranch":"main","suggestedTargetBranches":["8.16","8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195989","number":195989,"mergeCommit":{"message":"[ES|QL]
Create expression type evaluator (#195989)\n\n## Summary\r\n\r\nClose
https://github.com/elastic/kibana/issues/195682\r\nClose
https://github.com/elastic/kibana/issues/195430\r\n\r\nIntroduces
`getExpressionType`, the ES|QL expression type evaluator to\r\nrule them
all!\r\n\r\nAlso, fixes several validation bugs related to the faulty
logic that\r\nexisted before with variable type detection (some noted
in\r\nhttps://github.com/elastic/kibana/issues/192255#issuecomment-2394613881).\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"2173af79fde374008b181ca42cf98a7137a7bb24"}},{"branch":"8.16","label":"v8.16.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Drew Tate <drew.tate@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-19 04:59:18 +11:00 committed by GitHub
parent 568cfe885d
commit 806796f1f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 611 additions and 272 deletions

View file

@ -24,7 +24,7 @@ describe('literal expression', () => {
});
});
it('decimals vs integers', () => {
it('doubles vs integers', () => {
const text = 'ROW a(1.0, 1)';
const { ast } = parse(text);
@ -36,7 +36,7 @@ describe('literal expression', () => {
args: [
{
type: 'literal',
literalType: 'decimal',
literalType: 'double',
},
{
type: 'literal',

View file

@ -41,6 +41,7 @@ import type {
FunctionSubtype,
ESQLNumericLiteral,
ESQLOrderExpression,
InlineCastingType,
} from '../types';
import { parseIdentifier, getPosition } from './helpers';
import { Builder, type AstNodeParserFields } from '../builder';
@ -72,7 +73,7 @@ export const createCommand = (name: string, ctx: ParserRuleContext) =>
export const createInlineCast = (ctx: InlineCastContext, value: ESQLInlineCast['value']) =>
Builder.expression.inlineCast(
{ castType: ctx.dataType().getText(), value },
{ castType: ctx.dataType().getText().toLowerCase() as InlineCastingType, value },
createParserFields(ctx)
);
@ -107,7 +108,7 @@ export function createLiteralString(token: Token): ESQLLiteral {
const text = token.text!;
return {
type: 'literal',
literalType: 'string',
literalType: 'keyword',
text,
name: text,
value: text,
@ -149,13 +150,13 @@ export function createLiteral(
location: getPosition(node.symbol),
incomplete: isMissingText(text),
};
if (type === 'decimal' || type === 'integer') {
if (type === 'double' || type === 'integer') {
return {
...partialLiteral,
literalType: type,
value: Number(text),
paramType: 'number',
} as ESQLNumericLiteral<'decimal'> | ESQLNumericLiteral<'integer'>;
} as ESQLNumericLiteral<'double'> | ESQLNumericLiteral<'integer'>;
} else if (type === 'param') {
throw new Error('Should never happen');
}

View file

@ -346,7 +346,7 @@ function getConstant(ctx: ConstantContext): ESQLAstItem {
// Decimal type covers multiple ES|QL types: long, double, etc.
if (ctx instanceof DecimalLiteralContext) {
return createNumericLiteral(ctx.decimalValue(), 'decimal');
return createNumericLiteral(ctx.decimalValue(), 'double');
}
// Integer type encompasses integer
@ -358,7 +358,7 @@ function getConstant(ctx: ConstantContext): ESQLAstItem {
}
if (ctx instanceof StringLiteralContext) {
// String literal covers multiple ES|QL types: text and keyword types
return createLiteral('string', ctx.string_().QUOTED_STRING());
return createLiteral('keyword', ctx.string_().QUOTED_STRING());
}
if (
ctx instanceof NumericArrayLiteralContext ||
@ -371,14 +371,14 @@ function getConstant(ctx: ConstantContext): ESQLAstItem {
const isDecimal =
numericValue.decimalValue() !== null && numericValue.decimalValue() !== undefined;
const value = numericValue.decimalValue() || numericValue.integerValue();
values.push(createNumericLiteral(value!, isDecimal ? 'decimal' : 'integer'));
values.push(createNumericLiteral(value!, isDecimal ? 'double' : '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());
const literal = createLiteral('keyword', string.QUOTED_STRING());
if (literal) {
values.push(literal);
}
@ -534,7 +534,7 @@ function collectRegexExpression(ctx: BooleanExpressionContext): ESQLFunction[] {
const arg = visitValueExpression(regex.valueExpression());
if (arg) {
fn.args.push(arg);
const literal = createLiteral('string', regex._pattern.QUOTED_STRING());
const literal = createLiteral('keyword', regex._pattern.QUOTED_STRING());
if (literal) {
fn.args.push(literal);
}
@ -672,7 +672,7 @@ export function visitDissect(ctx: DissectCommandContext) {
return [
visitPrimaryExpression(ctx.primaryExpression()),
...(pattern && textExistsAndIsValid(pattern.getText())
? [createLiteral('string', pattern), ...visitDissectOptions(ctx.commandOptions())]
? [createLiteral('keyword', pattern), ...visitDissectOptions(ctx.commandOptions())]
: []),
].filter(nonNullable);
}
@ -682,7 +682,7 @@ export function visitGrok(ctx: GrokCommandContext) {
return [
visitPrimaryExpression(ctx.primaryExpression()),
...(pattern && textExistsAndIsValid(pattern.getText())
? [createLiteral('string', pattern)]
? [createLiteral('keyword', pattern)]
: []),
].filter(nonNullable);
}

View file

@ -399,7 +399,7 @@ ROW
// 2
/* 3 */
// 4
/* 5 */ /* 6 */ 1::INTEGER /* 7 */ /* 8 */ // 9`);
/* 5 */ /* 6 */ 1::integer /* 7 */ /* 8 */ // 9`);
});
});

View file

@ -64,10 +64,10 @@ export const LeafPrinter = {
return '?';
}
}
case 'string': {
case 'keyword': {
return String(node.value);
}
case 'decimal': {
case 'double': {
const isRounded = node.value % 1 === 0;
if (isRounded) {

View file

@ -193,10 +193,33 @@ export type BinaryExpressionAssignmentOperator = '=';
export type BinaryExpressionComparisonOperator = '==' | '=~' | '!=' | '<' | '<=' | '>' | '>=';
export type BinaryExpressionRegexOperator = 'like' | 'not_like' | 'rlike' | 'not_rlike';
// from https://github.com/elastic/elasticsearch/blob/122e7288200ee03e9087c98dff6cebbc94e774aa/docs/reference/esql/functions/kibana/inline_cast.json
export type InlineCastingType =
| 'bool'
| 'boolean'
| 'cartesian_point'
| 'cartesian_shape'
| 'date_nanos'
| 'date_period'
| 'datetime'
| 'double'
| 'geo_point'
| 'geo_shape'
| 'int'
| 'integer'
| 'ip'
| 'keyword'
| 'long'
| 'string'
| 'text'
| 'time_duration'
| 'unsigned_long'
| 'version';
export interface ESQLInlineCast<ValueType = ESQLAstItem> extends ESQLAstBaseItem {
type: 'inlineCast';
value: ValueType;
castType: string;
castType: InlineCastingType;
}
/**
@ -270,7 +293,7 @@ export interface ESQLList extends ESQLAstBaseItem {
values: ESQLLiteral[];
}
export type ESQLNumericLiteralType = 'decimal' | 'integer';
export type ESQLNumericLiteralType = 'double' | 'integer';
export type ESQLLiteral =
| ESQLDecimalLiteral
@ -290,7 +313,7 @@ export interface ESQLNumericLiteral<T extends ESQLNumericLiteralType> extends ES
}
// We cast anything as decimal (e.g. 32.12) as generic decimal numeric type here
// @internal
export type ESQLDecimalLiteral = ESQLNumericLiteral<'decimal'>;
export type ESQLDecimalLiteral = ESQLNumericLiteral<'double'>;
// @internal
export type ESQLIntegerLiteral = ESQLNumericLiteral<'integer'>;
@ -312,7 +335,7 @@ export interface ESQLNullLiteral extends ESQLAstBaseItem {
// @internal
export interface ESQLStringLiteral extends ESQLAstBaseItem {
type: 'literal';
literalType: 'string';
literalType: 'keyword';
value: string;
}

View file

@ -337,7 +337,7 @@ export class LimitCommandVisitorContext<
if (
arg &&
arg.type === 'literal' &&
(arg.literalType === 'integer' || arg.literalType === 'decimal')
(arg.literalType === 'integer' || arg.literalType === 'double')
) {
return arg;
}

View file

@ -342,7 +342,7 @@ describe('structurally can walk all nodes', () => {
},
{
type: 'literal',
literalType: 'string',
literalType: 'keyword',
name: '"foo"',
},
{
@ -375,7 +375,7 @@ describe('structurally can walk all nodes', () => {
},
{
type: 'literal',
literalType: 'string',
literalType: 'keyword',
name: '"2"',
},
{
@ -390,7 +390,7 @@ describe('structurally can walk all nodes', () => {
},
{
type: 'literal',
literalType: 'decimal',
literalType: 'double',
name: '3.14',
},
]);
@ -473,7 +473,7 @@ describe('structurally can walk all nodes', () => {
values: [
{
type: 'literal',
literalType: 'decimal',
literalType: 'double',
name: '3.3',
},
],
@ -492,7 +492,7 @@ describe('structurally can walk all nodes', () => {
},
{
type: 'literal',
literalType: 'decimal',
literalType: 'double',
name: '3.3',
},
]);
@ -600,27 +600,27 @@ describe('structurally can walk all nodes', () => {
expect(literals).toMatchObject([
{
type: 'literal',
literalType: 'string',
literalType: 'keyword',
name: '"a"',
},
{
type: 'literal',
literalType: 'string',
literalType: 'keyword',
name: '"b"',
},
{
type: 'literal',
literalType: 'string',
literalType: 'keyword',
name: '"c"',
},
{
type: 'literal',
literalType: 'string',
literalType: 'keyword',
name: '"d"',
},
{
type: 'literal',
literalType: 'string',
literalType: 'keyword',
name: '"e"',
},
]);

View file

@ -52,7 +52,7 @@ const extraFunctions: FunctionDefinition[] = [
{ name: 'value', type: 'any' },
],
minParams: 2,
returnType: 'any',
returnType: 'unknown',
},
],
examples: [

View file

@ -259,7 +259,6 @@ describe('autocomplete.suggest', () => {
...getFieldNamesByType('integer'),
...getFieldNamesByType('double'),
...getFieldNamesByType('long'),
'`avg(b)`',
...getFunctionSignaturesByReturnType('eval', ['integer', 'double', 'long'], {
scalar: true,
}),
@ -284,11 +283,19 @@ describe('autocomplete.suggest', () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [',', '| ']);
await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [',', '| '], {
triggerCharacter: ' ',
});
await assertSuggestions(
'from a | stats var0 = AVG(doubleField) BY var1 = BUCKET(dateField, 1 day)/',
[',', '| ', '+ $0', '- $0']
);
await assertSuggestions(
'from a | stats var0 = AVG(doubleField) BY var1 = BUCKET(dateField, 1 day) /',
[',', '| ', '+ $0', '- $0'],
{ triggerCharacter: ' ' }
);
});
test('on space within bucket()', async () => {
const { assertSuggestions } = await setup();

View file

@ -1770,10 +1770,15 @@ async function getOptionArgsSuggestions(
innerText,
command,
option,
{ type: argDef?.type || 'any' },
{ type: argDef?.type || 'unknown' },
nodeArg,
nodeArgType as string,
references,
{
fields: references.fields,
// you can't use a variable defined
// in the stats command in the by clause
variables: new Map(),
},
getFieldsByType
))
);

View file

@ -64,7 +64,6 @@ export const getBuiltinCompatibleFunctionDefinition = (
const compatibleFunctions = [...builtinFunctions, ...getTestFunctions()].filter(
({ name, supportedCommands, supportedOptions, signatures, ignoreAsSuggestion }) =>
!ignoreAsSuggestion &&
!/not_/.test(name) &&
(!skipAssign || name !== '=') &&
(option ? supportedOptions?.includes(option) : supportedCommands.includes(command)) &&
signatures.some(
@ -78,7 +77,10 @@ export const getBuiltinCompatibleFunctionDefinition = (
return compatibleFunctions
.filter((mathDefinition) =>
mathDefinition.signatures.some(
(signature) => returnTypes[0] === 'any' || returnTypes.includes(signature.returnType)
(signature) =>
returnTypes[0] === 'unknown' ||
returnTypes[0] === 'any' ||
returnTypes.includes(signature.returnType)
)
)
.map(getSuggestionBuiltinDefinition);

View file

@ -52,23 +52,6 @@ export function getFunctionsToIgnoreForStats(command: ESQLCommand, argIndex: num
return isFunctionItem(arg) ? getFnContent(arg) : [];
}
/**
* Given a function signature, returns the parameter at the given position.
*
* Takes into account variadic functions (minParams), returning the last
* parameter if the position is greater than the number of parameters.
*
* @param signature
* @param position
* @returns
*/
export function getParamAtPosition(
{ params, minParams }: FunctionDefinition['signatures'][number],
position: number
) {
return params.length > position ? params[position] : minParams ? params[params.length - 1] : null;
}
/**
* Given a function signature, returns the parameter at the given position, even if it's undefined or null
*

View file

@ -423,6 +423,20 @@ const likeFunctions: FunctionDefinition[] = [
],
returnType: 'boolean',
},
{
params: [
{ name: 'left', type: 'text' as const },
{ name: 'right', type: 'keyword' as const },
],
returnType: 'boolean',
},
{
params: [
{ name: 'left', type: 'keyword' as const },
{ name: 'right', type: 'text' as const },
],
returnType: 'boolean',
},
{
params: [
{ name: 'left', type: 'keyword' as const },
@ -609,25 +623,12 @@ const otherDefinitions: FunctionDefinition[] = [
{ name: 'left', type: 'any' },
{ name: 'right', type: 'any' },
],
returnType: 'void',
},
],
},
{
name: 'functions',
type: 'builtin',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definition.functionsDoc', {
defaultMessage: 'Show ES|QL avaialble functions with signatures',
}),
supportedCommands: ['meta'],
signatures: [
{
params: [],
returnType: 'void',
returnType: 'unknown',
},
],
},
{
// TODO — this shouldn't be a function or an operator...
name: 'info',
type: 'builtin',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definition.infoDoc', {
@ -637,21 +638,7 @@ const otherDefinitions: FunctionDefinition[] = [
signatures: [
{
params: [],
returnType: 'void',
},
],
},
{
name: 'order-expression',
type: 'builtin',
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definition.infoDoc', {
defaultMessage: 'Specify column sorting modifiers',
}),
supportedCommands: ['sort'],
signatures: [
{
params: [{ name: 'column', type: 'any' }],
returnType: 'void',
returnType: 'unknown', // meaningless
},
],
},

View file

@ -9033,7 +9033,7 @@ const caseDefinition: FunctionDefinition = {
},
],
minParams: 2,
returnType: 'any',
returnType: 'unknown',
},
],
supportedCommands: ['stats', 'inlinestats', 'metrics', 'eval', 'where', 'row', 'sort'],

View file

@ -129,7 +129,7 @@ export const appendSeparatorOption: CommandOptionsDefinition = {
const [firstArg] = option.args;
if (
!Array.isArray(firstArg) &&
(!isLiteralItem(firstArg) || firstArg.literalType !== 'string')
(!isLiteralItem(firstArg) || firstArg.literalType !== 'keyword')
) {
const value =
'value' in firstArg && !isInlineCastItem(firstArg) ? firstArg.value : firstArg.name;

View file

@ -100,12 +100,14 @@ export const isParameterType = (str: string | undefined): str is FunctionParamet
/**
* This is the return type of a function definition.
*
* TODO: remove `any`
*/
export type FunctionReturnType = Exclude<SupportedDataType, 'unsupported'> | 'any' | 'void';
export type FunctionReturnType = Exclude<SupportedDataType, 'unsupported'> | 'unknown' | 'any';
export const isReturnType = (str: string | FunctionParameterType): str is FunctionReturnType =>
str !== 'unsupported' &&
(dataTypes.includes(str as SupportedDataType) || str === 'any' || str === 'void');
(dataTypes.includes(str as SupportedDataType) || str === 'unknown' || str === 'any');
export interface FunctionDefinition {
type: 'builtin' | 'agg' | 'eval';

View file

@ -153,7 +153,7 @@ function isBuiltinFunction(node: ESQLFunction) {
export function getAstContext(queryString: string, ast: ESQLAst, offset: number) {
const { command, option, setting, node } = findAstPosition(ast, offset);
if (node) {
if (node.type === 'literal' && node.literalType === 'string') {
if (node.type === 'literal' && node.literalType === 'keyword') {
// command ... "<here>"
return { type: 'value' as const, command, node, option, setting };
}

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ESQLDecimalLiteral, ESQLLiteral, ESQLNumericLiteralType } from '@kbn/esql-ast/src/types';
import { ESQLLiteral, ESQLNumericLiteralType } from '@kbn/esql-ast/src/types';
import { FunctionParameterType } from '../definitions/types';
export const ESQL_COMMON_NUMERIC_TYPES = ['double', 'long', 'integer'] as const;
@ -27,15 +27,6 @@ export const ESQL_NUMBER_TYPES = [
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' &&
@ -43,37 +34,18 @@ export function isNumericType(type: unknown): type is ESQLNumericLiteralType {
);
}
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])
);
}
/**
* Compares two types, taking into account literal types
* @TODO strengthen typing here (remove `string`)
* @TODO clean up time duration and date period
*/
export const compareTypesWithLiterals = (
a: ESQLLiteral['literalType'] | FunctionParameterType | string,
b: ESQLLiteral['literalType'] | FunctionParameterType | string
a: ESQLLiteral['literalType'] | FunctionParameterType | 'timeInterval' | string,
b: ESQLLiteral['literalType'] | FunctionParameterType | 'timeInterval' | string
) => {
if (a === b) {
return true;
}
if (a === 'decimal') {
return isNumericDecimalType(b);
}
if (b === 'decimal') {
return isNumericDecimalType(a);
}
if (a === 'string') {
return isStringType(b);
}
if (b === 'string') {
return isStringType(a);
}
// In Elasticsearch function definitions, time_literal and time_duration are used
// time_duration is seconds/min/hour interval
// date_period is day/week/month/year interval

View file

@ -7,7 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { shouldBeQuotedSource } from './helpers';
import { parse } from '@kbn/esql-ast';
import { getExpressionType, shouldBeQuotedSource } from './helpers';
import { SupportedDataType } from '../definitions/types';
import { setTestFunctions } from './test_functions';
describe('shouldBeQuotedSource', () => {
it('does not have to be quoted for sources with acceptable characters @-+$', () => {
@ -47,3 +50,295 @@ describe('shouldBeQuotedSource', () => {
expect(shouldBeQuotedSource('index-[dd-mm]')).toBe(true);
});
});
describe('getExpressionType', () => {
const getASTForExpression = (expression: string) => {
const { root } = parse(`FROM index | EVAL ${expression}`);
return root.commands[1].args[0];
};
describe('literal expressions', () => {
const cases: Array<{ expression: string; expectedType: SupportedDataType }> = [
{
expression: '1.0',
expectedType: 'double',
},
{
expression: '1',
expectedType: 'integer',
},
{
expression: 'true',
expectedType: 'boolean',
},
{
expression: '"foobar"',
expectedType: 'keyword',
},
{
expression: 'NULL',
expectedType: 'null',
},
// TODO — consider whether we need to be worried about
// differentiating between time_duration, and date_period
// instead of just using time_literal
{
expression: '1 second',
expectedType: 'time_literal',
},
{
expression: '1 day',
expectedType: 'time_literal',
},
];
test.each(cases)('detects a literal of type $expectedType', ({ expression, expectedType }) => {
const ast = getASTForExpression(expression);
expect(getExpressionType(ast)).toBe(expectedType);
});
});
describe('inline casting', () => {
const cases: Array<{ expression: string; expectedType: SupportedDataType }> = [
{ expectedType: 'boolean', expression: '"true"::bool' },
{ expectedType: 'boolean', expression: '"false"::boolean' },
{ expectedType: 'boolean', expression: '"false"::BooLEAN' },
{ expectedType: 'cartesian_point', expression: '""::cartesian_point' },
{ expectedType: 'cartesian_shape', expression: '""::cartesian_shape' },
{ expectedType: 'date_nanos', expression: '1::date_nanos' },
{ expectedType: 'date_period', expression: '1::date_period' },
{ expectedType: 'date', expression: '1::datetime' },
{ expectedType: 'double', expression: '1::double' },
{ expectedType: 'geo_point', expression: '""::geo_point' },
{ expectedType: 'geo_shape', expression: '""::geo_shape' },
{ expectedType: 'integer', expression: '1.2::int' },
{ expectedType: 'integer', expression: '1.2::integer' },
{ expectedType: 'ip', expression: '"123.12.12.2"::ip' },
{ expectedType: 'keyword', expression: '1::keyword' },
{ expectedType: 'long', expression: '1::long' },
{ expectedType: 'keyword', expression: '1::string' },
{ expectedType: 'keyword', expression: '1::text' },
{ expectedType: 'time_duration', expression: '1::time_duration' },
{ expectedType: 'unsigned_long', expression: '1::unsigned_long' },
{ expectedType: 'version', expression: '"1.2.3"::version' },
{ expectedType: 'version', expression: '"1.2.3"::VERSION' },
];
test.each(cases)(
'detects a casted literal of type $expectedType ($expression)',
({ expression, expectedType }) => {
const ast = getASTForExpression(expression);
expect(getExpressionType(ast)).toBe(expectedType);
}
);
});
describe('fields and variables', () => {
it('detects the type of fields and variables which exist', () => {
expect(
getExpressionType(
getASTForExpression('fieldName'),
new Map([
[
'fieldName',
{
name: 'fieldName',
type: 'geo_shape',
},
],
]),
new Map()
)
).toBe('geo_shape');
expect(
getExpressionType(
getASTForExpression('var0'),
new Map(),
new Map([
[
'var0',
[
{
name: 'var0',
type: 'long',
location: { min: 0, max: 0 },
},
],
],
])
)
).toBe('long');
});
it('handles fields and variables which do not exist', () => {
expect(getExpressionType(getASTForExpression('fieldName'), new Map(), new Map())).toBe(
'unknown'
);
});
});
describe('functions', () => {
beforeAll(() => {
setTestFunctions([
{
type: 'eval',
name: 'test',
description: 'Test function',
supportedCommands: ['eval'],
signatures: [
{ params: [{ name: 'arg', type: 'keyword' }], returnType: 'keyword' },
{ params: [{ name: 'arg', type: 'double' }], returnType: 'double' },
{
params: [
{ name: 'arg', type: 'double' },
{ name: 'arg', type: 'keyword' },
],
returnType: 'long',
},
],
},
{
type: 'eval',
name: 'returns_keyword',
description: 'Test function',
supportedCommands: ['eval'],
signatures: [{ params: [], returnType: 'keyword' }],
},
{
type: 'eval',
name: 'accepts_dates',
description: 'Test function',
supportedCommands: ['eval'],
signatures: [
{
params: [
{ name: 'arg1', type: 'date' },
{ name: 'arg2', type: 'date_period' },
],
returnType: 'keyword',
},
],
},
]);
});
afterAll(() => {
setTestFunctions([]);
});
it('detects the return type of a function', () => {
expect(
getExpressionType(getASTForExpression('returns_keyword()'), new Map(), new Map())
).toBe('keyword');
});
it('selects the correct signature based on the arguments', () => {
expect(getExpressionType(getASTForExpression('test("foo")'), new Map(), new Map())).toBe(
'keyword'
);
expect(getExpressionType(getASTForExpression('test(1.)'), new Map(), new Map())).toBe(
'double'
);
expect(getExpressionType(getASTForExpression('test(1., "foo")'), new Map(), new Map())).toBe(
'long'
);
});
it('supports nested functions', () => {
expect(
getExpressionType(
getASTForExpression('test(1., test(test(test(returns_keyword()))))'),
new Map(),
new Map()
)
).toBe('long');
});
it('supports functions with casted results', () => {
expect(
getExpressionType(getASTForExpression('test(1.)::keyword'), new Map(), new Map())
).toBe('keyword');
});
it('handles nulls and string-date casting', () => {
expect(getExpressionType(getASTForExpression('test(NULL)'), new Map(), new Map())).toBe(
'null'
);
expect(getExpressionType(getASTForExpression('test(NULL, NULL)'), new Map(), new Map())).toBe(
'null'
);
expect(
getExpressionType(getASTForExpression('accepts_dates("", "")'), new Map(), new Map())
).toBe('keyword');
});
it('deals with functions that do not exist', () => {
expect(getExpressionType(getASTForExpression('does_not_exist()'), new Map(), new Map())).toBe(
'unknown'
);
});
it('deals with bad function invocations', () => {
expect(
getExpressionType(getASTForExpression('test(1., "foo", "bar")'), new Map(), new Map())
).toBe('unknown');
expect(getExpressionType(getASTForExpression('test()'), new Map(), new Map())).toBe(
'unknown'
);
expect(getExpressionType(getASTForExpression('test("foo", 1.)'), new Map(), new Map())).toBe(
'unknown'
);
});
it('deals with the CASE function', () => {
expect(getExpressionType(getASTForExpression('CASE(true, 1, 2)'), new Map(), new Map())).toBe(
'integer'
);
expect(
getExpressionType(getASTForExpression('CASE(true, 1., true, 1., 2.)'), new Map(), new Map())
).toBe('double');
expect(
getExpressionType(
getASTForExpression('CASE(true, "", true, "", keywordField)'),
new Map([[`keywordField`, { name: 'keywordField', type: 'keyword' }]]),
new Map()
)
).toBe('keyword');
});
});
describe('lists', () => {
const cases: Array<{ expression: string; expectedType: SupportedDataType | 'unknown' }> = [
{
expression: '["foo", "bar"]',
expectedType: 'keyword',
},
{
expression: '[1, 2]',
expectedType: 'integer',
},
{
expression: '[1., 2.]',
expectedType: 'double',
},
{
expression: '[null, null, null]',
expectedType: 'null',
},
{
expression: '[true, false]',
expectedType: 'boolean',
},
];
test.each(cases)(
'reports the type of $expression as $expectedType',
({ expression, expectedType }) => {
const ast = getASTForExpression(expression);
expect(getExpressionType(ast)).toBe(expectedType);
}
);
});
});

View file

@ -43,10 +43,10 @@ import {
FunctionParameterType,
FunctionReturnType,
ArrayType,
SupportedDataType,
} from '../definitions/types';
import type { ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types';
import { removeMarkerArgFromArgsList } from './context';
import { compareTypesWithLiterals, isNumericDecimalType } from './esql_types';
import type { ReasonTypes } from './types';
import { DOUBLE_TICKS_REGEX, EDITOR_MARKER, SINGLE_BACKTICK } from './constants';
import type { EditorContext } from '../autocomplete/types';
@ -225,27 +225,29 @@ export function getCommandOption(optionName: CommandOptionsDefinition['name']) {
}
function doesLiteralMatchParameterType(argType: FunctionParameterType, item: ESQLLiteral) {
if (item.literalType === argType) {
return true;
}
if (item.literalType === 'null') {
// all parameters accept null, but this is not yet reflected
// in our function definitions so we let it through here
return true;
}
if (item.literalType === 'decimal' && isNumericDecimalType(argType)) {
// some parameters accept string literals because of ES auto-casting
if (
item.literalType === 'keyword' &&
(argType === 'date' ||
argType === 'date_period' ||
argType === 'version' ||
argType === 'ip' ||
argType === 'boolean')
) {
return true;
}
if (item.literalType === 'string' && (argType === 'text' || argType === 'keyword')) {
return true;
}
if (item.literalType !== 'string') {
if (argType === item.literalType) {
return true;
}
return false;
}
// date-type parameters accept string literals because of ES auto-casting
return ['string', 'date', 'date_period'].includes(argType);
return false;
}
/**
@ -417,7 +419,7 @@ export function inKnownTimeInterval(item: ESQLTimeInterval): boolean {
*/
export function isValidLiteralOption(arg: ESQLLiteral, argDef: FunctionParameter) {
return (
arg.literalType === 'string' &&
arg.literalType === 'keyword' &&
argDef.acceptedValues &&
!argDef.acceptedValues
.map((option) => option.toLowerCase())
@ -447,7 +449,7 @@ export function checkFunctionArgMatchesDefinition(
if (isSupportedFunction(arg.name, parentCommand).supported) {
const fnDef = buildFunctionLookup().get(arg.name)!;
return fnDef.signatures.some(
(signature) => signature.returnType === 'any' || argType === signature.returnType
(signature) => signature.returnType === 'unknown' || argType === signature.returnType
);
}
}
@ -460,23 +462,15 @@ export function checkFunctionArgMatchesDefinition(
if (!validHit) {
return false;
}
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 ||
(ct === 'string' && ['text', 'keyword'].includes(argType as string))
);
const wrappedTypes: Array<(typeof validHit)['type']> = Array.isArray(validHit.type)
? validHit.type
: [validHit.type];
return wrappedTypes.some((ct) => ct === argType || ct === 'null' || ct === 'unknown');
}
if (arg.type === 'inlineCast') {
const lowerArgType = argType?.toLowerCase();
const lowerArgCastType = arg.castType?.toLowerCase();
return (
compareTypesWithLiterals(lowerArgCastType, lowerArgType) ||
// for valid shorthand casts like 321.12::int or "false"::bool
(['int', 'bool'].includes(lowerArgCastType) && argType.startsWith(lowerArgCastType))
);
const castedType = getExpressionType(arg);
return castedType === lowerArgType;
}
}
@ -725,3 +719,143 @@ export function correctQuerySyntax(_query: string, context: EditorContext) {
return query;
}
/**
* Gets the signatures of a function that match the number of arguments
* provided in the AST.
*/
export function getSignaturesWithMatchingArity(
fnDef: FunctionDefinition,
astFunction: ESQLFunction
) {
return fnDef.signatures.filter((def) => {
if (def.minParams) {
return astFunction.args.length >= def.minParams;
}
return (
astFunction.args.length >= def.params.filter(({ optional }) => !optional).length &&
astFunction.args.length <= def.params.length
);
});
}
/**
* Given a function signature, returns the parameter at the given position.
*
* Takes into account variadic functions (minParams), returning the last
* parameter if the position is greater than the number of parameters.
*
* @param signature
* @param position
* @returns
*/
export function getParamAtPosition(
{ params, minParams }: FunctionDefinition['signatures'][number],
position: number
) {
return params.length > position ? params[position] : minParams ? params[params.length - 1] : null;
}
/**
* Determines the type of the expression
*/
export function getExpressionType(
root: ESQLAstItem,
fields?: Map<string, ESQLRealField>,
variables?: Map<string, ESQLVariable[]>
): SupportedDataType | 'unknown' {
if (!isSingleItem(root)) {
if (root.length === 0) {
return 'unknown';
}
return getExpressionType(root[0], fields, variables);
}
if (isLiteralItem(root) && root.literalType !== 'param') {
return root.literalType;
}
if (isTimeIntervalItem(root)) {
return 'time_literal';
}
// from https://github.com/elastic/elasticsearch/blob/122e7288200ee03e9087c98dff6cebbc94e774aa/docs/reference/esql/functions/kibana/inline_cast.json
if (isInlineCastItem(root)) {
switch (root.castType) {
case 'int':
return 'integer';
case 'bool':
return 'boolean';
case 'string':
return 'keyword';
case 'text':
return 'keyword';
case 'datetime':
return 'date';
default:
return root.castType;
}
}
if (isColumnItem(root) && fields && variables) {
const column = getColumnForASTNode(root, { fields, variables });
if (!column) {
return 'unknown';
}
return column.type;
}
if (root.type === 'list') {
return getExpressionType(root.values[0], fields, variables);
}
if (isFunctionItem(root)) {
const fnDefinition = getFunctionDefinition(root.name);
if (!fnDefinition) {
return 'unknown';
}
if (fnDefinition.name === 'case' && root.args.length) {
// The CASE function doesn't fit our system of function definitions
// and needs special handling. This is imperfect, but it's a start because
// at least we know that the final argument to case will never be a conditional
// expression, always a result expression.
//
// One problem with this is that if a false case is not provided, the return type
// will be null, which we aren't detecting. But this is ok because we consider
// variables and fields to be nullable anyways and account for that during validation.
return getExpressionType(root.args[root.args.length - 1], fields, variables);
}
const signaturesWithCorrectArity = getSignaturesWithMatchingArity(fnDefinition, root);
if (!signaturesWithCorrectArity.length) {
return 'unknown';
}
const argTypes = root.args.map((arg) => getExpressionType(arg, fields, variables));
// When functions are passed null for any argument, they generally return null
// This is a special case that is not reflected in our function definitions
if (argTypes.some((argType) => argType === 'null')) return 'null';
const matchingSignature = signaturesWithCorrectArity.find((signature) => {
return argTypes.every((argType, i) => {
const param = getParamAtPosition(signature, i);
return (
param &&
(param.type === argType ||
(argType === 'keyword' && ['date', 'date_period'].includes(param.type)))
);
});
});
if (!matchingSignature) {
return 'unknown';
}
return matchingSignature.returnType === 'any' ? 'unknown' : matchingSignature.returnType;
}
return 'unknown';
}

View file

@ -11,7 +11,7 @@ import type { ESQLAst, ESQLAstItem, ESQLCommand, ESQLFunction } from '@kbn/esql-
import { Visitor } from '@kbn/esql-ast/src/visitor';
import type { ESQLVariable, ESQLRealField } from '../validation/types';
import { EDITOR_MARKER } from './constants';
import { isColumnItem, isFunctionItem, getFunctionDefinition } from './helpers';
import { isColumnItem, isFunctionItem, getExpressionType } from './helpers';
function addToVariableOccurrences(variables: Map<string, ESQLVariable[]>, instance: ESQLVariable) {
if (!variables.has(instance.name)) {
@ -43,62 +43,6 @@ function addToVariables(
}
}
/**
* Determines the type of the expression
*
* TODO - this function needs a lot of work. For example, it needs to find the best-matching function signature
* which it isn't currently doing. See https://github.com/elastic/kibana/issues/195682
*/
function getExpressionType(
root: ESQLAstItem,
fields: Map<string, ESQLRealField>,
variables: Map<string, ESQLVariable[]>
): string {
const fallback = 'double';
if (Array.isArray(root) || !root) {
return fallback;
}
if (root.type === 'literal') {
return root.literalType;
}
if (root.type === 'inlineCast') {
if (root.castType === 'int') {
return 'integer';
}
if (root.castType === 'bool') {
return 'boolean';
}
return root.castType;
}
if (isColumnItem(root)) {
const field = fields.get(root.parts.join('.'));
if (field) {
return field.type;
}
const variable = variables.get(root.parts.join('.'));
if (variable) {
return variable[0].type;
}
}
if (isFunctionItem(root)) {
const fnDefinition = getFunctionDefinition(root.name);
return fnDefinition?.signatures[0].returnType ?? fallback;
}
return fallback;
}
function getAssignRightHandSideType(
item: ESQLAstItem,
fields: Map<string, ESQLRealField>,
variables: Map<string, ESQLVariable[]>
) {
if (Array.isArray(item)) {
const firstArg = item[0];
return getExpressionType(firstArg, fields, variables);
}
}
export function excludeVariablesFromCurrentCommand(
commands: ESQLCommand[],
currentCommand: ESQLCommand,
@ -122,14 +66,10 @@ function addVariableFromAssignment(
fields: Map<string, ESQLRealField>
) {
if (isColumnItem(assignOperation.args[0])) {
const rightHandSideArgType = getAssignRightHandSideType(
assignOperation.args[1],
fields,
variables
);
const rightHandSideArgType = getExpressionType(assignOperation.args[1], fields, variables);
addToVariableOccurrences(variables, {
name: assignOperation.args[0].parts.join('.'),
type: rightHandSideArgType as string /* fallback to number */,
type: rightHandSideArgType /* fallback to number */,
location: assignOperation.args[0].location,
});
}
@ -138,14 +78,15 @@ function addVariableFromAssignment(
function addVariableFromExpression(
expressionOperation: ESQLFunction,
queryString: string,
variables: Map<string, ESQLVariable[]>
variables: Map<string, ESQLVariable[]>,
fields: Map<string, ESQLRealField>
) {
if (!expressionOperation.text.includes(EDITOR_MARKER)) {
const expressionText = queryString.substring(
expressionOperation.location.min,
expressionOperation.location.max + 1
);
const expressionType = 'double'; // TODO - use getExpressionType once it actually works
const expressionType = getExpressionType(expressionOperation, fields, variables);
addToVariableOccurrences(variables, {
name: expressionText,
type: expressionType,
@ -174,7 +115,7 @@ export function collectVariables(
if (ctx.node.name === '=') {
addVariableFromAssignment(ctx.node, variables, fields);
} else {
addVariableFromExpression(ctx.node, queryString, variables);
addVariableFromExpression(ctx.node, queryString, variables, fields);
}
})
.on('visitCommandOption', (ctx) => {

View file

@ -100,12 +100,12 @@ describe('function validation', () => {
// straight call
await expectErrors('FROM a_index | EVAL TEST(1.1)', [
'Argument of [test] must be [integer], found value [1.1] type [decimal]',
'Argument of [test] must be [integer], found value [1.1] type [double]',
]);
// assignment
await expectErrors('FROM a_index | EVAL var = TEST(1.1)', [
'Argument of [test] must be [integer], found value [1.1] type [decimal]',
'Argument of [test] must be [integer], found value [1.1] type [double]',
]);
// nested function
@ -115,7 +115,7 @@ describe('function validation', () => {
// inline cast
await expectErrors('FROM a_index | EVAL TEST(1::DOUBLE)', [
'Argument of [test] must be [integer], found value [1::DOUBLE] type [DOUBLE]',
'Argument of [test] must be [integer], found value [1::DOUBLE] type [double]',
]);
// field
@ -125,13 +125,13 @@ describe('function validation', () => {
// variables
await expectErrors('FROM a_index | EVAL var1 = 1. | EVAL TEST(var1)', [
'Argument of [test] must be [integer], found value [var1] type [decimal]',
'Argument of [test] must be [integer], found value [var1] type [double]',
]);
// multiple instances
await expectErrors('FROM a_index | EVAL TEST(1.1) | EVAL TEST(1.1)', [
'Argument of [test] must be [integer], found value [1.1] type [decimal]',
'Argument of [test] must be [integer], found value [1.1] type [decimal]',
'Argument of [test] must be [integer], found value [1.1] type [double]',
'Argument of [test] must be [integer], found value [1.1] type [double]',
]);
});
@ -190,7 +190,7 @@ describe('function validation', () => {
await expectErrors('ROW "a" IN ("a", "b", "c")', []);
await expectErrors('ROW "a" IN (1, "b", "c")', [
'Argument of [in] must be [keyword[]], found value [(1, "b", "c")] type [(integer, string, string)]',
'Argument of [in] must be [keyword[]], found value [(1, "b", "c")] type [(integer, keyword, keyword)]',
]);
});
});
@ -238,9 +238,9 @@ describe('function validation', () => {
// double, double, double
await expectErrors('FROM a_index | EVAL TEST(1., 1., 1.)', []);
await expectErrors('FROM a_index | EVAL TEST("", "", "")', [
'Argument of [test] must be [double], found value [""] type [string]',
'Argument of [test] must be [double], found value [""] type [string]',
'Argument of [test] must be [double], found value [""] type [string]',
'Argument of [test] must be [double], found value [""] type [keyword]',
'Argument of [test] must be [double], found value [""] type [keyword]',
'Argument of [test] must be [double], found value [""] type [keyword]',
]);
// int, int
@ -260,7 +260,7 @@ describe('function validation', () => {
// date
await expectErrors('FROM a_index | EVAL TEST(NOW())', []);
await expectErrors('FROM a_index | EVAL TEST(1.)', [
'Argument of [test] must be [date], found value [1.] type [decimal]',
'Argument of [test] must be [date], found value [1.] type [double]',
]);
});
});
@ -721,5 +721,7 @@ describe('function validation', () => {
// 'No nested aggregation functions.',
// ]);
});
// @TODO — test function aliases
});
});

View file

@ -6654,14 +6654,14 @@
{
"query": "from a_index | eval 1 * \"1\"",
"error": [
"Argument of [*] must be [double], found value [\"1\"] type [string]"
"Argument of [*] must be [double], found value [\"1\"] type [keyword]"
],
"warning": []
},
{
"query": "from a_index | eval \"1\" * 1",
"error": [
"Argument of [*] must be [double], found value [\"1\"] type [string]"
"Argument of [*] must be [double], found value [\"1\"] type [keyword]"
],
"warning": []
},
@ -6691,14 +6691,14 @@
{
"query": "from a_index | eval 1 / \"1\"",
"error": [
"Argument of [/] must be [double], found value [\"1\"] type [string]"
"Argument of [/] must be [double], found value [\"1\"] type [keyword]"
],
"warning": []
},
{
"query": "from a_index | eval \"1\" / 1",
"error": [
"Argument of [/] must be [double], found value [\"1\"] type [string]"
"Argument of [/] must be [double], found value [\"1\"] type [keyword]"
],
"warning": []
},
@ -6728,14 +6728,14 @@
{
"query": "from a_index | eval 1 % \"1\"",
"error": [
"Argument of [%] must be [double], found value [\"1\"] type [string]"
"Argument of [%] must be [double], found value [\"1\"] type [keyword]"
],
"warning": []
},
{
"query": "from a_index | eval \"1\" % 1",
"error": [
"Argument of [%] must be [double], found value [\"1\"] type [string]"
"Argument of [%] must be [double], found value [\"1\"] type [keyword]"
],
"warning": []
},
@ -9513,7 +9513,7 @@
"query": "from a_index | eval doubleField = \"5\"",
"error": [],
"warning": [
"Column [doubleField] of type double has been overwritten as new type: string"
"Column [doubleField] of type double has been overwritten as new type: keyword"
]
},
{
@ -9655,19 +9655,19 @@
"warning": []
},
{
"query": "from a_index | eval true AND \"false\"::boolean",
"query": "from a_index | eval true AND 0::boolean",
"error": [],
"warning": []
},
{
"query": "from a_index | eval true AND \"false\"::bool",
"query": "from a_index | eval true AND 0::bool",
"error": [],
"warning": []
},
{
"query": "from a_index | eval true AND \"false\"",
"query": "from a_index | eval true AND 0",
"error": [
"Argument of [and] must be [boolean], found value [\"false\"] type [string]"
"Argument of [and] must be [boolean], found value [0] type [integer]"
],
"warning": []
},

View file

@ -8,12 +8,15 @@
*/
import type { ESQLMessage, ESQLLocation } from '@kbn/esql-ast';
import { FieldType } from '../definitions/types';
import { FieldType, SupportedDataType } from '../definitions/types';
import type { EditorError } from '../types';
export interface ESQLVariable {
name: string;
type: string;
// invalid expressions produce columns of type "unknown"
// also, there are some cases where we can't yet infer the type of
// a valid expression as with `CASE` which can return union types
type: SupportedDataType | 'unknown';
location: ESQLLocation;
}

View file

@ -1129,13 +1129,13 @@ describe('validation logic', () => {
`from a_index | eval 1 ${op} "1"`,
['+', '-'].includes(op)
? [`Argument of [${op}] must be [date_period], found value [1] type [integer]`]
: [`Argument of [${op}] must be [double], found value [\"1\"] type [string]`]
: [`Argument of [${op}] must be [double], found value [\"1\"] type [keyword]`]
);
testErrorsAndWarnings(
`from a_index | eval "1" ${op} 1`,
['+', '-'].includes(op)
? [`Argument of [${op}] must be [date_period], found value [1] type [integer]`]
: [`Argument of [${op}] must be [double], found value [\"1\"] type [string]`]
: [`Argument of [${op}] must be [double], found value [\"1\"] type [keyword]`]
);
// TODO: enable when https://github.com/elastic/elasticsearch/issues/108432 is complete
// testErrorsAndWarnings(`from a_index | eval "2022" ${op} 1 day`, []);
@ -1478,7 +1478,7 @@ describe('validation logic', () => {
testErrorsAndWarnings(
'from a_index | eval doubleField = "5"',
[],
['Column [doubleField] of type double has been overwritten as new type: string']
['Column [doubleField] of type double has been overwritten as new type: keyword']
);
});
@ -1674,11 +1674,11 @@ describe('validation logic', () => {
testErrorsAndWarnings('from a_index | eval TRIM(23::text)', []);
testErrorsAndWarnings('from a_index | eval TRIM(23::keyword)', []);
testErrorsAndWarnings('from a_index | eval true AND "false"::boolean', []);
testErrorsAndWarnings('from a_index | eval true AND "false"::bool', []);
testErrorsAndWarnings('from a_index | eval true AND "false"', [
testErrorsAndWarnings('from a_index | eval true AND 0::boolean', []);
testErrorsAndWarnings('from a_index | eval true AND 0::bool', []);
testErrorsAndWarnings('from a_index | eval true AND 0', [
// just a counter-case to make sure the previous tests are meaningful
'Argument of [and] must be [boolean], found value ["false"] type [string]',
'Argument of [and] must be [boolean], found value [0] type [integer]',
]);
// enforces strings for cartesian_point conversion

View file

@ -26,7 +26,6 @@ import {
CommandModeDefinition,
CommandOptionsDefinition,
FunctionParameter,
FunctionDefinition,
} from '../definitions/types';
import {
areFieldAndVariableTypesCompatible,
@ -54,6 +53,7 @@ import {
isAggFunction,
getQuotedColumnName,
isInlineCastItem,
getSignaturesWithMatchingArity,
} from '../shared/helpers';
import { collectVariables } from '../shared/variables';
import { getMessageFromId, errors } from './errors';
@ -74,7 +74,7 @@ import {
retrieveFieldsFromStringSources,
} from './resources';
import { collapseWrongArgumentTypeMessages, getMaxMinNumberOfParams } from './helpers';
import { getParamAtPosition } from '../autocomplete/helper';
import { getParamAtPosition } from '../shared/helpers';
import { METADATA_FIELDS } from '../shared/constants';
import { compareTypesWithLiterals } from '../shared/esql_types';
@ -88,7 +88,7 @@ function validateFunctionLiteralArg(
const messages: ESQLMessage[] = [];
if (isLiteralItem(actualArg)) {
if (
actualArg.literalType === 'string' &&
actualArg.literalType === 'keyword' &&
argDef.acceptedValues &&
isValidLiteralOption(actualArg, argDef)
) {
@ -309,21 +309,6 @@ function validateFunctionColumnArg(
return messages;
}
function extractCompatibleSignaturesForFunction(
fnDef: FunctionDefinition,
astFunction: ESQLFunction
) {
return fnDef.signatures.filter((def) => {
if (def.minParams) {
return astFunction.args.length >= def.minParams;
}
return (
astFunction.args.length >= def.params.filter(({ optional }) => !optional).length &&
astFunction.args.length <= def.params.length
);
});
}
function removeInlineCasts(arg: ESQLAstItem): ESQLAstItem {
if (isInlineCastItem(arg)) {
return removeInlineCasts(arg.value);
@ -376,7 +361,7 @@ function validateFunction(
return messages;
}
}
const matchingSignatures = extractCompatibleSignaturesForFunction(fnDefinition, astFunction);
const matchingSignatures = getSignaturesWithMatchingArity(fnDefinition, astFunction);
if (!matchingSignatures.length) {
const { max, min } = getMaxMinNumberOfParams(fnDefinition);
if (max === min) {

View file

@ -5221,7 +5221,6 @@
"kbn-esql-validation-autocomplete.esql.definition.assignDoc": "Affecter (=)",
"kbn-esql-validation-autocomplete.esql.definition.divideDoc": "Diviser (/)",
"kbn-esql-validation-autocomplete.esql.definition.equalToDoc": "Égal à",
"kbn-esql-validation-autocomplete.esql.definition.functionsDoc": "Afficher les fonctions ES|QL disponibles avec signatures",
"kbn-esql-validation-autocomplete.esql.definition.greaterThanDoc": "Supérieur à",
"kbn-esql-validation-autocomplete.esql.definition.greaterThanOrEqualToDoc": "Supérieur ou égal à",
"kbn-esql-validation-autocomplete.esql.definition.inDoc": "Teste si la valeur d'une expression est contenue dans une liste d'autres expressions",

View file

@ -5214,7 +5214,6 @@
"kbn-esql-validation-autocomplete.esql.definition.assignDoc": "割り当て(=",
"kbn-esql-validation-autocomplete.esql.definition.divideDoc": "除算(/",
"kbn-esql-validation-autocomplete.esql.definition.equalToDoc": "等しい",
"kbn-esql-validation-autocomplete.esql.definition.functionsDoc": "ES|QLで使用可能な関数と署名を表示",
"kbn-esql-validation-autocomplete.esql.definition.greaterThanDoc": "より大きい",
"kbn-esql-validation-autocomplete.esql.definition.greaterThanOrEqualToDoc": "よりも大きいまたは等しい",
"kbn-esql-validation-autocomplete.esql.definition.inDoc": "ある式が取る値が、他の式のリストに含まれているかどうかをテストします",

View file

@ -5225,7 +5225,6 @@
"kbn-esql-validation-autocomplete.esql.definition.assignDoc": "分配 (=)",
"kbn-esql-validation-autocomplete.esql.definition.divideDoc": "除 (/)",
"kbn-esql-validation-autocomplete.esql.definition.equalToDoc": "等于",
"kbn-esql-validation-autocomplete.esql.definition.functionsDoc": "显示带签名的 ES|QL 可用函数",
"kbn-esql-validation-autocomplete.esql.definition.greaterThanDoc": "大于",
"kbn-esql-validation-autocomplete.esql.definition.greaterThanOrEqualToDoc": "大于或等于",
"kbn-esql-validation-autocomplete.esql.definition.inDoc": "测试某表达式接受的值是否包含在其他表达式列表中",