@@ -91,6 +91,8 @@ public final class Template {
9191 = Set .of ("n" , "x" , "varbyte" , "bx" , "bytea" , "date" , "time" , "timestamp" , "zone" );
9292 private static final Set <String > FETCH_BIGRAMS
9393 = Set .of ("first" , "next" );
94+ private static final Set <String > CURRENT_BIGRAMS
95+ = Set .of ("date" , "time" , "timestamp" );
9496
9597 private static final String PUNCTUATION = "=><!+-*/()',|&`" ;
9698
@@ -157,8 +159,8 @@ public static String renderWhereStringTemplate(
157159 // lookahead is truly necessary, use the lookahead() function provided below.
158160
159161 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 ();
162164
163165 boolean quoted = false ;
164166 boolean quotedIdentifier = false ;
@@ -169,6 +171,7 @@ public static String renderWhereStringTemplate(
169171 boolean inCast = false ;
170172 boolean afterCastAs = false ;
171173 boolean afterFetch = false ;
174+ boolean afterCurrent = false ;
172175
173176 boolean hasMore = tokens .hasMoreTokens ();
174177 String nextToken = hasMore ? tokens .nextToken () : null ;
@@ -220,8 +223,11 @@ else if ( quotedIdentifier && dialect.closeQuote()==token.charAt(0) ) {
220223
221224 final boolean isWhitespace = token .isBlank ();
222225
226+ // handle bigrams here
223227 final boolean wasAfterFetch = afterFetch ;
224228 afterFetch = afterFetch && isWhitespace ;
229+ final boolean wasAfterCurrent = afterCurrent ;
230+ afterCurrent = afterCurrent && isWhitespace ;
225231
226232 final boolean isQuoted =
227233 quoted || quotedIdentifier || isQuoteCharacter ;
@@ -234,25 +240,46 @@ else if ( beforeTable ) {
234240 afterFromTable = true ;
235241 }
236242 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 ;
239258 }
240259 result .append (token );
241260 }
242- else if ( isNamedParameter ( token ) ) {
261+ else if ( lcToken . length ()== 1 && symbols . contains ( lcToken ) ) {
243262 result .append (token );
244263 }
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+ }
246269 result .append (token );
247- inExtractOrTrim = true ;
248270 }
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'
250275 result .append ( token );
251- inCast = true ;
252276 }
253- else if ( inCast && ("as" .equals ( lcToken ) || afterCastAs ) ) {
277+ else if ( isNamedParameter (token ) ) {
278+ result .append (token );
279+ }
280+ else if ( "as" .equals ( lcToken ) ) {
254281 result .append ( token );
255- afterCastAs = true ;
282+ afterCastAs = inCast ;
256283 }
257284 else if ( isFetch ( dialect , lcToken ) ) {
258285 result .append ( token );
@@ -261,31 +288,28 @@ else if ( isFetch( dialect, lcToken ) ) {
261288 else if ( wasAfterFetch && FETCH_BIGRAMS .contains ( lcToken ) ) {
262289 result .append ( token );
263290 }
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 ;
271294 }
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 ;
285302 }
286- if ( isBoolean ( token ) ) {
287- token = dialect . toBooleanValueString ( parseBoolean ( token ) ) ;
303+ if ( "cast" . equals ( lcToken ) ) {
304+ inCast = true ;
288305 }
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 {
289313 result .append (token );
290314 }
291315
@@ -300,6 +324,37 @@ else if ( inFromClause && ",".equals(lcToken) ) {
300324 return result .toString ();
301325 }
302326
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+
303358 private static boolean isFetch (Dialect dialect , String lcToken ) {
304359 return "fetch" .equals ( lcToken )
305360 && dialect .getKeywords ().contains ( "fetch" );
@@ -320,7 +375,7 @@ else if ( LITERAL_PREFIXES.contains( lcToken ) ) {
320375 // we need to look ahead in the token stream
321376 // to find the first non-blank token
322377 return lookPastBlankTokens ( sqlWhereString , symbols , tokens , 1 ,
323- ( nextToken ) -> "'" .equals (nextToken )
378+ nextToken -> "'" .equals (nextToken )
324379 || lcToken .equals ("time" ) && "with" .equals (nextToken )
325380 || lcToken .equals ("timestamp" ) && "with" .equals (nextToken )
326381 || lcToken .equals ("time" ) && "zone" .equals (nextToken ) );
@@ -338,7 +393,7 @@ private static boolean lookPastBlankTokens(
338393 String sqlWhereString , String symbols , StringTokenizer tokens ,
339394 @ SuppressWarnings ("SameParameterValue" ) int skip ,
340395 Function <String , Boolean > check ) {
341- final StringTokenizer lookahead = lookahead ( sqlWhereString , symbols , tokens , skip );
396+ final var lookahead = lookahead ( sqlWhereString , symbols , tokens , skip );
342397 if ( lookahead .hasMoreTokens () ) {
343398 String nextToken ;
344399 do {
@@ -363,8 +418,7 @@ private static boolean lookPastBlankTokens(
363418 * @return a cloned token stream
364419 */
365420 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 );
368422 while ( lookahead .countTokens () > tokens .countTokens () + skip ) {
369423 lookahead .nextToken ();
370424 }
@@ -401,21 +455,18 @@ public static List<String> collectColumnNames(String template) {
401455 }
402456
403457 private static boolean isNamedParameter (String token ) {
404- return token .startsWith ( ":" ) ;
458+ return token .charAt ( 0 ) == ':' ;
405459 }
406460
407- private static boolean isFunctionOrKeyword (
461+ private static boolean isKeyword (
408462 String lcToken ,
409- String nextToken ,
463+ boolean afterCurrent ,
410464 Dialect dialect ,
411465 TypeConfiguration typeConfiguration ) {
412- if ( "(" .equals ( nextToken ) ) {
413- return true ;
414- }
415- else if ( SOFT_KEYWORDS .contains ( lcToken ) ) {
466+ if ( SOFT_KEYWORDS .contains ( lcToken ) ) {
416467 // 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 ;
419470 }
420471 else {
421472 return KEYWORDS .contains ( lcToken )
@@ -429,15 +480,15 @@ private static boolean isType(String lcToken, TypeConfiguration typeConfiguratio
429480 return typeConfiguration .getDdlTypeRegistry ().isTypeNameRegistered ( lcToken );
430481 }
431482
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
437488 }
438489
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 ) {
441492 case "true" , "false" -> true ;
442493 default -> false ;
443494 };
0 commit comments