@@ -91,6 +91,8 @@ public final class Template {
91
91
= Set .of ("n" , "x" , "varbyte" , "bx" , "bytea" , "date" , "time" , "timestamp" , "zone" );
92
92
private static final Set <String > FETCH_BIGRAMS
93
93
= Set .of ("first" , "next" );
94
+ private static final Set <String > CURRENT_BIGRAMS
95
+ = Set .of ("date" , "time" , "timestamp" );
94
96
95
97
private static final String PUNCTUATION = "=><!+-*/()',|&`" ;
96
98
@@ -157,8 +159,8 @@ public static String renderWhereStringTemplate(
157
159
// lookahead is truly necessary, use the lookahead() function provided below.
158
160
159
161
final String symbols = PUNCTUATION + WHITESPACE + dialect .openQuote () + dialect .closeQuote ();
160
- final StringTokenizer tokens = new StringTokenizer ( sql , symbols , true );
161
- final StringBuilder result = new StringBuilder ();
162
+ final var tokens = new StringTokenizer ( sql , symbols , true );
163
+ final var result = new StringBuilder ();
162
164
163
165
boolean quoted = false ;
164
166
boolean quotedIdentifier = false ;
@@ -169,6 +171,7 @@ public static String renderWhereStringTemplate(
169
171
boolean inCast = false ;
170
172
boolean afterCastAs = false ;
171
173
boolean afterFetch = false ;
174
+ boolean afterCurrent = false ;
172
175
173
176
boolean hasMore = tokens .hasMoreTokens ();
174
177
String nextToken = hasMore ? tokens .nextToken () : null ;
@@ -220,8 +223,11 @@ else if ( quotedIdentifier && dialect.closeQuote()==token.charAt(0) ) {
220
223
221
224
final boolean isWhitespace = token .isBlank ();
222
225
226
+ // handle bigrams here
223
227
final boolean wasAfterFetch = afterFetch ;
224
228
afterFetch = afterFetch && isWhitespace ;
229
+ final boolean wasAfterCurrent = afterCurrent ;
230
+ afterCurrent = afterCurrent && isWhitespace ;
225
231
226
232
final boolean isQuoted =
227
233
quoted || quotedIdentifier || isQuoteCharacter ;
@@ -234,25 +240,46 @@ else if ( beforeTable ) {
234
240
afterFromTable = true ;
235
241
}
236
242
else if ( afterFromTable ) {
237
- if ( !"as" .equals (lcToken ) ) {
238
- afterFromTable = false ;
243
+ afterFromTable = "as" .equals (lcToken );
244
+ result .append (token );
245
+ }
246
+ else if ( "(" .equals (lcToken ) ) {
247
+ result .append (token );
248
+ }
249
+ else if ( ")" .equals (lcToken ) ) {
250
+ inExtractOrTrim = false ;
251
+ inCast = false ;
252
+ afterCastAs = false ;
253
+ result .append (token );
254
+ }
255
+ else if ( "," .equals (lcToken ) ) {
256
+ if ( inFromClause ) {
257
+ beforeTable = true ;
239
258
}
240
259
result .append (token );
241
260
}
242
- else if ( isNamedParameter ( token ) ) {
261
+ else if ( lcToken . length ()== 1 && symbols . contains ( lcToken ) ) {
243
262
result .append (token );
244
263
}
245
- else if ( FUNCTION_WITH_FROM_KEYWORDS .contains (lcToken ) && "(" .equals ( nextToken ) ) {
264
+ else if ( BEFORE_TABLE_KEYWORDS .contains (lcToken ) ) {
265
+ if ( !inExtractOrTrim ) {
266
+ beforeTable = true ;
267
+ inFromClause = true ;
268
+ }
246
269
result .append (token );
247
- inExtractOrTrim = true ;
248
270
}
249
- else if ( "cast" .equals ( lcToken ) ) {
271
+ else if ( inFromClause || afterCastAs ) {
272
+ // Don't want to append alias to:
273
+ // 1. tokens inside the FROM clause
274
+ // 2. type names after 'CAST(expression AS'
250
275
result .append ( token );
251
- inCast = true ;
252
276
}
253
- else if ( inCast && ("as" .equals ( lcToken ) || afterCastAs ) ) {
277
+ else if ( isNamedParameter (token ) ) {
278
+ result .append (token );
279
+ }
280
+ else if ( "as" .equals ( lcToken ) ) {
254
281
result .append ( token );
255
- afterCastAs = true ;
282
+ afterCastAs = inCast ;
256
283
}
257
284
else if ( isFetch ( dialect , lcToken ) ) {
258
285
result .append ( token );
@@ -261,31 +288,31 @@ else if ( isFetch( dialect, lcToken ) ) {
261
288
else if ( wasAfterFetch && FETCH_BIGRAMS .contains ( lcToken ) ) {
262
289
result .append ( token );
263
290
}
264
- else if ( !inFromClause // don't want to append alias to tokens inside the FROM clause
265
- && isIdentifier ( token )
266
- && !isFunctionOrKeyword ( lcToken , nextToken , dialect , typeConfiguration )
267
- && !isLiteral ( lcToken , nextToken , sql , symbols , tokens ) ) {
268
- result .append (alias )
269
- .append ('.' )
270
- .append ( dialect .quote (token ) );
291
+ else if ( isCurrent ( lcToken , nextToken , sql , symbols , tokens ) ) {
292
+ result .append (token );
293
+ afterCurrent = true ;
271
294
}
272
- else {
273
- if ( ")" .equals (lcToken ) ) {
274
- inExtractOrTrim = false ;
275
- inCast = false ;
276
- afterCastAs = false ;
277
- }
278
- else if ( !inExtractOrTrim
279
- && BEFORE_TABLE_KEYWORDS .contains (lcToken ) ) {
280
- beforeTable = true ;
281
- inFromClause = true ;
282
- }
283
- else if ( inFromClause && "," .equals (lcToken ) ) {
284
- beforeTable = true ;
295
+ else if ( isBoolean ( lcToken ) ) {
296
+ result .append ( dialect .toBooleanValueString ( parseBoolean ( token ) ) );
297
+ }
298
+ else if ( isFunctionCall ( nextToken , sql , symbols , tokens ) ) {
299
+ result .append (token );
300
+ if ( FUNCTION_WITH_FROM_KEYWORDS .contains ( lcToken ) ) {
301
+ inExtractOrTrim = true ;
285
302
}
286
- if ( isBoolean ( token ) ) {
287
- token = dialect . toBooleanValueString ( parseBoolean ( token ) ) ;
303
+ if ( "cast" . equals ( lcToken ) ) {
304
+ inCast = true ;
288
305
}
306
+ }
307
+ else if ( isKeyword ( lcToken , wasAfterCurrent , dialect , typeConfiguration )
308
+ || isLiteral ( lcToken , nextToken , sql , symbols , tokens ) ) {
309
+ result .append (token );
310
+ }
311
+ else if ( isIdentifier ( token ) ) {
312
+ result .append (alias ).append ('.' ).append ( dialect .quote (token ) );
313
+ }
314
+ else {
315
+ // something strange
289
316
result .append (token );
290
317
}
291
318
@@ -300,6 +327,27 @@ else if ( inFromClause && ",".equals(lcToken) ) {
300
327
return result .toString ();
301
328
}
302
329
330
+ private static boolean isFunctionCall (
331
+ String nextToken ,
332
+ String sql , String symbols , StringTokenizer tokens ) {
333
+ if ( nextToken == null ) {
334
+ return false ;
335
+ }
336
+ else {
337
+ return nextToken .isBlank ()
338
+ ? lookPastBlankTokens ( sql , symbols , tokens , 1 , "(" ::equals )
339
+ : "(" .equals ( nextToken );
340
+ }
341
+ }
342
+
343
+ private static boolean isCurrent (
344
+ String lcToken , String nextToken ,
345
+ String sql , String symbols , StringTokenizer tokens ) {
346
+ return "current" .equals ( lcToken )
347
+ && nextToken .isBlank ()
348
+ && lookPastBlankTokens ( sql , symbols , tokens , 1 , CURRENT_BIGRAMS ::contains );
349
+ }
350
+
303
351
private static boolean isFetch (Dialect dialect , String lcToken ) {
304
352
return "fetch" .equals ( lcToken )
305
353
&& dialect .getKeywords ().contains ( "fetch" );
@@ -320,7 +368,7 @@ else if ( LITERAL_PREFIXES.contains( lcToken ) ) {
320
368
// we need to look ahead in the token stream
321
369
// to find the first non-blank token
322
370
return lookPastBlankTokens ( sqlWhereString , symbols , tokens , 1 ,
323
- ( nextToken ) -> "'" .equals (nextToken )
371
+ nextToken -> "'" .equals (nextToken )
324
372
|| lcToken .equals ("time" ) && "with" .equals (nextToken )
325
373
|| lcToken .equals ("timestamp" ) && "with" .equals (nextToken )
326
374
|| lcToken .equals ("time" ) && "zone" .equals (nextToken ) );
@@ -338,7 +386,7 @@ private static boolean lookPastBlankTokens(
338
386
String sqlWhereString , String symbols , StringTokenizer tokens ,
339
387
@ SuppressWarnings ("SameParameterValue" ) int skip ,
340
388
Function <String , Boolean > check ) {
341
- final StringTokenizer lookahead = lookahead ( sqlWhereString , symbols , tokens , skip );
389
+ final var lookahead = lookahead ( sqlWhereString , symbols , tokens , skip );
342
390
if ( lookahead .hasMoreTokens () ) {
343
391
String nextToken ;
344
392
do {
@@ -363,8 +411,7 @@ private static boolean lookPastBlankTokens(
363
411
* @return a cloned token stream
364
412
*/
365
413
private static StringTokenizer lookahead (String sql , String symbols , StringTokenizer tokens , int skip ) {
366
- final StringTokenizer lookahead =
367
- new StringTokenizer ( sql , symbols , true );
414
+ final var lookahead = new StringTokenizer ( sql , symbols , true );
368
415
while ( lookahead .countTokens () > tokens .countTokens () + skip ) {
369
416
lookahead .nextToken ();
370
417
}
@@ -401,21 +448,18 @@ public static List<String> collectColumnNames(String template) {
401
448
}
402
449
403
450
private static boolean isNamedParameter (String token ) {
404
- return token .startsWith ( ":" ) ;
451
+ return token .charAt ( 0 ) == ':' ;
405
452
}
406
453
407
- private static boolean isFunctionOrKeyword (
454
+ private static boolean isKeyword (
408
455
String lcToken ,
409
- String nextToken ,
456
+ boolean afterCurrent ,
410
457
Dialect dialect ,
411
458
TypeConfiguration typeConfiguration ) {
412
- if ( "(" .equals ( nextToken ) ) {
413
- return true ;
414
- }
415
- else if ( SOFT_KEYWORDS .contains ( lcToken ) ) {
459
+ if ( SOFT_KEYWORDS .contains ( lcToken ) ) {
416
460
// these can be column names on some databases
417
- // TODO: treat 'current date' as a function
418
- return false ;
461
+ // but treat 'current date', 'current time' bigrams as keywords
462
+ return afterCurrent ;
419
463
}
420
464
else {
421
465
return KEYWORDS .contains ( lcToken )
@@ -430,14 +474,14 @@ private static boolean isType(String lcToken, TypeConfiguration typeConfiguratio
430
474
}
431
475
432
476
private static boolean isIdentifier (String token ) {
433
- return token .charAt ( 0 ) == '`' // allow any identifier quoted with backtick
434
- || isLetter ( token . charAt ( 0 ) ) // only recognizes identifiers beginning with a letter
435
- && token . indexOf ( '.' ) < 0
436
- && ! isBoolean ( token ) ;
477
+ final char initialChar = token .charAt ( 0 );
478
+ return initialChar == '`' // allow any identifier quoted with backtick
479
+ || isLetter ( initialChar ) // only recognizes identifiers beginning with a letter
480
+ && token . indexOf ( '.' ) < 0 ;
437
481
}
438
482
439
- private static boolean isBoolean (String token ) {
440
- return switch ( token . toLowerCase ( Locale . ROOT ) ) {
483
+ private static boolean isBoolean (String lcToken ) {
484
+ return switch ( lcToken ) {
441
485
case "true" , "false" -> true ;
442
486
default -> false ;
443
487
};
0 commit comments