Expand Up
@@ -93,6 +93,13 @@ public final class Template {
= Set.of("first", "next");
private static final Set<String> CURRENT_BIGRAMS
= Set.of("date", "time", "timestamp");
// Ordered-set aggregate function names we want to recognize
private static final Set<String> ORDERED_SET_AGGREGATES
= Set.of("listagg", "percentile_cont", "percentile_disc", "mode");
// Soft keywords that are only treated as keywords in the LISTAGG extension immediately
// following the argument list and up to and including GROUP
private static final Set<String> LISTAGG_EXTENSION_KEYWORDS
= Set.of("on", "overflow", "error", "truncate", "without", "count", "within", "with", "group");
private static final String PUNCTUATION = "=><!+-*/()',|&`";
Expand Down
Expand Up
@@ -173,6 +180,12 @@ public static String renderWhereStringTemplate(
int inExtractOrTrim = -1;
int inCast = -1;
int nestingLevel = 0;
// State for ordered-set aggregates / LISTAGG extension handling
boolean inOrderedSetFunction = false;
int orderedSetParenDepth = 0;
boolean afterOrderedSetArgs = false;
boolean inListaggExtension = false;
boolean lastWasListagg = false;
boolean hasMore = tokens.hasMoreTokens();
String nextToken = hasMore ? tokens.nextToken() : null;
Expand Down
Expand Up
@@ -216,8 +229,8 @@ else if ( quotedIdentifier && dialect.closeQuote()==token.charAt(0) ) {
isOpenQuote = false;
}
if ( isOpenQuote
&& !inFromClause // don't want to append alias to tokens inside the FROM clause
&& !endsWithDot( previousToken ) ) {
&& !inFromClause // don't want to append alias to tokens inside the FROM clause
&& !endsWithDot( previousToken ) ) {
result.append( alias ).append( '.' );
}
}
Expand Down
Expand Up
@@ -246,6 +259,9 @@ else if ( afterFromTable ) {
processedToken = token;
}
else if ( "(".equals(lcToken) ) {
if ( inOrderedSetFunction ) {
orderedSetParenDepth++;
}
nestingLevel ++;
processedToken = token;
}
Expand All
@@ -258,6 +274,14 @@ else if ( ")".equals(lcToken) ) {
inCast = -1;
afterCastAs = false;
}
if ( inOrderedSetFunction ) {
orderedSetParenDepth--;
if ( orderedSetParenDepth == 0 ) {
inOrderedSetFunction = false;
afterOrderedSetArgs = true;
inListaggExtension = lastWasListagg;
}
}
processedToken = token;
}
else if ( ",".equals(lcToken) ) {
Expand Down
Expand Up
@@ -310,11 +334,31 @@ else if ( isFunctionCall( nextToken, sql, symbols, tokens ) ) {
if ( "cast".equals( lcToken ) ) {
inCast = nestingLevel;
}
if ( ORDERED_SET_AGGREGATES.contains( lcToken ) ) {
inOrderedSetFunction = true;
orderedSetParenDepth = 0;
lastWasListagg = "listagg".equals( lcToken );
}
processedToken = token;
}
else if ( afterOrderedSetArgs && (inListaggExtension
? ( LISTAGG_EXTENSION_KEYWORDS.contains( lcToken ) )
: "within".equals( lcToken )) ) {
if ( "group".equals( lcToken ) ) {
// end special handling after GROUP (inclusive)
afterOrderedSetArgs = false;
inListaggExtension = false;
}
processedToken = token;
}
else if ( isAliasableIdentifier( token, lcToken, nextToken,
sql, symbols, tokens, wasAfterCurrent,
dialect, typeConfiguration ) ) {
// Any aliasable identifier here cannot be one of the soft keywords allowed in the
// ordered-set/LISTAGG post-args region. We've left that region so must end special handling.
// (It's irrelevant at this point whether the dialect supports ordered-set/LISTAGG.)
afterOrderedSetArgs = false;
inListaggExtension = false;
processedToken = alias + '.' + dialect.quote(token);
}
else {
Expand All
@@ -325,8 +369,8 @@ else if ( isAliasableIdentifier( token, lcToken, nextToken,
//Yuck:
if ( inFromClause
&& KEYWORDS.contains( lcToken ) // "as" is not in KEYWORDS
&& !BEFORE_TABLE_KEYWORDS.contains( lcToken ) ) {
&& KEYWORDS.contains( lcToken ) // "as" is not in KEYWORDS
&& !BEFORE_TABLE_KEYWORDS.contains( lcToken ) ) {
inFromClause = false;
}
}
Expand All
@@ -340,8 +384,8 @@ private static boolean isAliasableIdentifier(
boolean wasAfterCurrent,
Dialect dialect, TypeConfiguration typeConfiguration) {
return isUnqualifiedIdentifier( token )
&& !isKeyword( lcToken, wasAfterCurrent, dialect, typeConfiguration )
&& !isLiteral( lcToken, nextToken, sql, symbols, tokens );
&& !isKeyword( lcToken, wasAfterCurrent, dialect, typeConfiguration )
&& !isLiteral( lcToken, nextToken, sql, symbols, tokens );
}
private static boolean isFunctionCall(
Expand All
@@ -361,13 +405,13 @@ private static boolean isCurrent(
String lcToken, String nextToken,
String sql, String symbols, StringTokenizer tokens) {
return "current".equals( lcToken )
&& nextToken.isBlank()
&& lookPastBlankTokens( sql, symbols, tokens, 1, CURRENT_BIGRAMS::contains );
&& nextToken.isBlank()
&& lookPastBlankTokens( sql, symbols, tokens, 1, CURRENT_BIGRAMS::contains );
}
private static boolean isFetch(Dialect dialect, String lcToken) {
return "fetch".equals( lcToken )
&& dialect.getKeywords().contains( "fetch" );
&& dialect.getKeywords().contains( "fetch" );
}
private static boolean endsWithDot(String token) {
Expand All
@@ -386,9 +430,9 @@ else if ( LITERAL_PREFIXES.contains( lcToken ) ) {
// to find the first non-blank token
return lookPastBlankTokens( sqlWhereString, symbols, tokens, 1,
nextToken -> "'".equals(nextToken)
|| lcToken.equals("time") && "with".equals(nextToken)
|| lcToken.equals("timestamp") && "with".equals(nextToken)
|| lcToken.equals("time") && "zone".equals(nextToken) );
|| lcToken.equals("time") && "with".equals(nextToken)
|| lcToken.equals("timestamp") && "with".equals(nextToken)
|| lcToken.equals("time") && "zone".equals(nextToken) );
}
else {
return "'".equals(next);
Expand Down
Expand Up
@@ -480,9 +524,9 @@ private static boolean isKeyword(
}
else {
return KEYWORDS.contains( lcToken )
|| isType( lcToken, typeConfiguration )
|| dialect.getKeywords().contains( lcToken )
|| FUNCTION_KEYWORDS.contains( lcToken );
|| isType( lcToken, typeConfiguration )
|| dialect.getKeywords().contains( lcToken )
|| FUNCTION_KEYWORDS.contains( lcToken );
}
}
Expand All
@@ -493,8 +537,8 @@ private static boolean isType(String lcToken, TypeConfiguration typeConfiguratio
private static boolean isUnqualifiedIdentifier(String token) {
final char initialChar = token.charAt( 0 );
return initialChar == '`' // allow any identifier quoted with backtick
|| isLetter( initialChar ) // only recognizes identifiers beginning with a letter
&& token.indexOf( '.' ) < 0; // don't qualify already-qualified identifiers
|| isLetter( initialChar ) // only recognizes identifiers beginning with a letter
&& token.indexOf( '.' ) < 0; // don't qualify already-qualified identifiers
}
private static boolean isBoolean(String lcToken) {
Expand Down