Skip to content

Commit e186185

Browse files
authored
Merge pull request #2737 from ClickHouse/02/05/26/jdbc_table_types
Made mapping for all known table engines to a table types in JDBC
2 parents 25e39a9 + 59f87e9 commit e186185

2 files changed

Lines changed: 359 additions & 26 deletions

File tree

jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java

Lines changed: 221 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,23 @@
1616

1717
import java.sql.Connection;
1818
import java.sql.JDBCType;
19+
import java.sql.PreparedStatement;
1920
import java.sql.ResultSet;
2021
import java.sql.RowIdLifetime;
2122
import java.sql.SQLException;
2223
import java.sql.SQLFeatureNotSupportedException;
2324
import java.sql.SQLType;
2425
import java.sql.Statement;
2526
import java.sql.Types;
27+
import java.util.ArrayList;
2628
import java.util.Arrays;
29+
import java.util.Collection;
2730
import java.util.Collections;
2831
import java.util.List;
2932
import java.util.Map;
33+
import java.util.Set;
3034
import java.util.function.Consumer;
35+
import java.util.stream.Collectors;
3136

3237
public 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

Comments
 (0)