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+
814const STRING_DELIMITER_TOKENS = [ '`' , "'" , '"' ] ;
915const 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 - Z a - 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+
97163function replaceAsKeywordWithAssignments ( command : string ) {
98164 return command . replaceAll ( / ^ S T A T S \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