forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ES|QL] separate
WHERE
autocomplete routine (elastic#198832)
## Summary Part of elastic#195418 **NOTES** - need to make sure these don't regress - elastic#195771 - elastic#197139 - suggesting variables after binary operator (e.g. `field + <suggest>` - I've noticed that incomplete null statements such as `is n` are corrected in our syntax tree. This sends the autocomplete down a "completed operator expression" route as opposed to an unknown operator or "to right of column" route. So, `... | EVAL foo IS N/` is interpreted as `... | EVAL foo IS NULL /`.<br><br>It accidentally works (lol) because the logic for `<operator-expression> <suggest>` suggests operators that accept the return type of the existing operator expression as their left-hand argument. Since `foo IS NULL` is of type `boolean` and `IS NULL` accepts boolean values, it gets included in the suggestion list which Monaco then filters by the actual prefix (`IS N`). 🤣 <br><br>([issue](elastic#199401)) ### Checklist - [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 --------- Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
- Loading branch information
1 parent
c8227a2
commit 2466a17
Showing
17 changed files
with
978 additions
and
495 deletions.
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.