Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.clickhouse.jdbc.internal;
package com.clickhouse.jdbc;

public enum ClientInfoProperties {

Expand Down
120 changes: 66 additions & 54 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.clickhouse.jdbc.internal;
package com.clickhouse.jdbc;

import java.util.Collections;
import java.util.List;

/**
* JDBC driver specific properties. Should not include any of ClientConfigProperties.
* JDBC driver specific properties. Does not include anything from ClientConfigProperties.
* Processing logic should be the follows
* 1. If property is among DriverProperties then Driver handles it specially and will not pass to a client
* 2. If property is not among DriverProperties then it is passed to a client
Expand Down
51 changes: 29 additions & 22 deletions jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.clickhouse.client.api.query.QueryResponse;
import com.clickhouse.client.api.query.QuerySettings;
import com.clickhouse.client.api.sql.SQLUtils;
import com.clickhouse.jdbc.internal.DriverProperties;
import com.clickhouse.jdbc.internal.ExceptionUtils;
import com.clickhouse.jdbc.internal.ParsedStatement;
import org.slf4j.Logger;
Expand Down Expand Up @@ -66,27 +65,34 @@ protected void ensureOpen() throws SQLException {
}
}


private String parseJdbcEscapeSyntax(String sql) {
LOG.trace("Original SQL: {}", sql);
if (escapeProcessingEnabled) {
// Replace {d 'YYYY-MM-DD'} with corresponding SQL date format
sql = sql.replaceAll("\\{d '([^']*)'\\}", "toDate('$1')");
sql = escapedSQLToNative(sql);
}
LOG.trace("Escaped SQL: {}", sql);
return sql;
}

// Replace {ts 'YYYY-MM-DD HH:mm:ss'} with corresponding SQL timestamp format
sql = sql.replaceAll("\\{ts '([^']*)'\\}", "timestamp('$1')");
public static String escapedSQLToNative(String sql) {
if (sql == null) {
throw new IllegalArgumentException("SQL may not be null");
}
// Replace {d 'YYYY-MM-DD'} with corresponding SQL date format
sql = sql.replaceAll("\\{d '([^']*)'\\}", "toDate('$1')");

// Replace function escape syntax {fn <function>} (e.g., {fn UCASE(name)})
sql = sql.replaceAll("\\{fn ([^\\}]*)\\}", "$1");
// Replace {ts 'YYYY-MM-DD HH:mm:ss'} with corresponding SQL timestamp format
sql = sql.replaceAll("\\{ts '([^']*)'\\}", "timestamp('$1')");

// Handle outer escape syntax
//sql = sql.replaceAll("\\{escape '([^']*)'\\}", "'$1'");
// Replace function escape syntax {fn <function>} (e.g., {fn UCASE(name)})
sql = sql.replaceAll("\\{fn ([^\\}]*)\\}", "$1");

// Handle outer escape syntax
//sql = sql.replaceAll("\\{escape '([^']*)'\\}", "'$1'");

// Note: do not remove new lines because they may be used to delimit comments
// Add more replacements as needed for other JDBC escape sequences

// Clean new empty lines in sql
sql = sql.replaceAll("(?m)^\\s*$\\n?", "");
// Add more replacements as needed for other JDBC escape sequences
}
LOG.trace("Escaped SQL: {}", sql);
return sql;
}

Expand Down Expand Up @@ -138,17 +144,18 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr
lastStatementSql = parseJdbcEscapeSyntax(sql);
LOG.trace("SQL Query: {}", lastStatementSql); // this is not secure for create statements because of passwords
if (queryTimeout == 0) {
response = connection.client.query(lastStatementSql, mergedSettings).get();
response = connection.getClient().query(lastStatementSql, mergedSettings).get();
} else {
response = connection.client.query(lastStatementSql, mergedSettings).get(queryTimeout, TimeUnit.SECONDS);
response = connection.getClient().query(lastStatementSql, mergedSettings).get(queryTimeout, TimeUnit.SECONDS);
}

if (response.getFormat().isText()) {
throw new SQLException("Only RowBinaryWithNameAndTypes is supported for output format. Please check your query.",
ExceptionUtils.SQL_STATE_CLIENT_ERROR);
}
ClickHouseBinaryFormatReader reader = connection.client.newBinaryFormatReader(response);
ClickHouseBinaryFormatReader reader = connection.getClient().newBinaryFormatReader(response);
if (reader.getSchema() == null) {
reader.close();
throw new SQLException("Called method expects empty or filled result set but query has returned none. Consider using `java.sql.Statement.execute(java.lang.String)`", ExceptionUtils.SQL_STATE_CLIENT_ERROR);
}
return new ResultSetImpl(this, response, reader);
Expand Down Expand Up @@ -191,8 +198,8 @@ protected long executeUpdateImpl(String sql, QuerySettings settings) throws SQLE
lastStatementSql = parseJdbcEscapeSyntax(sql);
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)) {
try (QueryResponse response = queryTimeout == 0 ? connection.getClient().query(lastStatementSql, mergedSettings).get()
: connection.getClient().query(lastStatementSql, mergedSettings).get(queryTimeout, TimeUnit.SECONDS)) {
updateCount = Math.max(0, (int) response.getWrittenRows()); // when statement alters schema no result rows returned.
lastQueryId = response.getQueryId();
} catch (Exception e) {
Expand All @@ -202,7 +209,7 @@ protected long executeUpdateImpl(String sql, QuerySettings settings) throws SQLE
return updateCount;
}

protected void postUpdateActions() {
protected void postUpdateActions() throws SQLException {
if (parsedStatement.getUseDatabase() != null) {
this.localSettings.setDatabase(parsedStatement.getUseDatabase());
}
Expand Down Expand Up @@ -278,7 +285,7 @@ public void cancel() throws SQLException {
return;
}

try (QueryResponse response = connection.client.query(String.format("KILL QUERY%sWHERE query_id = '%s'",
try (QueryResponse response = connection.getClient().query(String.format("KILL QUERY%sWHERE query_id = '%s'",
connection.onCluster ? " ON CLUSTER " + connection.cluster + " " : " ",
lastQueryId), connection.getDefaultQuerySettings()).get()){
LOG.debug("Query {} was killed by {}", lastQueryId, response.getQueryId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ public long executeLargeUpdate() throws SQLException {
InputStream in = new ByteArrayInputStream(out.toByteArray());
InsertSettings settings = new InsertSettings();
try (InsertResponse response = queryTimeout == 0 ?
connection.client.insert(tableSchema.getTableName(),in, writer.getFormat(), settings).get()
: connection.client.insert(tableSchema.getTableName(),in, writer.getFormat(), settings).get(queryTimeout, TimeUnit.SECONDS)) {
connection.getClient().insert(tableSchema.getTableName(),in, writer.getFormat(), settings).get()
: connection.getClient().insert(tableSchema.getTableName(),in, writer.getFormat(), settings).get(queryTimeout, TimeUnit.SECONDS)) {
updateCount = Math.max(0, (int) response.getWrittenRows()); // when statement alters schema no result rows returned.
lastQueryId = response.getQueryId();
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
import com.clickhouse.client.api.http.ClickHouseHttpProto;
import com.clickhouse.data.ClickHouseDataType;
import com.clickhouse.jdbc.Driver;
import com.clickhouse.jdbc.DriverProperties;
import com.google.common.collect.ImmutableMap;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.Comparator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
import com.clickhouse.jdbc.Driver;
import com.clickhouse.jdbc.JdbcV2Wrapper;
import com.clickhouse.jdbc.ResultSetImpl;
import com.clickhouse.jdbc.StatementImpl;
import com.clickhouse.jdbc.internal.ClientInfoProperties;
import com.clickhouse.jdbc.internal.DriverProperties;
import com.clickhouse.jdbc.ClientInfoProperties;
import com.clickhouse.jdbc.DriverProperties;
import com.clickhouse.jdbc.internal.ExceptionUtils;
import com.clickhouse.jdbc.internal.JdbcUtils;
import com.clickhouse.jdbc.internal.MetadataResultSet;
import com.clickhouse.jdbc.internal.SqlParser;
import com.clickhouse.logging.Logger;
import com.clickhouse.logging.LoggerFactory;

Expand Down
125 changes: 95 additions & 30 deletions jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@
import com.clickhouse.client.api.ClientConfigProperties;
import com.clickhouse.client.api.ServerException;
import com.clickhouse.client.api.internal.ServerSettings;
import com.clickhouse.jdbc.internal.ClientInfoProperties;
import com.clickhouse.jdbc.internal.DriverProperties;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.common.ConsoleNotifier;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;

import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
Expand All @@ -32,31 +29,75 @@
import java.util.Properties;
import java.util.UUID;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertThrows;
import static org.testng.Assert.fail;

public class ConnectionTest extends JdbcIntegrationTest {

@Test(groups = { "integration" }, enabled = false)
@Test(groups = { "integration" })
public void createAndCloseStatementTest() throws SQLException {
Connection localConnection = this.getJdbcConnection();
Statement statement = localConnection.createStatement();
Assert.assertNotNull(statement);
Connection conn = getJdbcConnection();
Statement stmt = conn.createStatement();
PreparedStatement pStmt = conn.prepareStatement("SELECT ? as v");
pStmt.setString(1, "test string");
conn.close();
conn.close(); // check second attempt doesn't throw anything
assertThrows(SQLException.class, conn::createStatement);

try {
stmt.executeQuery("SELECT 1");
fail("Exception expected");
} catch (SQLException e) {
Assert.assertTrue(e.getMessage().contains("closed"));
}

try {
pStmt.executeQuery();
fail("Exception expected");
} catch (SQLException e) {
Assert.assertTrue(e.getMessage().contains("closed"));

assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY));
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT));
}
}

@Test(groups = { "integration" }, enabled = false)
public void prepareStatementTest() throws SQLException {
Connection localConnection = this.getJdbcConnection();
PreparedStatement statement = localConnection.prepareStatement("SELECT 1");
Assert.assertNotNull(statement);
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.prepareStatement("SELECT 1", Statement.RETURN_GENERATED_KEYS));
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.prepareStatement("SELECT 1", new int[] { 1 }));
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.prepareStatement("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY));
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.prepareStatement("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT));
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.prepareStatement("SELECT 1", new String[] { "1" }));
@Test(groups = { "integration" })
public void testCreateUnsupportedStatements() throws Throwable {

boolean[] throwUnsupportedException = new boolean[] {false, true};

for (boolean flag : throwUnsupportedException) {
Properties props = new Properties();
if (flag) {
props.setProperty(DriverProperties.IGNORE_UNSUPPORTED_VALUES.getKey(), "true");
}

try (Connection conn = this.getJdbcConnection(props)) {
Assert.ThrowingRunnable[] createStatements = new Assert.ThrowingRunnable[]{
() -> conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY),
() -> conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE),
() -> conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT),
() -> conn.prepareStatement("SELECT 1", Statement.RETURN_GENERATED_KEYS),
() -> conn.prepareStatement("SELECT 1", new int[]{1}),
() -> conn.prepareStatement("SELECT 1", new String[]{"1"}),
() -> conn.prepareStatement("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE),
() -> conn.prepareStatement("SELECT 1", ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY),
() -> conn.prepareStatement("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT),
conn::setSavepoint,
() -> conn.setSavepoint("save point"),
() -> conn.createStruct("simple", null),
};

for (Assert.ThrowingRunnable createStatement : createStatements) {
if (!flag) {
Assert.assertThrows(SQLFeatureNotSupportedException.class, createStatement );
} else {
createStatement.run();
}
}
}
}
}

@Test(groups = { "integration" })
Expand All @@ -67,12 +108,16 @@ public void prepareCallTest() throws SQLException {
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.prepareCall("SELECT 1", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT));
}

@Test(groups = { "integration" }, enabled = false)
@Test(groups = { "integration" })
public void nativeSQLTest() throws SQLException {
// TODO: implement
Connection localConnection = this.getJdbcConnection();
String sql = "SELECT 1";
Assert.assertEquals(localConnection.nativeSQL(sql), sql);
try (Connection conn = this.getJdbcConnection()) {
String escapedSQL = "SELECT \n{ts '2024-01-02 02:01:01'} as v1,\n {d '2024-01-02 02:01:01'} as v2,\n {d ?} as v3";
String nativeSQL = "SELECT \ntimestamp('2024-01-02 02:01:01') as v1,\n toDate('2024-01-02 02:01:01') as v2,\n {d ?} as v3";
Assert.assertEquals(conn.nativeSQL(escapedSQL), nativeSQL);

Assert.expectThrows(IllegalArgumentException.class, () -> conn.nativeSQL(null));
Assert.assertEquals(conn.nativeSQL("SELECT 1 as t"), "SELECT 1 as t");
}
}

@Test(groups = { "integration" })
Expand All @@ -97,8 +142,8 @@ public void setAutoCommitTest() throws SQLException {
@Test(groups = { "integration" })
public void testCommitRollback() throws SQLException {
try (Connection localConnection = this.getJdbcConnection()) {
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.commit());
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.rollback());
assertThrows(SQLFeatureNotSupportedException.class, localConnection::commit);
assertThrows(SQLFeatureNotSupportedException.class, localConnection::rollback);
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.rollback(null));
}

Expand Down Expand Up @@ -183,7 +228,7 @@ public void clearWarningsTest() throws SQLException {
@Test(groups = { "integration" })
public void getTypeMapTest() throws SQLException {
Connection localConnection = this.getJdbcConnection();
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.getTypeMap());
assertThrows(SQLFeatureNotSupportedException.class, localConnection::getTypeMap);
}

@Test(groups = { "integration" })
Expand All @@ -207,7 +252,7 @@ public void getHoldabilityTest() throws SQLException {
@Test(groups = { "integration" })
public void setSavepointTest() throws SQLException {
Connection localConnection = this.getJdbcConnection();
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.setSavepoint());
assertThrows(SQLFeatureNotSupportedException.class, localConnection::setSavepoint);
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.setSavepoint("savepoint-name"));
}

Expand Down Expand Up @@ -268,7 +313,6 @@ public void setAndGetClientInfoTest(String clientName) throws SQLException {
try (ResultSet rs = stmt.executeQuery(logQuery)) {
Assert.assertTrue(rs.next());
String userAgent = rs.getString("http_user_agent");
System.out.println(userAgent);
if (clientName != null && !clientName.isEmpty()) {
Assert.assertTrue(userAgent.startsWith(clientName), "Expected to start with '" + clientName + "' but value was '" + userAgent + "'");
}
Expand Down Expand Up @@ -353,7 +397,7 @@ public void setNetworkTimeoutTest() throws SQLException {
@Test(groups = { "integration" })
public void getNetworkTimeoutTest() throws SQLException {
Connection localConnection = this.getJdbcConnection();
assertThrows(SQLFeatureNotSupportedException.class, () -> localConnection.getNetworkTimeout());
assertThrows(SQLFeatureNotSupportedException.class, localConnection::getNetworkTimeout);
}

@Test(groups = { "integration" })
Expand Down Expand Up @@ -606,4 +650,25 @@ private static Object[][] createValidDatabaseNames() {
};
}

@Test(groups = {"integration"})
public void testClientInfoProperties() throws Exception {
try (Connection conn = this.getJdbcConnection()) {

Properties properties = conn.getClientInfo();
assertEquals(properties.get(ClientInfoProperties.APPLICATION_NAME.getKey()), "");

properties.put(ClientInfoProperties.APPLICATION_NAME.getKey(), "test");
conn.setClientInfo(properties);

assertEquals(properties.get(ClientInfoProperties.APPLICATION_NAME.getKey()), "test");

conn.setClientInfo(new Properties());
assertNull(conn.getClientInfo(ClientInfoProperties.APPLICATION_NAME.getKey()));

conn.setClientInfo(ClientInfoProperties.APPLICATION_NAME.getKey(), "test 2");
assertEquals(conn.getClientInfo(ClientInfoProperties.APPLICATION_NAME.getKey()), "test 2");

assertNull(conn.getClientInfo("unknown"));
}
}
}
Loading
Loading