From d69f9d3cde6347f02bbd6a93fd66f264755676ca Mon Sep 17 00:00:00 2001 From: "Spencer.Chang" Date: Mon, 20 Jul 2026 16:54:51 +0800 Subject: [PATCH] fix(jdbc): avoid privileged Kingbase metadata query --- .../com/dbx/agent/kingbase/KingbaseAgent.java | 21 +- .../dbx/agent/kingbase/KingbaseAgentTest.java | 78 ++++++- .../main/java/app/dbx/jdbc/DbxJdbcPlugin.java | 212 ++++++++++++++++- .../java/app/dbx/jdbc/DbxJdbcPluginTest.java | 216 ++++++++++++++++++ 4 files changed, 502 insertions(+), 25 deletions(-) diff --git a/agents/drivers/kingbase/src/main/java/com/dbx/agent/kingbase/KingbaseAgent.java b/agents/drivers/kingbase/src/main/java/com/dbx/agent/kingbase/KingbaseAgent.java index 6d2f73970..35fecc222 100644 --- a/agents/drivers/kingbase/src/main/java/com/dbx/agent/kingbase/KingbaseAgent.java +++ b/agents/drivers/kingbase/src/main/java/com/dbx/agent/kingbase/KingbaseAgent.java @@ -869,8 +869,12 @@ public final class KingbaseAgent extends PostgresLikeAgent { .append("FROM sys_catalog.sys_class c ") .append("JOIN sys_catalog.sys_namespace n ON ").append(KINGBASE_NAMESPACE_OID).append(" = ").append(KINGBASE_REL_NAMESPACE).append(' ') .append("LEFT JOIN sys_catalog.sys_description d ON CAST(d.objoid AS varchar(64)) = ").append(KINGBASE_REL_OID).append(" AND d.objsubid = 0 ") - .append("WHERE ").append(KINGBASE_SCHEMA_NAME).append(" = ").append(sqlString(schema)); - appendRegularTablePredicate(sql); + .append("WHERE ").append(KINGBASE_SCHEMA_NAME).append(" = ").append(sqlString(schema)) + .append(" AND (EXISTS (SELECT 1 FROM sys_catalog.sys_tables t ") + .append("WHERE CAST(t.schemaname AS varchar(256)) = ").append(KINGBASE_SCHEMA_NAME) + .append(" AND CAST(t.tablename AS varchar(256)) = ").append(KINGBASE_REL_NAME).append(')') + .append(" OR EXISTS (SELECT 1 FROM sys_catalog.sys_foreign_table ft ") + .append("WHERE CAST(ft.ftrelid AS varchar(64)) = ").append(KINGBASE_REL_OID).append("))"); MetadataSqlSupport.appendNameFilter(sql, args, KINGBASE_REL_NAME, constraints); return sql.toString(); } @@ -919,19 +923,6 @@ public final class KingbaseAgent extends PostgresLikeAgent { return sql.toString(); } - private static void appendRegularTablePredicate(StringBuilder sql) { - sql.append(" AND NOT EXISTS (SELECT 1 FROM sys_catalog.sys_rewrite r ") - .append("WHERE CAST(r.ev_class AS varchar(64)) = ").append(KINGBASE_REL_OID) - .append(" AND CAST(r.rulename AS varchar(256)) = '_RETURN')") - .append(" AND NOT EXISTS (SELECT 1 FROM sys_catalog.sys_index ix ") - .append("WHERE CAST(ix.indexrelid AS varchar(64)) = ").append(KINGBASE_REL_OID).append(')') - .append(" AND NOT EXISTS (SELECT 1 FROM sys_catalog.sys_attribute sa1 ") - .append("JOIN sys_catalog.sys_attribute sa2 ON CAST(sa2.attrelid AS varchar(64)) = CAST(sa1.attrelid AS varchar(64)) ") - .append("WHERE CAST(sa1.attrelid AS varchar(64)) = ").append(KINGBASE_REL_OID) - .append(" AND CAST(sa1.attname AS varchar(256)) = 'last_value'") - .append(" AND CAST(sa2.attname AS varchar(256)) = 'log_cnt')"); - } - private static void appendRelationVisibilityPredicate(StringBuilder sql) { sql.append(" AND (SYS_HAS_ROLE(").append(KINGBASE_REL_OWNER).append(", 'USAGE')") .append(" OR HAS_TABLE_PRIVILEGE(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')") diff --git a/agents/drivers/kingbase/src/test/java/com/dbx/agent/kingbase/KingbaseAgentTest.java b/agents/drivers/kingbase/src/test/java/com/dbx/agent/kingbase/KingbaseAgentTest.java index 5270c0f9c..a1ee404b0 100644 --- a/agents/drivers/kingbase/src/test/java/com/dbx/agent/kingbase/KingbaseAgentTest.java +++ b/agents/drivers/kingbase/src/test/java/com/dbx/agent/kingbase/KingbaseAgentTest.java @@ -167,6 +167,30 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { Assertions.assertEquals("RESET search_path", agent.resetSchemaSQL()); } + @Test + void postgresCompatModePreservesMaterializedViews() throws Exception { + List sql = new ArrayList<>(); + KingbaseAgent agent = new KingbaseAgent(); + Connection connection = postgresCatalogConnection(sql, resultSet( + new String[]{"table_name", "table_type", "table_comment"}, + new Object[][]{{"orders", "TABLE", null}, {"order_summary", "MATERIALIZED VIEW", "cached orders"}} + )); + + Method afterConnect = KingbaseAgent.class.getDeclaredMethod("afterConnect", ConnectParams.class, Connection.class); + afterConnect.setAccessible(true); + afterConnect.invoke(agent, new ConnectParams(), connection); + TestSupport.setPrivateConnection(agent, connection); + + List tables = agent.listTables("public"); + + Assertions.assertEquals(2, tables.size()); + Assertions.assertEquals("MATERIALIZED VIEW", tables.get(1).getTable_type()); + Assertions.assertEquals("SELECT 1 FROM sys_catalog.sys_namespace WHERE 1 = 0", sql.get(0)); + Assertions.assertEquals("SELECT 1 FROM pg_catalog.pg_namespace WHERE 1 = 0", sql.get(1)); + Assertions.assertTrue(sql.get(2).contains("FROM pg_catalog.pg_class c"), sql.get(2)); + Assertions.assertTrue(sql.get(2).contains("c.relkind IN ('r','p','v','m','f')"), sql.get(2)); + } + @Test void detectsMysqlCompatModeFromServerDatabaseMode() throws Exception { List sql = new ArrayList<>(); @@ -331,14 +355,32 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { Assertions.assertEquals("TABLE", tables.get(0).getTable_type()); Assertions.assertEquals("app_view", tables.get(1).getName()); Assertions.assertEquals("VIEW", tables.get(1).getTable_type()); - Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_class"), sql.get(0)); - Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_rewrite"), sql.get(0)); - Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_index"), sql.get(0)); + Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_class c"), sql.get(0)); + Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_tables t"), sql.get(0)); + Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_foreign_table ft"), sql.get(0)); Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_views"), sql.get(0)); Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_matviews"), sql.get(0)); Assertions.assertFalse(sql.get(0).contains("relkind"), sql.get(0)); } + @Test + void regularTableDiscoveryExcludesCompositeTypesWithPositiveTableCatalog() { + List sql = new ArrayList<>(); + KingbaseAgent agent = new KingbaseAgent(); + TestSupport.setPrivateConnection(agent, compositeAwareTableConnection(sql)); + + List tables = agent.listTables("public", new MetadataListConstraints(null, null, null, List.of("TABLE"))); + + Assertions.assertEquals(1, tables.size()); + Assertions.assertEquals("orders", tables.get(0).getName()); + Assertions.assertFalse(tables.stream().anyMatch(table -> "address_type".equals(table.getName()))); + Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_tables t"), sql.get(0)); + Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_foreign_table ft"), sql.get(0)); + Assertions.assertFalse(sql.get(0).contains("information_schema.tables"), sql.get(0)); + Assertions.assertFalse(sql.get(0).contains("sys_rewrite"), sql.get(0)); + Assertions.assertFalse(sql.get(0).contains("sys_index"), sql.get(0)); + } + @Test void regularListObjectsIncludesKingbaseViewsProceduresAndFunctions() { List sql = new ArrayList<>(); @@ -431,7 +473,8 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { agent.listTables("public", new MetadataListConstraints("ord", 30, 60, List.of("TABLE", "VIEW"))); - Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_class"), sql.get(0)); + Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_class c"), sql.get(0)); + Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_tables t"), sql.get(0)); Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_views"), sql.get(0)); Assertions.assertTrue(sql.get(0).contains("UNION ALL"), sql.get(0)); Assertions.assertTrue(sql.get(0).contains("UPPER(CAST(c.relname AS varchar(256))) LIKE ? ESCAPE '\\\\'"), sql.get(0)); @@ -789,6 +832,33 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { }); } + private static Connection compositeAwareTableConnection(List sql) { + return proxy(Connection.class, (method, args) -> { + if ("prepareStatement".equals(method.getName())) { + String preparedSql = String.valueOf(args[0]); + sql.add(preparedSql); + boolean positivelySelectsTables = preparedSql.contains("FROM sys_catalog.sys_tables t") + && preparedSql.contains("FROM sys_catalog.sys_foreign_table ft"); + Object[][] rows = positivelySelectsTables + ? new Object[][]{{"orders", "TABLE", null}} + : new Object[][]{{"orders", "TABLE", null}, {"address_type", "TABLE", null}}; + return proxy(PreparedStatement.class, (statementMethod, statementArgs) -> { + if ("executeQuery".equals(statementMethod.getName())) { + return resultSet(new String[]{"table_name", "table_type", "table_comment"}, rows); + } + if ("setString".equals(statementMethod.getName()) || "close".equals(statementMethod.getName())) { + return null; + } + return defaultValue(statementMethod.getReturnType()); + }); + } + if ("isClosed".equals(method.getName())) { + return false; + } + return defaultValue(method.getReturnType()); + }); + } + private static Connection preparedConnectionWithFailure(List sql, String failingSqlFragment, ResultSet fallback) { return preparedConnectionWithFailures(sql, List.of(failingSqlFragment), fallback); } diff --git a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java index ef6893d6f..a099e95c6 100644 --- a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java +++ b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java @@ -1319,6 +1319,17 @@ public final class DbxJdbcPlugin { if (usePrestoInformationSchemaTables(connection)) { return prestoListTables(conn, database, schema, filter, limit, offset, objectTypes); } + if (isKingbaseUrl(optionalText(connection, "connection_string"))) { + return filterMetadataNodes( + (ArrayNode) kingbaseListTables(conn, schema, false), + filter, + limit, + offset, + objectTypes, + "table_type", + true + ); + } DatabaseMetaData meta = conn.getMetaData(); String[] types = constrainedJdbcTableTypes(jdbcTableTypes(meta), objectTypes); if (types.length == 0) { @@ -1358,16 +1369,22 @@ public final class DbxJdbcPlugin { if (usePrestoInformationSchemaTables(connection)) { return prestoListObjects(conn, database, schema, filter, limit, offset, objectTypes); } + boolean kingbase = isKingbaseUrl(optionalText(connection, "connection_string")); + if (kingbase) { + result.addAll((ArrayNode) kingbaseListTables(conn, schema, true)); + } DatabaseMetaData meta = conn.getMetaData(); JdbcDriverQuirks quirks = driverQuirks(connection); String catalog = metadataCatalog(database, quirks); String schemaPattern = resolveSchemaPattern(meta, database, schema, quirks); - String[] tableTypes = constrainedJdbcTableTypes(jdbcTableTypes(meta), objectTypes); - if (tableTypes.length > 0) { - appendTableObjects(result, meta, catalog, schemaPattern, schema, tableTypes); - if (result.isEmpty() && catalog != null) { - appendTableObjects(result, meta, null, schemaPattern, schema, tableTypes); + if (!kingbase) { + String[] tableTypes = constrainedJdbcTableTypes(jdbcTableTypes(meta), objectTypes); + if (tableTypes.length > 0) { + appendTableObjects(result, meta, catalog, schemaPattern, schema, tableTypes); + if (result.isEmpty() && catalog != null) { + appendTableObjects(result, meta, null, schemaPattern, schema, tableTypes); + } } } @@ -1880,9 +1897,192 @@ public final class DbxJdbcPlugin { }; } + private static JsonNode kingbaseListTables(Connection conn, String schema, boolean objectNodes) throws SQLException { + ArrayNode result = MAPPER.createArrayNode(); + String effectiveSchema = kingbaseEffectiveSchema(conn, schema); + KingbaseTableCatalogMode catalogMode = kingbaseTableCatalogMode(conn); + String sql = switch (catalogMode) { + case SYS_CATALOG -> kingbaseCastSafeTablesSql(); + case POSTGRES_CATALOG -> kingbasePostgresTablesSql(); + case INFORMATION_SCHEMA -> kingbaseCompatibilityTablesSql(); + }; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, effectiveSchema); + if (catalogMode == KingbaseTableCatalogMode.SYS_CATALOG) { + ps.setString(2, effectiveSchema); + ps.setString(3, effectiveSchema); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("table_name"); + String tableType = normalizeInformationSchemaTableType(rs.getString("table_type")); + ObjectNode item = MAPPER.createObjectNode(); + item.put("name", tableName); + if (objectNodes) { + item.put("object_type", tableType); + item.put("schema", effectiveSchema); + } else { + item.put("table_type", tableType); + } + putNullable(item, "comment", rs.getString("remarks")); + result.add(item); + } + } + } + return result; + } + + private enum KingbaseTableCatalogMode { + SYS_CATALOG, + POSTGRES_CATALOG, + INFORMATION_SCHEMA + } + + private static KingbaseTableCatalogMode kingbaseTableCatalogMode(Connection conn) { + if (!kingbaseCatalogExists(conn, "sys_catalog.sys_namespace")) { + return kingbaseCatalogExists(conn, "pg_catalog.pg_namespace") + ? KingbaseTableCatalogMode.POSTGRES_CATALOG + : KingbaseTableCatalogMode.INFORMATION_SCHEMA; + } + return kingbaseMysqlCompatibilityMode(conn) + ? KingbaseTableCatalogMode.INFORMATION_SCHEMA + : KingbaseTableCatalogMode.SYS_CATALOG; + } + + private static boolean kingbaseMysqlCompatibilityMode(Connection conn) { + try (Statement statement = conn.createStatement(); + ResultSet rs = statement.executeQuery( + "SELECT setting FROM sys_catalog.sys_settings WHERE LOWER(name) = 'database_mode'" + )) { + if (rs.next()) { + return "mysql".equalsIgnoreCase(rs.getString(1)); + } + } catch (Exception ignored) { + // Older Kingbase versions do not expose database_mode. + } + try (Statement statement = conn.createStatement(); + ResultSet rs = statement.executeQuery( + "SELECT 1 FROM sys_catalog.sys_settings WHERE LOWER(name) = 'sql_mode'" + )) { + return rs.next(); + } catch (Exception ignored) { + return false; + } + } + + private static boolean kingbaseCatalogExists(Connection conn, String catalog) { + try (Statement statement = conn.createStatement(); + ResultSet ignored = statement.executeQuery("SELECT 1 FROM " + catalog + " WHERE 1 = 0")) { + return true; + } catch (Exception ignored) { + return false; + } + } + + private static String kingbaseCompatibilityTablesSql() { + return """ + SELECT CAST(table_name AS varchar(256)) AS table_name, + CASE UPPER(CAST(table_type AS varchar(64))) + WHEN 'VIEW' THEN 'VIEW' + WHEN 'MATERIALIZED VIEW' THEN 'MATERIALIZED_VIEW' + ELSE 'TABLE' + END AS table_type, + NULL AS remarks + FROM information_schema.tables + WHERE CAST(table_schema AS varchar(256)) = ? + AND UPPER(CAST(table_type AS varchar(64))) IN ('BASE TABLE', 'TABLE', 'VIEW', 'MATERIALIZED VIEW') + ORDER BY CAST(table_name AS varchar(256)) + """; + } + + private static String kingbasePostgresTablesSql() { + return """ + SELECT CAST(c.relname AS varchar(256)) AS table_name, + CASE c.relkind + WHEN 'v' THEN 'VIEW' + WHEN 'm' THEN 'MATERIALIZED_VIEW' + WHEN 'f' THEN 'FOREIGN TABLE' + ELSE 'TABLE' + END AS table_type, + CAST(obj_description(c.oid) AS varchar(4000)) AS remarks + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = ? + AND c.relkind IN ('r', 'p', 'v', 'm', 'f') + ORDER BY c.relname + """; + } + + private static String kingbaseCastSafeTablesSql() { + return """ + SELECT table_name, table_type, remarks + FROM ( + SELECT CAST(c.relname AS varchar(256)) AS table_name, + 'TABLE' AS table_type, + CAST(d.description AS varchar(4000)) AS remarks + FROM sys_catalog.sys_class c + JOIN sys_catalog.sys_namespace n + ON CAST(n.oid AS varchar(64)) = CAST(c.relnamespace AS varchar(64)) + LEFT JOIN sys_catalog.sys_description d + ON CAST(d.objoid AS varchar(64)) = CAST(c.oid AS varchar(64)) AND d.objsubid = 0 + WHERE CAST(n.nspname AS varchar(256)) = ? + AND ( + EXISTS ( + SELECT 1 FROM sys_catalog.sys_tables t + WHERE CAST(t.schemaname AS varchar(256)) = CAST(n.nspname AS varchar(256)) + AND CAST(t.tablename AS varchar(256)) = CAST(c.relname AS varchar(256)) + ) + OR EXISTS ( + SELECT 1 FROM sys_catalog.sys_foreign_table ft + WHERE CAST(ft.ftrelid AS varchar(64)) = CAST(c.oid AS varchar(64)) + ) + ) + UNION ALL + SELECT CAST(v.viewname AS varchar(256)) AS table_name, + 'VIEW' AS table_type, + CAST(d.description AS varchar(4000)) AS remarks + FROM sys_catalog.sys_views v + JOIN sys_catalog.sys_namespace n + ON CAST(n.nspname AS varchar(256)) = CAST(v.schemaname AS varchar(256)) + JOIN sys_catalog.sys_class c + ON CAST(c.relnamespace AS varchar(64)) = CAST(n.oid AS varchar(64)) + AND CAST(c.relname AS varchar(256)) = CAST(v.viewname AS varchar(256)) + LEFT JOIN sys_catalog.sys_description d + ON CAST(d.objoid AS varchar(64)) = CAST(c.oid AS varchar(64)) AND d.objsubid = 0 + WHERE CAST(v.schemaname AS varchar(256)) = ? + UNION ALL + SELECT CAST(mv.matviewname AS varchar(256)) AS table_name, + 'MATERIALIZED_VIEW' AS table_type, + CAST(d.description AS varchar(4000)) AS remarks + FROM sys_catalog.sys_matviews mv + JOIN sys_catalog.sys_namespace n + ON CAST(n.nspname AS varchar(256)) = CAST(mv.schemaname AS varchar(256)) + JOIN sys_catalog.sys_class c + ON CAST(c.relnamespace AS varchar(64)) = CAST(n.oid AS varchar(64)) + AND CAST(c.relname AS varchar(256)) = CAST(mv.matviewname AS varchar(256)) + LEFT JOIN sys_catalog.sys_description d + ON CAST(d.objoid AS varchar(64)) = CAST(c.oid AS varchar(64)) AND d.objsubid = 0 + WHERE CAST(mv.schemaname AS varchar(256)) = ? + ) metadata_tables + ORDER BY table_name + """; + } + + private static String kingbaseEffectiveSchema(Connection conn, String schema) { + String effectiveSchema = emptyToNull(schema); + if (effectiveSchema != null) { + return effectiveSchema; + } + try { + effectiveSchema = emptyToNull(conn.getSchema()); + } catch (SQLException | AbstractMethodError | UnsupportedOperationException ignored) { + } + return effectiveSchema == null ? "PUBLIC" : effectiveSchema; + } + private static JsonNode kingbaseGetColumns(Connection conn, String schema, String table) throws SQLException { ArrayNode result = MAPPER.createArrayNode(); - String effectiveSchema = emptyToNull(schema) == null ? "PUBLIC" : schema; + String effectiveSchema = kingbaseEffectiveSchema(conn, schema); Set primaryKeys = kingbasePrimaryKeys(conn, effectiveSchema, table); String sql = "SELECT a.attname AS column_name, " + "format_type(a.atttypid, a.atttypmod) AS data_type, " + diff --git a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java index 23f971340..494d1178d 100644 --- a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java +++ b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java @@ -1061,6 +1061,115 @@ final class DbxJdbcPluginTest { assertEquals(true, sql.get(1).contains("FROM sys_catalog.sys_attribute")); } + @Test + void kingbaseListTablesReusesCastSafeAgentDiscovery() throws Exception { + Method method = DbxJdbcPlugin.class.getDeclaredMethod("kingbaseListTables", Connection.class, String.class, boolean.class); + method.setAccessible(true); + List sql = new ArrayList<>(); + ResultSet tables = rowsResultSet( + new String[] { "table_name", "table_type", "remarks" }, + new Object[][] { + { "orders", "TABLE", "Order records" }, + { "order_summary", "VIEW", null } + } + ); + + JsonNode result = (JsonNode) method.invoke(null, kingbaseTableConnection(sql, tables, false), "APP", false); + + assertEquals("orders", result.path(0).path("name").asText()); + assertEquals("TABLE", result.path(0).path("table_type").asText()); + assertEquals("Order records", result.path(0).path("comment").asText()); + assertEquals("VIEW", result.path(1).path("table_type").asText()); + String discoverySql = sql.get(2); + assertEquals(true, discoverySql.contains("FROM sys_catalog.sys_class c")); + assertEquals(true, discoverySql.contains("FROM sys_catalog.sys_tables t")); + assertEquals(true, discoverySql.contains("FROM sys_catalog.sys_foreign_table ft")); + assertEquals(true, discoverySql.contains("FROM sys_catalog.sys_views")); + assertEquals(true, discoverySql.contains("FROM sys_catalog.sys_matviews")); + assertEquals(true, discoverySql.contains("CAST(c.relname AS varchar(256))")); + assertEquals(false, discoverySql.contains("relkind")); + assertEquals(false, discoverySql.contains("sys_freespace")); + assertEquals(false, discoverySql.contains("pg_relation_size_ex")); + } + + @Test + void kingbaseCompatibilityListTablesAvoidsRelkind() throws Exception { + Method method = DbxJdbcPlugin.class.getDeclaredMethod("kingbaseListTables", Connection.class, String.class, boolean.class); + method.setAccessible(true); + List sql = new ArrayList<>(); + ResultSet tables = rowsResultSet( + new String[] { "table_name", "table_type", "remarks" }, + new Object[][] { { "orders", "BASE TABLE", null }, { "order_summary", "VIEW", null } } + ); + + JsonNode result = (JsonNode) method.invoke(null, kingbaseTableConnection(sql, tables, true), "APP", false); + + assertEquals("TABLE", result.path(0).path("table_type").asText()); + assertEquals("VIEW", result.path(1).path("table_type").asText()); + assertEquals(true, sql.get(1).contains("LOWER(name) = 'database_mode'")); + String discoverySql = sql.get(2); + assertEquals(true, discoverySql.contains("FROM information_schema.tables")); + assertEquals(false, discoverySql.contains("relkind")); + assertEquals(false, discoverySql.contains("sys_freespace")); + } + + @Test + void kingbasePostgresCatalogModePreservesMaterializedViews() throws Exception { + Method method = DbxJdbcPlugin.class.getDeclaredMethod("kingbaseListTables", Connection.class, String.class, boolean.class); + method.setAccessible(true); + List sql = new ArrayList<>(); + ResultSet tables = rowsResultSet( + new String[] { "table_name", "table_type", "remarks" }, + new Object[][] { { "orders", "TABLE", null }, { "order_summary", "MATERIALIZED_VIEW", "Cached orders" } } + ); + + JsonNode result = (JsonNode) method.invoke(null, kingbasePostgresTableConnection(sql, tables), "APP", false); + + assertEquals("TABLE", result.path(0).path("table_type").asText()); + assertEquals("MATERIALIZED_VIEW", result.path(1).path("table_type").asText()); + assertEquals("SELECT 1 FROM sys_catalog.sys_namespace WHERE 1 = 0", sql.get(0)); + assertEquals("SELECT 1 FROM pg_catalog.pg_namespace WHERE 1 = 0", sql.get(1)); + String discoverySql = sql.get(2); + assertEquals(true, discoverySql.contains("FROM pg_catalog.pg_class c")); + assertEquals(true, discoverySql.contains("JOIN pg_catalog.pg_namespace n")); + assertEquals(true, discoverySql.contains("c.relkind IN ('r', 'p', 'v', 'm', 'f')")); + } + + @Test + void kingbaseRegularTableDiscoveryExcludesCompositeTypesWithPositiveTableCatalog() throws Exception { + Method method = DbxJdbcPlugin.class.getDeclaredMethod("kingbaseListTables", Connection.class, String.class, boolean.class); + method.setAccessible(true); + List sql = new ArrayList<>(); + + JsonNode result = (JsonNode) method.invoke(null, kingbaseCompositeCatalogConnection(sql), "APP", false); + + assertEquals(1, result.size()); + assertEquals("orders", result.path(0).path("name").asText()); + String discoverySql = sql.get(2); + assertEquals(true, discoverySql.contains("FROM sys_catalog.sys_tables t")); + assertEquals(true, discoverySql.contains("FROM sys_catalog.sys_foreign_table ft")); + assertEquals(false, discoverySql.contains("information_schema.tables")); + assertEquals(false, discoverySql.contains("sys_rewrite")); + assertEquals(false, discoverySql.contains("sys_index")); + } + + @Test + void kingbaseEffectiveSchemaPreservesConnectionSchemaCase() throws Exception { + Method method = DbxJdbcPlugin.class.getDeclaredMethod("kingbaseEffectiveSchema", Connection.class, String.class); + method.setAccessible(true); + Connection connection = (Connection) Proxy.newProxyInstance( + DbxJdbcPluginTest.class.getClassLoader(), + new Class[] { Connection.class }, + (proxy, invokedMethod, args) -> switch (invokedMethod.getName()) { + case "getSchema" -> "CaseSensitiveSchema"; + default -> defaultValue(invokedMethod.getReturnType()); + } + ); + + assertEquals("CaseSensitiveSchema", method.invoke(null, connection, null)); + assertEquals("ExplicitSchema", method.invoke(null, connection, "ExplicitSchema")); + } + @Test void columnIsNullablePrefersIsNullableStringWhenNullableCodeIsWrong() throws Exception { Method method = DbxJdbcPlugin.class.getDeclaredMethod("columnIsNullable", ResultSet.class); @@ -1448,6 +1557,113 @@ final class DbxJdbcPluginTest { ); } + private static Connection kingbaseTableConnection(List sql, ResultSet rs, boolean compatibilityMode) { + return (Connection) Proxy.newProxyInstance( + DbxJdbcPluginTest.class.getClassLoader(), + new Class[] { Connection.class }, + (proxy, method, args) -> switch (method.getName()) { + case "createStatement" -> kingbaseCatalogProbeStatement(sql, compatibilityMode); + case "prepareStatement" -> { + sql.add(String.valueOf(args[0])); + yield preparedStatement(rs); + } + case "isClosed" -> false; + case "close" -> null; + default -> defaultValue(method.getReturnType()); + } + ); + } + + private static Connection kingbaseCompositeCatalogConnection(List sql) { + return (Connection) Proxy.newProxyInstance( + DbxJdbcPluginTest.class.getClassLoader(), + new Class[] { Connection.class }, + (proxy, method, args) -> switch (method.getName()) { + case "createStatement" -> kingbaseCatalogProbeStatement(sql, false); + case "prepareStatement" -> { + String preparedSql = String.valueOf(args[0]); + sql.add(preparedSql); + boolean positivelySelectsTables = preparedSql.contains("FROM sys_catalog.sys_tables t") + && preparedSql.contains("FROM sys_catalog.sys_foreign_table ft"); + Object[][] rows = positivelySelectsTables + ? new Object[][] { { "orders", "TABLE", null } } + : new Object[][] { { "orders", "TABLE", null }, { "address_type", "TABLE", null } }; + yield preparedStatement(rowsResultSet(new String[] { "table_name", "table_type", "remarks" }, rows)); + } + case "isClosed" -> false; + case "close" -> null; + default -> defaultValue(method.getReturnType()); + } + ); + } + + private static Connection kingbasePostgresTableConnection(List sql, ResultSet rs) { + return (Connection) Proxy.newProxyInstance( + DbxJdbcPluginTest.class.getClassLoader(), + new Class[] { Connection.class }, + (proxy, method, args) -> switch (method.getName()) { + case "createStatement" -> kingbasePostgresCatalogProbeStatement(sql); + case "prepareStatement" -> { + sql.add(String.valueOf(args[0])); + yield preparedStatement(rs); + } + case "isClosed" -> false; + case "close" -> null; + default -> defaultValue(method.getReturnType()); + } + ); + } + + private static Statement kingbaseCatalogProbeStatement(List sql, boolean compatibilityMode) { + return (Statement) Proxy.newProxyInstance( + DbxJdbcPluginTest.class.getClassLoader(), + new Class[] { Statement.class }, + (proxy, method, args) -> switch (method.getName()) { + case "executeQuery" -> { + String query = String.valueOf(args[0]); + sql.add(query); + if (query.contains("sys_catalog.sys_namespace")) { + yield rowsResultSet(new String[] { "exists" }, new Object[0][]); + } + if (query.contains("LOWER(name) = 'database_mode'")) { + yield rowsResultSet( + new String[] { "setting" }, + new Object[][] { { compatibilityMode ? "mysql" : "oracle" } } + ); + } + if (query.contains("LOWER(name) = 'sql_mode'")) { + yield rowsResultSet(new String[] { "exists" }, new Object[0][]); + } + throw new SQLException("Unexpected Kingbase catalog probe: " + query); + } + case "close" -> null; + default -> defaultValue(method.getReturnType()); + } + ); + } + + private static Statement kingbasePostgresCatalogProbeStatement(List sql) { + return (Statement) Proxy.newProxyInstance( + DbxJdbcPluginTest.class.getClassLoader(), + new Class[] { Statement.class }, + (proxy, method, args) -> switch (method.getName()) { + case "executeQuery" -> { + String query = String.valueOf(args[0]); + sql.add(query); + if (query.contains("sys_catalog.sys_namespace")) { + throw new SQLException("relation does not exist: sys_catalog.sys_namespace"); + } + if (query.contains("pg_catalog.pg_namespace")) { + yield rowsResultSet(new String[] { "exists" }, new Object[0][]); + } + throw new SQLException("Unexpected Kingbase catalog probe: " + query); + } + case "close" -> null; + default -> defaultValue(method.getReturnType()); + } + ); + } + private static Statement statement(List sql, ResultSet rs) { return (Statement) Proxy.newProxyInstance( DbxJdbcPluginTest.class.getClassLoader(),