Skip to content

Commit 69f1076

Browse files
committed
simplify the logic in Template and make handling of functions more robust
- use lookahead to find opening parens in function calls - handle 'current date', 'current time' bigrams
1 parent f545502 commit 69f1076

File tree

1 file changed

+96
-52
lines changed

1 file changed

+96
-52
lines changed

hibernate-core/src/main/java/org/hibernate/sql/Template.java

Lines changed: 96 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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,31 @@ 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 ( 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
289316
result.append(token);
290317
}
291318

@@ -300,6 +327,27 @@ else if ( inFromClause && ",".equals(lcToken) ) {
300327
return result.toString();
301328
}
302329

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+
303351
private static boolean isFetch(Dialect dialect, String lcToken) {
304352
return "fetch".equals( lcToken )
305353
&& dialect.getKeywords().contains( "fetch" );
@@ -320,7 +368,7 @@ else if ( LITERAL_PREFIXES.contains( lcToken ) ) {
320368
// we need to look ahead in the token stream
321369
// to find the first non-blank token
322370
return lookPastBlankTokens( sqlWhereString, symbols, tokens, 1,
323-
(nextToken) -> "'".equals(nextToken)
371+
nextToken -> "'".equals(nextToken)
324372
|| lcToken.equals("time") && "with".equals(nextToken)
325373
|| lcToken.equals("timestamp") && "with".equals(nextToken)
326374
|| lcToken.equals("time") && "zone".equals(nextToken) );
@@ -338,7 +386,7 @@ private static boolean lookPastBlankTokens(
338386
String sqlWhereString, String symbols, StringTokenizer tokens,
339387
@SuppressWarnings("SameParameterValue") int skip,
340388
Function<String, Boolean> check) {
341-
final StringTokenizer lookahead = lookahead( sqlWhereString, symbols, tokens, skip );
389+
final var lookahead = lookahead( sqlWhereString, symbols, tokens, skip );
342390
if ( lookahead.hasMoreTokens() ) {
343391
String nextToken;
344392
do {
@@ -363,8 +411,7 @@ private static boolean lookPastBlankTokens(
363411
* @return a cloned token stream
364412
*/
365413
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 );
368415
while ( lookahead.countTokens() > tokens.countTokens() + skip ) {
369416
lookahead.nextToken();
370417
}
@@ -401,21 +448,18 @@ public static List<String> collectColumnNames(String template) {
401448
}
402449

403450
private static boolean isNamedParameter(String token) {
404-
return token.startsWith( ":" );
451+
return token.charAt(0) == ':';
405452
}
406453

407-
private static boolean isFunctionOrKeyword(
454+
private static boolean isKeyword(
408455
String lcToken,
409-
String nextToken,
456+
boolean afterCurrent,
410457
Dialect dialect,
411458
TypeConfiguration typeConfiguration) {
412-
if ( "(".equals( nextToken ) ) {
413-
return true;
414-
}
415-
else if ( SOFT_KEYWORDS.contains( lcToken ) ) {
459+
if ( SOFT_KEYWORDS.contains( lcToken ) ) {
416460
// 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;
419463
}
420464
else {
421465
return KEYWORDS.contains( lcToken )
@@ -430,14 +474,14 @@ private static boolean isType(String lcToken, TypeConfiguration typeConfiguratio
430474
}
431475

432476
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;
437481
}
438482

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 ) {
441485
case "true", "false" -> true;
442486
default -> false;
443487
};

0 commit comments

Comments
 (0)