Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ES|QL] separate WHERE autocomplete routine #198832

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d78642e
move sort routine to command definition
drewdaemon Oct 24, 2024
9bbd6f6
make the separation between the legacy and new paths clearer
drewdaemon Oct 25, 2024
3271c79
keep
drewdaemon Oct 25, 2024
1723f4d
separate command autocomplete routines
drewdaemon Oct 25, 2024
f19de39
give command AST node to autocomplete routine
drewdaemon Oct 25, 2024
dea0546
handle new expression case
drewdaemon Oct 25, 2024
d4dfe34
rename getFieldsFor to getColumnsFor
drewdaemon Oct 28, 2024
88e9226
separate DROP
drewdaemon Oct 28, 2024
7ade691
Merge branch 'main' of github.com:elastic/kibana into 195418/separate…
drewdaemon Oct 29, 2024
e5ecfe5
first stab at STATS
drewdaemon Oct 29, 2024
3df44e8
separate out util
drewdaemon Oct 30, 2024
e4289dd
fix field suggestions
drewdaemon Oct 30, 2024
402105f
create unified function suggestion routine
drewdaemon Oct 30, 2024
e412687
assignment suggestion
drewdaemon Oct 30, 2024
3bb659d
fix function suggestions
drewdaemon Oct 31, 2024
d69963c
handle assignments
drewdaemon Oct 31, 2024
514c082
fix stats behavior
drewdaemon Oct 31, 2024
a8d2c25
make exception only for stats
drewdaemon Oct 31, 2024
de7d634
restore preferences behavior
drewdaemon Oct 31, 2024
46cf8aa
add deprecation comment
drewdaemon Oct 31, 2024
4f777f3
Move where command tests to new pattern
drewdaemon Nov 1, 2024
c79f8c3
custom WHERE autocomplete routine
drewdaemon Nov 1, 2024
4c5a03d
clean up operator suggestions
drewdaemon Nov 4, 2024
6e29178
Merge branch 'main' of github.com:elastic/kibana into 195418/separate…
drewdaemon Nov 4, 2024
c2447a7
suggest operators after a column
drewdaemon Nov 4, 2024
721a89c
fix stats tests
drewdaemon Nov 5, 2024
1d23948
preparing operator suggestion logic for export
drewdaemon Nov 5, 2024
3ed4507
support some expressions
drewdaemon Nov 6, 2024
13c1e34
cover some more expression cases
drewdaemon Nov 6, 2024
918ebf6
add some clarifying comments
drewdaemon Nov 6, 2024
159eb72
handle NOT cases
drewdaemon Nov 6, 2024
f40880e
add a few regressed test cases
drewdaemon Nov 6, 2024
2f9d2a3
suggest pipe after complete expression
drewdaemon Nov 7, 2024
8c0f0fa
support is null cases
drewdaemon Nov 7, 2024
38e70c7
add note
drewdaemon Nov 7, 2024
7f74e44
remove a problem test case
drewdaemon Nov 7, 2024
03be4b2
all where tests passing
drewdaemon Nov 7, 2024
d36ad1a
fix invoke trigger kind scenarios
drewdaemon Nov 7, 2024
ded8c7f
make it all pass
drewdaemon Nov 8, 2024
d27f963
remove superfluous type
drewdaemon Nov 8, 2024
9fd2f45
Merge branch 'main' into 195418/separate-where-autocomplete-routine
stratoula Nov 8, 2024
2871924
Merge branch 'main' into 195418/separate-where-autocomplete-routine
stratoula Nov 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { ESQL_COMMON_NUMERIC_TYPES } from '../../shared/esql_types';
import { pipeCompleteItem } from '../complete_items';
import { getDateLiterals } from '../factories';
import { log10ParameterTypes, powParameterTypes } from './constants';
import {
attachTriggerCommand,
fields,
getFieldNamesByType,
getFunctionSignaturesByReturnType,
setup,
} from './helpers';

describe('WHERE <expression>', () => {
const allEvalFns = getFunctionSignaturesByReturnType('where', 'any', {
scalar: true,
});
test('beginning an expression', async () => {
const { assertSuggestions } = await setup();

await assertSuggestions('from a | where /', [
...getFieldNamesByType('any')
.map((field) => `${field} `)
.map(attachTriggerCommand),
...allEvalFns,
]);
await assertSuggestions(
'from a | eval var0 = 1 | where /',
[
...getFieldNamesByType('any')
.map((name) => `${name} `)
.map(attachTriggerCommand),
attachTriggerCommand('var0 '),
...allEvalFns,
],
{
callbacks: {
getColumnsFor: () => Promise.resolve([...fields, { name: 'var0', type: 'integer' }]),
},
}
);
});

describe('within the expression', () => {
test('after a field name', async () => {
const { assertSuggestions } = await setup();

await assertSuggestions('from a | where keywordField /', [
// all functions compatible with a keywordField type
...getFunctionSignaturesByReturnType(
'where',
'boolean',
{
builtin: true,
},
undefined,
['and', 'or', 'not']
),
]);
});

test('suggests dates after a comparison with a date', async () => {
const { assertSuggestions } = await setup();

const expectedComparisonWithDateSuggestions = [
...getDateLiterals(),
...getFieldNamesByType(['date']),
// all functions compatible with a keywordField type
...getFunctionSignaturesByReturnType('where', ['date'], { scalar: true }),
];
await assertSuggestions(
'from a | where dateField == /',
expectedComparisonWithDateSuggestions
);

await assertSuggestions(
'from a | where dateField < /',
expectedComparisonWithDateSuggestions
);

await assertSuggestions(
'from a | where dateField >= /',
expectedComparisonWithDateSuggestions
);
});

test('after a comparison with a string field', async () => {
const { assertSuggestions } = await setup();

const expectedComparisonWithTextFieldSuggestions = [
...getFieldNamesByType(['text', 'keyword', 'ip', 'version']),
...getFunctionSignaturesByReturnType('where', ['text', 'keyword', 'ip', 'version'], {
scalar: true,
}),
];

await assertSuggestions(
'from a | where textField >= /',
expectedComparisonWithTextFieldSuggestions
);
await assertSuggestions(
'from a | where textField >= textField/',
expectedComparisonWithTextFieldSuggestions
);
});

test('after a logical operator', async () => {
const { assertSuggestions } = await setup();

for (const op of ['and', 'or']) {
await assertSuggestions(`from a | where keywordField >= keywordField ${op} /`, [
...getFieldNamesByType('any'),
...getFunctionSignaturesByReturnType('where', 'any', { scalar: true }),
]);
await assertSuggestions(`from a | where keywordField >= keywordField ${op} doubleField /`, [
...getFunctionSignaturesByReturnType('where', 'boolean', { builtin: true }, ['double']),
]);
await assertSuggestions(
`from a | where keywordField >= keywordField ${op} doubleField == /`,
[
...getFieldNamesByType(ESQL_COMMON_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('where', ESQL_COMMON_NUMERIC_TYPES, {
scalar: true,
}),
]
);
}
});

test('suggests operators after a field name', async () => {
const { assertSuggestions } = await setup();

await assertSuggestions('from a | stats a=avg(doubleField) | where a /', [
...getFunctionSignaturesByReturnType('where', 'any', { builtin: true, skipAssign: true }, [
'double',
]),
]);
});

test('accounts for fields lost in previous commands', async () => {
const { assertSuggestions } = await setup();

// Mind this test: suggestion is aware of previous commands when checking for fields
// in this case the doubleField has been wiped by the STATS command and suggest cannot find it's type
await assertSuggestions('from a | stats a=avg(doubleField) | where doubleField /', [], {
callbacks: { getColumnsFor: () => Promise.resolve([{ name: 'a', type: 'double' }]) },
});
});

test('suggests function arguments', async () => {
const { assertSuggestions } = await setup();

// The editor automatically inject the final bracket, so it is not useful to test with just open bracket
await assertSuggestions(
'from a | where log10(/)',
[
...getFieldNamesByType(log10ParameterTypes),
...getFunctionSignaturesByReturnType(
'where',
log10ParameterTypes,
{ scalar: true },
undefined,
['log10']
),
],
{ triggerCharacter: '(' }
);
await assertSuggestions(
'from a | WHERE pow(doubleField, /)',
[
...getFieldNamesByType(powParameterTypes),
...getFunctionSignaturesByReturnType(
'where',
powParameterTypes,
{ scalar: true },
undefined,
['pow']
),
],
{ triggerCharacter: ',' }
);
});

test('suggests boolean and numeric operators after a numeric function result', async () => {
const { assertSuggestions } = await setup();

await assertSuggestions('from a | where log10(doubleField) /', [
...getFunctionSignaturesByReturnType('where', 'double', { builtin: true }, ['double']),
...getFunctionSignaturesByReturnType('where', 'boolean', { builtin: true }, ['double']),
]);
});

test('suggestions after NOT', async () => {
const { assertSuggestions } = await setup();
await assertSuggestions('from index | WHERE keywordField not /', [
'LIKE $0',
'RLIKE $0',
'IN $0',
]);
await assertSuggestions('from index | WHERE keywordField NOT /', [
'LIKE $0',
'RLIKE $0',
'IN $0',
]);
await assertSuggestions('from index | WHERE not /', [
...getFieldNamesByType('boolean').map((name) => attachTriggerCommand(`${name} `)),
...getFunctionSignaturesByReturnType('where', 'boolean', { scalar: true }),
]);
await assertSuggestions('FROM index | WHERE NOT ENDS_WITH(keywordField, "foo") /', [
...getFunctionSignaturesByReturnType('where', 'boolean', { builtin: true }, ['boolean']),
pipeCompleteItem,
]);
await assertSuggestions('from index | WHERE keywordField IS NOT/', [
'!= $0',
'== $0',
'AND $0',
'IN $0',
'IS NOT NULL',
'IS NULL',
'NOT',
'OR $0',
'| ',
]);

await assertSuggestions('from index | WHERE keywordField IS NOT /', [
'!= $0',
'== $0',
'AND $0',
'IN $0',
'IS NOT NULL',
'IS NULL',
'NOT',
'OR $0',
'| ',
]);
});

test('suggestions after IN', async () => {
const { assertSuggestions } = await setup();

await assertSuggestions('from index | WHERE doubleField in /', ['( $0 )']);
await assertSuggestions('from index | WHERE doubleField not in /', ['( $0 )']);
await assertSuggestions(
'from index | WHERE doubleField not in (/)',
[
...getFieldNamesByType('double').filter((name) => name !== 'doubleField'),
...getFunctionSignaturesByReturnType('where', 'double', { scalar: true }),
],
{ triggerCharacter: '(' }
);
await assertSuggestions('from index | WHERE doubleField in ( `any#Char$Field`, /)', [
...getFieldNamesByType('double').filter(
(name) => name !== '`any#Char$Field`' && name !== 'doubleField'
),
...getFunctionSignaturesByReturnType('where', 'double', { scalar: true }),
]);
await assertSuggestions('from index | WHERE doubleField not in ( `any#Char$Field`, /)', [
...getFieldNamesByType('double').filter(
(name) => name !== '`any#Char$Field`' && name !== 'doubleField'
),
...getFunctionSignaturesByReturnType('where', 'double', { scalar: true }),
]);
});

test('suggestions after IS (NOT) NULL', async () => {
const { assertSuggestions } = await setup();

await assertSuggestions('FROM index | WHERE tags.keyword IS NULL /', [
'AND $0',
'OR $0',
'| ',
]);

await assertSuggestions('FROM index | WHERE tags.keyword IS NOT NULL /', [
'AND $0',
'OR $0',
'| ',
]);
});

test('suggestions after an arithmetic expression', async () => {
const { assertSuggestions } = await setup();

await assertSuggestions('FROM index | WHERE doubleField + doubleField /', [
...getFunctionSignaturesByReturnType('where', 'any', { builtin: true, skipAssign: true }, [
'double',
]),
]);
});

test('pipe suggestion after complete expression', async () => {
const { suggest } = await setup();
expect(await suggest('from index | WHERE doubleField != doubleField /')).toContainEqual(
expect.objectContaining({
label: '|',
})
);
});

test('attaches ranges', async () => {
const { suggest } = await setup();

const suggestions = await suggest('FROM index | WHERE doubleField IS N/');

expect(suggestions).toContainEqual(
expect.objectContaining({
text: 'IS NOT NULL',
rangeToReplace: {
start: 32,
end: 36,
},
})
);

expect(suggestions).toContainEqual(
expect.objectContaining({
text: 'IS NULL',
rangeToReplace: {
start: 32,
end: 36,
},
})
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -535,17 +535,6 @@ describe('autocomplete.suggest', () => {
{ triggerCharacter: ' ' }
);
await assertSuggestions('from a | eval a = 1 year /', [',', '| ', 'IS NOT NULL', 'IS NULL']);
await assertSuggestions('from a | eval a = 1 day + 2 /', [',', '| ']);
await assertSuggestions(
'from a | eval 1 day + 2 /',
[
...dateSuggestions,
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
'integer',
]),
],
{ triggerCharacter: ' ' }
);
Comment on lines -538 to -548
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 day + 2 is an invalid expression

await assertSuggestions(
'from a | eval var0=date_trunc(/)',
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('hidden functions', () => {
expect(suggestedFunctions).toContain('VISIBLE_FUNCTION($0)');
expect(suggestedFunctions).not.toContain('HIDDEN_FUNCTION($0)');
});

it('does not suggest hidden agg functions', async () => {
setTestFunctions([
{
Expand Down
Loading