Skip to content

Commit

Permalink
[ES|QL] Implements wrapping pretty-printer (#190589)
Browse files Browse the repository at this point in the history
## Summary

Partially addresses #182257

- Improves the basic "one-line" printer `BasicPrettyPrinter`, notable
changes:
- It is now possible better specify if query keywords should be
uppercased
- Better formatting columns names, adds backquotes when escaping needed:
`` `name👍` ``
- Wraps cast expressions into brackets, where needed: `(1 + 2)::string`
instead of `1 + 2::string`
- Adds initial implementations of the more complex
`WrappingPrettyPrinter`.
- "Initial implementation" because it probably covers 80-90% of the
cases, some follow up will be needed.
- The `WrappingPrettyPrinter` formats the query like `Prettier`, it
tries to format AST nodes horizontally as lists, but based on various
conditions breaks the lines and indents them.


#### Cases handled by the `WrappingPrettyPrinter`

Below are examples of some of the cases handled by the
`WrappingPrettyPrinter`. (See test files for many more cases.)

##### Short queries

Queries with less than 4 commands and if they do not require wrapping
are formatted to a single line.

Source:

```
FROM index | WHERE a == 123
```

Result:

```
FROM index | WHERE a == 123
```


##### Argument wrapping

Command arguments are wrapped (at wrapping threshold, defaults to 80).

Source:

```
FROM index, another_index, yet_another_index, on-more-index, last_index, very_last_index, ok_this_is_the_last_index
```

Result:

```
FROM index, another_index, yet_another_index, on-more-index, last_index,
     very_last_index, ok_this_is_the_last_index
```


##### Argument breaking

Command argument combinations which result into a single argument
occupying a whole line (due to that argument being long, or because the
surrounding argument combination results into such a case), except the
last argument, results into the argument list being broken by line.

Source:

```
FROM xxxxxxxxxx, yyyyyyyyyyy, zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,  // <------------ this one
  bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, ccccccc, ggggggggg
```

Result:

```
FROM
  xxxxxxxxxx,
  yyyyyyyyyyy,
  zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,
  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
  bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb,
  ccccccc,
  ggggggggg
```

##### Binary expression chain vertical flattening

Binary expressions of the same precedence are vertically flattened, if
wrapping is required. Same as it is done by `Prettier`, where there is
an indentation after the first line to allow for different precedence
expressions.

###### All expressions have the same precedence

Source:

```
FROM index
| STATS super_function_name(11111111111111.111 + 11111111111111.111 + 11111111111111.111 + 11111111111111.111 + 11111111111111.111))
```

Result:

```
FROM index
  | STATS
      SUPER_FUNCTION_NAME(
        11111111111111.111 +
          11111111111111.111 +
          11111111111111.111 +
          11111111111111.111 +
          11111111111111.111)
```

###### Expressions with `additive` and `multiplicative` precedence mixed

Source:

```
FROM index
| STATS super_function_name(11111111111111.111 + 3333333333333.3333 * 3333333333333.3333 * 3333333333333.3333 * 3333333333333.3333 + 11111111111111.111 + 11111111111111.111 + 11111111111111.111 + 11111111111111.111))
```

Result:

```
FROM index
  | STATS
      SUPER_FUNCTION_NAME(
        11111111111111.111 +
          3333333333333.3335 *
            3333333333333.3335 *
            3333333333333.3335 *
            3333333333333.3335 +
          11111111111111.111 +
          11111111111111.111 +
          11111111111111.111 +
          11111111111111.111)
```


### Checklist

Delete any items that are not applicable to this PR.

- [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

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
vadimkibana and elasticmachine authored Aug 20, 2024
1 parent 7027b0b commit 8316cbf
Show file tree
Hide file tree
Showing 19 changed files with 2,245 additions and 550 deletions.
24 changes: 24 additions & 0 deletions packages/kbn-esql-ast/src/__tests__/ast_parser.literal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,28 @@ describe('literal expression', () => {
value: 1,
});
});

it('decimals vs integers', () => {
const text = 'ROW a(1.0, 1)';
const { ast } = parse(text);

expect(ast[0]).toMatchObject({
type: 'command',
args: [
{
type: 'function',
args: [
{
type: 'literal',
literalType: 'decimal',
},
{
type: 'literal',
literalType: 'integer',
},
],
},
],
});
});
});
26 changes: 26 additions & 0 deletions packages/kbn-esql-ast/src/__tests__/ast_parser.rename.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

import { getAstAndSyntaxErrors as parse } from '../ast_parser';

describe('RENAME', () => {
/**
* Enable this test once RENAME commands are fixed:
* https://github.com/elastic/kibana/discussions/182393#discussioncomment-10313313
*/
it.skip('example from documentation', () => {
const text = `
FROM kibana_sample_data_logs
| RENAME total_visits as \`Unique Visits (Total)\`,
`;
const { ast } = parse(text);

// eslint-disable-next-line no-console
console.log(JSON.stringify(ast, null, 2));
});
});
19 changes: 19 additions & 0 deletions packages/kbn-esql-ast/src/ast/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

/**
* The group name of a binary expression. Groups are ordered by precedence.
*/
export enum BinaryExpressionGroup {
unknown = 0,
additive = 10,
multiplicative = 20,
assignment = 30,
comparison = 40,
regex = 50,
}
69 changes: 69 additions & 0 deletions packages/kbn-esql-ast/src/ast/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

import { ESQLAstNode, ESQLBinaryExpression, ESQLFunction } from '../types';
import { BinaryExpressionGroup } from './constants';

export const isFunctionExpression = (node: unknown): node is ESQLFunction =>
!!node && typeof node === 'object' && !Array.isArray(node) && (node as any).type === 'function';

/**
* Returns true if the given node is a binary expression, i.e. an operator
* surrounded by two operands:
*
* ```
* 1 + 1
* column LIKE "foo"
* foo = "bar"
* ```
*
* @param node Any ES|QL AST node.
*/
export const isBinaryExpression = (node: unknown): node is ESQLBinaryExpression =>
isFunctionExpression(node) && node.subtype === 'binary-expression';

/**
* Returns the group of a binary expression:
*
* - `additive`: `+`, `-`
* - `multiplicative`: `*`, `/`, `%`
* - `assignment`: `=`
* - `comparison`: `==`, `=~`, `!=`, `<`, `<=`, `>`, `>=`
* - `regex`: `like`, `not_like`, `rlike`, `not_rlike`
* @param node Any ES|QL AST node.
* @returns Binary expression group or undefined if the node is not a binary expression.
*/
export const binaryExpressionGroup = (node: ESQLAstNode): BinaryExpressionGroup => {
if (isBinaryExpression(node)) {
switch (node.name) {
case '+':
case '-':
return BinaryExpressionGroup.additive;
case '*':
case '/':
case '%':
return BinaryExpressionGroup.multiplicative;
case '=':
return BinaryExpressionGroup.assignment;
case '==':
case '=~':
case '!=':
case '<':
case '<=':
case '>':
case '>=':
return BinaryExpressionGroup.comparison;
case 'like':
case 'not_like':
case 'rlike':
case 'not_rlike':
return BinaryExpressionGroup.regex;
}
}
return BinaryExpressionGroup.unknown;
};
23 changes: 23 additions & 0 deletions packages/kbn-esql-ast/src/pretty_print/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Pretty-printing

*Pretty-printing* is the process of converting an ES|QL AST into a
human-readable string. This is useful for debugging or for displaying
the AST to the user.

This module provides a number of pretty-printing options.


## `BasicPrettyPrinter`

The `BasicPrettyPrinter` class provides the most basic pretty-printing&mdash;it
prints a query to a single line. Or it can print a query with each command on
a separate line, with the ability to customize the indentation before the pipe
character.

It can also print a single command to a single line; or an expression to a
single line.

- `BasicPrettyPrinter.print()` &mdash; prints query to a single line.
- `BasicPrettyPrinter.multiline()` &mdash; prints a query to multiple lines.
- `BasicPrettyPrinter.command()` &mdash; prints a command to a single line.
- `BasicPrettyPrinter.expression()` &mdash; prints an expression to a single line.
Loading

0 comments on commit 8316cbf

Please sign in to comment.