@@ -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,28 @@ 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 ( isAliasableIdentifier ( token , lcToken , nextToken ,
308
+ sql , symbols , tokens , wasAfterCurrent ,
309
+ dialect , typeConfiguration ) ) {
310
+ result .append (alias ).append ('.' ).append ( dialect .quote (token ) );
311
+ }
312
+ else {
289
313
result .append (token );
290
314
}
291
315
@@ -300,6 +324,37 @@ else if ( inFromClause && ",".equals(lcToken) ) {
300
324
return result .toString ();
301
325
}
302
326
327
+ private static boolean isAliasableIdentifier (
328
+ String token , String lcToken , String nextToken ,
329
+ String sql , String symbols , StringTokenizer tokens ,
330
+ boolean wasAfterCurrent ,
331
+ Dialect dialect , TypeConfiguration typeConfiguration ) {
332
+ return isUnqualifiedIdentifier ( token )
333
+ && !isKeyword ( lcToken , wasAfterCurrent , dialect , typeConfiguration )
334
+ && !isLiteral ( lcToken , nextToken , sql , symbols , tokens );
335
+ }
336
+
337
+ private static boolean isFunctionCall (
338
+ String nextToken ,
339
+ String sql , String symbols , StringTokenizer tokens ) {
340
+ if ( nextToken == null ) {
341
+ return false ;
342
+ }
343
+ else {
344
+ return nextToken .isBlank ()
345
+ ? lookPastBlankTokens ( sql , symbols , tokens , 1 , "(" ::equals )
346
+ : "(" .equals ( nextToken );
347
+ }
348
+ }
349
+
350
+ private static boolean isCurrent (
351
+ String lcToken , String nextToken ,
352
+ String sql , String symbols , StringTokenizer tokens ) {
353
+ return "current" .equals ( lcToken )
354
+ && nextToken .isBlank ()
355
+ && lookPastBlankTokens ( sql , symbols , tokens , 1 , CURRENT_BIGRAMS ::contains );
356
+ }
357
+
303
358
private static boolean isFetch (Dialect dialect , String lcToken ) {
304
359
return "fetch" .equals ( lcToken )
305
360
&& dialect .getKeywords ().contains ( "fetch" );
@@ -320,7 +375,7 @@ else if ( LITERAL_PREFIXES.contains( lcToken ) ) {
320
375
// we need to look ahead in the token stream
321
376
// to find the first non-blank token
322
377
return lookPastBlankTokens ( sqlWhereString , symbols , tokens , 1 ,
323
- ( nextToken ) -> "'" .equals (nextToken )
378
+ nextToken -> "'" .equals (nextToken )
324
379
|| lcToken .equals ("time" ) && "with" .equals (nextToken )
325
380
|| lcToken .equals ("timestamp" ) && "with" .equals (nextToken )
326
381
|| lcToken .equals ("time" ) && "zone" .equals (nextToken ) );
@@ -338,7 +393,7 @@ private static boolean lookPastBlankTokens(
338
393
String sqlWhereString , String symbols , StringTokenizer tokens ,
339
394
@ SuppressWarnings ("SameParameterValue" ) int skip ,
340
395
Function <String , Boolean > check ) {
341
- final StringTokenizer lookahead = lookahead ( sqlWhereString , symbols , tokens , skip );
396
+ final var lookahead = lookahead ( sqlWhereString , symbols , tokens , skip );
342
397
if ( lookahead .hasMoreTokens () ) {
343
398
String nextToken ;
344
399
do {
@@ -363,8 +418,7 @@ private static boolean lookPastBlankTokens(
363
418
* @return a cloned token stream
364
419
*/
365
420
private static StringTokenizer lookahead (String sql , String symbols , StringTokenizer tokens , int skip ) {
366
- final StringTokenizer lookahead =
367
- new StringTokenizer ( sql , symbols , true );
421
+ final var lookahead = new StringTokenizer ( sql , symbols , true );
368
422
while ( lookahead .countTokens () > tokens .countTokens () + skip ) {
369
423
lookahead .nextToken ();
370
424
}
@@ -401,21 +455,18 @@ public static List<String> collectColumnNames(String template) {
401
455
}
402
456
403
457
private static boolean isNamedParameter (String token ) {
404
- return token .startsWith ( ":" ) ;
458
+ return token .charAt ( 0 ) == ':' ;
405
459
}
406
460
407
- private static boolean isFunctionOrKeyword (
461
+ private static boolean isKeyword (
408
462
String lcToken ,
409
- String nextToken ,
463
+ boolean afterCurrent ,
410
464
Dialect dialect ,
411
465
TypeConfiguration typeConfiguration ) {
412
- if ( "(" .equals ( nextToken ) ) {
413
- return true ;
414
- }
415
- else if ( SOFT_KEYWORDS .contains ( lcToken ) ) {
466
+ if ( SOFT_KEYWORDS .contains ( lcToken ) ) {
416
467
// these can be column names on some databases
417
- // TODO: treat 'current date' as a function
418
- return false ;
468
+ // but treat 'current date', 'current time' bigrams as keywords
469
+ return afterCurrent ;
419
470
}
420
471
else {
421
472
return KEYWORDS .contains ( lcToken )
@@ -429,15 +480,15 @@ private static boolean isType(String lcToken, TypeConfiguration typeConfiguratio
429
480
return typeConfiguration .getDdlTypeRegistry ().isTypeNameRegistered ( lcToken );
430
481
}
431
482
432
- 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 );
483
+ private static boolean isUnqualifiedIdentifier (String token ) {
484
+ final char initialChar = token .charAt ( 0 );
485
+ return initialChar == '`' // allow any identifier quoted with backtick
486
+ || isLetter ( initialChar ) // only recognizes identifiers beginning with a letter
487
+ && token . indexOf ( '.' ) < 0 ; // don't qualify already-qualified identifiers
437
488
}
438
489
439
- private static boolean isBoolean (String token ) {
440
- return switch ( token . toLowerCase ( Locale . ROOT ) ) {
490
+ private static boolean isBoolean (String lcToken ) {
491
+ return switch ( lcToken ) {
441
492
case "true" , "false" -> true ;
442
493
default -> false ;
443
494
};
0 commit comments