mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[ES|QL] Adds parsing support for CHANGE_POINT
command (#213982)
## Summary Partially addresses https://github.com/elastic/kibana/issues/211543 - Implements parser support for `CHANGE_POINT` command. - Introduces `ESQLAstChangePointCommand` interface for `CHANGE_POINT` commands AST nodes. - Parses command arguments into `args` array as well as more structural fields `value`, `key?`, `target?`. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
bac2f0c6bd
commit
dd9b5bb1b7
6 changed files with 335 additions and 6 deletions
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { EsqlQuery } from '../../query';
|
||||
import { ESQLCommandOption } from '../../types';
|
||||
import { Walker } from '../../walker';
|
||||
|
||||
describe('CHANGE_POINT command', () => {
|
||||
describe('correctly formatted', () => {
|
||||
describe('CHANGE_POINT <value> ...', () => {
|
||||
it('can parse the command', () => {
|
||||
const text = `FROM index | CHANGE_POINT value`;
|
||||
const query = EsqlQuery.fromSrc(text);
|
||||
|
||||
expect(query.ast.commands[1]).toMatchObject({
|
||||
type: 'command',
|
||||
name: 'change_point',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses value expression as command field', () => {
|
||||
const text = `FROM index | CHANGE_POINT value`;
|
||||
const query = EsqlQuery.fromSrc(text);
|
||||
|
||||
expect(query.ast.commands[1]).toMatchObject({
|
||||
value: {
|
||||
type: 'column',
|
||||
name: 'value',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses value expression as the first argument', () => {
|
||||
const text = `FROM index | CHANGE_POINT value`;
|
||||
const query = EsqlQuery.fromSrc(text);
|
||||
|
||||
expect(query.ast.commands[1]).toMatchObject({
|
||||
args: [
|
||||
{
|
||||
type: 'column',
|
||||
name: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('... ON key ...', () => {
|
||||
it('parses key expression as command field', () => {
|
||||
const text = `FROM index | CHANGE_POINT value ON key`;
|
||||
const query = EsqlQuery.fromSrc(text);
|
||||
|
||||
expect(query.ast.commands[1]).toMatchObject({
|
||||
key: {
|
||||
type: 'column',
|
||||
name: 'key',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses key expression as command argument', () => {
|
||||
const text = `FROM index | CHANGE_POINT value ON key`;
|
||||
const query = EsqlQuery.fromSrc(text);
|
||||
|
||||
expect(query.ast.commands[1]).toMatchObject({
|
||||
args: [
|
||||
{},
|
||||
{
|
||||
type: 'option',
|
||||
name: 'on',
|
||||
args: [
|
||||
{
|
||||
type: 'column',
|
||||
name: 'key',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('parses correctly ON option location', () => {
|
||||
const text = `FROM index | CHANGE_POINT value ON key`;
|
||||
const query = EsqlQuery.fromSrc(text);
|
||||
const option = query.ast.commands[1].args[1] as ESQLCommandOption;
|
||||
|
||||
expect(option).toMatchObject({
|
||||
type: 'option',
|
||||
name: 'on',
|
||||
});
|
||||
expect(text.slice(option.location!.min, option.location!.max + 1)).toBe('ON key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('... AS type, pvalue', () => {
|
||||
it('parses AS option as command field', () => {
|
||||
const text = `FROM index | CHANGE_POINT value AS type, pvalue`;
|
||||
const query = EsqlQuery.fromSrc(text);
|
||||
|
||||
expect(query.ast.commands[1]).toMatchObject({
|
||||
target: {
|
||||
type: {
|
||||
type: 'column',
|
||||
name: 'type',
|
||||
},
|
||||
pvalue: {
|
||||
type: 'column',
|
||||
name: 'pvalue',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses AS option as command argument', () => {
|
||||
const text = `FROM index | CHANGE_POINT value AS type, pvalue`;
|
||||
const query = EsqlQuery.fromSrc(text);
|
||||
|
||||
expect(query.ast.commands[1]).toMatchObject({
|
||||
args: [
|
||||
{},
|
||||
{
|
||||
type: 'option',
|
||||
name: 'as',
|
||||
args: [
|
||||
{
|
||||
type: 'column',
|
||||
name: 'type',
|
||||
},
|
||||
{
|
||||
type: 'column',
|
||||
name: 'pvalue',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly reports AS option location', () => {
|
||||
const text = `FROM index | CHANGE_POINT value AS /* hello */ type, /* world */ pvalue`;
|
||||
const query = EsqlQuery.fromSrc(text);
|
||||
const option = query.ast.commands[1].args[1] as ESQLCommandOption;
|
||||
|
||||
expect(option).toMatchObject({
|
||||
type: 'option',
|
||||
name: 'as',
|
||||
});
|
||||
expect(text.slice(option.location!.min, option.location!.max + 1)).toBe(
|
||||
'AS /* hello */ type, /* world */ pvalue'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('parses example query with all options', () => {
|
||||
const text = `
|
||||
FROM k8s
|
||||
| STATS count=COUNT() BY @timestamp=BUCKET(@timestamp, 1 MINUTE)
|
||||
| CHANGE_POINT count ON @timestamp AS type, pvalue
|
||||
`;
|
||||
const query = EsqlQuery.fromSrc(text);
|
||||
const command = Walker.match(query.ast, { type: 'command', name: 'change_point' });
|
||||
|
||||
expect(command).toMatchObject({
|
||||
type: 'command',
|
||||
name: 'change_point',
|
||||
value: {
|
||||
type: 'column',
|
||||
name: 'count',
|
||||
},
|
||||
key: {
|
||||
type: 'column',
|
||||
name: '@timestamp',
|
||||
},
|
||||
target: {
|
||||
type: {
|
||||
type: 'column',
|
||||
name: 'type',
|
||||
},
|
||||
pvalue: {
|
||||
type: 'column',
|
||||
name: 'pvalue',
|
||||
},
|
||||
},
|
||||
args: [
|
||||
{
|
||||
type: 'column',
|
||||
name: 'count',
|
||||
},
|
||||
{
|
||||
type: 'option',
|
||||
name: 'on',
|
||||
args: [
|
||||
{
|
||||
type: 'column',
|
||||
name: '@timestamp',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'option',
|
||||
name: 'as',
|
||||
args: [
|
||||
{
|
||||
type: 'column',
|
||||
name: 'type',
|
||||
},
|
||||
{
|
||||
type: 'column',
|
||||
name: 'pvalue',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -31,6 +31,7 @@ import {
|
|||
IndexPatternContext,
|
||||
InlinestatsCommandContext,
|
||||
JoinCommandContext,
|
||||
type ChangePointCommandContext,
|
||||
} from '../antlr/esql_parser';
|
||||
import { default as ESQLParserListener } from '../antlr/esql_parser_listener';
|
||||
import {
|
||||
|
@ -61,6 +62,7 @@ import { createJoinCommand } from './factories/join';
|
|||
import { createDissectCommand } from './factories/dissect';
|
||||
import { createGrokCommand } from './factories/grok';
|
||||
import { createStatsCommand } from './factories/stats';
|
||||
import { createChangePointCommand } from './factories/change_point';
|
||||
|
||||
export class ESQLAstBuilderListener implements ESQLParserListener {
|
||||
private ast: ESQLAst = [];
|
||||
|
@ -317,6 +319,21 @@ export class ESQLAstBuilderListener implements ESQLParserListener {
|
|||
this.ast.push(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit a parse tree produced by `esql_parser.changePointCommand`.
|
||||
*
|
||||
* Parse the CHANGE_POINT command:
|
||||
*
|
||||
* CHANGE_POINT <value> [ ON <key> ] [ AS <target-type>, <target-pvalue> ]
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
*/
|
||||
exitChangePointCommand(ctx: ChangePointCommandContext): void {
|
||||
const command = createChangePointCommand(ctx);
|
||||
|
||||
this.ast.push(command);
|
||||
}
|
||||
|
||||
enterEveryRule(ctx: ParserRuleContext): void {
|
||||
// method not implemented, added to satisfy interface expectation
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ import type {
|
|||
ESQLIdentifier,
|
||||
ESQLBinaryExpression,
|
||||
BinaryExpressionOperator,
|
||||
ESQLCommand,
|
||||
} from '../types';
|
||||
import { parseIdentifier, getPosition } from './helpers';
|
||||
import { Builder, type AstNodeParserFields } from '../builder';
|
||||
|
@ -84,8 +85,22 @@ const createParserFields = (ctx: ParserRuleContext): AstNodeParserFields => ({
|
|||
incomplete: Boolean(ctx.exception),
|
||||
});
|
||||
|
||||
export const createCommand = <Name extends string>(name: Name, ctx: ParserRuleContext) =>
|
||||
Builder.command({ name, args: [] }, createParserFields(ctx));
|
||||
export const createCommand = <
|
||||
Name extends string,
|
||||
Cmd extends ESQLCommand<Name> = ESQLCommand<Name>
|
||||
>(
|
||||
name: Name,
|
||||
ctx: ParserRuleContext,
|
||||
partial?: Partial<Cmd>
|
||||
): Cmd => {
|
||||
const command = Builder.command({ name, args: [] }, createParserFields(ctx)) as Cmd;
|
||||
|
||||
if (partial) {
|
||||
Object.assign(command, partial);
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
|
||||
export const createInlineCast = (ctx: InlineCastContext, value: ESQLInlineCast['value']) =>
|
||||
Builder.expression.inlineCast(
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ChangePointCommandContext } from '../../antlr/esql_parser';
|
||||
import { Builder } from '../../builder';
|
||||
import { ESQLAstChangePointCommand } from '../../types';
|
||||
import { createColumn, createCommand } from '../factories';
|
||||
import { getPosition } from '../helpers';
|
||||
|
||||
export const createChangePointCommand = (
|
||||
ctx: ChangePointCommandContext
|
||||
): ESQLAstChangePointCommand => {
|
||||
const value = createColumn(ctx._value);
|
||||
const command = createCommand<'change_point', ESQLAstChangePointCommand>('change_point', ctx, {
|
||||
value,
|
||||
});
|
||||
|
||||
command.args.push(value);
|
||||
|
||||
if (ctx._key) {
|
||||
const key = createColumn(ctx._key);
|
||||
const option = Builder.option(
|
||||
{
|
||||
name: 'on',
|
||||
args: [key],
|
||||
},
|
||||
{
|
||||
location: getPosition(ctx.ON().symbol, ctx._key.stop),
|
||||
}
|
||||
);
|
||||
|
||||
command.key = key;
|
||||
command.args.push(option);
|
||||
}
|
||||
|
||||
if (ctx._targetType && ctx._targetPvalue) {
|
||||
const type = createColumn(ctx._targetType);
|
||||
const pvalue = createColumn(ctx._targetPvalue);
|
||||
const option = Builder.option(
|
||||
{
|
||||
name: 'as',
|
||||
args: [type, pvalue],
|
||||
},
|
||||
{
|
||||
location: getPosition(ctx.AS().symbol, ctx._targetPvalue.stop),
|
||||
}
|
||||
);
|
||||
|
||||
command.target = {
|
||||
type,
|
||||
pvalue,
|
||||
};
|
||||
command.args.push(option);
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { JoinCommandContext, JoinTargetContext } from '../../antlr/esql_parser';
|
||||
import { ESQLAstItem, ESQLCommand, ESQLIdentifier, ESQLSource } from '../../types';
|
||||
import { ESQLAstItem, ESQLAstJoinCommand, ESQLIdentifier, ESQLSource } from '../../types';
|
||||
import { createCommand, createOption, createSource } from '../factories';
|
||||
import { visitValueExpression } from '../walkers';
|
||||
|
||||
|
@ -16,11 +16,13 @@ const createNodeFromJoinTarget = (ctx: JoinTargetContext): ESQLSource | ESQLIden
|
|||
return createSource(ctx._index);
|
||||
};
|
||||
|
||||
export const createJoinCommand = (ctx: JoinCommandContext): ESQLCommand => {
|
||||
const command = createCommand('join', ctx);
|
||||
export const createJoinCommand = (ctx: JoinCommandContext): ESQLAstJoinCommand => {
|
||||
const command = createCommand<'join', ESQLAstJoinCommand>('join', ctx);
|
||||
|
||||
// Pick-up the <TYPE> of the command.
|
||||
command.commandType = (ctx._type_?.text ?? 'lookup').toLocaleLowerCase();
|
||||
command.commandType = (
|
||||
ctx._type_?.text ?? 'lookup'
|
||||
).toLocaleLowerCase() as ESQLAstJoinCommand['commandType'];
|
||||
|
||||
const joinTarget = createNodeFromJoinTarget(ctx.joinTarget());
|
||||
const joinCondition = ctx.joinCondition();
|
||||
|
|
|
@ -96,6 +96,15 @@ export interface ESQLAstJoinCommand extends ESQLCommand<'join'> {
|
|||
commandType: 'lookup' | 'left' | 'right';
|
||||
}
|
||||
|
||||
export interface ESQLAstChangePointCommand extends ESQLCommand<'change_point'> {
|
||||
value: ESQLColumn;
|
||||
key?: ESQLColumn;
|
||||
target?: {
|
||||
type: ESQLColumn;
|
||||
pvalue: ESQLColumn;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ESQLCommandOption extends ESQLAstBaseItem {
|
||||
type: 'option';
|
||||
args: ESQLAstItem[];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue