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 9729df18a0..bc34878d8c 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 @@ -30,6 +30,21 @@ import java.util.Map; import java.util.Set; public final class KingbaseAgent extends PostgresLikeAgent { + private static final int KINGBASE_VOID_TYPE_OID = 2278; + private static final String KINGBASE_REL_NAME = "CAST(c.relname AS varchar(256))"; + private static final String KINGBASE_REL_OID = "CAST(c.oid AS varchar(64))"; + private static final String KINGBASE_REL_NAMESPACE = "CAST(c.relnamespace AS varchar(64))"; + private static final String KINGBASE_SCHEMA_NAME = "CAST(n.nspname AS varchar(256))"; + private static final String KINGBASE_NAMESPACE_OID = "CAST(n.oid AS varchar(64))"; + private static final String KINGBASE_DESCRIPTION = "CAST(d.description AS varchar(4000))"; + private static final String KINGBASE_ROUTINE_NAME = "CAST(p.proname AS varchar(256))"; + private static final String KINGBASE_ROUTINE_OID = "CAST(p.oid AS varchar(64))"; + private static final String KINGBASE_ROUTINE_NAMESPACE = "CAST(p.pronamespace AS varchar(64))"; + private static final String KINGBASE_VIEW_NAME = "CAST(v.viewname AS varchar(256))"; + private static final String KINGBASE_VIEW_SCHEMA = "CAST(v.schemaname AS varchar(256))"; + private static final String KINGBASE_MATVIEW_NAME = "CAST(mv.matviewname AS varchar(256))"; + private static final String KINGBASE_MATVIEW_SCHEMA = "CAST(mv.schemaname AS varchar(256))"; + public static final PostgresLikeAgentProfile KINGBASE_PROFILE = new PostgresLikeAgentProfile( "com.kingbase8.Driver", "jdbc:kingbase8://{host}:{port}/{database}" @@ -110,36 +125,7 @@ public final class KingbaseAgent extends PostgresLikeAgent { if (isMysqlCompatMode()) { return listTables(schema, "table_type IN ('BASE TABLE', 'VIEW')"); } - return unchecked(() -> { - List result = new ArrayList<>(); - String sql = "SELECT c.relname AS table_name, " + - "CASE c.relkind " + - "WHEN 'r' THEN 'TABLE' " + - "WHEN 'p' THEN 'TABLE' " + - "WHEN 'v' THEN 'VIEW' " + - "WHEN 'm' THEN 'MATERIALIZED_VIEW' " + - "WHEN 'f' THEN 'FOREIGN_TABLE' " + - "ELSE 'TABLE' END AS table_type, " + - "d.description AS table_comment " + - "FROM sys_catalog.sys_class c " + - "JOIN sys_catalog.sys_namespace n ON n.oid = c.relnamespace " + - "LEFT JOIN sys_catalog.sys_description d ON d.objoid = c.oid AND d.objsubid = 0 " + - "WHERE n.nspname = ? AND c.relkind IN ('r','p','v','m','f') " + - "ORDER BY c.relname"; - try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) { - stmt.setString(1, effectiveSchema(schema)); - try (ResultSet rs = stmt.executeQuery()) { - while (rs.next()) { - result.add(new TableInfo( - rs.getString("table_name"), - normalizeTableType(rs.getString("table_type")), - rs.getString("table_comment") - )); - } - } - } - return result; - }); + return queryRegularTables(schema, MetadataListConstraints.NONE); } @Override @@ -164,23 +150,14 @@ public final class KingbaseAgent extends PostgresLikeAgent { return unchecked(() -> { List result = new ArrayList<>(); List args = new ArrayList<>(); - StringBuilder sql = new StringBuilder("SELECT c.relname AS table_name, ") - .append("CASE c.relkind ") - .append("WHEN 'r' THEN 'TABLE' ") - .append("WHEN 'p' THEN 'TABLE' ") - .append("WHEN 'v' THEN 'VIEW' ") - .append("WHEN 'm' THEN 'MATERIALIZED_VIEW' ") - .append("WHEN 'f' THEN 'FOREIGN_TABLE' ") - .append("ELSE 'TABLE' END AS table_type, ") - .append("d.description AS table_comment ") - .append("FROM sys_catalog.sys_class c ") - .append("JOIN sys_catalog.sys_namespace n ON n.oid = c.relnamespace ") - .append("LEFT JOIN sys_catalog.sys_description d ON d.objoid = c.oid AND d.objsubid = 0 ") - .append("WHERE n.nspname = ? AND c.relkind IN ('r','p','v','m','f')"); - args.add(effectiveSchema(schema)); - appendRelkindPredicate(sql, args, constraints); - MetadataSqlSupport.appendNameFilter(sql, args, "c.relname", constraints); - sql.append(" ORDER BY c.relname"); + List branches = new ArrayList<>(); + addRegularRelationBranches(branches, args, effectiveSchema(schema), constraints, "table_name", "table_type", "table_comment"); + if (branches.isEmpty()) { + return List.of(); + } + StringBuilder sql = new StringBuilder("SELECT table_name, table_type, table_comment FROM (") + .append(String.join(" UNION ALL ", branches)) + .append(") metadata_tables ORDER BY table_name"); MetadataSqlSupport.appendLiteralLimitOffset(sql, constraints); try (PreparedStatement stmt = requireConnected().prepareStatement(sql.toString())) { MetadataSqlSupport.bind(stmt, args); @@ -232,16 +209,15 @@ public final class KingbaseAgent extends PostgresLikeAgent { return result; } - String sql = "SELECT p.proname AS routine_name, " + - "CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS routine_type, " + - "d.description AS routine_comment " + + String sql = "SELECT " + KINGBASE_ROUTINE_NAME + " AS routine_name, " + + "CASE WHEN p.prorettype = " + KINGBASE_VOID_TYPE_OID + " THEN 'PROCEDURE' ELSE 'FUNCTION' END AS routine_type, " + + KINGBASE_DESCRIPTION + " AS routine_comment " + "FROM sys_catalog.sys_proc p " + - "JOIN sys_catalog.sys_namespace n ON n.oid = p.pronamespace " + - "LEFT JOIN sys_catalog.sys_description d ON d.objoid = p.oid AND d.objsubid = 0 " + - "WHERE n.nspname = ? AND p.prokind IN ('p','f') " + - "ORDER BY p.proname"; + "JOIN sys_catalog.sys_namespace n ON " + KINGBASE_NAMESPACE_OID + " = " + KINGBASE_ROUTINE_NAMESPACE + " " + + "LEFT JOIN sys_catalog.sys_description d ON CAST(d.objoid AS varchar(64)) = " + KINGBASE_ROUTINE_OID + " AND d.objsubid = 0 " + + "WHERE " + KINGBASE_SCHEMA_NAME + " = " + sqlString(effectiveSchema) + " " + + "ORDER BY " + KINGBASE_ROUTINE_NAME; try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) { - stmt.setString(1, effectiveSchema); try (ResultSet rs = stmt.executeQuery()) { while (rs.next()) { result.add(new ObjectInfo( @@ -282,29 +258,19 @@ public final class KingbaseAgent extends PostgresLikeAgent { List branches = new ArrayList<>(); List args = new ArrayList<>(); if (constraints.includesTableLikeTypes()) { - StringBuilder tableSql = new StringBuilder("SELECT c.relname AS object_name, ") - .append("CASE c.relkind WHEN 'r' THEN 'TABLE' WHEN 'p' THEN 'TABLE' WHEN 'v' THEN 'VIEW' WHEN 'm' THEN 'MATERIALIZED_VIEW' WHEN 'f' THEN 'FOREIGN_TABLE' ELSE 'TABLE' END AS object_type, ") - .append("d.description AS object_comment ") - .append("FROM sys_catalog.sys_class c ") - .append("JOIN sys_catalog.sys_namespace n ON n.oid = c.relnamespace ") - .append("LEFT JOIN sys_catalog.sys_description d ON d.objoid = c.oid AND d.objsubid = 0 ") - .append("WHERE n.nspname = ? AND c.relkind IN ('r','p','v','m','f')"); - args.add(effectiveSchema); - appendRelkindPredicate(tableSql, args, constraints); - MetadataSqlSupport.appendNameFilter(tableSql, args, "c.relname", constraints); - branches.add(tableSql.toString()); + addRegularRelationBranches(branches, args, effectiveSchema, constraints, "object_name", "object_type", "object_comment"); } if (constraints.objectTypeAllowed("PROCEDURE") || constraints.objectTypeAllowed("FUNCTION")) { - StringBuilder routineSql = new StringBuilder("SELECT p.proname AS object_name, ") - .append("CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type, ") - .append("d.description AS object_comment ") + StringBuilder routineSql = new StringBuilder("SELECT ") + .append(KINGBASE_ROUTINE_NAME).append(" AS object_name, ") + .append("CASE WHEN p.prorettype = ").append(KINGBASE_VOID_TYPE_OID).append(" THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type, ") + .append(KINGBASE_DESCRIPTION).append(" AS object_comment ") .append("FROM sys_catalog.sys_proc p ") - .append("JOIN sys_catalog.sys_namespace n ON n.oid = p.pronamespace ") - .append("LEFT JOIN sys_catalog.sys_description d ON d.objoid = p.oid AND d.objsubid = 0 ") - .append("WHERE n.nspname = ?"); - args.add(effectiveSchema); + .append("JOIN sys_catalog.sys_namespace n ON ").append(KINGBASE_NAMESPACE_OID).append(" = ").append(KINGBASE_ROUTINE_NAMESPACE).append(' ') + .append("LEFT JOIN sys_catalog.sys_description d ON CAST(d.objoid AS varchar(64)) = ").append(KINGBASE_ROUTINE_OID).append(" AND d.objsubid = 0 ") + .append("WHERE ").append(KINGBASE_SCHEMA_NAME).append(" = ").append(sqlString(effectiveSchema)); appendRoutineKindPredicate(routineSql, args, constraints); - MetadataSqlSupport.appendNameFilter(routineSql, args, "p.proname", constraints); + MetadataSqlSupport.appendNameFilter(routineSql, args, KINGBASE_ROUTINE_NAME, constraints); branches.add(routineSql.toString()); } if (branches.isEmpty()) { @@ -345,6 +311,14 @@ public final class KingbaseAgent extends PostgresLikeAgent { "FROM information_schema.views " + "WHERE table_schema = " + sqlString(effectiveSchema(schema)) + " AND table_name = " + sqlString(name); + if (!isMysqlCompatMode()) { + sql = "SELECT sys_get_viewdef(c.oid) AS view_definition " + + "FROM sys_catalog.sys_class c " + + "JOIN sys_catalog.sys_namespace n ON " + KINGBASE_NAMESPACE_OID + " = " + KINGBASE_REL_NAMESPACE + " " + + "WHERE " + KINGBASE_SCHEMA_NAME + " = " + sqlString(effectiveSchema(schema)) + + " AND " + KINGBASE_REL_NAME + " = " + sqlString(name) + + " LIMIT 1"; + } try (Statement stmt = requireConnected().createStatement()) { try (ResultSet rs = stmt.executeQuery(sql)) { if (rs.next()) { @@ -359,16 +333,15 @@ public final class KingbaseAgent extends PostgresLikeAgent { private ObjectSource routineSource(String schema, String name, String objectType) { return unchecked(() -> { String source = ""; - String prokind = "PROCEDURE".equalsIgnoreCase(objectType) ? "p" : "f"; String sql = "SELECT sys_get_functiondef(p.oid) AS source " + "FROM sys_catalog.sys_proc p " + - "JOIN sys_catalog.sys_namespace n ON n.oid = p.pronamespace " + - "WHERE n.nspname = ? AND p.proname = ? AND p.prokind = ? " + - "ORDER BY p.oid LIMIT 1"; + "JOIN sys_catalog.sys_namespace n ON " + KINGBASE_NAMESPACE_OID + " = " + KINGBASE_ROUTINE_NAMESPACE + " " + + "WHERE " + KINGBASE_SCHEMA_NAME + " = " + sqlString(effectiveSchema(schema)) + + " AND " + KINGBASE_ROUTINE_NAME + " = " + sqlString(name) + " " + + "ORDER BY CASE WHEN p.prorettype = " + KINGBASE_VOID_TYPE_OID + " THEN " + + ("PROCEDURE".equalsIgnoreCase(objectType) ? "0 ELSE 1" : "1 ELSE 0") + + " END, p.oid LIMIT 1"; try (PreparedStatement stmt = requireConnected().prepareStatement(sql)) { - stmt.setString(1, effectiveSchema(schema)); - stmt.setString(2, name); - stmt.setString(3, prokind); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { source = coalesce(rs.getString("source")); @@ -647,27 +620,103 @@ public final class KingbaseAgent extends PostgresLikeAgent { || constraints.objectTypeAllowed("FUNCTION"); } - private static void appendRelkindPredicate(StringBuilder sql, List args, MetadataListConstraints constraints) { - if (!constraints.hasObjectTypes()) { - return; - } - List kinds = new ArrayList<>(); + private static void addRegularRelationBranches( + List branches, + List args, + String schema, + MetadataListConstraints constraints, + String nameAlias, + String typeAlias, + String commentAlias + ) { if (constraints.tableTypeAllowed("TABLE")) { - kinds.add("r"); - kinds.add("p"); + branches.add(regularRelationBranch(schema, args, constraints, nameAlias, typeAlias, commentAlias, "TABLE")); } if (constraints.tableTypeAllowed("VIEW")) { - kinds.add("v"); + branches.add(regularViewBranch(schema, args, constraints, nameAlias, typeAlias, commentAlias)); } if (constraints.tableTypeAllowed("MATERIALIZED_VIEW")) { - kinds.add("m"); + branches.add(regularMaterializedViewBranch(schema, args, constraints, nameAlias, typeAlias, commentAlias)); } - if (kinds.isEmpty()) { - sql.append(" AND 1 = 0"); - return; - } - sql.append(" AND c.relkind IN (").append(MetadataSqlSupport.placeholders(kinds.size())).append(")"); - args.addAll(kinds); + } + + private static String regularRelationBranch( + String schema, + List args, + MetadataListConstraints constraints, + String nameAlias, + String typeAlias, + String commentAlias, + String objectType + ) { + StringBuilder sql = new StringBuilder("SELECT ") + .append(KINGBASE_REL_NAME).append(" AS ").append(nameAlias).append(", '") + .append(objectType).append("' AS ").append(typeAlias).append(", ") + .append(KINGBASE_DESCRIPTION).append(" AS ").append(commentAlias).append(' ') + .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); + MetadataSqlSupport.appendNameFilter(sql, args, KINGBASE_REL_NAME, constraints); + return sql.toString(); + } + + private static String regularViewBranch( + String schema, + List args, + MetadataListConstraints constraints, + String nameAlias, + String typeAlias, + String commentAlias + ) { + // sys_views/sys_matviews avoid relkind while preserving the sidebar's + // separate VIEW and MATERIALIZED_VIEW groups. + StringBuilder sql = new StringBuilder("SELECT ") + .append(KINGBASE_VIEW_NAME).append(" AS ").append(nameAlias).append(", 'VIEW' AS ").append(typeAlias).append(", ") + .append(KINGBASE_DESCRIPTION).append(" AS ").append(commentAlias).append(' ') + .append("FROM sys_catalog.sys_views v ") + .append("JOIN sys_catalog.sys_namespace n ON ").append(KINGBASE_SCHEMA_NAME).append(" = ").append(KINGBASE_VIEW_SCHEMA).append(' ') + .append("JOIN sys_catalog.sys_class c ON ").append(KINGBASE_REL_NAMESPACE).append(" = ").append(KINGBASE_NAMESPACE_OID) + .append(" AND ").append(KINGBASE_REL_NAME).append(" = ").append(KINGBASE_VIEW_NAME).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_VIEW_SCHEMA).append(" = ").append(sqlString(schema)); + MetadataSqlSupport.appendNameFilter(sql, args, KINGBASE_VIEW_NAME, constraints); + return sql.toString(); + } + + private static String regularMaterializedViewBranch( + String schema, + List args, + MetadataListConstraints constraints, + String nameAlias, + String typeAlias, + String commentAlias + ) { + StringBuilder sql = new StringBuilder("SELECT ") + .append(KINGBASE_MATVIEW_NAME).append(" AS ").append(nameAlias).append(", 'MATERIALIZED_VIEW' AS ").append(typeAlias).append(", ") + .append(KINGBASE_DESCRIPTION).append(" AS ").append(commentAlias).append(' ') + .append("FROM sys_catalog.sys_matviews mv ") + .append("JOIN sys_catalog.sys_namespace n ON ").append(KINGBASE_SCHEMA_NAME).append(" = ").append(KINGBASE_MATVIEW_SCHEMA).append(' ') + .append("JOIN sys_catalog.sys_class c ON ").append(KINGBASE_REL_NAMESPACE).append(" = ").append(KINGBASE_NAMESPACE_OID) + .append(" AND ").append(KINGBASE_REL_NAME).append(" = ").append(KINGBASE_MATVIEW_NAME).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_MATVIEW_SCHEMA).append(" = ").append(sqlString(schema)); + MetadataSqlSupport.appendNameFilter(sql, args, KINGBASE_MATVIEW_NAME, constraints); + 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 appendMysqlCompatTableTypePredicate(StringBuilder sql, List args, MetadataListConstraints constraints) { @@ -691,19 +740,21 @@ public final class KingbaseAgent extends PostgresLikeAgent { } private static void appendRoutineKindPredicate(StringBuilder sql, List args, MetadataListConstraints constraints) { - List kinds = new ArrayList<>(); - if (!constraints.hasObjectTypes() || constraints.objectTypeAllowed("PROCEDURE")) { - kinds.add("p"); + if (!constraints.hasObjectTypes()) { + return; } - if (!constraints.hasObjectTypes() || constraints.objectTypeAllowed("FUNCTION")) { - kinds.add("f"); + boolean includeProcedures = constraints.objectTypeAllowed("PROCEDURE"); + boolean includeFunctions = constraints.objectTypeAllowed("FUNCTION"); + if (includeProcedures && includeFunctions) { + return; } - if (kinds.isEmpty()) { + if (!includeProcedures && !includeFunctions) { sql.append(" AND 1 = 0"); return; } - sql.append(" AND p.prokind IN (").append(MetadataSqlSupport.placeholders(kinds.size())).append(")"); - args.addAll(kinds); + sql.append(includeProcedures + ? " AND p.prorettype = " + KINGBASE_VOID_TYPE_OID + : " AND p.prorettype <> " + KINGBASE_VOID_TYPE_OID); } private static List toObjects(List tables, String schema) { 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 80fdd5842f..1bc199ad5f 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 @@ -147,7 +147,11 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { 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("c.relkind IN ('r','p','v','m','f')"), 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_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 @@ -177,7 +181,8 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { Assertions.assertEquals("format_name", objects.get(3).getName()); Assertions.assertEquals("FUNCTION", objects.get(3).getObject_type()); Assertions.assertTrue(sql.get(1).contains("FROM sys_catalog.sys_proc"), sql.get(1)); - Assertions.assertTrue(sql.get(1).contains("p.prokind IN ('p','f')"), sql.get(1)); + Assertions.assertTrue(sql.get(1).contains("p.prorettype = 2278"), sql.get(1)); + Assertions.assertFalse(sql.get(1).contains("prokind"), sql.get(1)); } @Test @@ -192,11 +197,33 @@ 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("c.relkind IN (?, ?, ?)"), sql.get(0)); - Assertions.assertTrue(sql.get(0).contains("UPPER(c.relname) LIKE ? ESCAPE '\\\\'"), 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)); + Assertions.assertFalse(sql.get(0).contains("relkind"), sql.get(0)); Assertions.assertTrue(sql.get(0).endsWith("LIMIT 30 OFFSET 60"), sql.get(0)); } + @Test + void constrainedRegularMaterializedViewMetadataUsesMatviewCatalog() { + List sql = new ArrayList<>(); + KingbaseAgent agent = new KingbaseAgent(); + TestSupport.setPrivateConnection(agent, preparedConnection(sql, resultSet( + new String[]{"table_name", "table_type", "table_comment"}, + new Object[][]{{"mv_sales", "MATERIALIZED_VIEW", "cached sales"}} + ))); + + List tables = agent.listTables("public", new MetadataListConstraints("sales", 10, null, List.of("MATERIALIZED_VIEW"))); + + Assertions.assertEquals(1, tables.size()); + Assertions.assertEquals("mv_sales", tables.get(0).getName()); + Assertions.assertEquals("MATERIALIZED_VIEW", tables.get(0).getTable_type()); + Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_matviews"), sql.get(0)); + Assertions.assertFalse(sql.get(0).contains("FROM sys_catalog.sys_views"), sql.get(0)); + Assertions.assertTrue(sql.get(0).contains("UPPER(CAST(mv.matviewname AS varchar(256))) LIKE ? ESCAPE '\\\\'"), sql.get(0)); + Assertions.assertFalse(sql.get(0).contains("relkind"), sql.get(0)); + } + @Test void constrainedRegularObjectMetadataPushesRoutineTypesAndPaging() { List sql = new ArrayList<>(); @@ -209,7 +236,8 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { agent.listObjects("public", new MetadataListConstraints("sync", 10, null, List.of("PROCEDURE", "FUNCTION"))); Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_proc"), sql.get(0)); - Assertions.assertTrue(sql.get(0).contains("p.prokind IN (?, ?)"), sql.get(0)); + Assertions.assertTrue(sql.get(0).contains("p.prorettype = 2278"), sql.get(0)); + Assertions.assertFalse(sql.get(0).contains("prokind"), sql.get(0)); Assertions.assertTrue(sql.get(0).contains("ORDER BY CASE object_type"), sql.get(0)); Assertions.assertTrue(sql.get(0).endsWith("LIMIT 10"), sql.get(0)); } @@ -246,6 +274,7 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { Assertions.assertTrue(source.getSource().startsWith("CREATE FUNCTION public.format_name()"), source.getSource()); Assertions.assertTrue(sql.get(0).contains("SELECT sys_get_functiondef(p.oid) AS source"), sql.get(0)); Assertions.assertTrue(sql.get(0).contains("FROM sys_catalog.sys_proc"), sql.get(0)); + Assertions.assertFalse(sql.get(0).contains("prokind"), sql.get(0)); } @Test diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index e7721ae8f4..df638f3d53 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -1392,6 +1392,7 @@ const databaseLabel = computed(() => { }); const databasePlaceholder = computed(() => { + if (form.value.db_type === "kingbase") return t("connection.databasePlaceholderRequired"); const fallback = defaultDatabaseForProfile(); if (!fallback) return t("connection.databasePlaceholder"); return t("connection.databasePlaceholderWithDefault", { database: fallback }); @@ -1430,7 +1431,7 @@ function defaultDatabaseForProfile() { if (selectedType.value === "cockroachdb") return "defaultdb"; if (form.value.db_type === "highgo") return "highgo"; if (form.value.db_type === "yashandb") return "yasdb"; - if (form.value.db_type === "postgres" || form.value.db_type === "kingbase" || form.value.db_type === "vastbase") return "postgres"; + if (form.value.db_type === "postgres" || form.value.db_type === "vastbase") return "postgres"; if (form.value.db_type === "sqlserver") return "master"; if (form.value.db_type === "oracle") return "ORCL"; if (form.value.db_type === "h2" && h2ConnectionMode.value === "tcp") return "test"; @@ -1912,8 +1913,9 @@ async function testConnection() { const runId = ++testRunId; isTesting.value = true; testResult.value = null; - const config = connectionConfigForSubmit(editingId.value || draftTestConnectionId.value); + let config: ConnectionConfig | null = null; try { + config = connectionConfigForSubmit(editingId.value || draftTestConnectionId.value); await ensureRequiredAgentDriverInstalled(config); const msg = await testConnectionWithTimeout(config, runId); if (runId !== testRunId) return; @@ -1924,10 +1926,11 @@ async function testConnection() { clearEditedConnectionErrorAfterSuccessfulTest(); } catch (e: any) { if (runId !== testRunId) return; - const message = connectionErrorWithDriverUpdateHint(config, mongodbAuthFailureHint(String(e))); - const fallbackMessage = await tryNacosDockerConsoleFallback(config, message, runId); + const rawMessage = mongodbAuthFailureHint(errorMessage(e)); + const message = config ? connectionErrorWithDriverUpdateHint(config, rawMessage) : rawMessage; + const fallbackMessage = config ? await tryNacosDockerConsoleFallback(config, message, runId) : null; if (runId !== testRunId) return; - const shouldShowSqlServerLegacyMode = !fallbackMessage && config.db_type === "sqlserver" && !isSqlServerLegacyUnencryptedMode(config.url_params) && isSqlServerTlsHandshakeFailure(message); + const shouldShowSqlServerLegacyMode = !fallbackMessage && config?.db_type === "sqlserver" && !isSqlServerLegacyUnencryptedMode(config.url_params) && isSqlServerTlsHandshakeFailure(message); if (shouldShowSqlServerLegacyMode) { configTab.value = "advanced"; } @@ -2091,6 +2094,12 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { if (!config.name?.trim()) { config.name = generateConnectionName(); } + if (config.db_type === "kingbase") { + config.database = config.database?.trim() || undefined; + if (!config.database) { + throw new Error(t("connection.kingbaseDatabaseRequired")); + } + } config.transport_layers = (config.transport_layers || []).map(normalizeTransportLayer); config.transport_layers = config.transport_layers.map((layer) => { if (layer.type !== "ssh") return layer; @@ -2556,12 +2565,12 @@ async function preloadVisibleDatabaseNames() { if (visibleDatabaseNames.value.length > 0) return; isLoadingVisibleDatabases.value = true; const draftId = buildDraftVisibleDatabasesConnectionId(uuid()); - const draftConfig = { - ...connectionConfigForSubmit(draftId), - id: draftId, - one_time: true, - }; try { + const draftConfig = { + ...connectionConfigForSubmit(draftId), + id: draftId, + one_time: true, + }; await api.connectDb(draftConfig); visibleDatabaseNames.value = await loadVisibleDatabaseNames(draftId, draftConfig); } catch { @@ -2580,13 +2589,13 @@ async function openVisibleDatabasesPicker() { visibleDatabaseError.value = ""; visibleDatabaseSearchText.value = ""; const draftId = buildDraftVisibleDatabasesConnectionId(uuid()); - const draftConfig = { - ...connectionConfigForSubmit(draftId), - id: draftId, - one_time: true, - }; try { + const draftConfig = { + ...connectionConfigForSubmit(draftId), + id: draftId, + one_time: true, + }; await api.connectDb(draftConfig); const names = await loadVisibleDatabaseNames(draftId, draftConfig); visibleDatabaseNames.value = names; @@ -2598,8 +2607,9 @@ async function openVisibleDatabasesPicker() { } catch (e: any) { visibleDatabaseNames.value = []; visibleDatabaseSelection.value = new Set(); - visibleDatabaseError.value = mongodbAuthFailureHint(String(e?.message || e)); + visibleDatabaseError.value = mongodbAuthFailureHint(errorMessage(e)); testResult.value = { ok: false, message: visibleDatabaseError.value }; + showVisibleDatabasesDialog.value = true; } finally { await api.disconnectDb(draftId).catch(() => undefined); isLoadingVisibleDatabases.value = false; diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 4e57768cda..e963862c8b 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -180,7 +180,9 @@ export default { password: "Password", database: "Database", databasePlaceholder: "Optional", + databasePlaceholderRequired: "Required, enter an existing database", databasePlaceholderWithDefault: "Optional, defaults to {database}", + kingbaseDatabaseRequired: "Kingbase requires an existing database before connecting or loading the database list.", defaultDatabase: "Default DB", authDatabase: "Auth DB", authDatabasePlaceholder: "Optional, often admin", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 07ff81aab1..cb3ab6322f 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -182,7 +182,9 @@ export default withEnglishFallback({ password: "Contraseña", database: "Base de datos", databasePlaceholder: "Opcional", + databasePlaceholderRequired: "Obligatorio, ingresa una base de datos existente", databasePlaceholderWithDefault: "Opcional, por defecto {database}", + kingbaseDatabaseRequired: "Kingbase requiere una base de datos existente antes de conectar o cargar la lista de bases de datos.", defaultDatabase: "BD predeterminada", authDatabase: "BD de autenticación", authDatabasePlaceholder: "Opcional, normalmente admin", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index ad474c94ce..98c7e2c4e0 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -181,7 +181,9 @@ export default withEnglishFallback({ password: "Password", database: "Database", databasePlaceholder: "Opzionale", + databasePlaceholderRequired: "Obbligatorio, inserisci un database esistente", databasePlaceholderWithDefault: "Opzionale, predefinito {database}", + kingbaseDatabaseRequired: "Kingbase richiede un database esistente prima di connettersi o caricare l'elenco dei database.", defaultDatabase: "DB Predefinito", authDatabase: "DB di Autenticazione", authDatabasePlaceholder: "Opzionale, spesso admin", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 6952d7477c..63ba24ad1d 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -181,7 +181,9 @@ export default withEnglishFallback({ password: "パスワード", database: "データベース", databasePlaceholder: "任意", + databasePlaceholderRequired: "必須、既存のデータベースを入力", databasePlaceholderWithDefault: "任意、デフォルトは {database}", + kingbaseDatabaseRequired: "Kingbase は接続またはデータベース一覧の読み込み前に、既存のデータベース名が必要です。", defaultDatabase: "デフォルトDB", authDatabase: "認証DB", authDatabasePlaceholder: "任意、通常は admin", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 380ccfa940..fa71d14cce 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -182,7 +182,9 @@ export default withEnglishFallback({ password: "Senha", database: "Banco de dados", databasePlaceholder: "Opcional", + databasePlaceholderRequired: "Obrigatório, informe um banco de dados existente", databasePlaceholderWithDefault: "Opcional, padrão é {database}", + kingbaseDatabaseRequired: "Kingbase exige um banco de dados existente antes de conectar ou carregar a lista de bancos de dados.", defaultDatabase: "BD padrão", authDatabase: "BD de autenticação", authDatabasePlaceholder: "Opcional, geralmente admin", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 77be2259fb..827cfefa92 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -182,7 +182,9 @@ export default withEnglishFallback({ password: "密码", database: "数据库", databasePlaceholder: "可选", + databasePlaceholderRequired: "必填,请填写已存在的数据库", databasePlaceholderWithDefault: "可选,默认 {database}", + kingbaseDatabaseRequired: "Kingbase 需要先填写一个已存在的数据库后才能连接或加载数据库列表。", defaultDatabase: "默认库", authDatabase: "认证库", authDatabasePlaceholder: "可选,通常为 admin", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 71797b78db..219947f59e 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -182,7 +182,9 @@ export default withEnglishFallback({ password: "密碼", database: "資料庫", databasePlaceholder: "可選", + databasePlaceholderRequired: "必填,請填寫已存在的資料庫", databasePlaceholderWithDefault: "可選,預設 {database}", + kingbaseDatabaseRequired: "Kingbase 需要先填寫一個已存在的資料庫後才能連線或載入資料庫清單。", defaultDatabase: "預設 DB", authDatabase: "驗證 DB", authDatabasePlaceholder: "可選,通常為 admin", diff --git a/apps/desktop/src/lib/__tests__/database/databaseUserAdmin.spec.ts b/apps/desktop/src/lib/__tests__/database/databaseUserAdmin.spec.ts new file mode 100644 index 0000000000..7ed58623bf --- /dev/null +++ b/apps/desktop/src/lib/__tests__/database/databaseUserAdmin.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { getDatabaseUserAdminProvider, kingbaseShowGrantsSql } from "@/lib/database/databaseUserAdmin"; + +describe("database user admin providers", () => { + it("uses sys_catalog for Kingbase role metadata", () => { + const provider = getDatabaseUserAdminProvider("kingbase"); + + expect(provider).not.toBeNull(); + expect(provider?.dialect).toBe("postgres"); + expect(provider?.listUsersSql()).toContain("FROM sys_catalog.sys_roles r"); + expect(provider?.listUsersSql()).not.toContain("pg_catalog"); + }); + + it("builds Kingbase grant SQL without PostgreSQL catalog tables", () => { + const sql = kingbaseShowGrantsSql({ user: "role'o", host: "LOGIN" }); + + expect(sql).toContain("FROM sys_catalog.sys_roles r"); + expect(sql).toContain("FROM sys_catalog.sys_auth_members m"); + expect(sql).toContain("CROSS JOIN sys_catalog.sys_database d"); + expect(sql).toContain("CROSS JOIN sys_catalog.sys_namespace n"); + expect(sql).toContain("WHERE r.rolname = 'role''o'"); + expect(sql).not.toContain("pg_catalog"); + expect(sql).not.toContain("pg_roles"); + }); +}); diff --git a/apps/desktop/src/lib/database/databaseUserAdmin.ts b/apps/desktop/src/lib/database/databaseUserAdmin.ts index e1e94df6d9..a8316276d3 100644 --- a/apps/desktop/src/lib/database/databaseUserAdmin.ts +++ b/apps/desktop/src/lib/database/databaseUserAdmin.ts @@ -46,7 +46,8 @@ export interface DatabaseUserAdminProvider { } export const MYSQL_USER_ADMIN_TYPES = new Set(["mysql", "goldendb"]); -export const POSTGRES_USER_ADMIN_TYPES = new Set(["postgres", "gaussdb", "highgo", "kingbase", "kwdb", "opengauss", "questdb", "vastbase"]); +export const KINGBASE_USER_ADMIN_TYPES = new Set(["kingbase"]); +export const POSTGRES_USER_ADMIN_TYPES = new Set(["postgres", "gaussdb", "highgo", "kwdb", "opengauss", "questdb", "vastbase"]); export const MYSQL_COMMON_PRIVILEGES = ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "INDEX", "REFERENCES", "EXECUTE", "SHOW VIEW", "TRIGGER", "EVENT", "CREATE TEMPORARY TABLES"] as const; @@ -61,6 +62,7 @@ export function supportsDatabaseUserAdmin(dbType: DatabaseType | undefined): boo export function getDatabaseUserAdminProvider(dbType: DatabaseType | undefined): DatabaseUserAdminProvider | null { if (!dbType) return null; if (MYSQL_USER_ADMIN_TYPES.has(dbType)) return mysqlUserAdminProvider; + if (KINGBASE_USER_ADMIN_TYPES.has(dbType)) return kingbaseUserAdminProvider; if (POSTGRES_USER_ADMIN_TYPES.has(dbType)) return postgresUserAdminProvider; return null; } @@ -257,6 +259,91 @@ FROM ( ORDER BY sort, line;`.trim(); } +export function kingbaseListRolesSql(): string { + return ` +SELECT + r.rolname AS user, + CASE WHEN r.rolcanlogin THEN 'LOGIN' ELSE 'ROLE' END AS host, + concat_ws(', ', + CASE WHEN r.rolsuper THEN 'SUPERUSER' END, + CASE WHEN r.rolcreatedb THEN 'CREATEDB' END, + CASE WHEN r.rolcreaterole THEN 'CREATEROLE' END, + CASE WHEN r.rolreplication THEN 'REPLICATION' END, + CASE WHEN r.rolbypassrls THEN 'BYPASSRLS' END + ) AS plugin +FROM sys_catalog.sys_roles r +ORDER BY r.rolname;`.trim(); +} + +export function kingbaseShowGrantsSql(user: DatabaseUserIdentity): string { + const role = quoteSqlString(user.user); + return ` +WITH target AS ( + SELECT oid, rolname, rolsuper, rolcreatedb, rolcreaterole, rolcanlogin, rolreplication, rolbypassrls + FROM sys_catalog.sys_roles r + WHERE r.rolname = ${role} +) +SELECT line +FROM ( + SELECT 1 AS sort, 'Role: ' || quote_ident(rolname) AS line + FROM target + UNION ALL + SELECT 2, 'Attributes: ' || COALESCE(NULLIF(concat_ws(', ', + CASE WHEN rolsuper THEN 'SUPERUSER' END, + CASE WHEN rolcreatedb THEN 'CREATEDB' END, + CASE WHEN rolcreaterole THEN 'CREATEROLE' END, + CASE WHEN rolcanlogin THEN 'LOGIN' ELSE 'NOLOGIN' END, + CASE WHEN rolreplication THEN 'REPLICATION' END, + CASE WHEN rolbypassrls THEN 'BYPASSRLS' END + ), ''), 'none') + FROM target + UNION ALL + SELECT 10, 'Member of: ' || quote_ident(parent.rolname) || CASE WHEN m.admin_option THEN ' WITH ADMIN OPTION' ELSE '' END + FROM sys_catalog.sys_auth_members m + JOIN target t ON t.oid = m.member + JOIN sys_catalog.sys_roles parent ON parent.oid = m.roleid + UNION ALL + SELECT 20, 'Has member: ' || quote_ident(member.rolname) || CASE WHEN m.admin_option THEN ' WITH ADMIN OPTION' ELSE '' END + FROM sys_catalog.sys_auth_members m + JOIN target t ON t.oid = m.roleid + JOIN sys_catalog.sys_roles member ON member.oid = m.member + UNION ALL + SELECT 30, 'Database: ' || quote_ident(d.datname) || ' = ' || + concat_ws(', ', + CASE WHEN has_database_privilege(t.rolname, d.oid, 'CONNECT') THEN 'CONNECT' END, + CASE WHEN has_database_privilege(t.rolname, d.oid, 'CREATE') THEN 'CREATE' END, + CASE WHEN has_database_privilege(t.rolname, d.oid, 'TEMPORARY') THEN 'TEMPORARY' END + ) + FROM target t + CROSS JOIN sys_catalog.sys_database d + WHERE has_database_privilege(t.rolname, d.oid, 'CONNECT') + OR has_database_privilege(t.rolname, d.oid, 'CREATE') + OR has_database_privilege(t.rolname, d.oid, 'TEMPORARY') + UNION ALL + SELECT 40, 'Schema: ' || quote_ident(n.nspname) || ' = ' || + concat_ws(', ', + CASE WHEN has_schema_privilege(t.rolname, n.oid, 'USAGE') THEN 'USAGE' END, + CASE WHEN has_schema_privilege(t.rolname, n.oid, 'CREATE') THEN 'CREATE' END + ) + FROM target t + CROSS JOIN sys_catalog.sys_namespace n + WHERE UPPER(n.nspname) <> 'INFORMATION_SCHEMA' + AND UPPER(n.nspname) NOT LIKE 'SYS%' + AND UPPER(n.nspname) NOT LIKE 'XLOG%' + AND (has_schema_privilege(t.rolname, n.oid, 'USAGE') OR has_schema_privilege(t.rolname, n.oid, 'CREATE')) + UNION ALL + SELECT 50, 'Table: ' || quote_ident(table_schema) || '.' || quote_ident(table_name) || ' = ' || + string_agg(privilege_type || CASE WHEN is_grantable = 'YES' THEN ' WITH GRANT OPTION' ELSE '' END, ', ' ORDER BY privilege_type) + FROM information_schema.role_table_grants + WHERE grantee = ${role} + AND UPPER(table_schema) <> 'INFORMATION_SCHEMA' + AND UPPER(table_schema) NOT LIKE 'SYS%' + AND UPPER(table_schema) NOT LIKE 'XLOG%' + GROUP BY table_schema, table_name +) grants +ORDER BY sort, line;`.trim(); +} + export function postgresCreateRoleSql(input: CreatePrincipalInput): string { const login = input.canLogin === false ? "NOLOGIN" : "LOGIN"; return `CREATE ROLE ${quotePostgresIdentifier(input.user)} ${login} PASSWORD ${quoteSqlString(input.password)};`; @@ -373,3 +460,9 @@ export const postgresUserAdminProvider: DatabaseUserAdminProvider = { privilegesForScope: postgresPrivilegesForScope, defaultPrivilegesForScope: (scope) => (scope === "role" ? [] : [postgresDefaultPrivilege(scope)]), }; + +export const kingbaseUserAdminProvider: DatabaseUserAdminProvider = { + ...postgresUserAdminProvider, + listUsersSql: kingbaseListRolesSql, + showGrantsSql: kingbaseShowGrantsSql, +}; diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index b2b5a33970..0ddf8e8c61 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -676,7 +676,7 @@ impl ConnectionConfig { DatabaseType::Rqlite | DatabaseType::Turso => Some("main"), DatabaseType::Gaussdb | DatabaseType::OpenGauss => Some("postgres"), DatabaseType::Kwdb => Some("defaultdb"), - DatabaseType::Kingbase | DatabaseType::Vastbase => Some("postgres"), + DatabaseType::Vastbase => Some("postgres"), DatabaseType::Highgo => Some("highgo"), DatabaseType::Yashandb => Some("yasdb"), DatabaseType::Oscar => Some("osrdb"), @@ -2153,6 +2153,26 @@ mod tests { assert_eq!(config.effective_database(), Some("default")); } + #[test] + fn kingbase_empty_database_has_no_default() { + let mut config = mysql_config("SYSTEM", "secret", None); + config.db_type = DatabaseType::Kingbase; + config.port = 54321; + + assert_eq!(config.effective_database(), None); + assert_eq!(config.connection_url(), "kingbase://SYSTEM:secret@10.1.2.3:54321"); + } + + #[test] + fn vastbase_empty_database_uses_postgres_for_connection() { + let mut config = mysql_config("vastbase", "secret", None); + config.db_type = DatabaseType::Vastbase; + config.port = 5432; + + assert_eq!(config.effective_database(), Some("postgres")); + assert_eq!(config.connection_url(), "vastbase://vastbase:secret@10.1.2.3:5432/postgres"); + } + #[test] fn clickhouse_tls_uses_https_from_ssl_or_secure_param() { let mut config = mysql_config("default", "", None);