Skip to content

Commit

Permalink
[8.x] [ES|QL] Implement `OrderExpression` for `SORT&#x…
Browse files Browse the repository at this point in the history
…60; command arguments (#189959) (#193379)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ES|QL] Implement `OrderExpression` for `SORT`
command arguments
(#189959)](#189959)

<!--- 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-09-19T03:53:05Z","message":"[ES|QL]
Implement `OrderExpression` for `SORT` command arguments (#189959)\n\n##
Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/189491\r\n\r\n- Adds *order
expression* AST nodes, which are minted from `SORT`\r\ncommand.\r\n-
Improves SORT command autocomplete suggestions.\r\n\r\nShows fields on
first space:\r\n\r\n<img width=\"791\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/3fec96b4-4e61-4212-a856-ace7a33d9755\">\r\n\r\n\r\nIt
now shows `NULLS FIRST` and `NULLS LAST`, even before `ASC` or
`DESC`\r\nwas entered, as `ASC` and `DESC` are optional:\r\n\r\n<img
width=\"871\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/4b6d6c28-a7b0-4ac0-bafc-133df1207d54\">\r\n\r\nOnce
`ASC` or `DESC` is entered, shows only nulls options:\r\n\r\n<img
width=\"911\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/5b27bd3d-ccdc-4bd0-b09f-fe65e5975e28\">\r\n\r\nIt
also now suggests partial modifier, if the in-progress text that
user\r\nis typing matches it:\r\n\r\n<img width=\"504\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/9a047c40-b49b-4694-8477-7270cb9c0886\">\r\n\r\n(However,
we are not triggering autocomplete in those cases in UI, so no\r\nway to
see it in UI right now.)\r\n\r\n\r\n### Checklist\r\n\r\nDelete any
items that are not applicable to this PR.\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### For
maintainers\r\n\r\n- [x] This was checked for breaking API changes and
was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"2efd0f0d8bb9478aec0316d1760adc184feb6309","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["review","release_note:skip","v9.0.0","Team:DataDiscovery","Feature:ES|QL","Team:ESQL","v8.16.0"],"title":"[ES|QL]
Implement `OrderExpression` for `SORT` command
arguments","number":189959,"url":"https://github.com/elastic/kibana/pull/189959","mergeCommit":{"message":"[ES|QL]
Implement `OrderExpression` for `SORT` command arguments (#189959)\n\n##
Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/189491\r\n\r\n- Adds *order
expression* AST nodes, which are minted from `SORT`\r\ncommand.\r\n-
Improves SORT command autocomplete suggestions.\r\n\r\nShows fields on
first space:\r\n\r\n<img width=\"791\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/3fec96b4-4e61-4212-a856-ace7a33d9755\">\r\n\r\n\r\nIt
now shows `NULLS FIRST` and `NULLS LAST`, even before `ASC` or
`DESC`\r\nwas entered, as `ASC` and `DESC` are optional:\r\n\r\n<img
width=\"871\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/4b6d6c28-a7b0-4ac0-bafc-133df1207d54\">\r\n\r\nOnce
`ASC` or `DESC` is entered, shows only nulls options:\r\n\r\n<img
width=\"911\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/5b27bd3d-ccdc-4bd0-b09f-fe65e5975e28\">\r\n\r\nIt
also now suggests partial modifier, if the in-progress text that
user\r\nis typing matches it:\r\n\r\n<img width=\"504\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/9a047c40-b49b-4694-8477-7270cb9c0886\">\r\n\r\n(However,
we are not triggering autocomplete in those cases in UI, so no\r\nway to
see it in UI right now.)\r\n\r\n\r\n### Checklist\r\n\r\nDelete any
items that are not applicable to this PR.\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### For
maintainers\r\n\r\n- [x] This was checked for breaking API changes and
was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"2efd0f0d8bb9478aec0316d1760adc184feb6309"}},"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/189959","number":189959,"mergeCommit":{"message":"[ES|QL]
Implement `OrderExpression` for `SORT` command arguments (#189959)\n\n##
Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/189491\r\n\r\n- Adds *order
expression* AST nodes, which are minted from `SORT`\r\ncommand.\r\n-
Improves SORT command autocomplete suggestions.\r\n\r\nShows fields on
first space:\r\n\r\n<img width=\"791\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/3fec96b4-4e61-4212-a856-ace7a33d9755\">\r\n\r\n\r\nIt
now shows `NULLS FIRST` and `NULLS LAST`, even before `ASC` or
`DESC`\r\nwas entered, as `ASC` and `DESC` are optional:\r\n\r\n<img
width=\"871\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/4b6d6c28-a7b0-4ac0-bafc-133df1207d54\">\r\n\r\nOnce
`ASC` or `DESC` is entered, shows only nulls options:\r\n\r\n<img
width=\"911\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/5b27bd3d-ccdc-4bd0-b09f-fe65e5975e28\">\r\n\r\nIt
also now suggests partial modifier, if the in-progress text that
user\r\nis typing matches it:\r\n\r\n<img width=\"504\"
alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/9a047c40-b49b-4694-8477-7270cb9c0886\">\r\n\r\n(However,
we are not triggering autocomplete in those cases in UI, so no\r\nway to
see it in UI right now.)\r\n\r\n\r\n### Checklist\r\n\r\nDelete any
items that are not applicable to this PR.\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### For
maintainers\r\n\r\n- [x] This was checked for breaking API changes and
was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"2efd0f0d8bb9478aec0316d1760adc184feb6309"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent 72d0f2d commit 8bd6e11
Show file tree
Hide file tree
Showing 18 changed files with 616 additions and 136 deletions.
141 changes: 122 additions & 19 deletions packages/kbn-esql-ast/src/__tests__/ast_parser.sort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,107 @@ import { getAstAndSyntaxErrors as parse } from '../ast_parser';

describe('SORT', () => {
describe('correctly formatted', () => {
// Un-skip one https://github.com/elastic/kibana/issues/189491 fixed.
it.skip('example from documentation', () => {
it('sorting order without modifiers', () => {
const text = `FROM employees | SORT height`;
const { ast, errors } = parse(text);

expect(errors.length).toBe(0);
expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'sort',
args: [
{
type: 'column',
name: 'height',
},
],
},
]);
});

it('sort expression is a function call', () => {
const text = `from a_index | sort values(textField)`;
const { ast, errors } = parse(text);

expect(errors.length).toBe(0);
expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'sort',
args: [
{
type: 'function',
name: 'values',
},
],
},
]);
});

it('with order modifier "DESC"', () => {
const text = `
FROM employees
| KEEP first_name, last_name, height
| SORT height DESC
`;
const { ast, errors } = parse(text);

expect(errors.length).toBe(0);
expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'sort',
args: [
{
type: 'order',
order: 'DESC',
nulls: '',
args: [
{
type: 'column',
name: 'height',
},
],
},
],
},
]);
});

it('with nulls modifier "NULLS LAST"', () => {
const text = `
FROM employees
| SORT height NULLS LAST
`;
const { ast, errors } = parse(text);

expect(errors.length).toBe(0);
expect(ast).toMatchObject([
{},
{
type: 'command',
name: 'sort',
args: [
{
type: 'column',
name: 'height',
type: 'order',
order: '',
nulls: 'NULLS LAST',
args: [
{
type: 'column',
name: 'height',
},
],
},
],
},
]);
});

// Un-skip once https://github.com/elastic/kibana/issues/189491 fixed.
it.skip('can parse various sorting columns with options', () => {
it('can parse various sorting columns with options', () => {
const text =
'FROM a | SORT a, b ASC, c DESC, d NULLS FIRST, e NULLS LAST, f ASC NULLS FIRST, g DESC NULLS LAST';
const { ast, errors } = parse(text);
Expand All @@ -55,28 +128,58 @@ describe('SORT', () => {
name: 'a',
},
{
type: 'column',
name: 'b',
order: 'ASC',
nulls: '',
args: [
{
name: 'b',
},
],
},
{
type: 'column',
name: 'c',
order: 'DESC',
nulls: '',
args: [
{
name: 'c',
},
],
},
{
type: 'column',
name: 'd',
order: '',
nulls: 'NULLS FIRST',
args: [
{
name: 'd',
},
],
},
{
type: 'column',
name: 'e',
order: '',
nulls: 'NULLS LAST',
args: [
{
name: 'e',
},
],
},
{
type: 'column',
name: 'f',
order: 'ASC',
nulls: 'NULLS FIRST',
args: [
{
name: 'f',
},
],
},
{
type: 'column',
name: 'g',
order: 'DESC',
nulls: 'NULLS LAST',
args: [
{
name: 'g',
},
],
},
],
},
Expand Down
4 changes: 2 additions & 2 deletions packages/kbn-esql-ast/src/ast_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {
visitDissect,
visitGrok,
collectBooleanExpression,
visitOrderExpression,
visitOrderExpressions,
getPolicyName,
getMatchField,
getEnrichClauses,
Expand Down Expand Up @@ -238,7 +238,7 @@ export class AstListener implements ESQLParserListener {
exitSortCommand(ctx: SortCommandContext) {
const command = createCommand('sort', ctx);
this.ast.push(command);
command.args.push(...visitOrderExpression(ctx.orderExpression_list()));
command.args.push(...visitOrderExpressions(ctx.orderExpression_list()));
}

/**
Expand Down
21 changes: 21 additions & 0 deletions packages/kbn-esql-ast/src/ast_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import type {
ESQLNumericLiteralType,
FunctionSubtype,
ESQLNumericLiteral,
ESQLOrderExpression,
} from './types';
import { parseIdentifier } from './parser/helpers';

Expand Down Expand Up @@ -222,6 +223,26 @@ export function createFunction<Subtype extends FunctionSubtype>(
return node;
}

export const createOrderExpression = (
ctx: ParserRuleContext,
arg: ESQLAstItem,
order: ESQLOrderExpression['order'],
nulls: ESQLOrderExpression['nulls']
) => {
const node: ESQLOrderExpression = {
type: 'order',
name: '',
order,
nulls,
args: [arg],
text: ctx.getText(),
location: getPosition(ctx.start, ctx.stop),
incomplete: Boolean(ctx.exception),
};

return node;
};

function walkFunctionStructure(
args: ESQLAstItem[],
initialLocation: ESQLLocation,
Expand Down
63 changes: 37 additions & 26 deletions packages/kbn-esql-ast/src/ast_walker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {
textExistsAndIsValid,
createInlineCast,
createUnknownItem,
createOrderExpression,
} from './ast_helpers';
import { getPosition } from './ast_position_utils';
import {
Expand All @@ -97,6 +98,7 @@ import {
ESQLUnnamedParamLiteral,
ESQLPositionalParamLiteral,
ESQLNamedParamLiteral,
ESQLOrderExpression,
} from './types';

export function collectAllSourceIdentifiers(ctx: FromCommandContext): ESQLAstItem[] {
Expand Down Expand Up @@ -608,34 +610,43 @@ export function visitByOption(
return [option];
}

export function visitOrderExpression(ctx: OrderExpressionContext[]) {
const ast: ESQLAstItem[] = [];
for (const orderCtx of ctx) {
const expression = collectBooleanExpression(orderCtx.booleanExpression());
if (orderCtx._ordering) {
const terminalNode =
orderCtx.getToken(esql_parser.ASC, 0) || orderCtx.getToken(esql_parser.DESC, 0);
const literal = createLiteral('string', terminalNode);
if (literal) {
expression.push(literal);
}
}
if (orderCtx.NULLS()) {
expression.push(createLiteral('string', orderCtx.NULLS()!)!);
if (orderCtx._nullOrdering && orderCtx._nullOrdering.text !== '<first missing>') {
const innerTerminalNode =
orderCtx.getToken(esql_parser.FIRST, 0) || orderCtx.getToken(esql_parser.LAST, 0);
const literal = createLiteral('string', innerTerminalNode);
if (literal) {
expression.push(literal);
}
}
}
const visitOrderExpression = (ctx: OrderExpressionContext): ESQLOrderExpression | ESQLAstItem => {
const arg = collectBooleanExpression(ctx.booleanExpression())[0];

if (expression.length) {
ast.push(...expression);
}
let order: ESQLOrderExpression['order'] = '';
let nulls: ESQLOrderExpression['nulls'] = '';

const ordering = ctx._ordering?.text?.toUpperCase();

if (ordering) order = ordering as ESQLOrderExpression['order'];

const nullOrdering = ctx._nullOrdering?.text?.toUpperCase();

switch (nullOrdering) {
case 'LAST':
nulls = 'NULLS LAST';
break;
case 'FIRST':
nulls = 'NULLS FIRST';
break;
}

if (!order && !nulls) {
return arg;
}

return createOrderExpression(ctx, arg, order, nulls);
};

export function visitOrderExpressions(
ctx: OrderExpressionContext[]
): Array<ESQLOrderExpression | ESQLAstItem> {
const ast: Array<ESQLOrderExpression | ESQLAstItem> = [];

for (const orderCtx of ctx) {
ast.push(visitOrderExpression(orderCtx));
}

return ast;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,22 @@ describe('single line query', () => {
expect(text).toBe('FROM a | SORT b');
});

/** @todo Enable once order expressions are supported. */
test.skip('order expression with ASC modifier', () => {
test('order expression with ASC modifier', () => {
const { text } = reprint('FROM a | SORT b ASC');

expect(text).toBe('FROM a | SORT b ASC');
});

/** @todo Enable once order expressions are supported. */
test.skip('order expression with ASC and NULLS FIRST modifier', () => {
const { text } = reprint('FROM a | SORT b ASC NULLS FIRST');
test('order expression with NULLS LAST modifier', () => {
const { text } = reprint('FROM a | SORT b NULLS LAST');

expect(text).toBe('FROM a | SORT b ASC NULLS FIRST');
expect(text).toBe('FROM a | SORT b NULLS LAST');
});

test('order expression with DESC and NULLS FIRST modifier', () => {
const { text } = reprint('FROM a | SORT b DESC NULLS FIRST');

expect(text).toBe('FROM a | SORT b DESC NULLS FIRST');
});
});

Expand Down
Loading

0 comments on commit 8bd6e11

Please sign in to comment.