-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
stratoula
merged 42 commits into
elastic:main
from
drewdaemon:195418/separate-where-autocomplete-routine
Nov 11, 2024
Merged
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 9bbd6f6
make the separation between the legacy and new paths clearer
drewdaemon 3271c79
keep
drewdaemon 1723f4d
separate command autocomplete routines
drewdaemon f19de39
give command AST node to autocomplete routine
drewdaemon dea0546
handle new expression case
drewdaemon d4dfe34
rename getFieldsFor to getColumnsFor
drewdaemon 88e9226
separate DROP
drewdaemon 7ade691
Merge branch 'main' of github.com:elastic/kibana into 195418/separate…
drewdaemon e5ecfe5
first stab at STATS
drewdaemon 3df44e8
separate out util
drewdaemon e4289dd
fix field suggestions
drewdaemon 402105f
create unified function suggestion routine
drewdaemon e412687
assignment suggestion
drewdaemon 3bb659d
fix function suggestions
drewdaemon d69963c
handle assignments
drewdaemon 514c082
fix stats behavior
drewdaemon a8d2c25
make exception only for stats
drewdaemon de7d634
restore preferences behavior
drewdaemon 46cf8aa
add deprecation comment
drewdaemon 4f777f3
Move where command tests to new pattern
drewdaemon c79f8c3
custom WHERE autocomplete routine
drewdaemon 4c5a03d
clean up operator suggestions
drewdaemon 6e29178
Merge branch 'main' of github.com:elastic/kibana into 195418/separate…
drewdaemon c2447a7
suggest operators after a column
drewdaemon 721a89c
fix stats tests
drewdaemon 1d23948
preparing operator suggestion logic for export
drewdaemon 3ed4507
support some expressions
drewdaemon 13c1e34
cover some more expression cases
drewdaemon 918ebf6
add some clarifying comments
drewdaemon 159eb72
handle NOT cases
drewdaemon f40880e
add a few regressed test cases
drewdaemon 2f9d2a3
suggest pipe after complete expression
drewdaemon 8c0f0fa
support is null cases
drewdaemon 38e70c7
add note
drewdaemon 7f74e44
remove a problem test case
drewdaemon 03be4b2
all where tests passing
drewdaemon d36ad1a
fix invoke trigger kind scenarios
drewdaemon ded8c7f
make it all pass
drewdaemon d27f963
remove superfluous type
drewdaemon 9fd2f45
Merge branch 'main' into 195418/separate-where-autocomplete-routine
stratoula 2871924
Merge branch 'main' into 195418/separate-where-autocomplete-routine
stratoula File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
334 changes: 334 additions & 0 deletions
334
...sql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.where.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}) | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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