1616
1717import java .sql .Connection ;
1818import java .sql .JDBCType ;
19+ import java .sql .PreparedStatement ;
1920import java .sql .ResultSet ;
2021import java .sql .RowIdLifetime ;
2122import java .sql .SQLException ;
2223import java .sql .SQLFeatureNotSupportedException ;
2324import java .sql .SQLType ;
2425import java .sql .Statement ;
2526import java .sql .Types ;
27+ import java .util .ArrayList ;
2628import java .util .Arrays ;
29+ import java .util .Collection ;
2730import java .util .Collections ;
2831import java .util .List ;
2932import java .util .Map ;
33+ import java .util .Set ;
3034import java .util .function .Consumer ;
35+ import java .util .stream .Collectors ;
3136
3237public class DatabaseMetaDataImpl implements java .sql .DatabaseMetaData , JdbcV2Wrapper {
3338 private static final Logger log = LoggerFactory .getLogger (DatabaseMetaDataImpl .class );
@@ -38,12 +43,13 @@ public class DatabaseMetaDataImpl implements java.sql.DatabaseMetaData, JdbcV2Wr
3843 public enum TableType {
3944 DICTIONARY ("DICTIONARY" ),
4045 LOG_TABLE ("LOG TABLE" ),
46+ MATERIALIZED_VIEW ("MATERIALIZED VIEW" ),
4147 MEMORY_TABLE ("MEMORY TABLE" ),
4248 REMOTE_TABLE ("REMOTE TABLE" ),
43- TABLE ("TABLE" ),
44- VIEW ("VIEW" ),
4549 SYSTEM_TABLE ("SYSTEM TABLE" ),
46- TEMPORARY_TABLE ("TEMPORARY TABLE" );
50+ TABLE ("TABLE" ),
51+ TEMPORARY_TABLE ("TEMPORARY TABLE" ),
52+ VIEW ("VIEW" );
4753
4854 private final String typeName ;
4955
@@ -55,8 +61,9 @@ public String getTypeName() {
5561 return typeName ;
5662 }
5763 }
58- public static final String [] TABLE_TYPES = new String [] { "DICTIONARY" , "LOG TABLE" , "MEMORY TABLE" ,
59- "REMOTE TABLE" , "TABLE" , "VIEW" , "SYSTEM TABLE" , "TEMPORARY TABLE" };
64+
65+ static final Set <String > TABLE_TYPES = Arrays .stream (TableType .values ()).map (TableType ::getTypeName ).collect (Collectors .toSet ());
66+ private static final String TEMPORARY_ENGINE_PREFIX = "Temporary" ;
6067
6168 private static final String DATABASE_PRODUCT_NAME = "ClickHouse" ;
6269 private static final String DRIVER_NAME = DATABASE_PRODUCT_NAME + " JDBC Driver" ;
@@ -764,6 +771,164 @@ public ResultSet getProcedureColumns(String catalog, String schemaPattern, Strin
764771 }
765772 }
766773
774+ // Map of ClickHouse engine names to JDBC table types
775+ static final Map <String , String > ENGINE_TO_TABLE_TYPE ;
776+ static {
777+ Map <String , String > map = new java .util .HashMap <>();
778+
779+ // Log tables
780+ map .put ("Log" , TableType .LOG_TABLE .getTypeName ());
781+ map .put ("StripeLog" , TableType .LOG_TABLE .getTypeName ());
782+ map .put ("TinyLog" , TableType .LOG_TABLE .getTypeName ());
783+
784+ // Memory tables
785+ map .put ("Buffer" , TableType .MEMORY_TABLE .getTypeName ());
786+ map .put ("Memory" , TableType .MEMORY_TABLE .getTypeName ());
787+ map .put ("Set" , TableType .MEMORY_TABLE .getTypeName ());
788+
789+ // Views
790+ map .put ("View" , TableType .VIEW .getTypeName ());
791+ map .put ("LiveView" , TableType .VIEW .getTypeName ());
792+ map .put ("MaterializedView" , TableType .MATERIALIZED_VIEW .getTypeName ());
793+ map .put ("WindowView" , TableType .VIEW .getTypeName ());
794+
795+ // Dictionary
796+ map .put ("Dictionary" , TableType .DICTIONARY .getTypeName ());
797+
798+ // Remote/External tables
799+ map .put ("AzureBlobStorage" , TableType .REMOTE_TABLE .getTypeName ());
800+ map .put ("AzureQueue" , TableType .REMOTE_TABLE .getTypeName ());
801+ map .put ("ArrowFlight" , TableType .REMOTE_TABLE .getTypeName ());
802+ map .put ("DeltaLake" , TableType .REMOTE_TABLE .getTypeName ());
803+ map .put ("DeltaLakeAzure" , TableType .REMOTE_TABLE .getTypeName ());
804+ map .put ("DeltaLakeLocal" , TableType .REMOTE_TABLE .getTypeName ());
805+ map .put ("DeltaLakeS3" , TableType .REMOTE_TABLE .getTypeName ());
806+ map .put ("Distributed" , TableType .REMOTE_TABLE .getTypeName ());
807+ map .put ("GCS" , TableType .REMOTE_TABLE .getTypeName ());
808+ map .put ("HDFS" , TableType .REMOTE_TABLE .getTypeName ());
809+ map .put ("Hive" , TableType .REMOTE_TABLE .getTypeName ());
810+ map .put ("Hudi" , TableType .REMOTE_TABLE .getTypeName ());
811+ map .put ("Iceberg" , TableType .REMOTE_TABLE .getTypeName ());
812+ map .put ("IcebergAzure" , TableType .REMOTE_TABLE .getTypeName ());
813+ map .put ("IcebergHDFS" , TableType .REMOTE_TABLE .getTypeName ());
814+ map .put ("IcebergLocal" , TableType .REMOTE_TABLE .getTypeName ());
815+ map .put ("IcebergS3" , TableType .REMOTE_TABLE .getTypeName ());
816+ map .put ("JDBC" , TableType .REMOTE_TABLE .getTypeName ());
817+ map .put ("Kafka" , TableType .REMOTE_TABLE .getTypeName ());
818+ map .put ("MaterializedPostgreSQL" , TableType .REMOTE_TABLE .getTypeName ());
819+ map .put ("MongoDB" , TableType .REMOTE_TABLE .getTypeName ());
820+ map .put ("MySQL" , TableType .REMOTE_TABLE .getTypeName ());
821+ map .put ("NATS" , TableType .REMOTE_TABLE .getTypeName ());
822+ map .put ("ODBC" , TableType .REMOTE_TABLE .getTypeName ());
823+ map .put ("OSS" , TableType .REMOTE_TABLE .getTypeName ());
824+ map .put ("PostgreSQL" , TableType .REMOTE_TABLE .getTypeName ());
825+ map .put ("RabbitMQ" , TableType .REMOTE_TABLE .getTypeName ());
826+ map .put ("Redis" , TableType .REMOTE_TABLE .getTypeName ());
827+ map .put ("S3" , TableType .REMOTE_TABLE .getTypeName ());
828+ map .put ("S3Queue" , TableType .REMOTE_TABLE .getTypeName ());
829+ map .put ("URL" , TableType .REMOTE_TABLE .getTypeName ());
830+ map .put ("YTsaurus" , TableType .REMOTE_TABLE .getTypeName ());
831+
832+ // Regular tables (MergeTree family and others)
833+ map .put ("AggregatingMergeTree" , TableType .TABLE .getTypeName ());
834+ map .put ("Alias" , TableType .TABLE .getTypeName ());
835+ map .put ("CoalescingMergeTree" , TableType .TABLE .getTypeName ());
836+ map .put ("CollapsingMergeTree" , TableType .TABLE .getTypeName ());
837+ map .put ("EmbeddedRocksDB" , TableType .TABLE .getTypeName ());
838+ map .put ("Executable" , TableType .TABLE .getTypeName ());
839+ map .put ("ExecutablePool" , TableType .TABLE .getTypeName ());
840+ map .put ("GraphiteMergeTree" , TableType .TABLE .getTypeName ());
841+ map .put ("Join" , TableType .TABLE .getTypeName ());
842+ map .put ("KeeperMap" , TableType .TABLE .getTypeName ());
843+ map .put ("Merge" , TableType .TABLE .getTypeName ());
844+ map .put ("MergeTree" , TableType .TABLE .getTypeName ());
845+ map .put ("ReplacingMergeTree" , TableType .TABLE .getTypeName ());
846+ map .put ("ReplicatedAggregatingMergeTree" , TableType .TABLE .getTypeName ());
847+ map .put ("ReplicatedCoalescingMergeTree" , TableType .TABLE .getTypeName ());
848+ map .put ("ReplicatedCollapsingMergeTree" , TableType .TABLE .getTypeName ());
849+ map .put ("ReplicatedGraphiteMergeTree" , TableType .TABLE .getTypeName ());
850+ map .put ("ReplicatedMergeTree" , TableType .TABLE .getTypeName ());
851+ map .put ("ReplicatedReplacingMergeTree" , TableType .TABLE .getTypeName ());
852+ map .put ("ReplicatedSummingMergeTree" , TableType .TABLE .getTypeName ());
853+ map .put ("ReplicatedVersionedCollapsingMergeTree" , TableType .TABLE .getTypeName ());
854+ map .put ("SummingMergeTree" , TableType .TABLE .getTypeName ());
855+ map .put ("VersionedCollapsingMergeTree" , TableType .TABLE .getTypeName ());
856+ map .put ("COSN" , TableType .TABLE .getTypeName ());
857+ map .put ("SharedAggregatingMergeTree" , TableType .TABLE .getTypeName ());
858+ map .put ("SharedCoalescingMergeTree" , TableType .TABLE .getTypeName ());
859+ map .put ("SharedCollapsingMergeTree" , TableType .TABLE .getTypeName ());
860+ map .put ("SharedGraphiteMergeTree" , TableType .TABLE .getTypeName ());
861+ map .put ("SharedJoin" , TableType .TABLE .getTypeName ());
862+ map .put ("SharedMergeTree" , TableType .TABLE .getTypeName ());
863+ map .put ("SharedReplacingMergeTree" , TableType .TABLE .getTypeName ());
864+ map .put ("SharedSet" , TableType .TABLE .getTypeName ());
865+ map .put ("SharedSummingMergeTree" , TableType .TABLE .getTypeName ());
866+ map .put ("SharedVersionedCollapsingMergeTree" , TableType .TABLE .getTypeName ());
867+
868+ // Special
869+ map .put ("TimeSeries" , TableType .TABLE .getTypeName ());
870+ map .put ("Null" , TableType .TABLE .getTypeName ());
871+ map .put ("Loop" , TableType .TABLE .getTypeName ());
872+ map .put ("SQLite" , TableType .TABLE .getTypeName ());
873+ map .put ("File" , TableType .TABLE .getTypeName ());
874+ map .put ("FileLog" , TableType .TABLE .getTypeName ());
875+ map .put ("GenerateRandom" , TableType .TABLE .getTypeName ());
876+ map .put ("FuzzJSON" , TableType .TABLE .getTypeName ());
877+ map .put ("FuzzQuery" , TableType .TABLE .getTypeName ());
878+
879+
880+ ENGINE_TO_TABLE_TYPE = Collections .unmodifiableMap (map );
881+ }
882+
883+ /**
884+ * Converts engine name to table type. Returns TABLE as default for unknown engines.
885+ * Unknown engines are mapped to table to let user get them anyway.
886+ */
887+ private static String engineToTableType (String engine ) {
888+ if (engine == null ) {
889+ return TableType .TABLE .getTypeName ();
890+ }
891+ // Check for system tables (engines starting with "System" or "Async")
892+ if (isSystemTableEngine (engine )) {
893+ return TableType .SYSTEM_TABLE .getTypeName ();
894+ }
895+ return ENGINE_TO_TABLE_TYPE .getOrDefault (engine , TableType .TABLE .getTypeName ());
896+ }
897+
898+ /**
899+ * Returns set of engines that map to any of the given table types.
900+ */
901+ private static Set <String > getEnginesForTableTypes (final Set <String > requestedTypes ) {
902+ return ENGINE_TO_TABLE_TYPE .entrySet ().stream ()
903+ .filter (entry -> requestedTypes .contains (entry .getValue ()))
904+ .map (Map .Entry ::getKey )
905+ .collect (Collectors .toSet ());
906+ }
907+
908+ public static boolean isSystemTableEngine (String engine ) {
909+ return engine != null && (engine .startsWith ("System" ) || engine .startsWith ("Async" ));
910+ }
911+
912+ private static final String TABLE_TYPE_COL_IN_GET_TABLES = "TABLE_TYPE" ;
913+
914+ private static final Consumer <Map <String , Object >> TABLE_TYPE_MUTATOR = row -> {
915+ String engine = (String ) row .get (TABLE_TYPE_COL_IN_GET_TABLES );
916+
917+ String tableType ;
918+ if (engine != null && engine .startsWith (TEMPORARY_ENGINE_PREFIX )) {
919+ tableType = TableType .TEMPORARY_TABLE .getTypeName ();
920+ } else if (isSystemTableEngine (engine )) {
921+ tableType = TableType .SYSTEM_TABLE .getTypeName ();
922+ } else {
923+ tableType = engineToTableType (engine );
924+ }
925+
926+ row .put (TABLE_TYPE_COL_IN_GET_TABLES , tableType );
927+ };
928+
929+ private static final Collection <Consumer <Map <String , Object >>> GET_TABLES_MUTATORS = Collections .singletonList (TABLE_TYPE_MUTATOR );
930+
931+
767932 /**
768933 * Returns tables defined for a schema. Parameter {@code catalog} is ignored
769934 *
@@ -774,24 +939,47 @@ public ResultSet getProcedureColumns(String catalog, String schemaPattern, Strin
774939 public ResultSet getTables (String catalog , String schemaPattern , String tableNamePattern , String [] types ) throws SQLException {
775940 log .debug ("getTables: catalog={}, schemaPattern={}, tableNamePattern={}, types={}" , catalog , schemaPattern , tableNamePattern , types );
776941 // TODO: when switch between catalog and schema is implemented, then TABLE_SCHEMA and TABLE_CAT should be populated accordingly
777- // String commentColumn = connection.getServerVersion().check("[21.6,)") ? "t.comment" : "''";
778942 // TODO: handle useCatalogs == true and return schema catalog name
779- if (types == null || types .length == 0 ) {
780- types = TABLE_TYPES ;
943+
944+ // Get engines that map to the requested table types
945+ Set <String > requestedTypes = (types == null || types .length == 0 ) ? TABLE_TYPES : Arrays .stream (types ).collect (Collectors .toSet ()) ;
946+ Set <String > engines = getEnginesForTableTypes (requestedTypes );
947+
948+ // Build engine filter conditions
949+ List <String > filterConditions = new ArrayList <>();
950+
951+ // Add condition for engines that map to requested types
952+ if (!engines .isEmpty ()) {
953+ filterConditions .add ("(t.engine IN ('" + String .join ("','" , engines ) + "'))" );
954+ }
955+
956+ // If TABLE type is requested, also include engines not in our map (they default to TABLE)
957+ if (requestedTypes .contains (TableType .TABLE .getTypeName ())) {
958+ filterConditions .add ("(t.engine NOT IN ('" + String .join ("','" , ENGINE_TO_TABLE_TYPE .keySet ()) +
959+ "') AND NOT t.engine LIKE 'System%' AND NOT t.engine LIKE 'Async%' AND t.is_temporary = 0)" );
960+ }
961+
962+ // If SYSTEM TABLE is requested, include system engines (System* and Async*)
963+ if (requestedTypes .contains (TableType .SYSTEM_TABLE .getTypeName ())) {
964+ filterConditions .add ("(t.engine LIKE 'System%' OR t.engine LIKE 'Async%')" );
965+ }
966+
967+ // If TEMPORARY TABLE is requested, include temporary tables
968+ if (requestedTypes .contains (TableType .TEMPORARY_TABLE .getTypeName ())) {
969+ filterConditions .add ("(t.is_temporary = 1)" );
970+ }
971+
972+ String engineFilter = filterConditions .isEmpty () ? "" : " AND ( " + String .join (" OR " , filterConditions ) + ")" ;
973+ // Exclude temporary tables when not requested (they would otherwise match engine-based conditions)
974+ if (!requestedTypes .contains (TableType .TEMPORARY_TABLE .getTypeName ())) {
975+ engineFilter += " AND (t.is_temporary = 0)" ;
781976 }
782977
783978 String sql = "SELECT " +
784979 catalogPlaceholder + " AS TABLE_CAT, " +
785980 "t.database AS TABLE_SCHEM, " +
786981 "t.name AS TABLE_NAME, " +
787- "CASE WHEN t.engine LIKE '%Log' THEN 'LOG TABLE' " +
788- "WHEN t.engine in ('Buffer', 'Memory', 'Set') THEN 'MEMORY TABLE' " +
789- "WHEN t.is_temporary != 0 THEN 'TEMPORARY TABLE' " +
790- "WHEN t.engine like '%View' THEN 'VIEW'" +
791- "WHEN t.engine = 'Dictionary' THEN 'DICTIONARY' " +
792- "WHEN t.engine LIKE 'Async%' OR t.engine LIKE 'System%' THEN 'SYSTEM TABLE' " +
793- "WHEN empty(t.data_paths) THEN 'REMOTE TABLE' " +
794- "ELSE 'TABLE' END AS TABLE_TYPE, " +
982+ "if(t.is_temporary = 1, concat('Temporary', t.engine), t.engine) AS TABLE_TYPE, " +
795983 "t.comment AS REMARKS, " +
796984 "CAST(null as Nullable(String)) AS TYPE_CAT, " + // no types catalog
797985 "d.engine AS TYPE_SCHEM, " + // no types schema
@@ -800,12 +988,16 @@ public ResultSet getTables(String catalog, String schemaPattern, String tableNam
800988 "CAST(null as Nullable(String)) AS REF_GENERATION" +
801989 " FROM system.tables t" +
802990 " JOIN system.databases d ON system.tables.database = system.databases.name" +
803- " WHERE t.database LIKE '" + (schemaPattern == null ? "%" : schemaPattern ) + "'" +
804- " AND t.name LIKE '" + (tableNamePattern == null ? "%" : tableNamePattern ) + "'" +
805- " AND TABLE_TYPE IN ('" + String .join ("','" , types ) + "')" ;
806-
807- try {
808- return connection .createStatement ().executeQuery (sql );
991+ " WHERE t.database LIKE ?" +
992+ " AND t.name LIKE ?"
993+ + engineFilter ;
994+
995+ try (PreparedStatement stmt = connection .prepareStatement (sql )) {
996+ stmt .setString (1 , (schemaPattern == null ? "%" : schemaPattern ));
997+ stmt .setString (2 , (tableNamePattern == null ? "%" : tableNamePattern ));
998+ try (ResultSet rs = stmt .executeQuery ()) {
999+ return DetachedResultSet .createFromResultSet (rs , connection .getDefaultCalendar (), GET_TABLES_MUTATORS );
1000+ }
8091001 } catch (Exception e ) {
8101002 throw ExceptionUtils .toSqlState (e );
8111003 }
@@ -844,15 +1036,20 @@ public ResultSet getCatalogs() throws SQLException {
8441036 }
8451037 }
8461038
1039+
1040+ static final List <String > TABLE_TYPES_SQL_ARRAY = Arrays .stream (TableType .values ()).map (TableType ::getTypeName ).collect (Collectors .toList ());
8471041 /**
8481042 * Returns name of the ClickHouse table types as the broad category (rather than engine name).
8491043 * @return - ResultSet with one column TABLE_TYPE
8501044 * @throws SQLException - if an error occurs
8511045 */
8521046 @ Override
8531047 public ResultSet getTableTypes () throws SQLException {
854- try {
855- return connection .createStatement ().executeQuery ("SELECT arrayJoin(['" + String .join ("','" , TABLE_TYPES ) + "']) AS TABLE_TYPE ORDER BY TABLE_TYPE" );
1048+ try (PreparedStatement stmt = connection .prepareStatement ("SELECT arrayJoin(?) AS TABLE_TYPE ORDER BY TABLE_TYPE" )) {
1049+ stmt .setObject (1 , TABLE_TYPES_SQL_ARRAY );
1050+ try (ResultSet rs = stmt .executeQuery ()) {
1051+ return DetachedResultSet .createFromResultSet (rs , connection .getDefaultCalendar (), Collections .emptyList ());
1052+ }
8561053 } catch (Exception e ) {
8571054 throw ExceptionUtils .toSqlState (e );
8581055 }
0 commit comments