[8.x] [ES|QL] Improve the `Builder` class (#203558) (#204240)

# 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:
Kibana Machine 2024-12-14 04:08:33 +11:00 committed by GitHub
parent 3979f74e0a
commit cfe4d8375a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 614 additions and 40 deletions

View file

@ -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);

View file

@ -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');
});
});

View file

@ -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),

View file

@ -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);

View file

@ -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',
},
],
});
});
});

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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': {

View file

@ -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;
}