11package com .clickhouse .jdbc ;
22
3+ import com .clickhouse .client .api .metadata .TableSchema ;
4+ import com .clickhouse .data .ClickHouseColumn ;
35import com .clickhouse .data .Tuple ;
46import com .clickhouse .jdbc .internal .ExceptionUtils ;
57import org .slf4j .Logger ;
2628import java .sql .SQLFeatureNotSupportedException ;
2729import java .sql .SQLType ;
2830import java .sql .SQLXML ;
31+ import java .sql .Statement ;
2932import java .sql .Time ;
3033import java .sql .Timestamp ;
3134import java .sql .Types ;
4245import java .util .ArrayList ;
4346import java .util .Calendar ;
4447import java .util .Collection ;
45- import java .util .GregorianCalendar ;
4648import java .util .List ;
4749import 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}
0 commit comments