mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[ES|QL] Improve the `Builder` class (#203558)](https://github.com/elastic/kibana/pull/203558) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Vadim Kibana","email":"82822460+vadimkibana@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-12-13T15:19:10Z","message":"[ES|QL] Improve the `Builder` class (#203558)\n\n## Summary\r\n\r\nPartially addresses https://github.com/elastic/kibana/issues/202113\r\n\r\n- Makes sure it is possible to construct any AST using the `Builder`\r\nclass\r\n- Fixes few bugs in pretty-printer\r\n- No space is added before unary `-` or `+` expression, for example,\r\n`-123`.\r\n - Source cluster is now printed `cluster:my_index` \r\n\r\n\r\n### Checklist\r\n\r\n- [x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\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","sha":"8904cb068cf6140f6cc4a8db5cde2524a4fa2240","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["review","release_note:skip","v9.0.0","backport:prev-minor","Feature:ES|QL","Team:ESQL","v8.18.0"],"title":"[ES|QL] Improve the `Builder` class","number":203558,"url":"https://github.com/elastic/kibana/pull/203558","mergeCommit":{"message":"[ES|QL] Improve the `Builder` class (#203558)\n\n## Summary\r\n\r\nPartially addresses https://github.com/elastic/kibana/issues/202113\r\n\r\n- Makes sure it is possible to construct any AST using the `Builder`\r\nclass\r\n- Fixes few bugs in pretty-printer\r\n- No space is added before unary `-` or `+` expression, for example,\r\n`-123`.\r\n - Source cluster is now printed `cluster:my_index` \r\n\r\n\r\n### Checklist\r\n\r\n- [x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\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","sha":"8904cb068cf6140f6cc4a8db5cde2524a4fa2240"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/203558","number":203558,"mergeCommit":{"message":"[ES|QL] Improve the `Builder` class (#203558)\n\n## Summary\r\n\r\nPartially addresses https://github.com/elastic/kibana/issues/202113\r\n\r\n- Makes sure it is possible to construct any AST using the `Builder`\r\nclass\r\n- Fixes few bugs in pretty-printer\r\n- No space is added before unary `-` or `+` expression, for example,\r\n`-123`.\r\n - Source cluster is now printed `cluster:my_index` \r\n\r\n\r\n### Checklist\r\n\r\n- [x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\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","sha":"8904cb068cf6140f6cc4a8db5cde2524a4fa2240"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com>
This commit is contained in:
parent
3979f74e0a
commit
cfe4d8375a
10 changed files with 614 additions and 40 deletions
|
@ -52,7 +52,7 @@ export const FromCommand: React.FC = () => {
|
|||
onClick={() => {
|
||||
const length = from.args.length;
|
||||
const source = Builder.expression.source({
|
||||
name: `source${length + 1}`,
|
||||
index: `source${length + 1}`,
|
||||
sourceType: 'index',
|
||||
});
|
||||
from.args.push(source);
|
||||
|
|
|
@ -8,14 +8,367 @@
|
|||
*/
|
||||
|
||||
import { Builder } from '.';
|
||||
import { BasicPrettyPrinter } from '../pretty_print';
|
||||
|
||||
test('can mint a numeric literal', () => {
|
||||
const node = Builder.expression.literal.numeric({ value: 42, literalType: 'integer' });
|
||||
describe('command', () => {
|
||||
test('can create a LIMIT command', () => {
|
||||
const node = Builder.command({
|
||||
name: 'limit',
|
||||
args: [Builder.expression.literal.integer(10)],
|
||||
});
|
||||
const text = BasicPrettyPrinter.command(node);
|
||||
|
||||
expect(node).toMatchObject({
|
||||
type: 'literal',
|
||||
literalType: 'integer',
|
||||
name: '42',
|
||||
value: 42,
|
||||
expect(text).toBe('LIMIT 10');
|
||||
});
|
||||
|
||||
test('can create a FROM command with BY option', () => {
|
||||
const node = Builder.command({
|
||||
name: 'from',
|
||||
args: [
|
||||
Builder.expression.source({ index: 'my_index', sourceType: 'index' }),
|
||||
Builder.option({
|
||||
name: 'by',
|
||||
args: [
|
||||
Builder.expression.column({
|
||||
args: [Builder.identifier({ name: '_id' })],
|
||||
}),
|
||||
Builder.expression.column({
|
||||
args: [Builder.identifier('_source')],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
const text = BasicPrettyPrinter.command(node);
|
||||
|
||||
expect(text).toBe('FROM my_index BY _id, _source');
|
||||
});
|
||||
});
|
||||
|
||||
describe('function', () => {
|
||||
test('can mint a binary expression', () => {
|
||||
const node = Builder.expression.func.binary('+', [
|
||||
Builder.expression.literal.integer(1),
|
||||
Builder.expression.literal.integer(2),
|
||||
]);
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('1 + 2');
|
||||
});
|
||||
|
||||
test('can mint a unary expression', () => {
|
||||
const node = Builder.expression.func.unary('not', Builder.expression.literal.integer(123));
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('NOT 123');
|
||||
});
|
||||
|
||||
test('can mint "-" unary expression', () => {
|
||||
const node = Builder.expression.func.unary('-', Builder.expression.literal.integer(123));
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('-123');
|
||||
});
|
||||
|
||||
test('can mint a unary postfix expression', () => {
|
||||
const node = Builder.expression.func.postfix(
|
||||
'is not null',
|
||||
Builder.expression.literal.integer(123)
|
||||
);
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('123 IS NOT NULL');
|
||||
});
|
||||
|
||||
test('can mint a function call', () => {
|
||||
const node = Builder.expression.func.call('agg', [
|
||||
Builder.expression.literal.integer(1),
|
||||
Builder.expression.literal.integer(2),
|
||||
Builder.expression.literal.integer(3),
|
||||
]);
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('AGG(1, 2, 3)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('source', () => {
|
||||
test('basic index', () => {
|
||||
const node = Builder.expression.source({ index: 'my_index', sourceType: 'index' });
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('my_index');
|
||||
});
|
||||
|
||||
test('basic index using shortcut', () => {
|
||||
const node = Builder.expression.source('my_index');
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('my_index');
|
||||
});
|
||||
|
||||
test('index with cluster', () => {
|
||||
const node = Builder.expression.source({
|
||||
index: 'my_index',
|
||||
sourceType: 'index',
|
||||
cluster: 'my_cluster',
|
||||
});
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('my_cluster:my_index');
|
||||
});
|
||||
|
||||
test('can use .indexSource() shorthand to specify cluster', () => {
|
||||
const node = Builder.expression.indexSource('my_index', 'my_cluster');
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('my_cluster:my_index');
|
||||
});
|
||||
|
||||
test('policy index', () => {
|
||||
const node = Builder.expression.source({ index: 'my_policy', sourceType: 'policy' });
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('my_policy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('column', () => {
|
||||
test('a simple field', () => {
|
||||
const node = Builder.expression.column({ args: [Builder.identifier('my_field')] });
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('my_field');
|
||||
});
|
||||
|
||||
test('a simple field using shorthand', () => {
|
||||
const node = Builder.expression.column('my_field');
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('my_field');
|
||||
});
|
||||
|
||||
test('a nested field', () => {
|
||||
const node = Builder.expression.column({
|
||||
args: [Builder.identifier('locale'), Builder.identifier('region')],
|
||||
});
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('locale.region');
|
||||
});
|
||||
|
||||
test('a nested field using shortcut', () => {
|
||||
const node = Builder.expression.column(['locale', 'region']);
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('locale.region');
|
||||
});
|
||||
|
||||
test('a nested with params using shortcut', () => {
|
||||
const node = Builder.expression.column(['locale', '?param', 'region']);
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('locale.?param.region');
|
||||
});
|
||||
});
|
||||
|
||||
describe('literal', () => {
|
||||
describe('"time interval"', () => {
|
||||
test('a basic time Interval node', () => {
|
||||
const node = Builder.expression.literal.qualifiedInteger(42, 'days');
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('42 days');
|
||||
});
|
||||
});
|
||||
|
||||
describe('null', () => {
|
||||
test('can create a NULL node', () => {
|
||||
const node = Builder.expression.literal.nil();
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('NULL');
|
||||
expect(node).toMatchObject({
|
||||
type: 'literal',
|
||||
literalType: 'null',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('numeric', () => {
|
||||
test('integer shorthand', () => {
|
||||
const node = Builder.expression.literal.integer(42);
|
||||
|
||||
expect(node).toMatchObject({
|
||||
type: 'literal',
|
||||
literalType: 'integer',
|
||||
name: '42',
|
||||
value: 42,
|
||||
});
|
||||
});
|
||||
|
||||
test('decimal shorthand', () => {
|
||||
const node = Builder.expression.literal.decimal(3.14);
|
||||
|
||||
expect(node).toMatchObject({
|
||||
type: 'literal',
|
||||
literalType: 'double',
|
||||
name: '3.14',
|
||||
value: 3.14,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('string', () => {
|
||||
test('can create a basic string', () => {
|
||||
const node = Builder.expression.literal.string('abc');
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('"""abc"""');
|
||||
expect(node).toMatchObject({
|
||||
type: 'literal',
|
||||
literalType: 'keyword',
|
||||
name: '"""abc"""',
|
||||
value: '"""abc"""',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('boolean', () => {
|
||||
test('TRUE literal', () => {
|
||||
const node = Builder.expression.literal.boolean(true);
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('TRUE');
|
||||
expect(node).toMatchObject({
|
||||
type: 'literal',
|
||||
literalType: 'boolean',
|
||||
name: 'true',
|
||||
value: 'true',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('lists', () => {
|
||||
test('string list', () => {
|
||||
const node = Builder.expression.literal.list({
|
||||
values: [
|
||||
Builder.expression.literal.string('a'),
|
||||
Builder.expression.literal.string('b'),
|
||||
Builder.expression.literal.string('c'),
|
||||
],
|
||||
});
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('["""a""", """b""", """c"""]');
|
||||
});
|
||||
|
||||
test('integer list', () => {
|
||||
const node = Builder.expression.literal.list({
|
||||
values: [
|
||||
Builder.expression.literal.integer(1),
|
||||
Builder.expression.literal.integer(2),
|
||||
Builder.expression.literal.integer(3),
|
||||
],
|
||||
});
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('[1, 2, 3]');
|
||||
});
|
||||
|
||||
test('boolean list', () => {
|
||||
const node = Builder.expression.literal.list({
|
||||
values: [
|
||||
Builder.expression.literal.boolean(true),
|
||||
Builder.expression.literal.boolean(false),
|
||||
],
|
||||
});
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('[TRUE, FALSE]');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('identifier', () => {
|
||||
test('a single identifier node', () => {
|
||||
const node = Builder.identifier('text');
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('param', () => {
|
||||
test('unnamed', () => {
|
||||
const node = Builder.param.build('?');
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('?');
|
||||
expect(node).toMatchObject({
|
||||
type: 'literal',
|
||||
literalType: 'param',
|
||||
paramType: 'unnamed',
|
||||
});
|
||||
});
|
||||
|
||||
test('named', () => {
|
||||
const node = Builder.param.build('?the_name');
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('?the_name');
|
||||
expect(node).toMatchObject({
|
||||
type: 'literal',
|
||||
literalType: 'param',
|
||||
paramType: 'named',
|
||||
value: 'the_name',
|
||||
});
|
||||
});
|
||||
|
||||
test('positional', () => {
|
||||
const node = Builder.param.build('?123');
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('?123');
|
||||
expect(node).toMatchObject({
|
||||
type: 'literal',
|
||||
literalType: 'param',
|
||||
paramType: 'positional',
|
||||
value: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cast', () => {
|
||||
test('cast to integer', () => {
|
||||
const node = Builder.expression.inlineCast({
|
||||
value: Builder.expression.literal.decimal(123.45),
|
||||
castType: 'integer',
|
||||
});
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('123.45::INTEGER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('order', () => {
|
||||
test('field with no modifiers', () => {
|
||||
const node = Builder.expression.order(Builder.expression.column('my_field'), {
|
||||
nulls: '',
|
||||
order: '',
|
||||
});
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('my_field');
|
||||
});
|
||||
|
||||
test('field with ASC and NULL FIRST modifiers', () => {
|
||||
const node = Builder.expression.order(Builder.expression.column(['a', 'b', 'c']), {
|
||||
nulls: 'NULLS FIRST',
|
||||
order: 'ASC',
|
||||
});
|
||||
const text = BasicPrettyPrinter.expression(node);
|
||||
|
||||
expect(text).toBe('a.b.c ASC NULLS FIRST');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,6 +32,12 @@ import {
|
|||
ESQLParamLiteral,
|
||||
ESQLFunction,
|
||||
ESQLAstItem,
|
||||
ESQLBinaryExpression,
|
||||
ESQLUnaryExpression,
|
||||
ESQLTimeInterval,
|
||||
ESQLStringLiteral,
|
||||
ESQLBooleanLiteral,
|
||||
ESQLNullLiteral,
|
||||
} from '../types';
|
||||
import { AstNodeParserFields, AstNodeTemplate, PartialFields } from './types';
|
||||
|
||||
|
@ -100,14 +106,22 @@ export namespace Builder {
|
|||
};
|
||||
};
|
||||
|
||||
export type SourceTemplate = { index: string } & Omit<AstNodeTemplate<ESQLSource>, 'name'>;
|
||||
|
||||
export const source = (
|
||||
template: AstNodeTemplate<ESQLSource>,
|
||||
indexOrTemplate: string | SourceTemplate,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLSource => {
|
||||
const template: SourceTemplate =
|
||||
typeof indexOrTemplate === 'string'
|
||||
? { sourceType: 'index', index: indexOrTemplate }
|
||||
: indexOrTemplate;
|
||||
const { index, cluster } = template;
|
||||
return {
|
||||
...template,
|
||||
...Builder.parserFields(fromParser),
|
||||
type: 'source',
|
||||
name: (cluster ? cluster + ':' : '') + index,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -122,16 +136,29 @@ export namespace Builder {
|
|||
...Builder.parserFields(fromParser),
|
||||
index,
|
||||
cluster,
|
||||
name: (cluster ? cluster + ':' : '') + index,
|
||||
sourceType: 'index',
|
||||
type: 'source',
|
||||
name: (cluster ? cluster + ':' : '') + index,
|
||||
};
|
||||
};
|
||||
|
||||
export type ColumnTemplate = Omit<AstNodeTemplate<ESQLColumn>, 'name' | 'quoted' | 'parts'>;
|
||||
|
||||
export const column = (
|
||||
template: Omit<AstNodeTemplate<ESQLColumn>, 'name' | 'quoted' | 'parts'>,
|
||||
nameOrTemplate: string | string[] | ColumnTemplate,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLColumn => {
|
||||
if (typeof nameOrTemplate === 'string') {
|
||||
nameOrTemplate = [nameOrTemplate];
|
||||
}
|
||||
|
||||
const template: ColumnTemplate = Array.isArray(nameOrTemplate)
|
||||
? {
|
||||
args: nameOrTemplate.map((name: string) =>
|
||||
name[0] === '?' ? Builder.param.build(name) : Builder.identifier(name)
|
||||
),
|
||||
}
|
||||
: nameOrTemplate;
|
||||
const node: ESQLColumn = {
|
||||
...template,
|
||||
...Builder.parserFields(fromParser),
|
||||
|
@ -207,21 +234,83 @@ export namespace Builder {
|
|||
);
|
||||
};
|
||||
|
||||
export const unary = (
|
||||
name: string,
|
||||
arg: ESQLAstItem,
|
||||
template?: Omit<AstNodeTemplate<ESQLFunction>, 'subtype' | 'name' | 'operator' | 'args'>,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLUnaryExpression => {
|
||||
const operator = Builder.identifier({ name });
|
||||
return Builder.expression.func.node(
|
||||
{ ...template, name, operator, args: [arg], subtype: 'unary-expression' },
|
||||
fromParser
|
||||
) as ESQLUnaryExpression;
|
||||
};
|
||||
|
||||
export const postfix = (
|
||||
name: string,
|
||||
arg: ESQLAstItem,
|
||||
template?: Omit<AstNodeTemplate<ESQLFunction>, 'subtype' | 'name' | 'operator' | 'args'>,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLUnaryExpression => {
|
||||
const operator = Builder.identifier({ name });
|
||||
return Builder.expression.func.node(
|
||||
{ ...template, name, operator, args: [arg], subtype: 'postfix-unary-expression' },
|
||||
fromParser
|
||||
) as ESQLUnaryExpression;
|
||||
};
|
||||
|
||||
export const binary = (
|
||||
name: string,
|
||||
args: [left: ESQLAstItem, right: ESQLAstItem],
|
||||
template?: Omit<AstNodeTemplate<ESQLFunction>, 'subtype' | 'name' | 'operator' | 'args'>,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLFunction => {
|
||||
): ESQLBinaryExpression => {
|
||||
const operator = Builder.identifier({ name });
|
||||
return Builder.expression.func.node(
|
||||
{ ...template, name, operator, args, subtype: 'binary-expression' },
|
||||
fromParser
|
||||
);
|
||||
) as ESQLBinaryExpression;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace literal {
|
||||
/**
|
||||
* Constructs a NULL literal node.
|
||||
*/
|
||||
export const nil = (
|
||||
template?: Omit<AstNodeTemplate<ESQLNullLiteral>, 'name' | 'literalType'>,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLNullLiteral => {
|
||||
const node: ESQLNullLiteral = {
|
||||
...template,
|
||||
...Builder.parserFields(fromParser),
|
||||
type: 'literal',
|
||||
literalType: 'null',
|
||||
name: 'NULL',
|
||||
value: 'NULL',
|
||||
};
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
export const boolean = (
|
||||
value: boolean,
|
||||
template?: Omit<AstNodeTemplate<ESQLBooleanLiteral>, 'name' | 'literalType'>,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLBooleanLiteral => {
|
||||
const node: ESQLBooleanLiteral = {
|
||||
...template,
|
||||
...Builder.parserFields(fromParser),
|
||||
type: 'literal',
|
||||
literalType: 'boolean',
|
||||
name: String(value),
|
||||
value: String(value),
|
||||
};
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructs an integer literal node.
|
||||
*/
|
||||
|
@ -239,11 +328,16 @@ export namespace Builder {
|
|||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an integer literal.
|
||||
*
|
||||
* @example 42
|
||||
*/
|
||||
export const integer = (
|
||||
value: number,
|
||||
template?: Omit<AstNodeTemplate<ESQLIntegerLiteral | ESQLDecimalLiteral>, 'name'>,
|
||||
template?: Omit<AstNodeTemplate<ESQLIntegerLiteral>, 'name'>,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLIntegerLiteral | ESQLDecimalLiteral => {
|
||||
): ESQLIntegerLiteral => {
|
||||
return Builder.expression.literal.numeric(
|
||||
{
|
||||
...template,
|
||||
|
@ -251,7 +345,66 @@ export namespace Builder {
|
|||
literalType: 'integer',
|
||||
},
|
||||
fromParser
|
||||
);
|
||||
) as ESQLIntegerLiteral;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a floating point number literal.
|
||||
*
|
||||
* @example 3.14
|
||||
*/
|
||||
export const decimal = (
|
||||
value: number,
|
||||
template?: Omit<AstNodeTemplate<ESQLDecimalLiteral>, 'name'>,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLDecimalLiteral => {
|
||||
return Builder.expression.literal.numeric(
|
||||
{
|
||||
...template,
|
||||
value,
|
||||
literalType: 'double',
|
||||
},
|
||||
fromParser
|
||||
) as ESQLDecimalLiteral;
|
||||
};
|
||||
|
||||
export const string = (
|
||||
value: string,
|
||||
template?: Omit<AstNodeTemplate<ESQLStringLiteral>, 'name' | 'literalType'>,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLStringLiteral => {
|
||||
// TODO: Once (https://github.com/elastic/kibana/issues/203445) do not use
|
||||
// triple quotes and escape the string.
|
||||
const quotedValue = '"""' + value + '"""';
|
||||
const node: ESQLStringLiteral = {
|
||||
...template,
|
||||
...Builder.parserFields(fromParser),
|
||||
type: 'literal',
|
||||
literalType: 'keyword',
|
||||
name: quotedValue,
|
||||
value: quotedValue,
|
||||
};
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructs "time interval" literal node.
|
||||
*
|
||||
* @example 1337 milliseconds
|
||||
*/
|
||||
export const qualifiedInteger = (
|
||||
quantity: ESQLTimeInterval['quantity'],
|
||||
unit: ESQLTimeInterval['unit'],
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLTimeInterval => {
|
||||
return {
|
||||
...Builder.parserFields(fromParser),
|
||||
type: 'timeInterval',
|
||||
unit,
|
||||
quantity,
|
||||
name: `${quantity} ${unit}`,
|
||||
};
|
||||
};
|
||||
|
||||
export const list = (
|
||||
|
@ -269,9 +422,11 @@ export namespace Builder {
|
|||
}
|
||||
|
||||
export const identifier = (
|
||||
template: AstNodeTemplate<ESQLIdentifier>,
|
||||
nameOrTemplate: string | AstNodeTemplate<ESQLIdentifier>,
|
||||
fromParser?: Partial<AstNodeParserFields>
|
||||
): ESQLIdentifier => {
|
||||
const template: AstNodeTemplate<ESQLIdentifier> =
|
||||
typeof nameOrTemplate === 'string' ? { name: nameOrTemplate } : nameOrTemplate;
|
||||
return {
|
||||
...template,
|
||||
...Builder.parserFields(fromParser),
|
||||
|
|
|
@ -21,7 +21,7 @@ describe('generic.commands.args', () => {
|
|||
|
||||
generic.commands.args.insert(
|
||||
command!,
|
||||
Builder.expression.source({ name: 'test', sourceType: 'index' }),
|
||||
Builder.expression.source({ index: 'test', sourceType: 'index' }),
|
||||
123
|
||||
);
|
||||
|
||||
|
@ -37,7 +37,7 @@ describe('generic.commands.args', () => {
|
|||
|
||||
generic.commands.args.insert(
|
||||
command!,
|
||||
Builder.expression.source({ name: 'test', sourceType: 'index' }),
|
||||
Builder.expression.source({ index: 'test', sourceType: 'index' }),
|
||||
0
|
||||
);
|
||||
|
||||
|
@ -53,7 +53,7 @@ describe('generic.commands.args', () => {
|
|||
|
||||
generic.commands.args.insert(
|
||||
command!,
|
||||
Builder.expression.source({ name: 'test', sourceType: 'index' }),
|
||||
Builder.expression.source({ index: 'test', sourceType: 'index' }),
|
||||
1
|
||||
);
|
||||
|
||||
|
@ -70,7 +70,7 @@ describe('generic.commands.args', () => {
|
|||
|
||||
generic.commands.args.insert(
|
||||
command!,
|
||||
Builder.expression.source({ name: 'test', sourceType: 'index' }),
|
||||
Builder.expression.source({ index: 'test', sourceType: 'index' }),
|
||||
123
|
||||
);
|
||||
|
||||
|
@ -86,7 +86,7 @@ describe('generic.commands.args', () => {
|
|||
|
||||
generic.commands.args.insert(
|
||||
command!,
|
||||
Builder.expression.source({ name: 'test', sourceType: 'index' }),
|
||||
Builder.expression.source({ index: 'test', sourceType: 'index' }),
|
||||
0
|
||||
);
|
||||
|
||||
|
@ -102,7 +102,7 @@ describe('generic.commands.args', () => {
|
|||
|
||||
generic.commands.args.insert(
|
||||
command!,
|
||||
Builder.expression.source({ name: 'test', sourceType: 'index' }),
|
||||
Builder.expression.source({ index: 'test', sourceType: 'index' }),
|
||||
1
|
||||
);
|
||||
|
||||
|
@ -121,7 +121,7 @@ describe('generic.commands.args', () => {
|
|||
|
||||
generic.commands.args.append(
|
||||
command!,
|
||||
Builder.expression.source({ name: 'test', sourceType: 'index' })
|
||||
Builder.expression.source({ index: 'test', sourceType: 'index' })
|
||||
);
|
||||
|
||||
const src2 = BasicPrettyPrinter.print(root);
|
||||
|
|
|
@ -11,6 +11,19 @@ import { parse } from '..';
|
|||
import { ESQLLiteral } from '../../types';
|
||||
|
||||
describe('literal expression', () => {
|
||||
it('NULL', () => {
|
||||
const text = 'ROW NULL';
|
||||
const { ast } = parse(text);
|
||||
const literal = ast[0].args[0] as ESQLLiteral;
|
||||
|
||||
expect(literal).toMatchObject({
|
||||
type: 'literal',
|
||||
literalType: 'null',
|
||||
name: 'NULL',
|
||||
value: 'NULL',
|
||||
});
|
||||
});
|
||||
|
||||
it('numeric expression captures "value", and "name" fields', () => {
|
||||
const text = 'ROW 1';
|
||||
const { ast } = parse(text);
|
||||
|
@ -47,4 +60,55 @@ describe('literal expression', () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Un-skip once string parsing fixed: https://github.com/elastic/kibana/issues/203445
|
||||
it.skip('single-quoted string', () => {
|
||||
const text = 'ROW "abc"';
|
||||
const { root } = parse(text);
|
||||
|
||||
expect(root.commands[0]).toMatchObject({
|
||||
type: 'command',
|
||||
args: [
|
||||
{
|
||||
type: 'literal',
|
||||
literalType: 'keyword',
|
||||
value: 'abc',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Un-skip once string parsing fixed: https://github.com/elastic/kibana/issues/203445
|
||||
it.skip('unescapes characters', () => {
|
||||
const text = 'ROW "a\\nbc"';
|
||||
const { root } = parse(text);
|
||||
|
||||
expect(root.commands[0]).toMatchObject({
|
||||
type: 'command',
|
||||
args: [
|
||||
{
|
||||
type: 'literal',
|
||||
literalType: 'keyword',
|
||||
value: 'a\nbc',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Un-skip once string parsing fixed: https://github.com/elastic/kibana/issues/203445
|
||||
it.skip('triple-quoted string', () => {
|
||||
const text = 'ROW """abc"""';
|
||||
const { root } = parse(text);
|
||||
|
||||
expect(root.commands[0]).toMatchObject({
|
||||
type: 'command',
|
||||
args: [
|
||||
{
|
||||
type: 'literal',
|
||||
literalType: 'keyword',
|
||||
value: 'abc',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -119,19 +119,6 @@ export function createFakeMultiplyLiteral(
|
|||
};
|
||||
}
|
||||
|
||||
export function createLiteralString(token: Token): ESQLLiteral {
|
||||
const text = token.text!;
|
||||
return {
|
||||
type: 'literal',
|
||||
literalType: 'keyword',
|
||||
text,
|
||||
name: text,
|
||||
value: text,
|
||||
location: getPosition(token),
|
||||
incomplete: Boolean(token.text === ''),
|
||||
};
|
||||
}
|
||||
|
||||
function isMissingText(text: string) {
|
||||
return /<missing /.test(text);
|
||||
}
|
||||
|
|
|
@ -289,7 +289,8 @@ export class BasicPrettyPrinter {
|
|||
case 'unary-expression': {
|
||||
operator = this.keyword(operator);
|
||||
|
||||
const formatted = `${operator} ${ctx.visitArgument(0, undefined)}`;
|
||||
const separator = operator === '-' || operator === '+' ? '' : ' ';
|
||||
const formatted = `${operator}${separator}${ctx.visitArgument(0, undefined)}`;
|
||||
|
||||
return this.decorateWithComments(ctx.node, formatted);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,16 @@ const regexUnquotedIdPattern = /^([a-z\*_\@]{1})[a-z0-9_\*]*$/i;
|
|||
* atomic short string.
|
||||
*/
|
||||
export const LeafPrinter = {
|
||||
source: (node: ESQLSource) => node.name,
|
||||
source: (node: ESQLSource): string => {
|
||||
const { index, name, cluster } = node;
|
||||
let text = index || name || '';
|
||||
|
||||
if (cluster) {
|
||||
text = `${cluster}:${text}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
},
|
||||
|
||||
identifier: (node: ESQLIdentifier) => {
|
||||
const name = node.name;
|
||||
|
|
|
@ -526,7 +526,8 @@ export class WrappingPrettyPrinter {
|
|||
|
||||
switch (node.subtype) {
|
||||
case 'unary-expression': {
|
||||
txt = `${operator} ${ctx.visitArgument(0, inp).txt}`;
|
||||
const separator = operator === '-' || operator === '+' ? '' : ' ';
|
||||
txt = `${operator}${separator}${ctx.visitArgument(0, inp).txt}`;
|
||||
break;
|
||||
}
|
||||
case 'postfix-unary-expression': {
|
||||
|
|
|
@ -257,6 +257,7 @@ export interface ESQLUnknownItem extends ESQLAstBaseItem {
|
|||
}
|
||||
|
||||
export interface ESQLTimeInterval extends ESQLAstBaseItem {
|
||||
/** @todo For consistency with other literals, this should be `literal`, not `timeInterval`. */
|
||||
type: 'timeInterval';
|
||||
unit: string;
|
||||
quantity: number;
|
||||
|
@ -363,7 +364,10 @@ export interface ESQLNullLiteral extends ESQLAstBaseItem {
|
|||
// @internal
|
||||
export interface ESQLStringLiteral extends ESQLAstBaseItem {
|
||||
type: 'literal';
|
||||
|
||||
/** This really should be `string`, not `keyword`. */
|
||||
literalType: 'keyword';
|
||||
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue