Skip to content

Commit 8d21c0e

Browse files
committed
improvements:
- PreparedStatementImpl: getMetaData returns result set metadata using describe query - StatementImpl: maxRows handling - Driver: unload method fixed (DriverManager search driver for unregister by reference equality) - HttpAPIClientHelper: added sslmode option (presents in v1), support to skip ssl validation fixes: - PreparedStatementImpl: ignores question mark (?) in comments, string literals and quoted identifiers - StatementImpl: multiline block comments trim - HttpAPIClientHelper: null pointer exception with unpooled connection (poolControl is null)
1 parent da90573 commit 8d21c0e

File tree

9 files changed

+251
-25
lines changed

9 files changed

+251
-25
lines changed

client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ public enum ClientConfigProperties {
9292

9393
SSL_KEY_STORE_PASSWORD("key_store_password"),
9494

95+
SSL_MODE("sslmode", "strict", Arrays.asList("strict", "none")),
96+
9597
SSL_KEY("ssl_key"),
9698

9799
CA_CERTIFICATE("sslrootcert"),

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,11 @@
5858
import org.slf4j.Logger;
5959
import org.slf4j.LoggerFactory;
6060

61+
import javax.net.ssl.KeyManager;
6162
import javax.net.ssl.SSLContext;
6263
import javax.net.ssl.SSLException;
64+
import javax.net.ssl.TrustManager;
65+
import javax.net.ssl.X509TrustManager;
6366
import java.io.IOException;
6467
import java.io.InputStream;
6568
import java.io.OutputStream;
@@ -74,7 +77,10 @@
7477
import java.net.URL;
7578
import java.net.UnknownHostException;
7679
import java.nio.charset.StandardCharsets;
80+
import java.security.KeyManagementException;
7781
import java.security.NoSuchAlgorithmException;
82+
import java.security.SecureRandom;
83+
import java.security.cert.X509Certificate;
7884
import java.util.Base64;
7985
import java.util.Collection;
8086
import java.util.Collections;
@@ -169,6 +175,13 @@ public SSLContext createSSLContext() {
169175
} catch (SSLException e) {
170176
throw new ClientMisconfigurationException("Failed to create SSL context from certificates", e);
171177
}
178+
} else if ("none".equals(chConfiguration.get(ClientConfigProperties.SSL_MODE.getKey()))) {
179+
try {
180+
sslContext = SSLContext.getInstance("TLS");
181+
sslContext.init(new KeyManager[0], new TrustManager[]{new TrustAllManager()}, new SecureRandom());
182+
} catch (NoSuchAlgorithmException | KeyManagementException e) {
183+
throw new ClientException("Failed to create none validating SSL context", e);
184+
}
172185
}
173186
return sslContext;
174187
}
@@ -380,7 +393,7 @@ public Exception readError(ClassicHttpResponse httpResponse) {
380393

381394
public ClassicHttpResponse executeRequest(ClickHouseNode server, Map<String, Object> requestConfig, LZ4Factory lz4Factory,
382395
IOCallback<OutputStream> writeCallback) throws IOException {
383-
if (timeToPoolVent.get() < System.currentTimeMillis()) {
396+
if (poolControl != null && timeToPoolVent.get() < System.currentTimeMillis()) {
384397
timeToPoolVent.set(System.currentTimeMillis() + POOL_VENT_TIMEOUT);
385398
poolControl.closeExpired();
386399
}
@@ -828,4 +841,22 @@ public long getTime() {
828841
return count > 0 ? runningAverage / count : 0;
829842
}
830843
}
844+
845+
private static final class TrustAllManager implements X509TrustManager {
846+
847+
@Override
848+
public void checkClientTrusted(X509Certificate[] chain, String authType) {
849+
// ignore
850+
}
851+
852+
@Override
853+
public void checkServerTrusted(X509Certificate[] chain, String authType) {
854+
// ignore
855+
}
856+
857+
@Override
858+
public X509Certificate[] getAcceptedIssuers() {
859+
return new X509Certificate[0];
860+
}
861+
}
831862
}

client-v2/src/test/java/com/clickhouse/client/ClientTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.clickhouse.client;
22

33
import com.clickhouse.client.api.Client;
4+
import com.clickhouse.client.api.ClientConfigProperties;
45
import com.clickhouse.client.api.ClientException;
56
import com.clickhouse.client.api.enums.Protocol;
67
import com.clickhouse.client.api.query.GenericRecord;
@@ -64,6 +65,12 @@ private static Client[] secureClientProvider() throws Exception {
6465
.setPassword("")
6566
.setRootCertificate("containers/clickhouse-server/certs/localhost.crt")
6667
.build(),
68+
new Client.Builder()
69+
.addEndpoint("https://" + node.getHost() + ":" + node.getPort())
70+
.setUsername("default")
71+
.setPassword("")
72+
.setOption(ClientConfigProperties.SSL_MODE.getKey(), "none")
73+
.build(),
6774
new Client.Builder()
6875
.addEndpoint(Protocol.HTTP, node.getHost(), node.getPort(), true)
6976
.setUsername("default")
@@ -108,6 +115,13 @@ public void testPing() {
108115
}
109116
}
110117

118+
@Test
119+
public void testPingUnpooled() {
120+
try (Client client = newClient().enableConnectionPool(false).build()) {
121+
Assert.assertTrue(client.ping());
122+
}
123+
}
124+
111125
@Test
112126
public void testPingFailure() {
113127
try (Client client = new Client.Builder()

jdbc-v2/src/main/java/com/clickhouse/jdbc/Driver.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,15 @@ public Driver(DataSourceImpl dataSourceImpl) {
9898

9999
public static void load() {
100100
try {
101-
DriverManager.registerDriver(new Driver());
101+
DriverManager.registerDriver(DriverHolder.INSTANCE);
102102
} catch (SQLException e) {
103103
log.error("Failed to register ClickHouse JDBC driver", e);
104104
}
105105
}
106106

107107
public static void unload() {
108108
try {
109-
DriverManager.deregisterDriver(new Driver());
109+
DriverManager.deregisterDriver(DriverHolder.INSTANCE);
110110
} catch (SQLException e) {
111111
log.error("Failed to deregister ClickHouse JDBC driver", e);
112112
}
@@ -163,4 +163,8 @@ public static String chSettingKey(String key) {
163163
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
164164
throw new SQLFeatureNotSupportedException("Method not supported", ExceptionUtils.SQL_STATE_FEATURE_NOT_SUPPORTED);
165165
}
166+
167+
private static final class DriverHolder {
168+
private static final Driver INSTANCE = new Driver();
169+
}
166170
}

jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.clickhouse.jdbc;
22

3+
import com.clickhouse.client.api.metadata.TableSchema;
4+
import com.clickhouse.data.ClickHouseColumn;
35
import com.clickhouse.data.Tuple;
46
import com.clickhouse.jdbc.internal.ExceptionUtils;
57
import org.slf4j.Logger;
@@ -26,6 +28,7 @@
2628
import java.sql.SQLFeatureNotSupportedException;
2729
import java.sql.SQLType;
2830
import java.sql.SQLXML;
31+
import java.sql.Statement;
2932
import java.sql.Time;
3033
import java.sql.Timestamp;
3134
import java.sql.Types;
@@ -42,7 +45,6 @@
4245
import java.util.ArrayList;
4346
import java.util.Calendar;
4447
import java.util.Collection;
45-
import java.util.GregorianCalendar;
4648
import java.util.List;
4749
import java.util.Map;
4850

@@ -58,17 +60,18 @@ public class PreparedStatementImpl extends StatementImpl implements PreparedStat
5860
private final Calendar defaultCalendar;
5961

6062
String originalSql;
61-
String [] sqlSegments;
62-
String [] valueSegments;
63-
Object [] parameters;
63+
String[] sqlSegments;
64+
String[] valueSegments;
65+
Object[] parameters;
6466
String insertIntoSQL;
6567

6668
StatementType statementType;
69+
6770
public PreparedStatementImpl(ConnectionImpl connection, String sql) throws SQLException {
6871
super(connection);
6972
this.originalSql = sql.trim();
7073
//Split the sql string into an array of strings around question mark tokens
71-
this.sqlSegments = originalSql.split("\\?");
74+
this.sqlSegments = splitStatement(originalSql);
7275
this.statementType = parseStatementType(originalSql);
7376

7477
if (statementType == StatementType.INSERT) {
@@ -77,17 +80,11 @@ public PreparedStatementImpl(ConnectionImpl connection, String sql) throws SQLEx
7780
}
7881

7982
//Create an array of objects to store the parameters
80-
if (originalSql.contains("?")) {
81-
int count = originalSql.length() - originalSql.replace("?", "").length();
82-
this.parameters = new Object[count];
83-
} else {
84-
this.parameters = new Object[0];
85-
}
86-
83+
this.parameters = new Object[sqlSegments.length - 1];
8784
this.defaultCalendar = connection.defaultCalendar;
8885
}
8986

90-
private String compileSql(String []segments) {
87+
private String compileSql(String[] segments) {
9188
StringBuilder sb = new StringBuilder();
9289
for (int i = 0; i < segments.length; i++) {
9390
sb.append(segments[i]);
@@ -98,6 +95,7 @@ private String compileSql(String []segments) {
9895
LOG.trace("Compiled SQL: {}", sb);
9996
return sb.toString();
10097
}
98+
10199
@Override
102100
public ResultSet executeQuery() throws SQLException {
103101
checkClosed();
@@ -305,7 +303,26 @@ public void setArray(int parameterIndex, Array x) throws SQLException {
305303
@Override
306304
public ResultSetMetaData getMetaData() throws SQLException {
307305
checkClosed();
308-
return null;
306+
if (this.currentResultSet != null) {
307+
return currentResultSet.getMetaData();
308+
} else if (statementType != StatementType.SELECT) {
309+
return null;
310+
}
311+
312+
String sql = compileSql(sqlSegments);
313+
String describe = String.format("describe (\n%s\n)", sql);
314+
315+
List<ClickHouseColumn> columns = new ArrayList<>();
316+
try (Statement stmt = connection.createStatement()) {
317+
try (ResultSet rs = stmt.executeQuery(describe)) {
318+
while (rs.next()) {
319+
ClickHouseColumn column = ClickHouseColumn.of(rs.getString(1), rs.getString(2));
320+
columns.add(column);
321+
}
322+
}
323+
}
324+
TableSchema schema = new TableSchema(columns);
325+
return new com.clickhouse.jdbc.metadata.ResultSetMetaData(schema);
309326
}
310327

311328
@Override
@@ -517,14 +534,14 @@ private static String encodeObject(Object x) throws SQLException {
517534
} else if (x instanceof ZonedDateTime) {
518535
return encodeObject(((ZonedDateTime) x).toInstant());
519536
} else if (x instanceof Instant) {
520-
return "fromUnixTimestamp64Nano(" + (((Instant) x).getEpochSecond() * 1_000_000_000L + ((Instant) x).getNano())+ ")";
537+
return "fromUnixTimestamp64Nano(" + (((Instant) x).getEpochSecond() * 1_000_000_000L + ((Instant) x).getNano()) + ")";
521538
} else if (x instanceof InetAddress) {
522539
return "'" + ((InetAddress) x).getHostAddress() + "'";
523540
} else if (x instanceof Array) {
524541
StringBuilder listString = new StringBuilder();
525542
listString.append("[");
526543
int i = 0;
527-
for (Object item : (Object[])((Array) x).getArray()) {
544+
for (Object item : (Object[]) ((Array) x).getArray()) {
528545
if (i > 0) {
529546
listString.append(", ");
530547
}
@@ -592,7 +609,7 @@ private static String encodeObject(Object x) throws SQLException {
592609
StringBuilder tupleString = new StringBuilder();
593610
tupleString.append("(");
594611
Tuple t = (Tuple) x;
595-
Object [] values = t.getValues();
612+
Object[] values = t.getValues();
596613
int i = 0;
597614
for (Object item : values) {
598615
if (i > 0) {
@@ -616,4 +633,70 @@ private static String encodeObject(Object x) throws SQLException {
616633
private static String escapeString(String x) {
617634
return x.replace("\\", "\\\\").replace("'", "\\'");//Escape single quotes
618635
}
636+
637+
private static String[] splitStatement(String sql) {
638+
List<String> segments = new ArrayList<>();
639+
char[] chars = sql.toCharArray();
640+
int segmentStart = 0;
641+
for (int i = 0; i < chars.length; i++) {
642+
char c = chars[i];
643+
if (c == '\'' || c == '"' || c == '`') {
644+
// string literal or identifier
645+
i = skip(chars, i + 1, c, true);
646+
} else if (c == '/' && lookahead(chars, i) == '*') {
647+
// block comment
648+
int end = sql.indexOf("*/", i);
649+
if (end == -1) {
650+
// missing comment end
651+
break;
652+
}
653+
i = end + 1;
654+
} else if (c == '#' || (c == '-' && lookahead(chars, i) == '-')) {
655+
// line comment
656+
i = skip(chars, i + 1, '\n', false);
657+
} else if (c == '?') {
658+
// question mark
659+
segments.add(sql.substring(segmentStart, i));
660+
segmentStart = i + 1;
661+
}
662+
}
663+
if (segmentStart < chars.length) {
664+
segments.add(sql.substring(segmentStart));
665+
} else {
666+
// add empty segment in case question mark was last char of sql
667+
segments.add("");
668+
}
669+
return segments.toArray(new String[0]);
670+
}
671+
672+
private static int skip(char[] chars, int from, char until, boolean escape) {
673+
for (int i = from; i < chars.length; i++) {
674+
char curr = chars[i];
675+
if (escape) {
676+
char next = lookahead(chars, i);
677+
if ((curr == '\\' && (next == '\\' || next == until)) || (curr == until && next == until)) {
678+
// should skip:
679+
// 1) double \\ (backslash escaped with backslash)
680+
// 2) \[until] ([until] char, escaped with backslash)
681+
// 3) [until][until] ([until] char, escaped with [until])
682+
i++;
683+
continue;
684+
}
685+
}
686+
687+
if (curr == until) {
688+
return i;
689+
}
690+
}
691+
return chars.length;
692+
}
693+
694+
private static char lookahead(char[] chars, int pos) {
695+
pos = pos + 1;
696+
if (pos >= chars.length) {
697+
return '\0';
698+
}
699+
return chars[pos];
700+
}
701+
619702
}

jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
import java.util.List;
2525
import java.util.UUID;
2626
import java.util.concurrent.TimeUnit;
27+
import java.util.regex.Pattern;
2728

2829
public class StatementImpl implements Statement, JdbcV2Wrapper {
2930
private static final Logger LOG = LoggerFactory.getLogger(StatementImpl.class);
3031

3132
ConnectionImpl connection;
3233
private int queryTimeout;
3334
protected boolean closed;
34-
private ResultSetImpl currentResultSet;
35+
protected ResultSetImpl currentResultSet;
3536
private OperationMetrics metrics;
3637
protected List<String> batch;
3738
private String lastSql;
@@ -70,7 +71,7 @@ protected static StatementType parseStatementType(String sql) {
7071
return StatementType.OTHER;
7172
}
7273

73-
trimmedSql = trimmedSql.replaceAll("/\\*.*?\\*/", "").trim(); // remove comments
74+
trimmedSql = BLOCK_COMMENT.matcher(trimmedSql).replaceAll("").trim(); // remove comments
7475
String[] lines = trimmedSql.split("\n");
7576
for (String line : lines) {
7677
String trimmedLine = line.trim();
@@ -172,7 +173,10 @@ public ResultSetImpl executeQuery(String sql, QuerySettings settings) throws SQL
172173
closePreviousResultSet();
173174

174175
QuerySettings mergedSettings = QuerySettings.merge(connection.getDefaultQuerySettings(), settings);
175-
176+
if (maxRows > 0) {
177+
mergedSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.MAX_RESULT_ROWS), maxRows);
178+
mergedSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.RESULT_OVERFLOW_MODE), "break");
179+
}
176180

177181
if (mergedSettings.getQueryId() != null) {
178182
lastQueryId = mergedSettings.getQueryId();
@@ -627,4 +631,6 @@ public String enquoteNCharLiteral(String val) throws SQLException {
627631
public String getLastQueryId() {
628632
return lastQueryId;
629633
}
634+
635+
private static final Pattern BLOCK_COMMENT = Pattern.compile("/\\*.*?\\*/", Pattern.DOTALL);
630636
}

0 commit comments

Comments
 (0)