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] Stronger typing for ESQL field interface #189941

Merged
merged 35 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
185b81c
add some smoother autocompletion
drewdaemon Jul 30, 2024
c874871
update test
drewdaemon Jul 31, 2024
a73faa3
from tests
drewdaemon Jul 31, 2024
4bce660
add more test cases
drewdaemon Jul 31, 2024
bc1e5e3
fill in support for LIMIT
drewdaemon Jul 31, 2024
898027c
SORT command support
drewdaemon Aug 1, 2024
a45fe37
fix comparison bug
drewdaemon Aug 1, 2024
a38b527
checkpoint
drewdaemon Aug 1, 2024
c531ec6
Fix date suggestions displaying in STATS
drewdaemon Aug 1, 2024
bd4ee2a
automatically advance for literal suggestions
drewdaemon Aug 1, 2024
81ae3ac
fix invoke trigger kind for subsequent function parameters
drewdaemon Aug 1, 2024
bf0229b
Refocus editor and advance cursor when date selected
drewdaemon Aug 2, 2024
883b7a9
Remove unused code
drewdaemon Aug 2, 2024
a4b7e5a
Support BY ...
drewdaemon Aug 5, 2024
0f8903e
Support WHERE <field>
drewdaemon Aug 5, 2024
5011ebc
update tests
drewdaemon Aug 5, 2024
a2e8fc3
update test
drewdaemon Aug 5, 2024
0690f20
Merge branch 'main' of github.com:elastic/kibana into faster-suggesti…
drewdaemon Aug 5, 2024
7a70676
Take into account literals when selecting signatures to suggest against
drewdaemon Aug 5, 2024
c003581
fix date trunc test
drewdaemon Aug 6, 2024
ad77eac
fix final tests
drewdaemon Aug 6, 2024
9f1dc23
remove 'string' from parameter type
drewdaemon Aug 6, 2024
d43de1d
remove chrono_literal
drewdaemon Aug 6, 2024
e7f6eed
remove 'string[]'
drewdaemon Aug 6, 2024
f63b4b2
reformat types
drewdaemon Aug 6, 2024
87aaa31
add typing to ESQL field definition
drewdaemon Aug 6, 2024
be7149f
add typing to test helpers
drewdaemon Aug 7, 2024
e9168a1
Fix some tests
drewdaemon Aug 7, 2024
47baba2
fix all further tests
drewdaemon Aug 7, 2024
87284aa
fix validation test
drewdaemon Aug 7, 2024
fbc8aa4
fix quick-fix test
drewdaemon Aug 7, 2024
a74c6d4
make all tests pass
drewdaemon Aug 7, 2024
ae9c59f
make types happy
drewdaemon Aug 7, 2024
6c98aca
Merge branch 'main' of github.com:elastic/kibana into fix-types
drewdaemon Aug 7, 2024
71fc4f1
Differentiate between field types and data types
drewdaemon Aug 7, 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
11 changes: 6 additions & 5 deletions examples/esql_validation_example/public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {

import type { CoreStart } from '@kbn/core/public';

import { ESQLCallbacks, validateQuery } from '@kbn/esql-validation-autocomplete';
import { ESQLCallbacks, ESQLRealField, validateQuery } from '@kbn/esql-validation-autocomplete';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import type { StartDependencies } from './plugin';
import { CodeSnippet } from './code_snippet';
Expand Down Expand Up @@ -52,10 +52,11 @@ export const App = (props: { core: CoreStart; plugins: StartDependencies }) => {
['index1', 'anotherIndex', 'dataStream'].map((name) => ({ name, hidden: false }))
: undefined,
getFieldsFor: callbacksEnabled.fields
? async () => [
{ name: 'numberField', type: 'number' },
{ name: 'stringField', type: 'string' },
]
? async () =>
[
{ name: 'doubleField', type: 'double' },
{ name: 'keywordField', type: 'keyword' },
] as ESQLRealField[]
: undefined,
getPolicies: callbacksEnabled.policies
? async () => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ import { getFunctionSignatures } from '../src/definitions/helpers';
import { timeUnits } from '../src/definitions/literals';
import { nonNullable } from '../src/shared/helpers';
import {
SupportedFieldType,
SupportedDataType,
FunctionDefinition,
supportedFieldTypes,
isSupportedFieldType,
dataTypes,
isSupportedDataType,
fieldTypes,
} from '../src/definitions/types';
import { FUNCTION_DESCRIBE_BLOCK_NAME } from '../src/validation/function_describe_block_name';
import { getMaxMinNumberOfParams } from '../src/validation/helpers';
import { ESQL_NUMBER_TYPES, isNumericType, isStringType } from '../src/shared/esql_types';

export const fieldNameFromType = (type: SupportedFieldType) => `${camelCase(type)}Field`;
export const fieldNameFromType = (type: SupportedDataType) => `${camelCase(type)}Field`;

function main() {
const testCasesByFunction: Map<string, Map<string, string[]>> = new Map();
Expand Down Expand Up @@ -301,8 +302,8 @@ function generateWhereCommandTestsForEvalFunction(
// TODO: not sure why there's this constraint...
const supportedFunction = signatures.some(
({ returnType, params }) =>
[...ESQL_NUMBER_TYPES, 'string'].includes(returnType) &&
params.every(({ type }) => [...ESQL_NUMBER_TYPES, 'string'].includes(type))
[...ESQL_NUMBER_TYPES, 'string'].includes(returnType as string) &&
params.every(({ type }) => [...ESQL_NUMBER_TYPES, 'string'].includes(type as string))
);

if (!supportedFunction) {
Expand All @@ -312,7 +313,7 @@ function generateWhereCommandTestsForEvalFunction(
const supportedSignatures = signatures.filter(({ returnType }) =>
// TODO — not sure why the tests have this limitation... seems like any type
// that can be part of a boolean expression should be allowed in a where clause
[...ESQL_NUMBER_TYPES, 'string'].includes(returnType)
[...ESQL_NUMBER_TYPES, 'string'].includes(returnType as string)
);
for (const { params, returnType, ...restSign } of supportedSignatures) {
const correctMapping = getFieldMapping(params);
Expand Down Expand Up @@ -905,7 +906,7 @@ function generateStatsCommandTestsForGroupingFunction(
fieldReplacedType
// if a param of type time_literal or chrono_literal it will always be a literal
// so no way to test the constantOnly thing
.filter((type) => !['time_literal', 'chrono_literal'].includes(type))
.filter((type) => !['time_literal'].includes(type as string))
.map((type) => `Argument of [${name}] must be a constant, received [${type}Field]`)
);
}
Expand Down Expand Up @@ -965,7 +966,7 @@ function generateSortCommandTestsForAggFunction(

const generateSortCommandTestsForGroupingFunction = generateSortCommandTestsForAggFunction;

const fieldTypesToConstants: Record<SupportedFieldType, string> = {
const fieldTypesToConstants: Record<SupportedDataType, string> = {
text: '"a"',
keyword: '"a"',
double: '5.5',
Expand All @@ -984,14 +985,20 @@ const fieldTypesToConstants: Record<SupportedFieldType, string> = {
geo_shape: 'to_geoshape("POINT (30 10)")',
cartesian_point: 'to_cartesianpoint("POINT (30 10)")',
cartesian_shape: 'to_cartesianshape("POINT (30 10)")',
null: 'NULL',
time_duration: '1 day',
// the following are never supplied
// by the ES function definitions. Just making types happy
time_literal: '1 day',
unsupported: '',
};

const supportedTypesAndFieldNames = supportedFieldTypes.map((type) => ({
const supportedTypesAndFieldNames = fieldTypes.map((type) => ({
name: fieldNameFromType(type),
type,
}));

const supportedTypesAndConstants = supportedFieldTypes.map((type) => ({
const supportedTypesAndConstants = dataTypes.map((type) => ({
name: fieldTypesToConstants[type],
type,
}));
Expand Down Expand Up @@ -1029,7 +1036,7 @@ const toCartesianShapeSignature = evalFunctionDefinitions.find(
const toVersionSignature = evalFunctionDefinitions.find(({ name }) => name === 'to_version')!;

// We don't have full list for long, unsigned_long, etc.
const nestedFunctions: Record<SupportedFieldType, string> = {
const nestedFunctions: Record<SupportedDataType, string> = {
double: prepareNestedFunction(toDoubleSignature),
integer: prepareNestedFunction(toInteger),
text: prepareNestedFunction(toStringSignature),
Expand All @@ -1046,7 +1053,7 @@ const nestedFunctions: Record<SupportedFieldType, string> = {
};

function getFieldName(
typeString: SupportedFieldType,
typeString: SupportedDataType,
{ useNestedFunction, isStats }: { useNestedFunction: boolean; isStats: boolean }
) {
if (useNestedFunction && isStats) {
Expand Down Expand Up @@ -1082,7 +1089,7 @@ function tweakSignatureForRowCommand(signature: string): string {
*/
let ret = signature;
for (const [type, value] of Object.entries(fieldTypesToConstants)) {
ret = ret.replace(new RegExp(fieldNameFromType(type as SupportedFieldType), 'g'), value);
ret = ret.replace(new RegExp(fieldNameFromType(type as SupportedDataType), 'g'), value);
}
return ret;
}
Expand All @@ -1101,8 +1108,8 @@ function getFieldMapping(
};

return params.map(({ name: _name, type, constantOnly, literalOptions, ...rest }) => {
const typeString: string = type;
if (isSupportedFieldType(typeString)) {
const typeString: string = type as string;
if (isSupportedDataType(typeString)) {
if (useLiterals && literalOptions) {
return {
name: `"${literalOptions[0]}"`,
Expand Down Expand Up @@ -1146,7 +1153,7 @@ function generateIncorrectlyTypedParameters(
name: string,
signatures: FunctionDefinition['signatures'],
currentParams: FunctionDefinition['signatures'][number]['params'],
availableFields: Array<{ name: string; type: SupportedFieldType }>
availableFields: Array<{ name: string; type: SupportedDataType }>
) {
const literalValues = {
string: `"a"`,
Expand All @@ -1167,7 +1174,7 @@ function generateIncorrectlyTypedParameters(

if (type !== 'any') {
// try to find an unacceptable field
const unacceptableField: { name: string; type: SupportedFieldType } | undefined =
const unacceptableField: { name: string; type: SupportedDataType } | undefined =
availableFields
// sort to make the test deterministic
.sort((a, b) => a.type.localeCompare(b.type))
Expand All @@ -1187,7 +1194,7 @@ function generateIncorrectlyTypedParameters(
}

// failed to find a bad field... they must all be acceptable
const acceptableField: { name: string; type: SupportedFieldType } | undefined =
const acceptableField: { name: string; type: SupportedDataType } | undefined =
type === 'any'
? availableFields[0]
: availableFields.find(({ type: fieldType }) => fieldType === type);
Expand Down
18 changes: 12 additions & 6 deletions packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,27 @@
*/

import { camelCase } from 'lodash';
import { supportedFieldTypes } from '../definitions/types';
import { ESQLRealField } from '../validation/types';
import { fieldTypes } from '../definitions/types';

export const fields = [
...supportedFieldTypes.map((type) => ({ name: `${camelCase(type)}Field`, type })),
export const fields: ESQLRealField[] = [
...fieldTypes
.map((type) => ({ name: `${camelCase(type)}Field`, type }))
.filter((f) => f.type !== 'unsupported'),
{ name: 'any#Char$Field', type: 'double' },
{ name: 'kubernetes.something.something', type: 'double' },
{ name: '@timestamp', type: 'date' },
];

export const enrichFields = [
export const enrichFields: ESQLRealField[] = [
{ name: 'otherField', type: 'text' },
{ name: 'yetAnotherField', type: 'double' },
];

// eslint-disable-next-line @typescript-eslint/naming-convention
export const unsupported_field = [{ name: 'unsupported_field', type: 'unsupported' }];
export const unsupported_field: ESQLRealField[] = [
{ name: 'unsupported_field', type: 'unsupported' },
];

export const indexes = [
'a_index',
Expand Down Expand Up @@ -58,7 +63,8 @@ export function getCallbackMocks() {
return unsupported_field;
}
if (/dissect|grok/.test(query)) {
return [{ name: 'firstWord', type: 'text' }];
const field: ESQLRealField = { name: 'firstWord', type: 'text' };
return [field];
}
return fields;
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
import { ESQL_COMMON_NUMERIC_TYPES, ESQL_NUMBER_TYPES } from '../../shared/esql_types';
import { setup, getFunctionSignaturesByReturnType, getFieldNamesByType } from './helpers';

const ESQL_NUMERIC_TYPES = ESQL_NUMBER_TYPES as unknown as string[];

const allAggFunctions = getFunctionSignaturesByReturnType('stats', 'any', {
agg: true,
});
Expand Down Expand Up @@ -84,51 +82,51 @@ describe('autocomplete.suggest', () => {
scalar: true,
}).map((s) => ({ ...s, text: `${s.text},` })),
]);

const roundParameterTypes = ['double', 'integer', 'long', 'unsigned_long'] as const;
await assertSuggestions('from a | stats round(/', [
...getFunctionSignaturesByReturnType('stats', ESQL_NUMERIC_TYPES, {
...getFunctionSignaturesByReturnType('stats', roundParameterTypes, {
agg: true,
grouping: true,
}),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFieldNamesByType(roundParameterTypes),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
roundParameterTypes,
{ scalar: true },
undefined,
['round']
),
]);
await assertSuggestions('from a | stats round(round(/', [
...getFunctionSignaturesByReturnType('stats', ESQL_NUMERIC_TYPES, { agg: true }),
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('stats', roundParameterTypes, { agg: true }),
...getFieldNamesByType(roundParameterTypes),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
ESQL_NUMBER_TYPES,
{ scalar: true },
undefined,
['round']
),
]);
await assertSuggestions('from a | stats avg(round(/', [
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFieldNamesByType(roundParameterTypes),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
ESQL_NUMBER_TYPES,
{ scalar: true },
undefined,
['round']
),
]);
await assertSuggestions('from a | stats avg(/', [
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_NUMERIC_TYPES, { scalar: true }),
...getFieldNamesByType(ESQL_NUMBER_TYPES),
...getFunctionSignaturesByReturnType('eval', ESQL_NUMBER_TYPES, { scalar: true }),
]);
await assertSuggestions('from a | stats round(avg(/', [
...getFieldNamesByType(ESQL_NUMERIC_TYPES),
...getFieldNamesByType(ESQL_NUMBER_TYPES),
...getFunctionSignaturesByReturnType(
'eval',
ESQL_NUMERIC_TYPES,
ESQL_NUMBER_TYPES,
{ scalar: true },
undefined,
['round']
Expand All @@ -142,7 +140,7 @@ describe('autocomplete.suggest', () => {
...getFieldNamesByType([...ESQL_COMMON_NUMERIC_TYPES, 'date', 'boolean', 'ip']),
...getFunctionSignaturesByReturnType(
'stats',
[...ESQL_COMMON_NUMERIC_TYPES, 'date', 'boolean', 'ip'],
[...ESQL_COMMON_NUMERIC_TYPES, 'date', 'date_period', 'boolean', 'ip'],
{
scalar: true,
}
Expand All @@ -158,7 +156,7 @@ describe('autocomplete.suggest', () => {
const { assertSuggestions } = await setup();

await assertSuggestions('from a | stats avg(b/) by stringField', [
...getFieldNamesByType('double'),
...getFieldNamesByType(ESQL_NUMBER_TYPES),
...getFunctionSignaturesByReturnType(
'eval',
['double', 'integer', 'long', 'unsigned_long'],
Expand Down
Loading