diff --git a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4/ClickHouseParser.g4 b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4/ClickHouseParser.g4 index a45ac2fe8..2a422e3b9 100644 --- a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4/ClickHouseParser.g4 +++ b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4/ClickHouseParser.g4 @@ -1217,8 +1217,10 @@ interval ; keyword - // except NULL_SQL, INF, NAN_SQL - : AFTER + : ACCESS + | ADD + | ADMIN + | AFTER | ALIAS | ALL | ALTER @@ -1232,53 +1234,83 @@ keyword | AST | ASYNC | ATTACH + | AVG + | AZURE + | BACKUP + | BCRYPT_HASH + | BCRYPT_PASSWORD | BETWEEN | BOTH | BY + | CACHE + | CACHES | CASE | CAST + | CHANGEABLE_IN_READONLY + | CHANGED | CHECK + | CLEANUP | CLEAR | CLUSTER + | CLUSTERS + | CN | CODEC | COLLATE + | COLLECTION | COLUMN + | COLUMNS | COMMENT + | CONNECTIONS + | CONST | CONSTRAINT | CREATE | CROSS | CUBE | CURRENT + | CURRENT_USER | DATABASE | DATABASES | DATE + | DAY | DEDUPLICATE | DEFAULT + | DEFINER | DELAY | DELETE - | DESCRIBE | DESC | DESCENDING + | DESCRIBE | DETACH | DICTIONARIES | DICTIONARY | DISK | DISTINCT | DISTRIBUTED + | DOUBLE_SHA1_HASH + | DOUBLE_SHA1_PASSWORD | DROP | ELSE + | ENABLED | END | ENGINE + | ENGINES + | ESTIMATE | EVENTS + | EXCEPT + | EXCHANGE | EXISTS | EXPLAIN | EXPRESSION + | EXTENDED | EXTRACT + | FETCH | FETCHES + | FILE + | FILESYSTEM + | FILTER | FINAL | FIRST | FLUSH - | FOR | FOLLOWING | FOR | FORMAT @@ -1286,17 +1318,29 @@ keyword | FROM | FULL | FUNCTION + | FUNCTIONS | GLOBAL + | GRANT + | GRANTEES + | GRANTS | GRANULARITY | GROUP - | GRANT | HAVING + | HDFS | HIERARCHICAL + | HOST + | HOUR + | HTTP | ID + | IDENTIFIED | IF | ILIKE + | IMPLICIT | IN | INDEX + | INDEXES + | INDICES + | INHERIT | INJECTIVE | INNER | INSERT @@ -1308,147 +1352,430 @@ keyword | JOIN | JSON_FALSE | JSON_TRUE + | KERBEROS | KEY + | KEYED + | KEYS | KILL | LAST | LAYOUT + | LDAP | LEADING | LEFT | LIFETIME + | LIGHTWEIGHT | LIKE | LIMIT + | LIMITS | LIVE | LOCAL - | LOGS | LOG + | LOGS | MATERIALIZE | MATERIALIZED | MAX | MERGES + | METRICS | MIN + | MINUTE | MODIFY + | MONTH | MOVE | MUTATION + | NAME + | NAMED | NO + | NONE + | NO_PASSWORD | NOT - | NULLS | NULL_SQL - | NAME + | NULLS | OFFSET | ON + | ONLY | OPTIMIZE + | OPTION | OR | ORDER | OUTER | OUTFILE | OVER + | OVERRIDE + | PART | PARTITION + | PARTS + | PERMISSIVE + | PIPELINE + | PLAINTEXT_PASSWORD + | PLAN + | POLICY | POPULATE | PRECEDING | PREWHERE | PRIMARY + | PROCESSLIST | PROFILE + | PROFILES + | PROJECTION + | PULL + | QUARTER + | QUERIES + | QUERY + | QUOTA + | RANDOMIZED | RANGE + | READONLY + | REALM + | REFRESH + | REGEXP | RELOAD - | REMOVE | REMOTE + | REMOVE | RENAME | REPLACE | REPLICA | REPLICATED + | RESOURCE + | RESTORE + | RESTRICTIVE + | REVOKE | RIGHT + | ROLE + | ROLES | ROLLUP | ROW | ROWS - | REVOKE + | S3 | SAMPLE + | SCRAM_SHA256_HASH + | SCRAM_SHA256_PASSWORD + | SECOND + | SECURITY | SELECT | SEMI | SENDS + | SERVER | SET + | SETTING | SETTINGS + | SHA256_HASH + | SHA256_PASSWORD + | SHARD | SHOW | SOURCE + | SQL + | SSH_KEY + | SSL_CERTIFICATE | START | STATISTICS | STOP + | STRICT | SUBSTRING + | SUM | SYNC | SYNTAX | SYSTEM | TABLE | TABLES + | TAG | TEMPORARY | TEST | THEN + | THREAD | TIES | TIMEOUT | TIMESTAMP + | TO + | TOP | TOTALS + | TRACKING | TRAILING + | TRANSACTION + | TREE | TRIM | TRUNCATE - | TRACKING - | TO - | TOP | TTL | TYPE | UNBOUNDED + | UNDROP + | UNFREEZE | UNION + | UNTIL | UPDATE + | URL | USE - | USING | USER | USERS + | USING | UUID - | URL + | VALID | VALUES | VIEW | VOLUME | WATCH + | WEEK | WHEN | WHERE | WINDOW | WITH - | QUERIES - | SUM - | AVG - | REFRESH - | EXPLAIN + | WORKLOAD + | WRITABLE + | YEAR + | ZKPATH ; keywordForAlias - : DATE - | FIRST - | ID - | KEY - | SOURCE + : ACCESS + | ADD + | ADMIN | AFTER + | ALIAS + | ALTER + | AND + | ASCENDING + | AST + | ASYNC + | ATTACH + | AZURE + | BACKUP + | BCRYPT_HASH + | BCRYPT_PASSWORD + | BOTH + | BY + | CACHE + | CACHES | CASE + | CAST + | CHANGEABLE_IN_READONLY + | CHANGED + | CHECK + | CLEANUP + | CLEAR | CLUSTER + | CLUSTERS + | CN + | CODEC + | COLLATE + | COLLECTION + | COLUMN + | COLUMNS + | COMMENT + | CONNECTIONS + | CONST + | CONSTRAINT + | CREATE + | CUBE | CURRENT - | INDEX - | TABLES - | TABLE - | TEST - | VIEW - | PRIMARY - | GRANT - | YEAR + | CURRENT_USER + | DATABASE + | DATABASES + | DATE | DAY - | MONTH + | DEDUPLICATE + | DEFAULT + | DEFINER + | DELAY + | DELETE + | DESC + | DESCENDING + | DESCRIBE + | DETACH + | DICTIONARIES + | DICTIONARY + | DISK + | DISTINCT + | DOUBLE_SHA1_HASH + | DOUBLE_SHA1_PASSWORD + | DROP + | ENABLED + | END + | ENGINE + | ENGINES + | ESTIMATE + | EVENTS + | EXCHANGE + | EXISTS + | EXPLAIN + | EXPRESSION + | EXTENDED + | FETCH + | FILE + | FILESYSTEM + | FILTER + | FIRST + | FOLLOWING + | FOR + | FREEZE + | FUNCTION + | FUNCTIONS + | GRANT + | GRANTEES + | GRANTS + | GRANULARITY + | HDFS + | HIERARCHICAL + | HOST | HOUR + | HTTP + | ID + | IDENTIFIED + | IF + | IMPLICIT + | IN + | INDEX + | INDEXES + | INDICES + | INHERIT + | INJECTIVE + | INSERT + | INTERVAL + | IP + | IS + | IS_OBJECT_ID + | KERBEROS + | KEY + | KEYED + | KEYS + | KILL + | LAST + | LAYOUT + | LDAP + | LEADING + | LIFETIME + | LIGHTWEIGHT + | LIMITS + | LIVE + | LOCAL + | MATERIALIZE + | MATERIALIZED + | MAX + | MERGES + | METRICS + | MIN | MINUTE - | SECOND + | MODIFY + | MONTH + | MOVE + | MUTATION + | NAME + | NAMED + | NO + | NONE + | NO_PASSWORD + | NULL_SQL + | NULLS + | OPTIMIZE + | OPTION + | OR + | OUTER + | OUTFILE + | OVER + | OVERRIDE + | PART + | PARTITION + | PARTS + | PERMISSIVE + | PIPELINE + | PLAINTEXT_PASSWORD + | PLAN + | POLICY + | POPULATE + | PRECEDING + | PRIMARY + | PROCESSLIST + | PROFILE + | PROFILES + | PROJECTION + | PULL + | QUARTER + | QUERY + | QUOTA + | RANDOMIZED + | RANGE + | READONLY + | REALM + | REFRESH + | REGEXP + | REMOVE + | RENAME + | REPLACE + | REPLICATED + | RESOURCE + | RESTORE + | RESTRICTIVE | REVOKE - | URL + | ROLE + | ROLES + | ROLLUP + | ROW + | ROWS + | S3 + | SCRAM_SHA256_HASH + | SCRAM_SHA256_PASSWORD + | SECOND + | SECURITY + | SELECT + | SERVER + | SET + | SETTING + | SHA256_HASH + | SHA256_PASSWORD + | SHARD + | SHOW + | SOURCE + | SQL + | SSH_KEY + | SSL_CERTIFICATE + | START | STATISTICS + | STRICT + | SYNC + | SYNTAX + | SYSTEM + | TABLE + | TABLES + | TAG + | TEMPORARY + | TEST + | THEN + | THREAD + | TIES + | TIMESTAMP + | TO + | TOP + | TOTALS + | TRACKING + | TRAILING + | TRANSACTION + | TREE + | TRUNCATE + | TTL + | TYPE + | UNBOUNDED + | UNDROP + | UNFREEZE + | UNTIL + | UPDATE + | URL + | USE + | USER + | VALID + | VALUES + | VIEW + | VOLUME + | WATCH + | WEEK + | WHEN + | WORKLOAD + | WRITABLE + | YEAR + | ZKPATH ; alias : IDENTIFIER | keywordForAlias - ; // |interval| can't be an alias, otherwise 'INTERVAL 1 SOMETHING' becomes ambiguous. + ; identifier : BACKTICK_ID diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/parser/javacc/ClickHouseSqlUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/parser/javacc/ClickHouseSqlUtils.java index 6faf418c4..08494889d 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/parser/javacc/ClickHouseSqlUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/parser/javacc/ClickHouseSqlUtils.java @@ -1,6 +1,80 @@ package com.clickhouse.jdbc.internal.parser.javacc; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + public final class ClickHouseSqlUtils { + public static final String KEYWORD_GROUP_ALLOWED_ALIASES = "allowed_keyword_aliases"; + + private static final Set ALLOWED_KEYWORD_ALIASES = initAllowedKeywordAliases(); + + private static Set initAllowedKeywordAliases() { + return buildKeywordSet( + "ACCESS", "ACTION", "ADD", "ADMIN", "AFTER", "ALGORITHM", "ALIAS", "ALLOWED_LATENESS", "ALTER", + "AND", "APPEND", "APPLY", "ASC", "ASCENDING", "ASSUME", "AST", "ASYNC", "ATTACH", + "AUTHENTICATION", "AUTO_INCREMENT", "AZURE", "BACKUP", "BAGEXPANSION", "BASE_BACKUP", + "BCRYPT_HASH", "BCRYPT_PASSWORD", "BEGIN", "BIDIRECTIONAL", "BOTH", "BY", "CACHE", "CACHES", + "CASCADE", "CASE", "CAST", "CHANGE", "CHANGEABLE_IN_READONLY", "CHANGED", "CHAR", "CHARACTER", + "CHECK", "CLEANUP", "CLEAR", "CLONE", "CLUSTER", "CLUSTERS", "CLUSTER_HOST_IDS", "CN", "CODEC", + "COLLATE", "COLLECTION", "COLUMN", "COLUMNS", "COMMENT", "COMMIT", "COMPRESSION", "CONNECTIONS", + "CONST", "CONSTRAINT", "COPY", "CREATE", "CUBE", "CURRENT", "CURRENT_USER", "CURRENTUSER", + "D", "DATA", "DATABASE", "DATABASES", "DATE", "DAY", "DAYS", "DD", "DDL", "DEALLOCATE", + "DEDUPLICATE", "DEFAULT", "DEFINER", "DELAY", "DELETE", "DELETED", "DEPENDS", "DESC", + "DESCENDING", "DESCRIBE", "DETACH", "DETACHED", "DICTIONARIES", "DICTIONARY", "DISK", "DISTINCT", + "DIV", "DOUBLE_SHA1_HASH", "DOUBLE_SHA1_PASSWORD", "DROP", "EMPTY", "ENABLED", "END", "ENFORCED", + "ENGINE", "ENGINES", "EPHEMERAL", "ESTIMATE", "EVENT", "EVENTS", "EVERY", "EXCHANGE", "EXECUTE", + "EXISTS", "EXPLAIN", "EXPRESSION", "EXTENDED", "EXTERNAL", "FAKE", "FALSE", "FETCH", "FIELDS", + "FILE", "FILES", "FILESYSTEM", "FILL", "FILTER", "FIRST", "FOLLOWING", "FOR", "FORCE", "FOREIGN", + "FORGET", "FREEZE", "FULLTEXT", "FUNCTION", "FUNCTIONS", "GRANT", "GRANTEES", "GRANTS", + "GRANULARITY", "GROUPING", "GROUPS", "H", "HASH", "HDFS", "HH", "HIERARCHICAL", "HOST", "HOUR", + "HOURS", "HTTP", "ID", "IDENTIFIED", "IF", "IGNORE", "IMPLICIT", "IN", "INDEX", "INDEXES", + "INDICES", "INFILE", "INHERIT", "INJECTIVE", "INSERT", "INTERPOLATE", "INTERVAL", "INVISIBLE", + "INVOKER", "IP", "IS", "IS_OBJECT_ID", "JWT", "KERBEROS", "KEY", "KEYED", "KEYS", "KILL", "KIND", + "LARGE", "LAST", "LAYOUT", "LDAP", "LEADING", "LESS", "LEVEL", "LIFETIME", "LIGHTWEIGHT", + "LIMITS", "LINEAR", "LIST", "LIVE", "LOCAL", "M", "MASK", "MASKING", "MASTER", "MATCH", + "MATERIALIZE", "MATERIALIZED", "MAX", "MCS", "MEMORY", "MERGES", "METHODS", "METRICS", "MI", + "MICROSECOND", "MICROSECONDS", "MILLISECOND", "MILLISECONDS", "MIN", "MINUTE", "MINUTES", "MM", + "MOD", "MODIFY", "MONTH", "MONTHS", "MOVE", "MS", "MUTATION", "N", "NAME", "NAMED", "NANOSECOND", + "NANOSECONDS", "NEW", "NEXT", "NO", "NO_AUTHENTICATION", "NONE", "NO_PASSWORD", "NS", "NULL", + "NULLS", "OBJECT", "OPTIMIZE", "OPTION", "OR", "OUTER", "OUTFILE", "OVER", "OVERRIDABLE", + "OVERRIDE", "PART", "PARTIAL", "PARTITION", "PARTITIONS", "PART_MOVE_TO_SHARD", "PARTS", + "PATCHES", "PAUSE", "PERIODIC", "PERMANENTLY", "PERMISSIVE", "PERSISTENT", "PIPELINE", "PLAN", + "PLAINTEXT_PASSWORD", "POLICY", "POPULATE", "PRECEDING", "PRECISION", "PREFIX", "PREPARE", + "PRIMARY", "PRIORITY", "PRIVILEGES", "PROCESSLIST", "PROFILE", "PROFILES", "PROJECTION", + "PROTOBUF", "PULL", "Q", "QQ", "QUARTER", "QUARTERS", "QUERY", "QUOTA", "RANDOMIZE", + "RANDOMIZED", "RANGE", "READ", "READONLY", "REALM", "RECOMPRESS", "RECURSIVE", "REFERENCES", + "REFRESH", "REGEXP", "REMOVE", "RENAME", "REPLACE", "REPLICATED", "RESET", "RESOURCE", "RESPECT", + "RESTORE", "RESTRICT", "RESTRICTIVE", "RESUME", "REVOKE", "REWRITE", "ROLE", "ROLES", "ROLLBACK", + "ROLLUP", "ROW", "ROWS", "S", "S3", "SALT", "SAN", "SCHEME", "SCRAM_SHA256_HASH", + "SCRAM_SHA256_PASSWORD", "SECOND", "SECONDS", "SECURITY", "SELECT", "SEQUENTIAL", "SERVER", + "SET", "SETS", "SETTING", "SHA256_HASH", "SHA256_PASSWORD", "SHARD", "SHOW", "SIGNED", "SIMPLE", + "SKIP", "SNAPSHOT", "SOURCE", "SPATIAL", "SQL", "SQL_TSI_DAY", "SQL_TSI_HOUR", + "SQL_TSI_MICROSECOND", "SQL_TSI_MILLISECOND", "SQL_TSI_MINUTE", "SQL_TSI_MONTH", + "SQL_TSI_NANOSECOND", "SQL_TSI_QUARTER", "SQL_TSI_SECOND", "SQL_TSI_WEEK", "SQL_TSI_YEAR", "SS", + "SSH_KEY", "SSL_CERTIFICATE", "STALENESS", "START", "STATISTIC", "STATISTICS", "STDOUT", "STEP", + "STORAGE", "STRICT", "STRICTLY_ASCENDING", "SUBPARTITION", "SUBPARTITIONS", "SUSPEND", "SYNC", + "SYNTAX", "SYSTEM", "TABLE", "TABLES", "TAG", "TAGS", "TEMPORARY", "TEST", "THAN", "THEN", + "THREAD", "TIES", "TIME", "TIMESTAMP", "TO", "TOP", "TOTALS", "TRACKING", "TRAILING", + "TRANSACTION", "TREE", "TRIGGER", "TRUE", "TRUNCATE", "TTL", "TYPE", "TYPEOF", "UNBOUNDED", + "UNDROP", "UNFREEZE", "UNIQUE", "UNLOCK", "UNSET", "UNSIGNED", "UNTIL", "UPDATE", "URL", "USE", + "USER", "VALID", "VALUES", "VARYING", "VIEW", "VISIBLE", "VOLUME", "WATCH", "WATERMARK", "WEEK", + "WEEKS", "WHEN", "WITH_ITEMINDEX", "WK", "WORKER", "WORKLOAD", "WRITABLE", "WRITE", "WW", + "YEAR", "YEARS", "YY", "YYYY", "ZKPATH"); + } + + private static Set buildKeywordSet(String... values) { + LinkedHashSet keywords = new LinkedHashSet<>(); + if (values != null) { + Collections.addAll(keywords, values); + } + return Collections.unmodifiableSet(keywords); + } + + public static Set getKeywordGroup(String groupName) { + return KEYWORD_GROUP_ALLOWED_ALIASES.equals(groupName) ? ALLOWED_KEYWORD_ALIASES : Collections.emptySet(); + } + public static boolean isQuote(char ch) { return ch == '"' || ch == '\'' || ch == '`'; } diff --git a/jdbc-v2/src/main/javacc/ClickHouseSqlParser.jj b/jdbc-v2/src/main/javacc/ClickHouseSqlParser.jj index e6922223a..f076e1185 100644 --- a/jdbc-v2/src/main/javacc/ClickHouseSqlParser.jj +++ b/jdbc-v2/src/main/javacc/ClickHouseSqlParser.jj @@ -49,6 +49,13 @@ public class ClickHouseSqlParser { private static final Logger log = LoggerFactory.getLogger(ClickHouseSqlParser.class); + private static final Set ALLOWED_ALIAS_KEYWORDS = + ClickHouseSqlUtils.getKeywordGroup(ClickHouseSqlUtils.KEYWORD_GROUP_ALLOWED_ALIASES); + + private static boolean isAllowedAlias(Token t) { + return t != null && ALLOWED_ALIAS_KEYWORDS.contains(t.image.toUpperCase(Locale.ROOT)); + } + private final List statements = new ArrayList<>(); private ParseHandler handler; @@ -520,7 +527,12 @@ void deleteStmt(): {} { // https://clickhouse.tech/docs/en/sql-reference/statements/describe-table/ void describeStmt(): {} { ( | ) { token_source.table = "columns"; } - (LOOKAHEAD({ getToken(1).kind == TABLE }) )? tableIdentifier(true) (anyExprList())? + (LOOKAHEAD({ getToken(1).kind == TABLE })
)? + ( + LOOKAHEAD({ getToken(1).kind == LPAREN }) anyExprList() + | tableIdentifier(true) + ) + (anyExprList())? } // https://clickhouse.tech/docs/en/sql-reference/statements/detach/ @@ -575,8 +587,13 @@ void grantStmt(): {} { // not interested void insertStmt(): {} { ( - LOOKAHEAD({ getToken(1).kind == FUNCTION }) functionExpr() - | (LOOKAHEAD(2)
)? tableIdentifier(true) + LOOKAHEAD({ getToken(1).kind == FUNCTION + && !tokenIn(2, VALUES, FORMAT, SETTINGS, SELECT, WITH, INFILE) + && getToken(3).kind == LPAREN }) functionExpr() + | ( + LOOKAHEAD({ getToken(1).kind == TABLE + && !tokenIn(2, VALUES, FORMAT, SETTINGS, SELECT, WITH, LPAREN) })
+ )? tableIdentifier(true) ) ( LOOKAHEAD(2) infilePart() @@ -678,6 +695,7 @@ void showStmt(): {} { ( databaseIdentifier(true)) | LOOKAHEAD(2) (LOOKAHEAD(1) )? (LOOKAHEAD(1) )? anyIdentifier() | LOOKAHEAD(2) ( tableIdentifier(true)) + | LOOKAHEAD(2) (LOOKAHEAD(1) )? { token_source.table = "settings"; } | LOOKAHEAD(2) ((LOOKAHEAD(2) )? (LOOKAHEAD(2)
)? tableIdentifier(true)) ) ) @@ -725,12 +743,12 @@ void columnExprList(): {} { void withExpr(): {} { nestedExpr() + (LOOKAHEAD(2) anyExprList() )* ( ( LOOKAHEAD({ getToken(1).kind == FLOATING_LITERAL }) | )+ - | (LOOKAHEAD(2) anyExprList() )+ | LOOKAHEAD(2) ()? | LOOKAHEAD(2) ()? betweenExpr() | LOOKAHEAD(2) ()? ( | ) nestedExpr() @@ -747,12 +765,12 @@ void columnsExpr(): {} { LOOKAHEAD(2) ( | | ) anyExprList() )* | nestedExpr() + (LOOKAHEAD(2) anyExprList() )* ( ( LOOKAHEAD({ getToken(1).kind == FLOATING_LITERAL }) | )+ - | (LOOKAHEAD(2) anyExprList() )+ | LOOKAHEAD(2) ()? | LOOKAHEAD(2) ()? betweenExpr() | LOOKAHEAD(2) ()? ( | ) nestedExpr() @@ -773,11 +791,11 @@ void nestedExpr(): {} { ( nestedExpr() nestedExpr())+ ( nestedExpr())? | LOOKAHEAD(2) (LOOKAHEAD(2) | nestedExpr() interval()) | columnExpr() + (LOOKAHEAD(2) anyExprList() )* ( ( | )+ - | (LOOKAHEAD(2) anyExprList() )+ | LOOKAHEAD(2) ()? | LOOKAHEAD(2) ()? betweenExpr() | LOOKAHEAD(2) ()? ( | ) nestedExpr() @@ -843,7 +861,7 @@ void outfilePart(): {} { } void settingsPart(): {} { - { token_source.addPosition(token); } settingExprList() + (LOOKAHEAD(1) )? { token_source.addPosition(token); } (LOOKAHEAD(2) settingExprList())? } void withTotalPart(): {} { @@ -888,13 +906,25 @@ void anyColumnExpr(): {} { | nestedIdentifier() } +Token aliasIdentifier(): { Token t; } { + ( + t = + | t = + | t = + | t = variable() + | LOOKAHEAD({ isAllowedAlias(getToken(1)) }) + t = anyKeyword() + ) + { return t; } +} + Token aliasExpr(): { Token t = null; } { ( - LOOKAHEAD(2) t = anyIdentifier() + LOOKAHEAD(2) t = aliasIdentifier() | LOOKAHEAD(2) formatPart() | LOOKAHEAD(2) settingsPart() | LOOKAHEAD(2) outfilePart() - | t = identifier() + | t = aliasIdentifier() ) { return t; } } @@ -950,10 +980,12 @@ void settingExprList(): {} { } void settingExpr(): { String key; } { - identifier() { key = token.image; } literal() { token_source.addSetting(key, token.image); } + anyIdentifier() { key = token.image; } literal() { token_source.addSetting(key, token.image); } } -// basics +// --- Base definitions + + Token anyIdentifier(): { Token t; } { ( t = @@ -965,17 +997,6 @@ Token anyIdentifier(): { Token t; } { { return t; } } -Token identifier(): { Token t; } { - ( - t = - | t = - | t = variable() - | t = - | t = keyword() - ) - { return t; } -} - void interval(): {} { | | | | | | | } @@ -1045,15 +1066,15 @@ Token anyKeyword(): { Token t; } { | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t =
| t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = | t = + | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = | t = // interval | t = | t = | t = | t = | t = | t = | t = | t = @@ -1063,32 +1084,11 @@ Token anyKeyword(): { Token t; } { { return t; } } -Token keyword(): { Token t; } { - ( - // leading keywords(except with) - t = | t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t =
| t = | t = | t = | t = | t = | t = - | t = | t = | t = | t = | t = - // interval - | t = | t = | t = | t = | t = | t = | t = | t = - // values - | t = | t = | t = - ) - { return t; } -} - // keywords TOKEN: { > | > + | > | > | > | > diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementSQLTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementSQLTest.java index bbb86ff7c..b6a464393 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementSQLTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementSQLTest.java @@ -5,11 +5,20 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import com.clickhouse.jdbc.internal.parser.javacc.ClickHouseSqlUtils; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -47,4 +56,113 @@ public static Object[][] testSQLStatementsDP() throws Exception { return loadTestData("datasets.yaml", "StatementSQLTests.yaml"); } + /** + * Test which SQL keywords can be used as table names (unquoted). + * This test reads keywords from sql-keywords.txt and attempts to create + * a table with each keyword as the name, then select from it. + */ + @Test(groups = {"integration"}, enabled = false) + public void testKeywordsAsTableNames() throws Exception { + List keywords = loadKeywordsFromResource("sql-keywords.txt"); + List allowedKeywords = new ArrayList<>(); + List disallowedKeywords = new ArrayList<>(); + + try (Connection connection = getJdbcConnection()) { + try (Statement stmt = connection.createStatement()) { + for (String keyword : keywords) { + String tableName = keyword; // Use keyword directly without quoting + String createSql = "CREATE TABLE IF NOT EXISTS " + tableName + " (id Int32) ENGINE = Memory"; + String selectSql = "SELECT * FROM " + tableName; + String dropSql = "DROP TABLE IF EXISTS " + tableName; + + try { + stmt.execute(createSql); + try (ResultSet rs = stmt.executeQuery(selectSql)) { + // Just consume the result set + while (rs.next()) { + // no-op + } + } + stmt.execute(dropSql); + allowedKeywords.add(keyword); + } catch (Exception e) { + disallowedKeywords.add(keyword); + // Try to drop in case table was created but select failed + try { + stmt.execute(dropSql); + } catch (Exception ignored) { + // Ignore cleanup errors + } + } + } + } + } + + System.out.println("=== Keywords ALLOWED as table names (" + allowedKeywords.size() + ") ==="); + for (String kw : allowedKeywords) { + System.out.println(kw); + } + + System.out.println("\n=== Keywords NOT ALLOWED as table names (" + disallowedKeywords.size() + ") ==="); + for (String kw : disallowedKeywords) { + System.out.println(kw); + } + + // The test passes regardless - we're just collecting information + // If you want to assert something specific, add it here + Assert.assertTrue(allowedKeywords.size() + disallowedKeywords.size() == keywords.size(), + "All keywords should be categorized"); + } + + @Test(groups = {"integration"}) + public void testAllowedKeywordAliasesMatchSystemKeywords() throws Exception { + Set reservedKeywords = new TreeSet<>(loadKeywordsFromResource("reserved_keywords.txt")); + + Set systemKeywords = new TreeSet<>(); + try (Connection connection = getJdbcConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT keyword FROM system.keywords")) { + while (rs.next()) { + for (String word : rs.getString(1).toUpperCase().split("\\s+")) { + systemKeywords.add(word); + } + } + } + + Assert.assertFalse(systemKeywords.isEmpty(), "system.keywords should not be empty"); + + Set nonReservedSystemKeywords = new TreeSet<>(systemKeywords); + nonReservedSystemKeywords.removeAll(reservedKeywords); + + Set allowedAliases = ClickHouseSqlUtils.getKeywordGroup( + ClickHouseSqlUtils.KEYWORD_GROUP_ALLOWED_ALIASES); + + Set missingFromAllowed = new TreeSet<>(nonReservedSystemKeywords); + missingFromAllowed.removeAll(allowedAliases); + + Assert.assertTrue(missingFromAllowed.isEmpty(), + "New keywords found in system.keywords (non-reserved) that must be added to " + + "ALLOWED_KEYWORD_ALIASES or reserved_keywords.txt: " + missingFromAllowed); + } + + private List loadKeywordsFromResource(String resourceName) throws Exception { + List keywords = new ArrayList<>(); + try (InputStream is = getClass().getClassLoader().getResourceAsStream(resourceName)) { + if (is == null) { + throw new RuntimeException("Resource not found: " + resourceName); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + // Skip empty lines and comments + if (!line.isEmpty() && !line.startsWith("#")) { + keywords.add(line); + } + } + } + } + return keywords; + } + } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/BaseSqlParserFacadeTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/BaseSqlParserFacadeTest.java index b97c2f3e3..58cfe2327 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/BaseSqlParserFacadeTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/BaseSqlParserFacadeTest.java @@ -1,10 +1,19 @@ package com.clickhouse.jdbc.internal; +import com.clickhouse.jdbc.internal.parser.javacc.ClickHouseSqlUtils; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; @@ -328,121 +337,122 @@ public void testMiscStatements(String sql, int args) { @DataProvider public Object[][] testMiscStmtDp() { - return new Object[][] { - {"SELECT x, a FROM (SELECT arrayJoin(['Hello', 'Goodbye']) AS x, [1, 2, 3] AS arr) ARRAY JOIN arr AS a", 0}, - {"SELECT quantilesTiming(0.1, 0.5, 0.9)(dummy) FROM remote('127.0.0.{2,3}', 'system', 'one') GROUP BY 1 WITH TOTALS", 0}, // FROM remote issue - {"SELECT StartDate, sumMerge(Visits) AS Visits, uniqMerge(Users) AS Users FROM basic_00040 GROUP BY StartDate ORDER BY StartDate", 0}, // keywords - {"SELECT uniq(URL) FROM test.hits WHERE TraficSourceID IN (7)", 0}, // keywords URL - {"SELECT INTERVAL '1 day'", 0}, - {"SELECT INTERVAL 1 day", 0}, - {"SET extremes = 1", 0}, - {"CREATE TABLE check_query_log (N UInt32,S String) Engine = Log", 0 }, - {"CREATE TABLE log (x UInt8) ENGINE = StripeLog", 0}, - {"CREATE TABLE check_query_log (N UInt32,S String) Engine = MergeTree", 0 }, - {"CREATE TABLE check_query_log (N UInt32,S String) Engine = ReplacingMergeTree", 0 }, - {"select abs(log(e()) - 1) < 1e-8", 0}, - {"SELECT SearchEngineID, ClientIP, count() AS c, sum(Refresh), avg(ResolutionWidth) " + + return new Object[][]{ + {"SELECT x, a FROM (SELECT arrayJoin(['Hello', 'Goodbye']) AS x, [1, 2, 3] AS arr) ARRAY JOIN arr AS a", 0}, + {"SELECT quantilesTiming(0.1, 0.5, 0.9)(dummy) FROM remote('127.0.0.{2,3}', 'system', 'one') GROUP BY 1 WITH TOTALS", 0}, // FROM remote issue + {"SELECT StartDate, sumMerge(Visits) AS Visits, uniqMerge(Users) AS Users FROM basic_00040 GROUP BY StartDate ORDER BY StartDate", 0}, // keywords + {"SELECT uniq(URL) FROM test.hits WHERE TraficSourceID IN (7)", 0}, // keywords URL + {"SELECT INTERVAL '1 day'", 0}, + {"SELECT INTERVAL 1 day", 0}, + {"SET extremes = 1", 0}, + {"CREATE TABLE check_query_log (N UInt32,S String) Engine = Log", 0}, + {"CREATE TABLE log (x UInt8) ENGINE = StripeLog", 0}, + {"CREATE TABLE check_query_log (N UInt32,S String) Engine = MergeTree", 0}, + {"CREATE TABLE check_query_log (N UInt32,S String) Engine = ReplacingMergeTree", 0}, + {"select abs(log(e()) - 1) < 1e-8", 0}, + {"SELECT SearchEngineID, ClientIP, count() AS c, sum(Refresh), avg(ResolutionWidth) " + " FROM test.hits_s3 WHERE SearchPhrase != '' GROUP BY SearchEngineID, ClientIP " + " ORDER BY c DESC LIMIT 10", 0}, - {"SELECT (id % 10) AS key, count() FROM 03279_test_database.test_table_1 GROUP BY key ORDER BY key", 0}, - {"SELECT ?", 1}, - {"(SELECT ?)", 1}, - {"SELECT * FROM table key WHERE ts = ?", 1}, - {"SELECT * FROM table source WHERE ts = ?", 1}, - {"SELECT * FROM table after WHERE ts = ?", 1}, - {"SELECT * FROM table before WHERE ts = ?", 1}, - {"SELECT * FROM table case WHERE ts = ?", 1}, - {"SELECT * FROM table cluster WHERE ts = ?", 1}, - {"SELECT * FROM table current WHERE ts = ?", 1}, - {"SELECT * FROM table index WHERE ts = ?", 1}, - {"SELECT * FROM table tables WHERE ts = ?", 1}, - {"SELECT * FROM table test WHERE ts = ?", 1}, - {"SELECT * FROM table view WHERE ts = ?", 1}, - {"SELECT * FROM table primary WHERE ts = ?", 1}, - {"insert into events (s) values ('a')", 0}, - {"insert into `events` (s) values ('a')", 0}, - {"SELECT COUNT(*) > 0 FROM system.databases WHERE name = ?", 1}, - {"SELECT count(*) > 0 FROM system.databases WHERE c1 = ?", 1}, - {"SELECT COUNT() FROM system.databases WHERE name = ?", 1}, - {"alter table user delete where reg_time = ?", 1}, - {"SELECT * FROM a,b WHERE id > ?", 1}, - {"select ip from myusers where tenant=?", 1}, - {"SELECT myColumn FROM myTable WHERE myColumn in (?, ?, ?)", 3}, - {"DROP USER IF EXISTS default_impersonation_user", 0}, - {"DROP ROLE IF EXISTS `vkonfwxapllzkkgkqdvt`", 0}, - {"CREATE ROLE `kjxrsscptauligukwgmf` ON CLUSTER '{cluster}'", 0}, - {"GRANT SELECT ON `test_data`.`venues` TO `vkonfwxapllzkkgkqdvt`", 0}, - {"GRANT `uqkczgnpmpuktxhwvqqd` TO `default_impersonation_user`", 0}, - {"SET ROLE NONE", 0}, - {"CREATE ROLE IF NOT EXISTS row_a ON CLUSTER '{cluster}'", 0}, - {"CREATE ROW POLICY role_policy_BTABPUVDDLXZPYBCJGGZ ON `test_data`.`products` AS RESTRICTIVE FOR SELECT USING (`id` = 1) TO `annhpwyelooonsmqjldo`", 0}, - {"CREATE ROW POLICY role_policy_BTABPUVDDLXZPYBCJGGZ ON `products` AS RESTRICTIVE FOR SELECT USING (`id` = 1) TO `annhpwyelooonsmqjldo`", 0}, - {"GRANT ON CLUSTER '{cluster}' row_a, row_b, row_c TO metabase_impersonation_test_user", 0}, - {"GRANT ON CLUSTER '{cluster}' SELECT ON metabase_impersonation_test.test_1751397165968 TO metabase_impersonation_test_user", 0}, - {"CREATE ROW POLICY OR REPLACE policy_row_a ON CLUSTER '{cluster}' ON metabase_impersonation_test.test_1751397165968 FOR SELECT USING s = 'a' TO row_a", 0}, - {"CREATE ROW POLICY OR REPLACE policy_row_b ON CLUSTER '{cluster}' ON metabase_impersonation_test.test_1751397165968 FOR SELECT USING s = 'b' TO row_b", 0}, - {"CREATE ROW POLICY OR REPLACE policy_row_c ON CLUSTER '{cluster}' ON metabase_impersonation_test.test_1751397165968 FOR SELECT USING s = 'c' TO row_c", 0}, - {"GRANT SELECT ON `metabase_test_role_db`.`*` TO `metabase_test_role`,`metabase-test-role`", 0}, - {"GRANT SELECT ON `metabase_test_role_db`.* TO `metabase_test_role`,`metabase-test-role`", 0}, - {"GRANT `metabase_test_role`, `metabase-test-role` TO `metabase_test_user`", 0}, - {"GRANT ON CLUSTER '{cluster}' SELECT ON `metabase_test_role_db`.* TO `metabase_test_role`, `metabase-test-role`", 0}, - {"GRANT ON CLUSTER '{cluster}' `metabase_test_role`, `metabase-test-role` TO `metabase_test_user`", 0}, - {"SELECT * FROM `test_data`.`categories` WHERE id = 1::String or id = ?", 1}, - {"SELECT * FROM `test_data`.`categories` WHERE id = cast(1 as String) or id = ?", 1}, - {"select * from test_data.categories WHERE test_data.categories.name = ? limit 4", 1}, - {INSERT_INLINE_DATA, 0}, - {"select sum(value) from `uuid_filter_db`.`uuid_filter_table` WHERE `uuid_filter_db`.`uuid_filter_table`.`uuid` IN (CAST('36f7f85c-d7f4-49e2-af05-f45d5f6636ad' AS UUID))", 0}, - {"SELECT DISTINCT ON (column) FROM table WHERE column > ?", 1}, - {"SELECT * FROM test_table \nUNION\n DISTINCT SELECT * FROM test_table", 0}, - {"SELECT * FROM test_table \nUNION\n ALL SELECT * FROM test_table", 0}, - {"SELECT * FROM test_table1 \nUNION\n SELECT * FROM test_table2 WHERE test_table2.column1 = ?", 1}, - {COMPLEX_CTE, 4}, - {SIMPLE_CTE, 0}, - {CTE_CONSTANT_AS_VARIABLE, 1}, - {"select toYear(dt) year from test WHERE val=?", 1}, - {"select 1 year, 2 hour, 3 minute, 4 second", 0}, - {"select toYear(dt) AS year from test WHERE val=?", 1}, - {"select toYear(dt) AS yearx from test WHERE val=?", 1}, - {"SELECT v FROM t WHERE f in (?)", 1}, - {"SELECT v FROM t WHERE a > 10 AND event NOT IN (?)", 1}, - {"SELECT v FROM t WHERE f in (1, 2, 3)", 0}, - {"with ? as val1, numz as (select val1, number from system.numbers limit 10) select * from numz", 1}, - {"WITH 'hello' REGEXP 'h' AS result SELECT 1", 0}, - {"WITH (select 1) as a, z AS (select 2) SELECT 1", 0}, - {"SELECT result FROM test_view(myParam = ?)", 1}, - {"WITH toDate('2025-08-20') as DATE_END, events AS ( SELECT 1 ) SELECT * FROM events", 0}, - {"select 1 table where 1 = ?", 1}, - {"insert into t (i, t) values (1, timestamp '2010-01-01 00:00:00')", 0}, - {"insert into t (i, t) values (1, date '2010-01-01')", 0}, - {"SELECT timestamp '2010-01-01 00:00:00' as ts, date '2010-01-01' as d", 0}, - {INSERT_WITH_COMMENTS, 4}, - {" /* INSERT TESTING ?? */\n SELECT ? AS num", 1}, - {"/* SELECT ? TESTING */\n INSERT INTO test_table VALUES (?)", 1}, - {"/* INSERT ? T??ESTING */\n\n\n UPDATE test_table SET num = ?", 1}, - {"-- INSERT ? TESTING */\n SELECT ? AS num", 1}, - {" -- SELECT ? TESTING \n -- SELECT AGAIN ?\n INSERT INTO test_table VALUES (?)", 1}, - {" SELECT ? -- INSERT ? TESTING", 1}, - {"#! INSERT ? TESTING \n SELECT ? AS num", 1}, - {"#!INSERT ? TESTING \n SELECT ? AS num", 1}, - {"# INSERT ? TESTING \n SELECT ? AS num", 1}, - {"#INSERT ? TESTING \n SELECT ? AS num", 1}, - {"\nINSERT INTO TESTING \n SELECT ? AS num", 1}, - {" \n INSERT INTO TESTING \n SELECT ? AS num", 1}, - {" SELECT '##?0.1' as f, ? as a\n #this is debug \n FROM table", 1}, - {"WITH '#!?0.1' as f, ? as a\n #this is debug \n SELECT * FROM a", 1}, - {SELECT_WITH_WHERE_CLAUSE_FUNC_WITH_PARAMS, 2}, - {"SELECT arrayFilter(x -> x > 0, [0, 1, 2, -3])", 0}, - {"SELECT [0, 1, 2, -3] arr, arrayFilter(x -> x > 0, arr)", 0}, - {"SELECT arrayFill(x, y, z -> x > y AND x < z, [5, 3, 6, 2], [4, 7, 1, 3], [10, 2, 8, 5]) AS res", 0}, - {"SELECT arrayFilter(x -> x LIKE '%World%', ['Hello', 'abc World']) AS res", 0}, - {"SELECT arrayFilter(x -> not (x is null), ['Hello', 'abc World']) AS res", 0}, - {"SELECT arrayDistinct(arrayFilter(x -> not (x is null), " + - " arrayConcat(t.s.arr1, t.s.arr2)" + - " )" + - ")", 0}, - {"select count(*) filter (where 1 = 1)", 0}, - {"select countIf(*, 1 = ?)", 1}, - {"select count(*) filter (where 1 = ?)", 1} + {"SELECT (id % 10) AS key, count() FROM 03279_test_database.test_table_1 GROUP BY key ORDER BY key", 0}, + {"SELECT ?", 1}, + {"(SELECT ?)", 1}, + {"SELECT * FROM table key WHERE ts = ?", 1}, + {"SELECT * FROM table source WHERE ts = ?", 1}, + {"SELECT * FROM table after WHERE ts = ?", 1}, + {"SELECT * FROM table before WHERE ts = ?", 1}, + {"SELECT * FROM table case WHERE ts = ?", 1}, + {"SELECT * FROM table cluster WHERE ts = ?", 1}, + {"SELECT * FROM table current WHERE ts = ?", 1}, + {"SELECT * FROM table index WHERE ts = ?", 1}, + {"SELECT * FROM table tables WHERE ts = ?", 1}, + {"SELECT * FROM table test WHERE ts = ?", 1}, + {"SELECT * FROM table view WHERE ts = ?", 1}, + {"SELECT * FROM table primary WHERE ts = ?", 1}, + {"insert into events (s) values ('a')", 0}, + {"insert into `events` (s) values ('a')", 0}, + {"SELECT COUNT(*) > 0 FROM system.databases WHERE name = ?", 1}, + {"SELECT count(*) > 0 FROM system.databases WHERE c1 = ?", 1}, + {"SELECT COUNT() FROM system.databases WHERE name = ?", 1}, + {"alter table user delete where reg_time = ?", 1}, + {"SELECT * FROM a,b WHERE id > ?", 1}, + {"select ip from myusers where tenant=?", 1}, + {"SELECT myColumn FROM myTable WHERE myColumn in (?, ?, ?)", 3}, + {"DROP USER IF EXISTS default_impersonation_user", 0}, + {"DROP ROLE IF EXISTS `vkonfwxapllzkkgkqdvt`", 0}, + {"CREATE ROLE `kjxrsscptauligukwgmf` ON CLUSTER '{cluster}'", 0}, + {"GRANT SELECT ON `test_data`.`venues` TO `vkonfwxapllzkkgkqdvt`", 0}, + {"GRANT `uqkczgnpmpuktxhwvqqd` TO `default_impersonation_user`", 0}, + {"SET ROLE NONE", 0}, + {"CREATE ROLE IF NOT EXISTS row_a ON CLUSTER '{cluster}'", 0}, + {"CREATE ROW POLICY role_policy_BTABPUVDDLXZPYBCJGGZ ON `test_data`.`products` AS RESTRICTIVE FOR SELECT USING (`id` = 1) TO `annhpwyelooonsmqjldo`", 0}, + {"CREATE ROW POLICY role_policy_BTABPUVDDLXZPYBCJGGZ ON `products` AS RESTRICTIVE FOR SELECT USING (`id` = 1) TO `annhpwyelooonsmqjldo`", 0}, + {"GRANT ON CLUSTER '{cluster}' row_a, row_b, row_c TO metabase_impersonation_test_user", 0}, + {"GRANT ON CLUSTER '{cluster}' SELECT ON metabase_impersonation_test.test_1751397165968 TO metabase_impersonation_test_user", 0}, + {"CREATE ROW POLICY OR REPLACE policy_row_a ON CLUSTER '{cluster}' ON metabase_impersonation_test.test_1751397165968 FOR SELECT USING s = 'a' TO row_a", 0}, + {"CREATE ROW POLICY OR REPLACE policy_row_b ON CLUSTER '{cluster}' ON metabase_impersonation_test.test_1751397165968 FOR SELECT USING s = 'b' TO row_b", 0}, + {"CREATE ROW POLICY OR REPLACE policy_row_c ON CLUSTER '{cluster}' ON metabase_impersonation_test.test_1751397165968 FOR SELECT USING s = 'c' TO row_c", 0}, + {"GRANT SELECT ON `metabase_test_role_db`.`*` TO `metabase_test_role`,`metabase-test-role`", 0}, + {"GRANT SELECT ON `metabase_test_role_db`.* TO `metabase_test_role`,`metabase-test-role`", 0}, + {"GRANT `metabase_test_role`, `metabase-test-role` TO `metabase_test_user`", 0}, + {"GRANT ON CLUSTER '{cluster}' SELECT ON `metabase_test_role_db`.* TO `metabase_test_role`, `metabase-test-role`", 0}, + {"GRANT ON CLUSTER '{cluster}' `metabase_test_role`, `metabase-test-role` TO `metabase_test_user`", 0}, + {"SELECT * FROM `test_data`.`categories` WHERE id = 1::String or id = ?", 1}, + {"SELECT * FROM `test_data`.`categories` WHERE id = cast(1 as String) or id = ?", 1}, + {"select * from test_data.categories WHERE test_data.categories.name = ? limit 4", 1}, + {INSERT_INLINE_DATA, 0}, + {"select sum(value) from `uuid_filter_db`.`uuid_filter_table` WHERE `uuid_filter_db`.`uuid_filter_table`.`uuid` IN (CAST('36f7f85c-d7f4-49e2-af05-f45d5f6636ad' AS UUID))", 0}, + {"SELECT DISTINCT ON (column) FROM table WHERE column > ?", 1}, + {"SELECT * FROM test_table \nUNION\n DISTINCT SELECT * FROM test_table", 0}, + {"SELECT * FROM test_table \nUNION\n ALL SELECT * FROM test_table", 0}, + {"SELECT * FROM test_table1 \nUNION\n SELECT * FROM test_table2 WHERE test_table2.column1 = ?", 1}, + {COMPLEX_CTE, 4}, + {SIMPLE_CTE, 0}, + {CTE_CONSTANT_AS_VARIABLE, 1}, + {"select toYear(dt) year from test WHERE val=?", 1}, + {"select 1 year, 2 hour, 3 minute, 4 second", 0}, + {"select toYear(dt) AS year from test WHERE val=?", 1}, + {"select toYear(dt) AS yearx from test WHERE val=?", 1}, + {"SELECT v FROM t WHERE f in (?)", 1}, + {"SELECT v FROM t WHERE a > 10 AND event NOT IN (?)", 1}, + {"SELECT v FROM t WHERE f in (1, 2, 3)", 0}, + {"with ? as val1, numz as (select val1, number from system.numbers limit 10) select * from numz", 1}, + {"WITH 'hello' REGEXP 'h' AS result SELECT 1", 0}, + {"WITH (select 1) as a, z AS (select 2) SELECT 1", 0}, + {"SELECT result FROM test_view(myParam = ?)", 1}, + {"WITH toDate('2025-08-20') as DATE_END, events AS ( SELECT 1 ) SELECT * FROM events", 0}, + {"select 1 table where 1 = ?", 1}, + {"insert into t (i, t) values (1, timestamp '2010-01-01 00:00:00')", 0}, + {"insert into t (i, t) values (1, date '2010-01-01')", 0}, + {"SELECT timestamp '2010-01-01 00:00:00' as ts, date '2010-01-01' as d", 0}, + {INSERT_WITH_COMMENTS, 4}, + {" /* INSERT TESTING ?? */\n SELECT ? AS num", 1}, + {"/* SELECT ? TESTING */\n INSERT INTO test_table VALUES (?)", 1}, + {"/* INSERT ? T??ESTING */\n\n\n UPDATE test_table SET num = ?", 1}, + {"-- INSERT ? TESTING */\n SELECT ? AS num", 1}, + {" -- SELECT ? TESTING \n -- SELECT AGAIN ?\n INSERT INTO test_table VALUES (?)", 1}, + {" SELECT ? -- INSERT ? TESTING", 1}, + {"#! INSERT ? TESTING \n SELECT ? AS num", 1}, + {"#!INSERT ? TESTING \n SELECT ? AS num", 1}, + {"# INSERT ? TESTING \n SELECT ? AS num", 1}, + {"#INSERT ? TESTING \n SELECT ? AS num", 1}, + {"\nINSERT INTO TESTING \n SELECT ? AS num", 1}, + {" \n INSERT INTO TESTING \n SELECT ? AS num", 1}, + {" SELECT '##?0.1' as f, ? as a\n #this is debug \n FROM table", 1}, + {"WITH '#!?0.1' as f, ? as a\n #this is debug \n SELECT * FROM a", 1}, + {SELECT_WITH_WHERE_CLAUSE_FUNC_WITH_PARAMS, 2}, + {"SELECT arrayFilter(x -> x > 0, [0, 1, 2, -3])", 0}, + {"SELECT [0, 1, 2, -3] arr, arrayFilter(x -> x > 0, arr)", 0}, + {"SELECT arrayFill(x, y, z -> x > y AND x < z, [5, 3, 6, 2], [4, 7, 1, 3], [10, 2, 8, 5]) AS res", 0}, + {"SELECT arrayFilter(x -> x LIKE '%World%', ['Hello', 'abc World']) AS res", 0}, + {"SELECT arrayFilter(x -> not (x is null), ['Hello', 'abc World']) AS res", 0}, + {"SELECT arrayDistinct(arrayFilter(x -> not (x is null), " + + " arrayConcat(t.s.arr1, t.s.arr2)" + + " )" + + ")", 0}, + {"select count(*) filter (where 1 = 1)", 0}, + {"select countIf(*, 1 = ?)", 1}, + {"select count(*) filter (where 1 = ?)", 1}, + {WHEN_HAS_ARRAY, 0}, }; } @@ -542,6 +552,22 @@ public Object[][] testMiscStmtDp() { " EventDate = toDate(?) AND\n" + " EventTime <= ts_upper_bound;"; + private static final String WHEN_HAS_ARRAY = "SELECT\n" + + " field1,\n" + + " CASE\n" + + " WHEN position(field1, 'a') > 0 THEN 'Action1'\n" + + " WHEN position(field1, 'b') > 0 THEN 'Action2'\n" + + " WHEN\n" + + " splitByChar('_', field1)[3] IN ('type1', 'type2')\n" + + " AND match(\n" + + " splitByChar('_', field1)[4],\n" + + " '(SUBTYPE1|SUBTYPE2|SUBTYPE3)'\n" + + " )\n" + + " THEN 'Action3'\n" + + " ELSE null\n" + + " END AS action_to_do\n" + + "FROM db.table1"; + @Test(dataProvider = "testStatementWithoutResultSetDP") public void testStatementsForResultSet(String sql, int args, boolean hasResultSet) { System.out.println("sql: " + sql); @@ -565,6 +591,7 @@ public static Object[][] testStatementWithoutResultSetDP() { /* has result set */ {"SELECT * FROM test_table", 0, true}, {"SELECT 1 table WHERE 1 = ?", 1, true}, + {"SELECT * FROM transaction", 0, true}, {"SHOW CREATE TABLE `db`.`test_table`", 0, true}, {"SHOW CREATE TEMPORARY TABLE `db1`.`tmp_table`", 0, true}, {"SHOW CREATE DICTIONARY dict1", 0, true}, @@ -616,6 +643,9 @@ public static Object[][] testStatementWithoutResultSetDP() { {"EXPLAIN SELECT 1", 0, true}, {"EXPLAIN SELECT sum(number) FROM numbers(10) UNION ALL SELECT sum(number) FROM numbers(10) ORDER BY sum(number) ASC FORMAT TSV", 0, true}, {"DESCRIBE TABLE table", 0, true}, + {"DESCRIBE TABLE (select 1::Uint32)", 0, true}, + {"DESCRIBE (select 1::Uint32)", 0, true}, + {"DESCRIBE (select column.sub_column.field FROM some table)", 0, true}, {"DESC TABLE table1", 0, true}, {"EXISTS TABLE `db`.`table01`", 0, true}, {"CHECK GRANT SELECT(col2) ON table_2", 0, true}, @@ -766,4 +796,108 @@ public static Object[][] testStatementWithoutResultSetDP() { }; } + + /** + * Reads SQL keywords from the resource file. + * Keywords are listed one per line, comments start with #. + */ + private List loadKeywords(String resourceName) throws Exception { + List keywords = new ArrayList<>(); + try (InputStream is = getClass().getClassLoader().getResourceAsStream(resourceName); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + // Skip empty lines and comments + if (!line.isEmpty() && !line.startsWith("#")) { + keywords.add(line); + } + } + } + return keywords; + } + + /** + * Test that keywords allowed as aliases can be used as table names and aliases. + */ + @Test + public void testKeywordAliasesAsTableNames() throws Exception { + List keywords = new ArrayList<>( + ClickHouseSqlUtils.getKeywordGroup(ClickHouseSqlUtils.KEYWORD_GROUP_ALLOWED_ALIASES)); + Assert.assertFalse(keywords.isEmpty(), "Keywords list should not be empty"); + + List failedKeywords = new ArrayList<>(); + + for (String keyword : keywords) { + + // Test 1: SELECT * FROM table AS + String sql3 = "SELECT * FROM table AS " + keyword; + ParsedPreparedStatement stmt3 = parser.parsePreparedStatement(sql3); + if (stmt3.isHasErrors()) { + failedKeywords.add(keyword + " (test: SELECT * FROM table AS " + keyword + ")"); + } + + + // Test 2: SELECT * FROM table (implicit alias) + String sql4 = "SELECT * FROM table " + keyword; + ParsedPreparedStatement stmt4 = parser.parsePreparedStatement(sql4); + if (stmt4.isHasErrors()) { + failedKeywords.add(keyword + " (test: SELECT * FROM table " + keyword + ")"); + } + + } + + // Report all failures at once + if (!failedKeywords.isEmpty()) { + String failureMessage = "The following keywords caused parsing errors:\n" + + failedKeywords.stream().collect(Collectors.joining("\n")); + Assert.fail(failureMessage); + } + } + + /** + * Test that keywords allowed as table names can be used as table names. + */ + @Test + public void testAllowedTableKeywords() throws Exception { + List keywords = loadKeywords("allowed_keyword_tablenames.txt"); + Assert.assertFalse(keywords.isEmpty()); + List failedKeywords = new ArrayList<>(); + + for (String keyword : keywords) { + // Test 1: SELECT * FROM + String sql1 = "SELECT * FROM " + keyword; + ParsedPreparedStatement stmt1 = parser.parsePreparedStatement(sql1); + if (stmt1.isHasErrors()) { + failedKeywords.add(keyword + " (test: SELECT * FROM " + keyword + ")"); + } + + // Test 1: SELECT * FROM WHERE col = ? + String sql2 = "SELECT * FROM " + keyword + " WHERE col = ?"; + ParsedPreparedStatement stmt2 = parser.parsePreparedStatement(sql2); + if (stmt2.isHasErrors()) { + failedKeywords.add(keyword + " (test: SELECT * FROM " + keyword + " WHERE col = ?)"); + } + Assert.assertEquals(stmt2.getArgCount(), 1, "Should have 1 parameter for: " + sql2); + + // Test 2: INSERT INTO VALUES (?) + String sql5 = "INSERT INTO " + keyword + " VALUES (?)"; + ParsedPreparedStatement stmt5 = parser.parsePreparedStatement(sql5); + if (stmt5.isHasErrors()) { + failedKeywords.add(keyword + " (test: INSERT INTO " + keyword + " VALUES (?))"); + } + Assert.assertEquals(stmt5.getArgCount(), 1, "Should have 1 parameter for: " + sql5); + if (!stmt5.getTable().equalsIgnoreCase(keyword)) { + failedKeywords.add(keyword + " (test: INSERT INTO " + keyword + " VALUES (?)) table name check failed"); + } + } + + // Report all failures at once + if (!failedKeywords.isEmpty()) { + String failureMessage = "The following keywords caused parsing errors:\n" + + failedKeywords.stream().collect(Collectors.joining("\n")); + Assert.fail(failureMessage); + } + } } \ No newline at end of file diff --git a/jdbc-v2/src/test/resources/allowed_keyword_tablenames.txt b/jdbc-v2/src/test/resources/allowed_keyword_tablenames.txt new file mode 100644 index 000000000..101145dfc --- /dev/null +++ b/jdbc-v2/src/test/resources/allowed_keyword_tablenames.txt @@ -0,0 +1,439 @@ +ACCESS +ACTION +ADD +ADMIN +AFTER +ALGORITHM +ALIAS +ALL +ALLOWED_LATENESS +ALTER +AND +ANTI +ANY +APPEND +APPLY +ARRAY +AS +ASC +ASCENDING +ASOF +ASSUME +AST +ASYNC +ATTACH +AUTHENTICATION +AUTO_INCREMENT +AZURE +BACKUP +BCRYPT_HASH +BCRYPT_PASSWORD +BEGIN +BETWEEN +BIDIRECTIONAL +BOTH +BY +CACHE +CACHES +CASCADE +CASE +CAST +CHANGE +CHANGEABLE_IN_READONLY +CHANGED +CHAR +CHARACTER +CHECK +CLEANUP +CLEAR +CLONE +CLUSTER +CLUSTERS +CN +CODEC +COLLATE +COLLECTION +COLUMN +COLUMNS +COMMENT +COMMIT +COMPRESSION +CONNECTIONS +CONST +CONSTRAINT +COPY +CREATE +CROSS +CUBE +CURRENT +CURRENT_USER +CURRENTUSER +DATA +DATABASE +DATABASES +DATE +DAY +DAYS +DDL +DEALLOCATE +DEDUPLICATE +DEFAULT +DEFINER +DELAY +DELETE +DELETED +DEPENDS +DESC +DESCENDING +DESCRIBE +DETACH +DETACHED +DICTIONARIES +DICTIONARY +DISK +DISTINCT +DIV +DOUBLE_SHA1_HASH +DOUBLE_SHA1_PASSWORD +DROP +ELSE +EMPTY +ENABLED +END +ENFORCED +ENGINE +ENGINES +EPHEMERAL +ESTIMATE +EVENT +EVENTS +EVERY +EXCEPT +EXCHANGE +EXECUTE +EXISTS +EXPLAIN +EXPRESSION +EXTENDED +EXTERNAL +FAKE +FALSE +FETCH +FIELDS +FILE +FILESYSTEM +FILL +FILTER +FINAL +FIRST +FOLLOWING +FOR +FORCE +FOREIGN +FORGET +FORMAT +FREEZE +FROM +FULL +FULLTEXT +FUNCTION +FUNCTIONS +GLOBAL +GRANT +GRANTEES +GRANTS +GRANULARITY +GROUP +GROUPING +GROUPS +HASH +HAVING +HDFS +HIERARCHICAL +HOST +HOUR +HOURS +HTTP +ID +IDENTIFIED +IF +IGNORE +ILIKE +IMPLICIT +IN +INDEX +INDEXES +INDICES +INFILE +INHERIT +INJECTIVE +INNER +INSERT +INTERPOLATE +INTERSECT +INTERVAL +INTO +INVISIBLE +INVOKER +IP +IS +IS_OBJECT_ID +JOIN +JWT +KERBEROS +KEY +KEYED +KEYS +KILL +KIND +LARGE +LAST +LAYOUT +LDAP +LEADING +LEFT +LESS +LEVEL +LIFETIME +LIGHTWEIGHT +LIKE +LIMIT +LIMITS +LINEAR +LIST +LIVE +LOCAL +MASK +MASTER +MATCH +MATERIALIZE +MATERIALIZED +MAX +MEMORY +MERGES +METHODS +METRICS +MICROSECOND +MICROSECONDS +MILLISECOND +MILLISECONDS +MIN +MINUTE +MINUTES +MOD +MODIFY +MONTH +MONTHS +MOVE +MUTATION +NAME +NAMED +NANOSECOND +NANOSECONDS +NEW +NEXT +NO +NO_AUTHENTICATION +NONE +NO_PASSWORD +NOT +NULL +NULLS +OBJECT +OFFSET +ON +ONLY +OPTIMIZE +OPTION +OR +ORDER +OUTER +OUTFILE +OVER +OVERRIDABLE +OVERRIDE +PARALLEL +PART +PARTIAL +PARTITION +PARTITIONS +PART_MOVE_TO_SHARD +PARTS +PASTE +PATCHES +PERIODIC +PERMANENTLY +PERMISSIVE +PERSISTENT +PIPELINE +PLAINTEXT_PASSWORD +PLAN +POLICY +POPULATE +PRECEDING +PRECISION +PREFIX +PREPARE +PREWHERE +PRIMARY +PRIVILEGES +PROCESSLIST +PROFILE +PROFILES +PROJECTION +PULL +QUALIFY +QUARTER +QUARTERS +QUERY +QUOTA +RANDOMIZE +RANDOMIZED +RANGE +READ +READONLY +REALM +RECOMPRESS +RECURSIVE +REFERENCES +REFRESH +REGEXP +REMOVE +RENAME +REPLACE +REPLICATED +RESET +RESOURCE +RESPECT +RESTORE +RESTRICT +RESTRICTIVE +RESUME +REVOKE +REWRITE +RIGHT +ROLE +ROLES +ROLLBACK +ROLLUP +ROW +ROWS +S3 +SALT +SAMPLE +SAN +SCHEME +SCRAM_SHA256_HASH +SCRAM_SHA256_PASSWORD +SECOND +SECONDS +SECURITY +SELECT +SEMI +SEQUENTIAL +SERVER +SET +SETS +SETTING +SETTINGS +SHA256_HASH +SHA256_PASSWORD +SHARD +SHOW +SIGNED +SIMPLE +SKIP +SNAPSHOT +SOURCE +SPATIAL +SQL +SQL_TSI_DAY +SQL_TSI_HOUR +SQL_TSI_MICROSECOND +SQL_TSI_MILLISECOND +SQL_TSI_MINUTE +SQL_TSI_MONTH +SQL_TSI_NANOSECOND +SQL_TSI_QUARTER +SQL_TSI_SECOND +SQL_TSI_WEEK +SQL_TSI_YEAR +SSH_KEY +SSL_CERTIFICATE +STALENESS +START +STATISTIC +STATISTICS +STDOUT +STEP +STORAGE +STRICT +STRICTLY_ASCENDING +SUBPARTITION +SUBPARTITIONS +SUSPEND +SYNC +SYNTAX +SYSTEM +TABLE +TABLES +TAG +TAGS +TEMPORARY +TEST +THAN +THEN +THREAD +TIES +TIME +TIMESTAMP +TO +TOP +TOTALS +TRACKING +TRAILING +TRANSACTION +TREE +TRIGGER +TRUE +TRUNCATE +TTL +TYPE +TYPEOF +UNBOUNDED +UNDROP +UNFREEZE +UNION +UNIQUE +UNLOCK +UNSET +UNSIGNED +UNTIL +UPDATE +URL +USE +USER +USING +UUID +VALID +VALUES +VARYING +VIEW +VISIBLE +VOLUME +WATCH +WATERMARK +WEEK +WEEKS +WHEN +WHERE +WINDOW +WITH +WITH_ITEMINDEX +WORKER +WORKLOAD +WRITABLE +WRITE +YEAR +YEARS +ZKPATH \ No newline at end of file diff --git a/jdbc-v2/src/test/resources/reserved_keywords.txt b/jdbc-v2/src/test/resources/reserved_keywords.txt new file mode 100644 index 000000000..3668aeaf9 --- /dev/null +++ b/jdbc-v2/src/test/resources/reserved_keywords.txt @@ -0,0 +1,44 @@ +ALL +ANTI +ANY +ARRAY +AS +ASOF +BETWEEN +CROSS +ELSE +EXCEPT +FINAL +FORMAT +FROM +FULL +GLOBAL +GROUP +HAVING +ILIKE +INNER +INTERSECT +INTO +JOIN +LEFT +LIKE +LIMIT +NOT +OFFSET +ON +ONLY +ORDER +PARALLEL +PASTE +PREWHERE +QUALIFY +RIGHT +SAMPLE +SEMI +SETTINGS +UNION +USING +UUID +WHERE +WINDOW +WITH \ No newline at end of file diff --git a/jdbc-v2/src/test/resources/sql-keywords.txt b/jdbc-v2/src/test/resources/sql-keywords.txt new file mode 100644 index 000000000..26a46d9aa --- /dev/null +++ b/jdbc-v2/src/test/resources/sql-keywords.txt @@ -0,0 +1,470 @@ +# ClickHouse SQL Keywords +# Source: https://clickhouse.joesstuff.co.uk/keywords.html +# Individual keywords extracted and deduplicated from system.keywords table + +ACCESS +ACTION +ADD +ADMIN +AFTER +ALGORITHM +ALIAS +ALL +ALLOWED_LATENESS +ALTER +AND +ANTI +ANY +APPEND +APPLY +ARRAY +AS +ASC +ASCENDING +ASOF +ASSUME +AST +ASYNC +ATTACH +AUTHENTICATION +AUTO_INCREMENT +AZURE +BACKUP +BAGEXPANSION +BASE_BACKUP +BCRYPT_HASH +BCRYPT_PASSWORD +BEGIN +BETWEEN +BIDIRECTIONAL +BOTH +BY +CACHE +CACHES +CASCADE +CASE +CAST +CHANGE +CHANGEABLE_IN_READONLY +CHANGED +CHAR +CHARACTER +CHECK +CLEANUP +CLEAR +CLONE +CLUSTER +CLUSTERS +CLUSTER_HOST_IDS +CN +CODEC +COLLATE +COLLECTION +COLUMN +COLUMNS +COMMENT +COMMIT +COMPRESSION +CONNECTIONS +CONST +CONSTRAINT +COPY +CREATE +CROSS +CUBE +CURRENT +CURRENT_USER +CURRENTUSER +D +DATA +DATABASE +DATABASES +DATE +DAY +DAYS +DD +DDL +DEALLOCATE +DEDUPLICATE +DEFAULT +DEFINER +DELAY +DELETE +DELETED +DEPENDS +DESC +DESCENDING +DESCRIBE +DETACH +DETACHED +DICTIONARIES +DICTIONARY +DISK +DISTINCT +DIV +DOUBLE_SHA1_HASH +DOUBLE_SHA1_PASSWORD +DROP +ELSE +EMPTY +ENABLED +END +ENFORCED +ENGINE +ENGINES +EPHEMERAL +ESTIMATE +EVENT +EVENTS +EVERY +EXCEPT +EXCHANGE +EXECUTE +EXISTS +EXPLAIN +EXPRESSION +EXTENDED +EXTERNAL +FAKE +FALSE +FETCH +FIELDS +FILE +FILES +FILESYSTEM +FILL +FILTER +FINAL +FIRST +FOLLOWING +FOR +FORCE +FOREIGN +FORGET +FORMAT +FREEZE +FROM +FULL +FULLTEXT +FUNCTION +FUNCTIONS +GLOBAL +GRANT +GRANTEES +GRANTS +GRANULARITY +GROUP +GROUPING +GROUPS +H +HASH +HAVING +HDFS +HH +HIERARCHICAL +HOST +HOUR +HOURS +HTTP +ID +IDENTIFIED +IF +IGNORE +ILIKE +IMPLICIT +IN +INDEX +INDEXES +INDICES +INFILE +INHERIT +INJECTIVE +INNER +INSERT +INTERPOLATE +INTERSECT +INTERVAL +INTO +INVISIBLE +INVOKER +IP +IS +IS_OBJECT_ID +JOIN +JWT +KERBEROS +KEY +KEYED +KEYS +KILL +KIND +LARGE +LAST +LAYOUT +LDAP +LEADING +LEFT +LESS +LEVEL +LIFETIME +LIGHTWEIGHT +LIKE +LIMIT +LIMITS +LINEAR +LIST +LIVE +LOCAL +M +MASK +MASKING +MASTER +MATCH +MATERIALIZE +MATERIALIZED +MAX +MCS +MEMORY +MERGES +METHODS +METRICS +MI +MICROSECOND +MICROSECONDS +MILLISECOND +MILLISECONDS +MIN +MINUTE +MINUTES +MM +MOD +MODIFY +MONTH +MONTHS +MOVE +MS +MUTATION +N +NAME +NAMED +NANOSECOND +NANOSECONDS +NEW +NEXT +NO +NO_AUTHENTICATION +NONE +NO_PASSWORD +NOT +NS +NULL +NULLS +OBJECT +OFFSET +ON +ONLY +OPTIMIZE +OPTION +OR +ORDER +OUTER +OUTFILE +OVER +OVERRIDABLE +OVERRIDE +PARALLEL +PART +PARTIAL +PARTITION +PARTITIONS +PART_MOVE_TO_SHARD +PARTS +PASTE +PATCHES +PAUSE +PERIODIC +PERMANENTLY +PERMISSIVE +PERSISTENT +PIPELINE +PLAINTEXT_PASSWORD +PLAN +POLICY +POPULATE +PRECEDING +PRECISION +PREFIX +PREPARE +PREWHERE +PRIMARY +PRIORITY +PRIVILEGES +PROCESSLIST +PROFILE +PROFILES +PROJECTION +PROTOBUF +PULL +Q +QQ +QUALIFY +QUARTER +QUARTERS +QUERY +QUOTA +RANDOMIZE +RANDOMIZED +RANGE +READ +READONLY +REALM +RECOMPRESS +RECURSIVE +REFERENCES +REFRESH +REGEXP +REMOVE +RENAME +REPLACE +REPLICATED +RESET +RESOURCE +RESPECT +RESTORE +RESTRICT +RESTRICTIVE +RESUME +REVOKE +REWRITE +RIGHT +ROLE +ROLES +ROLLBACK +ROLLUP +ROW +ROWS +S +S3 +SALT +SAMPLE +SAN +SCHEME +SCRAM_SHA256_HASH +SCRAM_SHA256_PASSWORD +SECOND +SECONDS +SECURITY +SELECT +SEMI +SEQUENTIAL +SERVER +SET +SETS +SETTING +SETTINGS +SHA256_HASH +SHA256_PASSWORD +SHARD +SHOW +SIGNED +SIMPLE +SKIP +SNAPSHOT +SOURCE +SPATIAL +SQL +SQL_TSI_DAY +SQL_TSI_HOUR +SQL_TSI_MICROSECOND +SQL_TSI_MILLISECOND +SQL_TSI_MINUTE +SQL_TSI_MONTH +SQL_TSI_NANOSECOND +SQL_TSI_QUARTER +SQL_TSI_SECOND +SQL_TSI_WEEK +SQL_TSI_YEAR +SS +SSH_KEY +SSL_CERTIFICATE +STALENESS +START +STATISTIC +STATISTICS +STDOUT +STEP +STORAGE +STRICT +STRICTLY_ASCENDING +SUBPARTITION +SUBPARTITIONS +SUSPEND +SYNC +SYNTAX +SYSTEM +TABLE +TABLES +TAG +TAGS +TEMPORARY +TEST +THAN +THEN +THREAD +TIES +TIME +TIMESTAMP +TO +TOP +TOTALS +TRACKING +TRAILING +TRANSACTION +TREE +TRIGGER +TRUE +TRUNCATE +TTL +TYPE +TYPEOF +UNBOUNDED +UNDROP +UNFREEZE +UNION +UNIQUE +UNLOCK +UNSET +UNSIGNED +UNTIL +UPDATE +URL +USE +USER +USING +UUID +VALID +VALUES +VARYING +VIEW +VISIBLE +VOLUME +WATCH +WATERMARK +WEEK +WEEKS +WHEN +WHERE +WINDOW +WITH +WITH_ITEMINDEX +WK +WORKER +WORKLOAD +WRITABLE +WRITE +WW +YEAR +YEARS +YY +YYYY +ZKPATH