Skip to content

Commit c19df4d

Browse files
committed
[O11y AI Assistant] Correct quotes in ES|QL queries for function arguments (#217680)
(cherry picked from commit ccf23d0)
1 parent 5b416cd commit c19df4d

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

x-pack/platform/plugins/shared/inference/common/tasks/nl_to_esql/non_ast/correct_common_esql_mistakes.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,19 @@ describe('correctCommonEsqlMistakes', () => {
215215
| STATS success_rate = AVG(successful)`,
216216
});
217217
});
218+
219+
it('escapes special characters in column names', () => {
220+
expectQuery({
221+
input: `FROM "custom-test"
222+
| STATS
223+
count = COUNT(*),
224+
min = MIN("Total Bytes"),
225+
max = MAX("Total Bytes"),
226+
avg = AVG("Total Bytes"),
227+
sum = SUM("Total Bytes")
228+
`,
229+
expectedOutput: `FROM "custom-test"
230+
| STATS count = COUNT(*), min = MIN(\`Total Bytes\`), max = MAX(\`Total Bytes\`), avg = AVG(\`Total Bytes\`), sum = SUM(\`Total Bytes\`)`,
231+
});
232+
});
218233
});

x-pack/platform/plugins/shared/inference/common/tasks/nl_to_esql/non_ast/correct_common_esql_mistakes.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
* 2.0.
66
*/
77

8+
import { scalarFunctionDefinitions } from '@kbn/esql-validation-autocomplete/src/definitions/generated/scalar_functions';
9+
import { groupingFunctionDefinitions } from '@kbn/esql-validation-autocomplete/src/definitions/generated/grouping_functions';
10+
import { aggFunctionDefinitions } from '@kbn/esql-validation-autocomplete/src/definitions/generated/aggregation_functions';
11+
import type { FunctionDefinition } from '@kbn/esql-validation-autocomplete';
12+
import { memoize } from 'lodash';
13+
814
const STRING_DELIMITER_TOKENS = ['`', "'", '"'];
915
const ESCAPE_TOKEN = '\\\\';
1016

@@ -94,6 +100,66 @@ function removeColumnQuotesAndEscape(column: string) {
94100
return '`' + plainColumnIdentifier + '`';
95101
}
96102

103+
const getFunctionDefinitionMap = memoize(() => {
104+
const functionDefinitionMap = new Map<string, FunctionDefinition>();
105+
const allFunctionDefinitions = [
106+
...scalarFunctionDefinitions,
107+
...aggFunctionDefinitions,
108+
...groupingFunctionDefinitions,
109+
];
110+
allFunctionDefinitions.forEach((definition) => {
111+
const functionName = definition.name.toLowerCase();
112+
if (!functionDefinitionMap.has(functionName)) {
113+
functionDefinitionMap.set(functionName, definition);
114+
}
115+
});
116+
return functionDefinitionMap;
117+
});
118+
119+
/**
120+
* Replaces quotes for fields in function argument if present.
121+
* @example
122+
* Example 1: Without quotes
123+
* escapeColumnsInFunctions('MIN(total_bytes)'); // 'MIN(total_bytes)'
124+
*
125+
* @example
126+
* Example 2: With quotes
127+
* escapeColumnsInFunctions('MIN("Total Bytes")'); // 'MIN(`Total Bytes`)'
128+
*/
129+
function escapeColumnsInFunctions(string: string): string {
130+
const regex = /([A-Za-z_]+)\s*\(([^()]*?)\)/g;
131+
132+
return string.replace(regex, (match: string, functionName: string, args: string) => {
133+
const functionDefinition = getFunctionDefinitionMap().get(functionName.toLowerCase());
134+
if (!functionDefinition) {
135+
// function definition not found, return the original match
136+
return match;
137+
}
138+
139+
const escapedArgs = args.length
140+
? args
141+
.split(',')
142+
.map((arg, index) => {
143+
const trimmedArg = arg.trim();
144+
// Only escape field names
145+
const paramName = functionDefinition.signatures[0].params[index]?.name;
146+
if (paramName !== 'field' && paramName !== 'number') {
147+
// It should be just a field, but some functions like SUM and AVG have a "number" name 🤷‍♂️
148+
return trimmedArg;
149+
}
150+
// If the string is not wrapped in quotes, return it as is
151+
if (!trimmedArg.match(/^["'].*["']$/)) {
152+
return trimmedArg;
153+
}
154+
return removeColumnQuotesAndEscape(trimmedArg);
155+
})
156+
.join(', ')
157+
: args;
158+
159+
return `${functionName}(${escapedArgs})`;
160+
});
161+
}
162+
97163
function replaceAsKeywordWithAssignments(command: string) {
98164
return command.replaceAll(/^STATS\s*(.*)/g, (__, statsOperations: string) => {
99165
return `STATS ${statsOperations.replaceAll(
@@ -113,10 +179,12 @@ function escapeColumns(line: string) {
113179
const escapedBody = split(body.trim(), ',')
114180
.map((statement) => {
115181
const [lhs, rhs] = split(statement, '=');
182+
116183
if (!rhs) {
117184
return lhs;
118185
}
119-
return `${removeColumnQuotesAndEscape(lhs)} = ${rhs}`;
186+
const escapedRhs = escapeColumnsInFunctions(rhs);
187+
return `${removeColumnQuotesAndEscape(lhs)} = ${escapedRhs}`;
120188
})
121189
.join(', ');
122190

0 commit comments

Comments
 (0)