Skip to content

Commit 6ce46a3

Browse files
authored
[ES|QL] Update other AST Tools (elastic#233130)
## Summary part of elastic#232885 - Remove inferenceId from ESQLAstRerankCommand type - Update Pretty printer, visitor, walker, mutate .... note: most of these updates are tests
1 parent 8f01c0c commit 6ce46a3

File tree

10 files changed

+486
-140
lines changed

10 files changed

+486
-140
lines changed

src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/rerank/index.test.ts

Lines changed: 89 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,13 @@
1010
import { BasicPrettyPrinter } from '../../../pretty_print';
1111
import * as commands from '..';
1212
import { EsqlQuery } from '../../../query';
13+
import type { ESQLAstItem, ESQLCommandOption, ESQLMap } from '../../../types';
1314

14-
/**
15-
* @todo Tests skipped, while RERANK command grammar is being stabilized. We will
16-
* get back to it after 9.1 release.
17-
*/
18-
describe.skip('commands.rerank', () => {
15+
describe('commands.rerank', () => {
1916
describe('.list()', () => {
2017
it('lists the only "RERANK" commands', () => {
2118
const src =
22-
'FROM index | LIMIT 1 | RERANK "star wars" ON title, overview=SUBSTRING(overview, 0, 100), actors WITH rerankerInferenceId | LIMIT 2';
19+
'FROM index | LIMIT 1 | RERANK "star wars" ON title, overview=SUBSTRING(overview, 0, 100), actors WITH { "inference_id": "model_id" } | LIMIT 2';
2320
const query = EsqlQuery.fromSrc(src);
2421

2522
const nodes = [...commands.rerank.list(query.ast)];
@@ -35,22 +32,74 @@ describe.skip('commands.rerank', () => {
3532

3633
describe('.setQuery()', () => {
3734
it('can change query', () => {
38-
const src = 'FROM index | LIMIT 1 | RERANK "star wars" ON field WITH id | LIMIT 2';
35+
const src =
36+
'FROM index | RERANK "star wars" ON field WITH { "inference_id": "model_id" } | LIMIT 2';
3937
const query = EsqlQuery.fromSrc(src);
4038

4139
const cmd = [...commands.rerank.list(query.ast)][0];
4240
commands.rerank.setQuery(cmd, 'new query');
4341

4442
expect(BasicPrettyPrinter.expression(cmd.query)).toBe('"new query"');
45-
expect(query.print()).toBe(
46-
'FROM index | LIMIT 1 | RERANK "new query" ON field WITH id | LIMIT 2'
43+
expect(query.print({ wrap: Infinity })).toBe(
44+
'FROM index | RERANK "new query" ON field WITH {"inference_id": "model_id"} | LIMIT 2'
4745
);
4846
});
4947
});
5048

49+
it('can change query on a command with a target assignment', () => {
50+
const src =
51+
'FROM index | RERANK my_target = "star wars" ON field WITH { "inference_id": "model_id" }';
52+
const query = EsqlQuery.fromSrc(src);
53+
54+
const cmd = [...commands.rerank.list(query.ast)][0];
55+
commands.rerank.setQuery(cmd, 'new query');
56+
57+
expect(BasicPrettyPrinter.expression(cmd.query)).toBe('"new query"');
58+
expect(query.print({ wrap: Infinity })).toBe(
59+
'FROM index | RERANK my_target = "new query" ON field WITH {"inference_id": "model_id"}'
60+
);
61+
});
62+
});
63+
64+
describe('.setTargetField()', () => {
65+
it('can add a target field to a simple command', () => {
66+
const src = 'FROM index | RERANK "star wars" ON field';
67+
const query = EsqlQuery.fromSrc(src);
68+
69+
const cmd = [...commands.rerank.list(query.ast)][0];
70+
commands.rerank.setTargetField(cmd, 'my_target');
71+
72+
expect(query.print({ wrap: Infinity })).toBe(
73+
'FROM index | RERANK my_target = "star wars" ON field'
74+
);
75+
});
76+
77+
it('can change an existing target field', () => {
78+
const src = 'FROM index | RERANK old_target = "star wars" ON field';
79+
const query = EsqlQuery.fromSrc(src);
80+
81+
const cmd = [...commands.rerank.list(query.ast)][0];
82+
commands.rerank.setTargetField(cmd, 'new_target');
83+
84+
expect(query.print({ wrap: Infinity })).toBe(
85+
'FROM index | RERANK new_target = "star wars" ON field'
86+
);
87+
});
88+
89+
it('can remove an existing target field', () => {
90+
const src = 'FROM index | RERANK my_target = "star wars" ON field';
91+
const query = EsqlQuery.fromSrc(src);
92+
93+
const cmd = [...commands.rerank.list(query.ast)][0];
94+
commands.rerank.setTargetField(cmd, null);
95+
96+
expect(query.print({ wrap: Infinity })).toBe('FROM index | RERANK "star wars" ON field');
97+
});
98+
5199
describe('.setFields()', () => {
52100
it('can change query', () => {
53-
const src = 'FROM index | LIMIT 1 | RERANK "star wars" ON field WITH id | LIMIT 2';
101+
const src =
102+
'FROM index | RERANK "star wars" ON field WITH { "inference_id": "model_id" } | LIMIT 2';
54103
const query = EsqlQuery.fromSrc(src);
55104

56105
const cmd = [...commands.rerank.list(query.ast)][0];
@@ -61,23 +110,44 @@ describe.skip('commands.rerank', () => {
61110
'b',
62111
'@timestamp',
63112
]);
64-
expect(query.print()).toBe(
65-
'FROM index | LIMIT 1 | RERANK "star wars" ON a, b, @timestamp WITH id | LIMIT 2'
113+
expect(query.print({ wrap: Infinity })).toBe(
114+
'FROM index | RERANK "star wars" ON a, b, @timestamp WITH {"inference_id": "model_id"} | LIMIT 2'
115+
);
116+
});
117+
118+
it('should throw error when ON option is missing', () => {
119+
const src = 'FROM index | RERANK "star wars"';
120+
const query = EsqlQuery.fromSrc(src);
121+
122+
const cmd = [...commands.rerank.list(query.ast)][0];
123+
124+
expect(() => commands.rerank.setFields(cmd, ['newField'])).toThrow(
125+
'RERANK command must have a ON option'
66126
);
67127
});
68128
});
69129

70-
describe('.setInferenceId()', () => {
71-
it('can change query', () => {
72-
const src = 'FROM index | LIMIT 1 | RERANK "star wars" ON field WITH id | LIMIT 2';
130+
describe('.setWithParameter()', () => {
131+
it('can add and update a new parameter to WITH map', () => {
132+
const src =
133+
'FROM index | RERANK "star wars" ON field WITH { "inference_id": "model_id" } | LIMIT 2';
73134
const query = EsqlQuery.fromSrc(src);
74135

75136
const cmd = [...commands.rerank.list(query.ast)][0];
76-
commands.rerank.setInferenceId(cmd, 'new_id');
137+
commands.rerank.setWithParameter(cmd, 'scoreColumn', 'first_rank_score'); // create
138+
commands.rerank.setWithParameter(cmd, 'scoreColumn', 'rank_score'); // update
139+
140+
const isWithOption = (arg: ESQLAstItem): arg is ESQLCommandOption =>
141+
!!arg && !Array.isArray(arg) && arg.type === 'option' && arg.name === 'with';
142+
143+
const map = cmd.args.find(isWithOption)!.args[0] as ESQLMap;
144+
const scoreColumnEntry = map?.entries?.find(
145+
(entry) => entry.key.valueUnquoted === 'scoreColumn'
146+
);
77147

78-
expect(BasicPrettyPrinter.expression(cmd.inferenceId)).toBe('new_id');
79-
expect(query.print()).toBe(
80-
'FROM index | LIMIT 1 | RERANK "star wars" ON field WITH new_id | LIMIT 2'
148+
expect(BasicPrettyPrinter.expression(scoreColumnEntry!.value)).toBe('"rank_score"');
149+
expect(query.print({ wrap: Infinity })).toBe(
150+
'FROM index | RERANK "star wars" ON field WITH {"inference_id": "model_id", "scoreColumn": "rank_score"} | LIMIT 2'
81151
);
82152
});
83153
});

src/platform/packages/shared/kbn-esql-ast/src/mutate/commands/rerank/index.ts

Lines changed: 154 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ import type {
1212
ESQLAstQueryExpression,
1313
ESQLAstRerankCommand,
1414
ESQLCommandOption,
15-
ESQLIdentifierOrParam,
1615
ESQLStringLiteral,
16+
ESQLParamLiteral,
17+
ESQLMap,
18+
ESQLAstItem,
19+
ESQLMapEntry,
20+
ESQLLiteral,
1721
} from '../../../types';
1822
import * as generic from '../../generic';
1923

@@ -30,15 +34,79 @@ export const list = (ast: ESQLAstQueryExpression): IterableIterator<ESQLAstReran
3034
) as IterableIterator<ESQLAstRerankCommand>;
3135
};
3236

37+
/**
38+
* Sets or updates the query text for the RERANK command.
39+
*
40+
* @param cmd The RERANK command AST node to modify.
41+
* @param query The query text to set.
42+
*/
3343
export const setQuery = (cmd: ESQLAstRerankCommand, query: string | ESQLStringLiteral) => {
34-
if (typeof query === 'string') {
35-
query = Builder.expression.literal.string(query);
44+
const queryLiteral = typeof query === 'string' ? Builder.expression.literal.string(query) : query;
45+
const firstArg = cmd.args[0];
46+
47+
cmd.query = queryLiteral;
48+
49+
if (
50+
firstArg &&
51+
!Array.isArray(firstArg) &&
52+
firstArg.type === 'function' &&
53+
firstArg.name === '='
54+
) {
55+
// It's an assignment, update the right side of the expression
56+
firstArg.args[1] = queryLiteral;
57+
} else {
58+
// It's a simple query, replace the first argument
59+
cmd.args[0] = queryLiteral;
3660
}
61+
};
62+
63+
/**
64+
* Sets, updates, or removes the target field for the RERANK command.
65+
* This refers to the `targetField =` portion of the command, which specifies │
66+
* the new column where the rerank score will be stored. │
67+
*
68+
* @param cmd The RERANK command AST node to modify.
69+
* @param target The name of the target field to set. If `null` the assignment is removed.
70+
*/
71+
export const setTargetField = (cmd: ESQLAstRerankCommand, target: string | null) => {
72+
const firstArg = cmd.args[0];
73+
const isAssignment =
74+
firstArg && !Array.isArray(firstArg) && firstArg.type === 'function' && firstArg.name === '=';
3775

38-
cmd.query = query;
39-
cmd.args[0] = query;
76+
// Case 1: Set a new target field
77+
if (target !== null) {
78+
const newTargetColumn = Builder.expression.column(target);
79+
80+
if (isAssignment) {
81+
// An assignment already exists => update the target field
82+
firstArg.args[0] = newTargetColumn;
83+
} else {
84+
// No assignment exists => create one
85+
const queryLiteral = cmd.query;
86+
const assignment = Builder.expression.func.binary('=', [newTargetColumn, queryLiteral]);
87+
cmd.args[0] = assignment;
88+
}
89+
90+
cmd.targetField = newTargetColumn;
91+
}
92+
// Case 2: Remove the target field
93+
else {
94+
if (isAssignment) {
95+
// An assignment exists, keep only the query
96+
const queryLiteral = cmd.query;
97+
cmd.args[0] = queryLiteral;
98+
}
99+
// If no assignment exists, do nothing
100+
cmd.targetField = undefined;
101+
}
40102
};
41103

104+
/**
105+
* Sets or updates the fields to be used for reranking in the ON clause.
106+
*
107+
* @param cmd The RERANK command AST node to modify.
108+
* @param fields An array of field names or field nodes.
109+
*/
42110
export const setFields = (
43111
cmd: ESQLAstRerankCommand,
44112
fields: string[] | ESQLAstRerankCommand['fields']
@@ -51,21 +119,93 @@ export const setFields = (
51119
});
52120
}
53121

122+
const isOnOption = (arg: ESQLAstItem): arg is ESQLCommandOption =>
123+
!!arg && !Array.isArray(arg) && arg.type === 'option' && arg.name === 'on';
124+
125+
const onOption = cmd.args.find(isOnOption);
126+
127+
if (!onOption) {
128+
throw new Error('RERANK command must have a ON option');
129+
}
130+
54131
cmd.fields.length = 0;
55132
cmd.fields.push(...(fields as ESQLAstRerankCommand['fields']));
133+
134+
onOption.args.length = 0;
135+
onOption.args.push(...(fields as ESQLAstRerankCommand['fields']));
56136
};
57137

58-
export const setInferenceId = (cmd: ESQLAstRerankCommand, id: string | ESQLIdentifierOrParam) => {
59-
if (typeof id === 'string') {
60-
id = id[0] === '?' ? Builder.param.build(id) : Builder.identifier(id);
61-
}
138+
/**
139+
* Sets a parameter in the WITH clause of the RERANK command (e.g., 'inference_id').
140+
* If the parameter already exists, its value is updated. Otherwise, it is added.
141+
*
142+
* @param cmd The RERANK command AST node to modify.
143+
* @param key The name of the parameter to set.
144+
* @param value The value of the parameter.
145+
*/
146+
export const setWithParameter = (
147+
cmd: ESQLAstRerankCommand,
148+
key: string,
149+
value: string | ESQLStringLiteral | ESQLParamLiteral
150+
) => {
151+
// Converts a value to an appropriate ESQLExpression based on its type
152+
const toExpression = (val: string | ESQLLiteral | ESQLParamLiteral) => {
153+
if (typeof val === 'string') {
154+
return val.startsWith('?')
155+
? Builder.param.build(val)
156+
: Builder.expression.literal.string(val);
157+
}
158+
return val;
159+
};
62160

63-
if (id.type !== 'identifier' && id.type !== 'literal') {
64-
throw new Error(`Invalid RERANK inferenceId: ${id}`);
161+
const isWithOption = (arg: ESQLAstItem): arg is ESQLCommandOption =>
162+
!!arg && !Array.isArray(arg) && arg.type === 'option' && arg.name === 'with';
163+
164+
// Validates and retrieves the map from a WITH option
165+
const getWithOptionMap = (withOption: ESQLCommandOption): ESQLMap => {
166+
const mapArg = withOption.args[0];
167+
168+
if (!mapArg || typeof mapArg === 'string' || Array.isArray(mapArg) || mapArg.type !== 'map') {
169+
throw new Error('WITH option must contain a map');
170+
}
171+
172+
return mapArg as ESQLMap;
173+
};
174+
175+
// Normalizes a key for comparison by removing quotes
176+
const normalizeKey = (keyValue: string | undefined): string => {
177+
return keyValue?.replace(/"/g, '') ?? '';
178+
};
179+
180+
// Checks if an entry in the map has the specified key
181+
// normalizeKey avoid cases like: "inference_id" === "'inferenceId"' -> this is false
182+
const getExistingEntry = (entry: ESQLMapEntry) =>
183+
normalizeKey(entry.key.valueUnquoted ?? entry.key.value) === key;
184+
185+
// Creates a new map entry with the given key and value
186+
const createMapEntry = (entryKey: string, entryValue: ESQLLiteral | ESQLParamLiteral) => ({
187+
type: 'map-entry' as const,
188+
name: 'map-entry' as const,
189+
key: Builder.expression.literal.string(entryKey),
190+
value: entryValue,
191+
location: { min: 0, max: 0 },
192+
text: '',
193+
incomplete: false,
194+
});
195+
196+
const withOption = cmd.args.find(isWithOption);
197+
198+
if (!withOption) {
199+
throw new Error('RERANK command must have a WITH option');
65200
}
66201

67-
cmd.inferenceId = id;
202+
const valueExpression = toExpression(value);
203+
const map = getWithOptionMap(withOption);
204+
const existingEntry = map.entries.find(getExistingEntry);
68205

69-
const withOption = cmd.args[2] as ESQLCommandOption;
70-
withOption.args[0] = id;
206+
if (existingEntry) {
207+
existingEntry.value = valueExpression;
208+
} else {
209+
map.entries.push(createMapEntry(key, valueExpression));
210+
}
71211
};

0 commit comments

Comments
 (0)