Skip to content

Commit b678081

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 a6504de commit b678081

File tree

1 file changed

+104
-53
lines changed

1 file changed

+104
-53
lines changed

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

Lines changed: 104 additions & 53 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,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

Comments
 (0)