diff --git a/jdbc-v2/src/main/javacc/ClickHouseSqlParser.jj b/jdbc-v2/src/main/javacc/ClickHouseSqlParser.jj index e6922223a..1f6772094 100644 --- a/jdbc-v2/src/main/javacc/ClickHouseSqlParser.jj +++ b/jdbc-v2/src/main/javacc/ClickHouseSqlParser.jj @@ -27,6 +27,8 @@ PARSER_BEGIN(ClickHouseSqlParser) package com.clickhouse.jdbc.internal.parser.javacc; +import java.io.BufferedReader; +import java.io.InputStreamReader; import java.io.StringReader; import java.util.ArrayList; @@ -49,6 +51,32 @@ public class ClickHouseSqlParser { private static final Logger log = LoggerFactory.getLogger(ClickHouseSqlParser.class); + private static final Set ALLOWED_ALIAS_KEYWORDS = loadKeywords("allowed_keyword_aliases.txt"); + + private static Set loadKeywords(String resource) { + Set keywords = new HashSet<>(); + try (java.io.InputStream in = ClickHouseSqlParser.class.getResourceAsStream("/" + resource)) { + if (in != null) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty()) { + keywords.add(line.toUpperCase(Locale.ROOT)); + } + } + } + } + } catch (Exception e) { + log.error("Failed to load keywords from " + resource, e); + } + return keywords; + } + + 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; @@ -678,6 +706,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)) ) ) @@ -843,7 +872,7 @@ void outfilePart(): {} { } void settingsPart(): {} { - { token_source.addPosition(token); } settingExprList() + (LOOKAHEAD(1) )? { token_source.addPosition(token); } (LOOKAHEAD(2) settingExprList())? } void withTotalPart(): {} { @@ -888,13 +917,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 +991,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 +1008,6 @@ Token anyIdentifier(): { Token t; } { { return t; } } -Token identifier(): { Token t; } { - ( - t = - | t = - | t = variable() - | t = - | t = keyword() - ) - { return t; } -} - void interval(): {} { | | | | | | | } @@ -1045,15 +1077,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 +1095,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/main/resources/allowed_keyword_aliases.txt b/jdbc-v2/src/main/resources/allowed_keyword_aliases.txt new file mode 100644 index 000000000..b135d11be --- /dev/null +++ b/jdbc-v2/src/main/resources/allowed_keyword_aliases.txt @@ -0,0 +1,395 @@ +ACCESS +ACTION +ADD +ADMIN +AFTER +ALGORITHM +ALIAS +ALLOWED_LATENESS +ALTER +AND +APPEND +APPLY +ASC +ASCENDING +ASSUME +AST +ASYNC +ATTACH +AUTHENTICATION +AUTO_INCREMENT +AZURE +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 +CN +CODEC +COLLATE +COLLECTION +COLUMN +COLUMNS +COMMENT +COMMIT +COMPRESSION +CONNECTIONS +CONST +CONSTRAINT +COPY +CREATE +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 +EMPTY +ENABLED +END +ENFORCED +ENGINE +ENGINES +EPHEMERAL +ESTIMATE +EVENT +EVENTS +EVERY +EXCHANGE +EXECUTE +EXISTS +EXPLAIN +EXPRESSION +EXTENDED +EXTERNAL +FAKE +FALSE +FETCH +FIELDS +FILE +FILESYSTEM +FILL +FILTER +FIRST +FOLLOWING +FOR +FORCE +FOREIGN +FORGET +FREEZE +FULLTEXT +FUNCTION +FUNCTIONS +GRANT +GRANTEES +GRANTS +GRANULARITY +GROUPING +GROUPS +HASH +HDFS +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 +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 +NULL +NULLS +OBJECT +OPTIMIZE +OPTION +OR +OUTER +OUTFILE +OVER +OVERRIDABLE +OVERRIDE +PART +PARTIAL +PARTITION +PARTITIONS +PART_MOVE_TO_SHARD +PARTS +PATCHES +PERIODIC +PERMANENTLY +PERMISSIVE +PERSISTENT +PIPELINE +PLAINTEXT_PASSWORD +PLAN +POLICY +POPULATE +PRECEDING +PRECISION +PREFIX +PREPARE +PRIMARY +PRIVILEGES +PROCESSLIST +PROFILE +PROFILES +PROJECTION +PULL +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 +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 +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 +WORKER +WORKLOAD +WRITABLE +WRITE +YEAR +YEARS +ZKPATH \ No newline at end of file diff --git a/jdbc-v2/src/main/resources/allowed_keyword_tablenames.txt b/jdbc-v2/src/main/resources/allowed_keyword_tablenames.txt new file mode 100644 index 000000000..101145dfc --- /dev/null +++ b/jdbc-v2/src/main/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/main/resources/reserved_keywords.txt b/jdbc-v2/src/main/resources/reserved_keywords.txt new file mode 100644 index 000000000..3668aeaf9 --- /dev/null +++ b/jdbc-v2/src/main/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/main/resources/sql-keywords.txt b/jdbc-v2/src/main/resources/sql-keywords.txt new file mode 100644 index 000000000..2aa96790c --- /dev/null +++ b/jdbc-v2/src/main/resources/sql-keywords.txt @@ -0,0 +1,443 @@ +# 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 +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 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..fa7d71740 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementSQLTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementSQLTest.java @@ -5,10 +5,16 @@ 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.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.concurrent.atomic.AtomicBoolean; @@ -47,4 +53,82 @@ 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"); + } + + 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..f90909cdb 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 @@ -5,6 +5,14 @@ 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; @@ -565,6 +573,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}, @@ -766,4 +775,119 @@ 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))) { + + if (is == null) { + return keywords; + } + + 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 = loadKeywords("allowed_keyword_aliases.txt"); + 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 + ")"); + } + if (!stmt1.getTable().equalsIgnoreCase(keyword)) { + failedKeywords.add(keyword + " (test: SELECT * FROM " + keyword + ") table name check failed"); + } + + // 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); + if (!stmt2.getTable().equalsIgnoreCase(keyword)) { + failedKeywords.add(keyword + " (test: SELECT * FROM " + keyword + " WHERE col = ?) table name check failed"); + } +// Assert.assertEquals(stmt2.getTable(), keyword, "Table name mismatch 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 (!stmt2.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