diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractParseContext.java b/jOOQ/src/main/java/org/jooq/impl/AbstractParseContext.java index 83ef9ba8b2..e75bd23a91 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractParseContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractParseContext.java @@ -227,6 +227,10 @@ abstract class AbstractParseContext extends AbstractScope { abstract int afterWhitespace(int p); + final char lower(char c) { + return c >= 'A' && c <= 'Z' ? (char) (c + ('a' - 'A')) : c; + } + final char upper(char c) { return c >= 'a' && c <= 'z' ? (char) (c - ('a' - 'A')) : c; } @@ -340,6 +344,10 @@ abstract class AbstractParseContext extends AbstractScope { return character() == c; } + final boolean peek(char c, int p) { + return character(p) == c; + } + public final boolean peek(String string) { return peek(string, position()); } diff --git a/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java b/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java index 016b0f8ddb..c2ac72d064 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java @@ -55,6 +55,7 @@ import static org.jooq.JoinType.JOIN; // ... // ... import static org.jooq.SQLDialect.DERBY; +import static org.jooq.SQLDialect.FIREBIRD; // ... import static org.jooq.SQLDialect.HSQLDB; // ... @@ -515,6 +516,7 @@ import static org.jooq.impl.Tools.asInt; import static org.jooq.impl.Tools.deleteQueryImpl; import static org.jooq.impl.Tools.map; import static org.jooq.impl.Tools.normaliseNameCase; +import static org.jooq.impl.Tools.parseNameCase; import static org.jooq.impl.Tools.selectQueryImpl; import static org.jooq.impl.Tools.updateQueryImpl; import static org.jooq.impl.Tools.BooleanDataKey.DATA_PARSE_ON_CONFLICT; @@ -755,6 +757,7 @@ import org.jooq.XMLAttributes; import org.jooq.XMLTableColumnPathStep; import org.jooq.XMLTableColumnsStep; import org.jooq.XMLTablePassingStep; +import org.jooq.conf.ParseNameCase; import org.jooq.conf.ParseSearchSchema; import org.jooq.conf.ParseUnknownFunctions; import org.jooq.conf.ParseUnsupportedSyntax; @@ -927,7 +930,7 @@ final class DefaultParseContext extends AbstractParseContext implements ParseCon static final Set SUPPORTS_HASH_COMMENT_SYNTAX = SQLDialect.supportedBy(MARIADB, MYSQL); - static final Set NO_SUPPORT_QUOTED_BUILT_IN_FUNCION_NAMES = SQLDialect.supportedBy(DERBY, HSQLDB); + static final Set NO_SUPPORT_QUOTED_BUILT_IN_FUNCION_NAMES = SQLDialect.supportedBy(DERBY, FIREBIRD, HSQLDB); final Queries parse() { return wrap(() -> { @@ -8982,7 +8985,22 @@ final class DefaultParseContext extends AbstractParseContext implements ParseCon - switch (characterUpper()) { + char u = characterUpper(); + + + + + + // [#18480] Allow for quoted built-in function identifiers + switch (u) { + case '`': + case '[': + case '"': + u = characterNextUpper(); + break; + } + + switch (u) { // [#8821] Known prefixes so far: case ':': @@ -9419,7 +9437,7 @@ final class DefaultParseContext extends AbstractParseContext implements ParseCon case 'L': if (parseFunctionNameIf("LOWER", "LCASE")) - return lower((Field) parseFieldParenthesised()); + return DSL.lower((Field) parseFieldParenthesised()); else if (parseFunctionNameIf("LPAD", "leftPad")) return parseFunctionArgs3(DSL::lpad, DSL::lpad); else if (parseFunctionNameIf("LTRIM")) @@ -13603,6 +13621,14 @@ final class DefaultParseContext extends AbstractParseContext implements ParseCon : 0; } + private final char peekQuote(boolean allowAposQuotes, int p) { + return peek('"', p) ? '"' + : peek('`', p) ? '`' + : peek('[', p) ? ']' + : allowAposQuotes && peek('\'', p) ? '\'' + : 0; + } + private final DataType parseCastDataType() { char character = characterUpper(); @@ -15287,8 +15313,41 @@ final class DefaultParseContext extends AbstractParseContext implements ParseCon int skip = afterWhitespace(p, peekIntoParens) - p; + // [#18480] Function names are allowed to be quoted + char quoteEnd = requireFunction ? peekQuote(false, p + skip) : 0; + boolean quoted = quoteEnd != 0; + ParseNameCase nameCase = null; + + if (quoted) { + if (NO_SUPPORT_QUOTED_BUILT_IN_FUNCION_NAMES.contains(parseDialect())) + return false; + + nameCase = parseNameCase(configuration); + switch (parseDialect()) { + + + + + + + default: + caseSensitive = + nameCase != ParseNameCase.AS_IS + && nameCase != ParseNameCase.LOWER; + break; + } + + skip++; + } + for (int i = 0; i < length; i++) { char c = keyword.charAt(i); + + if (caseSensitive && ( + nameCase == ParseNameCase.LOWER_IF_UNQUOTED || + nameCase == ParseNameCase.LOWER)) + c = lower(c); + int pos = p + i + skip; switch (c) { @@ -15313,6 +15372,15 @@ final class DefaultParseContext extends AbstractParseContext implements ParseCon int pos = p + length + skip; + if (quoted) { + if (character(pos) == quoteEnd) { + pos++; + skip++; + } + else + return false; + } + // [#8806] A keyword that is followed by a period is very likely an identifier if (isIdentifierPart(pos) || character(pos) == '.') return false; diff --git a/jOOQ/src/main/java/org/jooq/impl/Tools.java b/jOOQ/src/main/java/org/jooq/impl/Tools.java index 2f1a514d02..80bc37b655 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Tools.java +++ b/jOOQ/src/main/java/org/jooq/impl/Tools.java @@ -7212,7 +7212,7 @@ final class Tools { * Get the {@link ParseNameCase}, looking up the default value from the * parse dialect. */ - private static final ParseNameCase parseNameCase(Configuration configuration) { + static final ParseNameCase parseNameCase(Configuration configuration) { ParseNameCase result = defaultIfNull(configuration.settings().getParseNameCase(), ParseNameCase.DEFAULT); if (result == ParseNameCase.DEFAULT) {