Skip to content

Commit 7c6068a

Browse files
bartovalkibanamachine
authored andcommitted
[ES|QL] : Support INLINESTATS in autocomplete (elastic#231627)
## Summary This PR implements the Autocomplete part for the INLINESTATS command: elastic#189356 <img width="1015" height="263" alt="stats0" src="https://github.com/user-attachments/assets/829916e0-e1de-46b0-bf1a-9af6306d685b" /> <img width="1048" height="403" alt="stats3" src="https://github.com/user-attachments/assets/7670c900-c0fb-4a86-b8d4-84aa7b97b0b3" /> <img width="1002" height="216" alt="stats2" src="https://github.com/user-attachments/assets/77b5731a-4b01-4824-9c52-c0d472b600af" /> <img width="2070" height="1008" alt="test2" src="https://github.com/user-attachments/assets/dc7b548b-779a-436e-a0cb-8310030533a9" /> <img width="2085" height="874" alt="test1" src="https://github.com/user-attachments/assets/65e4588b-3c49-4c58-ad8b-6be11a303bf6" /> --------- Co-authored-by: kibanamachine <[email protected]>
1 parent 9d75a37 commit 7c6068a

File tree

13 files changed

+1294
-325
lines changed

13 files changed

+1294
-325
lines changed

src/platform/packages/shared/kbn-esql-ast/src/commands_registry/commands/inlinestats/autocomplete.test.ts

Lines changed: 833 additions & 0 deletions
Large diffs are not rendered by default.

src/platform/packages/shared/kbn-esql-ast/src/commands_registry/commands/inlinestats/autocomplete.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
import { synth } from '../../../..';
10+
import type { ESQLFieldWithMetadata, ESQLUserDefinedColumn } from '../../types';
11+
import { columnsAfter } from './columns_after';
12+
13+
// Test data fixtures
14+
const createPreviousFields = (fields: Array<[string, string]>): ESQLFieldWithMetadata[] =>
15+
fields.map(([name, type]) => ({ name, type } as ESQLFieldWithMetadata));
16+
17+
describe('INLINESTATS', () => {
18+
const createUserDefinedColumn = (
19+
name: string,
20+
type: ESQLFieldWithMetadata['type'],
21+
location = { min: 0, max: 10 }
22+
): ESQLUserDefinedColumn => ({ name, type, location });
23+
24+
const createContext = (userDefinedColumns: Array<[string, ESQLUserDefinedColumn[]]> = []) => ({
25+
userDefinedColumns: new Map(userDefinedColumns),
26+
fields: new Map<string, ESQLFieldWithMetadata>([
27+
['field1', { name: 'field1', type: 'keyword' }],
28+
['count', { name: 'count', type: 'double' }],
29+
]),
30+
});
31+
32+
const expectColumnsAfter = (
33+
command: string,
34+
previousFields: ESQLFieldWithMetadata[],
35+
userColumns: Array<[string, string]>,
36+
expectedResult: Array<[string, string]>
37+
) => {
38+
const context = createContext(
39+
userColumns.map(([name, type]) => [name, [createUserDefinedColumn(name, type as 'keyword')]])
40+
);
41+
const result = columnsAfter(synth.cmd(command), previousFields, context);
42+
const expected = createPreviousFields(expectedResult);
43+
expect(result).toEqual(expected);
44+
};
45+
it('preserves all previous columns and adds the user defined column, when no grouping is given', () => {
46+
const previousFields = createPreviousFields([
47+
['field1', 'keyword'],
48+
['field2', 'double'],
49+
]);
50+
51+
expectColumnsAfter(
52+
'INLINESTATS var0=AVG(field2)',
53+
previousFields,
54+
[['var0', 'double']],
55+
[
56+
['field1', 'keyword'],
57+
['field2', 'double'],
58+
['var0', 'double'],
59+
]
60+
);
61+
});
62+
63+
it('preserves all previous columns and adds the escaped column, when no grouping is given', () => {
64+
const previousFields = createPreviousFields([
65+
['field1', 'keyword'],
66+
['field2', 'double'],
67+
]);
68+
69+
expectColumnsAfter(
70+
'INLINESTATS AVG(field2)',
71+
previousFields,
72+
[['AVG(field2)', 'double']],
73+
[
74+
['field1', 'keyword'],
75+
['field2', 'double'],
76+
['AVG(field2)', 'double'],
77+
]
78+
);
79+
});
80+
81+
it('preserves all previous columns and adds the escaped column, with grouping', () => {
82+
const previousFields = createPreviousFields([
83+
['field1', 'keyword'],
84+
['field2', 'double'],
85+
]);
86+
87+
// Note: Unlike STATS, INLINESTATS doesn't care about BY clause for column preservation
88+
expectColumnsAfter(
89+
'INLINESTATS AVG(field2) BY field1',
90+
previousFields,
91+
[['AVG(field2)', 'double']],
92+
[
93+
['field1', 'keyword'],
94+
['field2', 'double'],
95+
['AVG(field2)', 'double'],
96+
]
97+
);
98+
});
99+
100+
it('preserves all previous columns and adds user defined and grouping columns', () => {
101+
const previousFields = createPreviousFields([
102+
['field1', 'keyword'],
103+
['field2', 'double'],
104+
['@timestamp', 'date'],
105+
]);
106+
107+
expectColumnsAfter(
108+
'INLINESTATS AVG(field2) BY buckets=BUCKET(@timestamp,50,?_tstart,?_tend)',
109+
previousFields,
110+
[
111+
['AVG(field2)', 'double'],
112+
['buckets', 'unknown'],
113+
],
114+
[
115+
['field1', 'keyword'],
116+
['field2', 'double'],
117+
['@timestamp', 'date'],
118+
['AVG(field2)', 'double'],
119+
['buckets', 'unknown'],
120+
]
121+
);
122+
});
123+
124+
it('handles duplicate column names by keeping the original column type', () => {
125+
const previousFields = createPreviousFields([
126+
['field1', 'keyword'],
127+
['field2', 'double'],
128+
['avg_field', 'integer'], // This will be preserved since it comes first
129+
]);
130+
131+
expectColumnsAfter(
132+
'INLINESTATS avg_field=AVG(field2)',
133+
previousFields,
134+
[['avg_field', 'double']], // This will be ignored due to duplicate name
135+
[
136+
['field1', 'keyword'],
137+
['field2', 'double'],
138+
['avg_field', 'integer'],
139+
]
140+
);
141+
});
142+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
import uniqBy from 'lodash/uniqBy';
10+
import type { ESQLCommand } from '../../../types';
11+
import type { ESQLFieldWithMetadata, ESQLUserDefinedColumn } from '../../types';
12+
import type { ICommandContext } from '../../types';
13+
import type { FieldType } from '../../../definitions/types';
14+
15+
function transformMapToESQLFields(
16+
inputMap: Map<string, ESQLUserDefinedColumn[]>
17+
): ESQLFieldWithMetadata[] {
18+
const esqlFields: ESQLFieldWithMetadata[] = [];
19+
20+
for (const [, userDefinedColumns] of inputMap) {
21+
for (const userDefinedColumn of userDefinedColumns) {
22+
if (userDefinedColumn.type) {
23+
esqlFields.push({
24+
name: userDefinedColumn.name,
25+
type: userDefinedColumn.type as FieldType,
26+
});
27+
}
28+
}
29+
}
30+
31+
return esqlFields;
32+
}
33+
34+
export const columnsAfter = (
35+
_command: ESQLCommand,
36+
previousColumns: ESQLFieldWithMetadata[],
37+
context?: ICommandContext
38+
) => {
39+
const userDefinedColumns =
40+
context?.userDefinedColumns ?? new Map<string, ESQLUserDefinedColumn[]>();
41+
42+
const arrayOfUserDefinedColumns: ESQLFieldWithMetadata[] =
43+
transformMapToESQLFields(userDefinedColumns);
44+
45+
return uniqBy([...previousColumns, ...arrayOfUserDefinedColumns], 'name');
46+
};

src/platform/packages/shared/kbn-esql-ast/src/commands_registry/commands/inlinestats/index.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99
import { i18n } from '@kbn/i18n';
10-
import type { ICommandMethods } from '../../registry';
11-
import { autocomplete } from './autocomplete';
12-
import { validate } from './validate';
10+
import { columnsAfter } from './columns_after';
11+
import { autocomplete } from '../stats/autocomplete';
12+
import { validate } from '../stats/validate';
1313
import type { ICommandContext } from '../../types';
14+
import type { ICommandMethods } from '../../registry';
1415

1516
const inlineStatsCommandMethods: ICommandMethods<ICommandContext> = {
1617
autocomplete,
1718
validate,
19+
columnsAfter,
1820
};
1921

2022
export const inlineStatsCommand = {
@@ -24,9 +26,16 @@ export const inlineStatsCommand = {
2426
hidden: true,
2527
description: i18n.translate('kbn-esql-ast.esql.definitions.inlineStatsDoc', {
2628
defaultMessage:
27-
'Calculates an aggregate result and merges that result back into the stream of input data. Without the optional `BY` clause this will produce a single result which is appended to each row. With a `BY` clause this will produce one result per grouping and merge the result into the stream based on matching group keys.',
29+
'Unlike STATS, INLINESTATS preserves all columns from the previous pipe and returns them together with the new aggregate columns.',
2830
}),
29-
declaration: '',
30-
examples: ['… | EVAL bar = a * b | INLINESTATS m = MAX(bar) BY b'],
31+
declaration: `INLINESTATS [column1 =] expression1 [WHERE boolean_expression1][,
32+
...,
33+
[columnN =] expressionN [WHERE boolean_expressionN]]
34+
[BY grouping_expression1[, ..., grouping_expressionN]]`,
35+
examples: [
36+
'… | inlinestats avg = avg(a)',
37+
'… | inlinestats sum(b) by b',
38+
'… | inlinestats sum(b) by b % 2',
39+
],
3140
},
3241
};

0 commit comments

Comments
 (0)