diff --git a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 index 67ffdd846..f6a2b4462 100644 --- a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 +++ b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 @@ -27,11 +27,14 @@ ATTACH : A T T A C H; BETWEEN : B E T W E E N; BOTH : B O T H; BY : B Y; +BCRYPT_PASSWORD : B C R Y P T '_' P A S S W O R D; +BCRYPT_HASH : B C R Y P T '_' H A S H; CASE : C A S E; CAST : C A S T; CHECK : C H E C K; CLEAR : C L E A R; CLUSTER : C L U S T E R; +CN : C N; CODEC : C O D E C; COLLATE : C O L L A T E; COLUMN : C O L U M N; @@ -59,6 +62,8 @@ DICTIONARY : D I C T I O N A R Y; DISK : D I S K; DISTINCT : D I S T I N C T; DISTRIBUTED : D I S T R I B U T E D; +DOUBLE_SHA1_PASSWORD : D O U B L E '_' S H A '1' '_' P A S S W O R D; +DOUBLE_SHA1_HASH : D O U B L E '_' S H A '1' '_' H A S H; DROP : D R O P; ELSE : E L S E; END : E N D; @@ -82,11 +87,15 @@ FULL : F U L L; FUNCTION : F U N C T I O N; GLOBAL : G L O B A L; GRANULARITY : G R A N U L A R I T Y; +GRANTEES : G R A N T E E S; GROUP : G R O U P; HAVING : H A V I N G; HIERARCHICAL : H I E R A R C H I C A L; +HTTP : H T T P; +HOST : H O S T; HOUR : H O U R; ID : I D; +IDENTIFIED : I D E N T I F I E D; IF : I F; ILIKE : I L I K E; IN : I N; @@ -97,13 +106,16 @@ INNER : I N N E R; INSERT : I N S E R T; INTERVAL : I N T E R V A L; INTO : I N T O; +IP : I P; IS : I S; IS_OBJECT_ID : I S UNDERSCORE O B J E C T UNDERSCORE I D; JOIN : J O I N; KEY : K E Y; +KERBEROS : K E R B E R O S; KILL : K I L L; LAST : L A S T; LAYOUT : L A Y O U T; +LDAP : L D A P; LEADING : L E A D I N G; LEFT : L E F T; LIFETIME : L I F E T I M E; @@ -123,7 +135,9 @@ MONTH : M O N T H; MOVE : M O V E; MUTATION : M U T A T I O N; NAN_SQL : N A N; // conflicts with macro NAN +NAME : N A M E; NO : N O; +NO_PASSWORD : N O '_' P A S S W O R D; NONE : N O N E; NOT : N O T; NULL_SQL : N U L L; // conflicts with macro NULL @@ -142,8 +156,11 @@ PRECEDING : P R E C E D I N G; PREWHERE : P R E W H E R E; PRIMARY : P R I M A R Y; PROJECTION : P R O J E C T I O N; +PLAINTEXT_PASSWORD : P L A I N T E X T '_' P A S S W O R D; QUARTER : Q U A R T E R; RANGE : R A N G E; +REALM : R E A L M; +REGEXP : R E G E X P; RELOAD : R E L O A D; REMOVE : R E M O V E; RENAME : R E N A M E; @@ -156,13 +173,21 @@ ROLLUP : R O L L U P; ROW : R O W; ROWS : R O W S; SAMPLE : S A M P L E; +SCHEMA : S C H E M A; +SCRAM_SHA256_PASSWORD : S C R A M '_' S H A '2' '5' '6' '_' P A S S W O R D; +SCRAM_SHA256_HASH : S C R A M '_' S H A '2' '5' '6' '_' H A S H; SECOND : S E C O N D; SELECT : S E L E C T; SEMI : S E M I; SENDS : S E N D S; +SERVER : S E R V E R; +SSL_CERTIFICATE : S S L '_' C E R T I F I C A T E; +SSH_KEY : S S H '_' K E Y; SET : S E T; SETTINGS : S E T T I N G S; SHOW : S H O W; +SHA256_PASSWORD : S H A '2' '5' '6' '_' P A S S W O R D; +SHA256_HASH : S H A '2' '5' '6' '_' H A S H; SOURCE : S O U R C E; START : S T A R T; STOP : S T O P; @@ -190,6 +215,7 @@ UNBOUNDED : U N B O U N D E D; UNION : U N I O N; UPDATE : U P D A T E; USE : U S E; +USER : U S E R; USING : U S I N G; UUID : U U I D; VALUES : V A L U E S; diff --git a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 index a589069dd..e94537c15 100644 --- a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 +++ b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 @@ -142,6 +142,50 @@ createStmt engineClause? subqueryClause? # CreateTableStmt | (ATTACH | CREATE) (OR REPLACE)? VIEW (IF NOT EXISTS)? tableIdentifier uuidClause? clusterClause? tableSchemaClause? subqueryClause # CreateViewStmt + | CREATE USER ((IF NOT EXISTS) | (OR REPLACE))? userIdentifier (COMMA userIdentifier)* clusterClause? + userIdentifiedClause? + userCreateHostClause? + validUntilClause? + (DEFAULT ROLE identifier (COMMA identifier)*)? + (DEFAULT DATABASE identifier | NONE)? + + settingsClause? #CreateUserStmt + ; + +userIdentifier + : (IDENTIFIER | STRING_LITERAL) + ; + +userIdentifiedClause + : IDENTIFIED BY literal + | IDENTIFIED WITH userIdentifiedWithClause validUntilClause? (COMMA userIdentifiedWithClause VALID UNTIL literal)* + | NOT IDENTIFIED + ; + +userIdentifiedWithClause + : (PLAINTEXT_PASSWORD | SHA256_PASSWORD | SHA256_HASH | DOUBLE_SHA1_PASSWORD | DOUBLE_SHA1_HASH | SCRAM_SHA256_PASSWORD | SCRAM_SHA256_HASH | BCRYPT_PASSWORD | BCRYPT_HASH) BY literal + | NO_PASSWORD + | LDAP SERVER literal + | KERBEROS (REALM literal)? + | SSL_CERTIFICATE CN literal + | SSH_KEY BY KEY literal TYPE literal (COMMA KEY literal TYPE literal)* + | HTTP SERVER literal (SCHEMA literal)? + ; + +userCreateHostClause + : HOST (userCreateHostDef (COMMA userCreateHostDef)*) | ANY | NONE + ; + +userCreateHostDef + : LOCAL | NAME literal | REGEXP literal | IP literal | LIKE literal + ; + +userCreateGranteesClause + : GRANTEES (identifier | STRING_LITERAL | ANY | NONE ) (COMMA (identifier | STRING_LITERAL | ANY | NONE ))* + (EXCEPT (identifier | STRING_LITERAL) (COMMA (identifier | STRING_LITERAL ))*) + ; +validUntilClause + : VALID UNTIL interval ; dictionarySchemaClause @@ -951,4 +995,4 @@ identifierOrNull enumValue : STRING_LITERAL EQ_SINGLE numberLiteral - ; \ No newline at end of file + ; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java index cff7530ef..f79a7da07 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -129,7 +129,7 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr try { lastStatementSql = parseJdbcEscapeSyntax(sql); - LOG.debug("SQL Query: {}", lastStatementSql); + LOG.trace("SQL Query: {}", lastStatementSql); // this is not secure for create statements because of passwords QueryResponse response; if (queryTimeout == 0) { response = connection.client.query(lastStatementSql, mergedSettings).get(); @@ -179,7 +179,7 @@ protected int executeUpdateImpl(String sql, QuerySettings settings) throws SQLEx } lastStatementSql = parseJdbcEscapeSyntax(sql); - LOG.debug("SQL Query: {}", lastStatementSql); + LOG.trace("SQL Query: {}", lastStatementSql); int updateCount = 0; try (QueryResponse response = queryTimeout == 0 ? connection.client.query(lastStatementSql, mergedSettings).get() : connection.client.query(lastStatementSql, mergedSettings).get(queryTimeout, TimeUnit.SECONDS)) { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java index 709fda0ca..f12fb8ccb 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java @@ -103,4 +103,6 @@ public void enterSetRoleStmt(ClickHouseParser.SetRoleStmtContext ctx) { setRoles(roles); } } + + } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java index 620ff5b4d..47ca141c3 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java @@ -1,25 +1,33 @@ package com.clickhouse.jdbc.internal; +import org.antlr.v4.runtime.BaseErrorListener; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; import org.antlr.v4.runtime.tree.IterativeParseTreeWalker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.regex.Matcher; import java.util.regex.Pattern; public class SqlParser { + private static final Logger LOG = LoggerFactory.getLogger(SqlParser.class); + public ParsedStatement parsedStatement(String sql) { CharStream charStream = CharStreams.fromString(sql); ClickHouseLexer lexer = new ClickHouseLexer(charStream); ClickHouseParser parser = new ClickHouseParser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + parser.addErrorListener(new ParserErrorListener()); ClickHouseParser.QueryStmtContext parseTree = parser.queryStmt(); ParsedStatement parserListener = new ParsedStatement(); IterativeParseTreeWalker.DEFAULT.walk(parserListener, parseTree); - return parserListener; } @@ -54,4 +62,11 @@ public static String escapeQuotes(String str) { .replace("'", "\\'") .replace("\"", "\\\""); } + + private static class ParserErrorListener extends BaseErrorListener { + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { + LOG.warn("SQL syntax error at line: " + line + ", pos: " + charPositionInLine + ", " + msg); + } + } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java index f82211563..6f7a252c9 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java @@ -362,6 +362,10 @@ void testMultipleWithClauses() throws Exception { @Test(groups = { "integration" }) void testRecursiveWithClause() throws Exception { + if (ClickHouseVersion.of(getServerVersion()).check("(,24.3]")) { + return; // recursive CTEs were introduced in 24.4 + } + try (Connection conn = getJdbcConnection(); PreparedStatement stmt = conn.prepareStatement( "WITH RECURSIVE numbers AS (" + diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java index b9f7433cd..ad45659fe 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -342,7 +342,7 @@ public void testExecuteQueryTimeout() throws Exception { @Test(groups = { "integration" }) - private void testSettingRole() throws SQLException { + public void testSettingRole() throws SQLException { if (earlierThan(24, 4)) {//Min version is 24.4 return; } @@ -551,8 +551,8 @@ public void testSwitchDatabase() throws Exception { } } } - - + + @Test(groups = { "integration" }) public void testNewLineSQLParsing() throws Exception { try (Connection conn = getJdbcConnection()) { @@ -617,7 +617,7 @@ public void testNewLineSQLParsing() throws Exception { } } - + @Test(groups = { "integration" }) public void testNullableFixedStringType() throws Exception { try (Connection conn = getJdbcConnection()) { @@ -708,4 +708,25 @@ public void testExecuteWithMaxRows() throws Exception { } } + @Test(groups = {"integration"}) + public void testDDLStatements() throws Exception { + if (isCloud()) { + return; // skip because we do not want to create extra on cloud instance + } + try (Connection conn = getJdbcConnection()) { + try (Statement stmt = conn.createStatement()){ + Assert.assertFalse(stmt.execute("CREATE USER IF NOT EXISTS 'user011' IDENTIFIED BY 'password'")); + + try (ResultSet rs = stmt.executeQuery("SHOW USERS")) { + boolean found = false; + while (rs.next()) { + if (rs.getString("name").equals("user011")) { + found = true; + } + } + Assert.assertTrue(found); + } + } + } + } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java index 83adc500d..992a5643e 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java @@ -2,6 +2,7 @@ import org.testng.Assert; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static org.testng.Assert.assertEquals; @@ -210,4 +211,34 @@ public void testStmtWithFunction() { ParsedPreparedStatement stmt = parser.parsePreparedStatement(sql); Assert.assertEquals(stmt.getArgCount(), 4); } + + @Test(dataProvider = "testCreateStmtDP") + public void testCreateStatement(String sql) { + SqlParser parser = new SqlParser(); + ParsedPreparedStatement stmt = parser.parsePreparedStatement(sql); + Assert.assertFalse(stmt.isHasErrors()); + } + + @DataProvider + public static Object[][] testCreateStmtDP() { + return new Object[][] { + {"CREATE USER 'user01' IDENTIFIED WITH no_password"}, + {"CREATE USER 'user01' IDENTIFIED WITH plaintext_password BY 'qwerty'"}, + {"CREATE USER 'user01' IDENTIFIED WITH sha256_password BY 'qwerty' or IDENTIFIED BY 'password'"}, + {"CREATE USER 'user01' IDENTIFIED WITH sha256_hash BY 'hash' SALT 'salt'"}, + {"CREATE USER 'user01' IDENTIFIED WITH sha256_hash BY 'hash'"}, + {"CREATE USER 'user01' IDENTIFIED WITH double_sha1_password BY 'qwerty'"}, + {"CREATE USER 'user01' IDENTIFIED WITH double_sha1_hash BY 'hash'"}, + {"CREATE USER 'user01' IDENTIFIED WITH bcrypt_password BY 'qwerty'"}, + {"CREATE USER 'user01' IDENTIFIED WITH bcrypt_hash BY 'hash'"}, + {"CREATE USER 'user01' IDENTIFIED WITH ldap SERVER 'server_name'"}, + {"CREATE USER 'user01' IDENTIFIED WITH kerberos"}, + {"CREATE USER 'user01' IDENTIFIED WITH kerberos REALM 'realm'"}, + {"CREATE USER 'user01' IDENTIFIED WITH ssl_certificate CN 'mysite.com:user'"}, + {"CREATE USER 'user01' IDENTIFIED WITH ssh_key BY KEY 'public_key' TYPE 'ssh-rsa', KEY 'another_public_key' TYPE 'ssh-ed25519'"}, + {"CREATE USER 'user01' IDENTIFIED WITH http SERVER 'http_server' SCHEME 'basic'"}, + {"CREATE USER 'user01' IDENTIFIED WITH http SERVER 'http_server'"}, + {"CREATE USER 'user01' IDENTIFIED BY 'qwerty'"}, + }; + } } \ No newline at end of file