diff --git a/.github/scripts/database-issue-catalog.mjs b/.github/scripts/database-issue-catalog.mjs index 625610fd64..69b55e217d 100644 --- a/.github/scripts/database-issue-catalog.mjs +++ b/.github/scripts/database-issue-catalog.mjs @@ -19,7 +19,7 @@ const extraAliases = { saphana: ["hana", "sap hana"], opengauss: ["open gauss"], "oceanbase-oracle": ["oceanbase oracle", "oceanbase oracle mode", "oceanbase oracle 模式"], - gbase: ["gbase 8a", "gbase8a", "gbase 8s", "gbase8s"], + gbase: ["gbase 8a", "gbase8a", "gbase 8c", "gbase8c", "gbase 8s", "gbase8s"], access: ["microsoft access", "ms access"], vastbase: ["vastbase g", "vastbaseg"], prestosql: ["presto", "presto sql"], diff --git a/.github/scripts/label-pull-request.mjs b/.github/scripts/label-pull-request.mjs index 63ce79fef9..fdd2d9229a 100644 --- a/.github/scripts/label-pull-request.mjs +++ b/.github/scripts/label-pull-request.mjs @@ -38,6 +38,7 @@ const LABEL_PALETTE = ["0e8a16", "1d76db", "5319e7", "c2e0c6", "bfd4f2", "d4c5f9 const DRIVER_DATABASE_ALIASES = { "cassandra-go": "cassandra", gbase8a: "gbase", + gbase8c: "gbase", gbase8s: "gbase", "h2-legacy": "h2", kafka: "mq", diff --git a/agents/README.md b/agents/README.md index f2cbd77289..22d788ee1a 100644 --- a/agents/README.md +++ b/agents/README.md @@ -25,6 +25,7 @@ Each agent runs as a standalone process and communicates with DBX via stdin/stdo | exasol | Exasol | Exasol JDBC | | oceanbase-oracle | OceanBase Oracle Mode | OceanBase JDBC | | gbase8a | GBase 8a | External GBase 8a JDBC | +| gbase8c | GBase 8c | External GBase 8c JDBC | | gbase8s | GBase 8s | External GBase 8s JDBC | | oracle | Oracle 10g+ | go-ora native agent | | oracle-oci | Oracle OCI (thick driver) | godror native agent (Windows x64, CGO; shares `drivers/oracle-go`, built with `-tags oci`) | diff --git a/agents/README.zh-CN.md b/agents/README.zh-CN.md index c56f70dbc1..57d2303a9b 100644 --- a/agents/README.zh-CN.md +++ b/agents/README.zh-CN.md @@ -25,6 +25,7 @@ DBX 的 Agent 驱动 —— 通过 JDBC 和原生数据库驱动支持各种数 | exasol | Exasol | Exasol JDBC | | oceanbase-oracle | OceanBase Oracle 模式 | OceanBase JDBC | | gbase8a | GBase 8a | 外部 GBase 8a JDBC | +| gbase8c | GBase 8c | 外部 GBase 8c JDBC | | gbase8s | GBase 8s | 外部 GBase 8s JDBC | | oracle | Oracle 10g+ | go-ora 原生 agent | | h2 | H2 | H2 JDBC | diff --git a/agents/build.gradle b/agents/build.gradle index a7534be918..07f423b1ed 100644 --- a/agents/build.gradle +++ b/agents/build.gradle @@ -6,7 +6,7 @@ def infrastructureProjects = ['common', 'test-support'] as Set def legacyStandaloneProjects = ['mongodb', 'kafka'] as Set def pooledJdbcProjects = [ 'access', 'bigquery', 'dameng', 'databend', 'databricks', 'db2', 'exasol', - 'firebird', 'gbase8a', 'gbase8s', 'goldendb', 'h2', 'h2-legacy', 'highgo', + 'firebird', 'gbase8a', 'gbase8c', 'gbase8s', 'goldendb', 'h2', 'h2-legacy', 'highgo', 'ignite', 'ignite3', 'informix', 'iris', 'cache', 'kylin', 'oceanbase-oracle', 'oscar', 'saphana', 'snowflake', 'spanner', 'spark', 'sqlserver-legacy', 'sundb', 'teradata', 'trino', 'uxdb', 'vertica', 'yashandb', 'transwarp' diff --git a/agents/drivers/gbase8c/build.gradle b/agents/drivers/gbase8c/build.gradle new file mode 100644 index 0000000000..72ad94a4c8 --- /dev/null +++ b/agents/drivers/gbase8c/build.gradle @@ -0,0 +1,17 @@ +dependencies { + // 厂商只公开提供一个 zip 下载,Maven Central 没有 cn.gbase 坐标,也没有任何再分发条款, + // 因此驱动不入库:发布产物由 manifest 的 Agent-External-Driver 标记为需要用户自备, + // 经 jdbc_driver_paths 传入后由 AbstractJdbcAgent 用隔离的 URLClassLoader 加载。 + // 本地开发/冒烟时可把厂商 jar 放进 libs/(agents/.gitignore 的 **/libs/*.jar 已忽略)。 + implementation fileTree(dir: 'libs', include: ['*.jar']) +} + +tasks.named('shadowJar') { + manifest { + attributes( + 'Agent-Label': 'GBase 8c', + 'Agent-External-Driver': 'true', + 'Main-Class': 'com.dbx.agent.gbase8c.Gbase8cAgent' + ) + } +} diff --git a/agents/drivers/gbase8c/src/main/java/com/dbx/agent/gbase8c/Gbase8cAgent.java b/agents/drivers/gbase8c/src/main/java/com/dbx/agent/gbase8c/Gbase8cAgent.java new file mode 100644 index 0000000000..af062b53f8 --- /dev/null +++ b/agents/drivers/gbase8c/src/main/java/com/dbx/agent/gbase8c/Gbase8cAgent.java @@ -0,0 +1,118 @@ +package com.dbx.agent.gbase8c; + +import com.dbx.agent.ConfiguredJdbcAgent; +import com.dbx.agent.DatabaseInfo; +import com.dbx.agent.JdbcAgentProfile; +import com.dbx.agent.MultiSessionJsonRpcServer; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * 南大通用 GBase 8c Agent。 + * + *

GBase 8c 的内核是 openGauss(PostgreSQL 9.2 世代),元数据本可以手写 + * {@code pg_catalog} 查询,但真机实测 {@code JOIN LATERAL} 不支持、 + * {@code pg_catalog.to_regclass} 不存在,两者分别打死 {@code PostgresLikeAgent} + * 的三条热路径(primaryKeys / listIndexes / listForeignKeys)与视图源码,所以这里走 + * 厂商 JDBC 驱动的 {@link java.sql.DatabaseMetaData}({@link ConfiguredJdbcAgent} 路线)。 + * + *

厂商驱动必须由用户自备:{@code jdbc:gbase8c://} + {@code com.gbase8c.Driver} + * 是其唯一入口(它拒绝 {@code jdbc:postgresql://}),而该 jar 不在任何公开 Maven 仓库。 + */ +public final class Gbase8cAgent extends ConfiguredJdbcAgent { + /** + * GBase 8c 自带的系统 schema。 + * + *

实测 {@code getSchemas()} 会返回 19 个 schema,其中 17 个是 openGauss/GaussDB + * 内部使用的;只有 {@code public} 与用户自己建的 schema 才用户可见。名单必须大写, + * 因为 {@code StandardJdbcMetadata} 用 {@code name.toUpperCase(Locale.ROOT)} 比对。 + */ + static final Set GBASE8C_EXCLUDED_SCHEMAS = Set.of( + "AUDITADMIN", + "AUDITUSER", + "BLOCKCHAIN", + "COVERAGE", + "CSTORE", + "DB4AI", + "DBE_PERF", + "DBE_PLDEBUGGER", + "DBE_PLDEVELOPER", + "DBE_SQL_UTIL", + "INFORMATION_SCHEMA", + "PG_CATALOG", + "PKG_SERVICE", + "SECURITYADMIN", + "SNAPSHOT", + "SQLADVISOR", + "SYSADMIN" + ); + + /** + * 只向驱动索取 TABLE / VIEW。 + * + *

{@link JdbcAgentProfile} 的默认白名单含 {@code SYSTEM TABLE} 与 {@code SYSTEM VIEW}, + * 而厂商驱动确实会交出来:实测跨 schema 共 531 个对象,其中 331 个是系统对象 + * ({@code pg_catalog.gs_*}、{@code information_schema.sql_*})。驱动遵守 types 参数 + * (只传 {@code TABLE}/{@code VIEW} 时索引行与系统对象都不出现),所以收窄白名单即可, + * 不需要覆写 {@code listTables}。 + */ + static final List GBASE8C_TABLE_TYPES = List.of("TABLE", "VIEW"); + + /** + * 库列表必须自己查。 + * + *

继承路径是 {@code StandardJdbcMetadata.listDatabases} 的 + * {@code conn.getMetaData().getCatalogs()},而厂商驱动对它的实现只返回当前连接的库 + * (实测 1 行,服务器上却有 7 个库),因此改查 {@code pg_database}。 + * {@code datistemplate = false} 与 {@code PostgresLikeAgent} 保持一致。 + */ + static final String LIST_DATABASES_SQL = + "SELECT datname FROM pg_catalog.pg_database WHERE datistemplate = false ORDER BY datname"; + + public static final JdbcAgentProfile GBASE8C_PROFILE = new JdbcAgentProfile( + "com.gbase8c.Driver", + "jdbc:gbase8c://{host}:{port}/{database}", + 5432, + false, + GBASE8C_EXCLUDED_SCHEMAS, + GBASE8C_TABLE_TYPES, + // 驱动自己的 getIdentifierQuoteString() 在 A/B/C/PG 四种兼容模式下都返回双引号, + // 且实测双引号标识符四种模式都可用、反引号四种模式全部报语法错,所以这个回落值就是正解: + // 不要把它接到 gaussdbIdentifierQuote 的 M|B|MYSQL → 反引号 映射上。 + "\"", + "SET search_path TO", + // 末尾四个位置参数依次是:catalogFallbackEnabled=true(驱动 getSchemas(catalog) 不可靠时 + // 回落 getSchemas(null))、nativeTableDdlSupported=false、objectSourceSupported=false + // (ConfiguredJdbcAgent.getObjectSource 抛 UnsupportedOperationException)、 + // triggersSupported=false。三者与 plugins/connection-types/gbase8c.yaml 的能力矩阵一致。 + true, + false, + false, + false + ); + + public Gbase8cAgent() { + super(GBASE8C_PROFILE); + } + + @Override + public List listDatabases() { + return unchecked(() -> { + List result = new ArrayList<>(); + try (PreparedStatement statement = requireConnection().prepareStatement(LIST_DATABASES_SQL); + ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + result.add(new DatabaseInfo(resultSet.getString("datname"))); + } + } + return result; + }); + } + + public static void main(String[] args) { + new MultiSessionJsonRpcServer(Gbase8cAgent::new).run(); + } +} diff --git a/agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentLiveTest.java b/agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentLiveTest.java new file mode 100644 index 0000000000..6483be324d --- /dev/null +++ b/agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentLiveTest.java @@ -0,0 +1,179 @@ +package com.dbx.agent.gbase8c; + +import com.dbx.agent.ColumnInfo; +import com.dbx.agent.ConnectParams; +import com.dbx.agent.DatabaseInfo; +import com.dbx.agent.ExecuteQueryOptions; +import com.dbx.agent.ForeignKeyInfo; +import com.dbx.agent.IndexInfo; +import com.dbx.agent.ObjectInfo; +import com.dbx.agent.QueryResult; +import com.dbx.agent.TableInfo; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +/** + * 对真实 GBase 8c 实例跑的端到端冒烟,覆盖「本 Agent 相对基类新增/覆写的每一处」。 + * + *

需要环境变量 {@code DBX_TEST_GBASE8C_URL}(完整 {@code jdbc:gbase8c://…} URL)、 + * {@code DBX_TEST_GBASE8C_USER}、{@code DBX_TEST_GBASE8C_PASSWORD};可选 + * {@code DBX_TEST_GBASE8C_SCHEMA}(默认 {@code public})与 + * {@code DBX_TEST_GBASE8C_DATABASE}(默认 {@code postgres})。 + * + *

依赖:厂商 JDBC 驱动必须能从 classpath 解析(放到 + * {@code agents/drivers/gbase8c/libs/} 即可,该目录被 {@code agents/.gitignore} 忽略)。 + * 环境变量缺失时整个用例被跳过,与 {@code gbase8s/Gbase8sCharacterTypesLiveTest} 一致。 + */ +class Gbase8cAgentLiveTest { + @Test + @EnabledIfEnvironmentVariable(named = "DBX_TEST_GBASE8C_URL", matches = ".+") + void drivesMetadataQueryAndSchemaSwitchingAgainstALiveServer() throws Exception { + String schema = envOr("DBX_TEST_GBASE8C_SCHEMA", "public"); + String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 8); + String parent = "dbx_g8c_parent_" + suffix; + String child = "dbx_g8c_child_" + suffix; + Gbase8cAgent agent = new Gbase8cAgent(); + agent.connect(new ConnectParams( + "", + 0, + envOr("DBX_TEST_GBASE8C_DATABASE", "postgres"), + System.getenv("DBX_TEST_GBASE8C_USER"), + System.getenv("DBX_TEST_GBASE8C_PASSWORD"), + "", + System.getenv("DBX_TEST_GBASE8C_URL"), + false + )); + List created = new ArrayList<>(); + try (Statement statement = agent.getConnection().createStatement()) { + try { + statement.execute("CREATE TABLE " + schema + "." + parent + " (id int PRIMARY KEY, name varchar(20) NOT NULL)"); + created.add(parent); + statement.execute("CREATE TABLE " + schema + "." + child + + " (cid int PRIMARY KEY, pid int REFERENCES " + schema + "." + parent + " (id))"); + created.add(child); + statement.execute("INSERT INTO " + schema + "." + parent + " VALUES (1, 'one'), (2, 'two')"); + statement.execute("COMMENT ON TABLE " + schema + "." + parent + " IS 'smoke table'"); + + // 1. 库列表:驱动的 getCatalogs() 只返回当前库,必须由 listDatabases 覆写改查 pg_database。 + List databases = agent.listDatabases().stream().map(DatabaseInfo::getName).toList(); + Assertions.assertTrue(databases.size() > 1, "expected every database, got " + databases); + Assertions.assertTrue(databases.contains("postgres"), databases.toString()); + + // 2. schema 列表:openGauss 自带的系统 schema 必须被 excludedSchemas 过滤掉。 + List schemas = agent.listSchemas(); + Assertions.assertTrue(schemas.contains(schema), schemas.toString()); + for (String hidden : List.of("dbe_perf", "cstore", "db4ai", "sqladvisor", "pkg_service", "snapshot")) { + Assertions.assertFalse( + schemas.stream().anyMatch(name -> name.equalsIgnoreCase(hidden)), + "system schema leaked: " + hidden + " in " + schemas + ); + } + + // 3. 表列表:tableTypes 白名单必须把系统对象与索引行挡在外面(默认白名单会多出 331 行)。 + List tables = agent.listTables(schema); + List names = tables.stream().map(TableInfo::getName).toList(); + Assertions.assertTrue(names.contains(parent) && names.contains(child), names.toString()); + Set types = tables.stream().map(TableInfo::getTable_type).collect(Collectors.toSet()); + Assertions.assertTrue( + types.stream().allMatch(type -> "TABLE".equals(type) || "VIEW".equals(type)), + "unexpected table types: " + types + ); + Assertions.assertTrue( + tables.stream().noneMatch(table -> table.getName().startsWith("gs_") || table.getName().startsWith("sql_")), + "system object leaked: " + names + ); + Assertions.assertTrue( + tables.stream().filter(table -> table.getName().equals(parent)).findFirst().orElseThrow() + .getComment().contains("smoke table"), + "table comment should come back through REMARKS" + ); + + // 4. 列 / 主键 / 索引 / 外键 + List columns = agent.getColumns(schema, parent); + Assertions.assertEquals(List.of("id", "name"), columns.stream().map(ColumnInfo::getName).toList()); + Assertions.assertTrue( + columns.stream().filter(column -> column.getName().equals("id")).findFirst().orElseThrow() + .getIs_primary_key(), + "id should be reported as a primary key column" + ); + List indexes = agent.listIndexes(schema, parent); + // 实测:厂商驱动的 getIndexInfo 把主键索引报成普通唯一索引(is_primary=false), + // 与 getPrimaryKeys 的 PK_NAME 对不上;主键本身由 columns 的 is_primary_key 报告, + // 上面已断言。所以这里只断言索引与它的列确实被列出来。 + IndexInfo primaryKeyIndex = indexes.stream() + .filter(index -> index.getName().equals(parent + "_pkey")) + .findFirst() + .orElseThrow(() -> new AssertionError("primary key index missing: " + indexes)); + Assertions.assertEquals(List.of("id"), primaryKeyIndex.getColumns(), indexes.toString()); + Assertions.assertTrue(primaryKeyIndex.getIs_unique(), indexes.toString()); + List foreignKeys = agent.listForeignKeys(schema, child); + Assertions.assertEquals(1, foreignKeys.size(), foreignKeys.toString()); + Assertions.assertEquals(parent, foreignKeys.get(0).getRef_table(), foreignKeys.toString()); + + // 5. 标识符引号与 schema 切换:实测 A/B/C/PG 四种兼容模式都是双引号 + SET search_path。 + Assertions.assertEquals("SET search_path TO \"" + schema + "\"", agent.setSchemaSQL(schema)); + Assertions.assertEquals("\"", agent.getIdentifierQuote()); + + // 6. 查询执行 + QueryResult result = agent.executeQuery( + "SELECT id, name FROM " + schema + "." + parent + " ORDER BY id", + null, + new ExecuteQueryOptions() + ); + Assertions.assertEquals(List.of("id", "name"), result.getColumns()); + Assertions.assertEquals(2, result.getRows().size(), result.getRows().toString()); + + // 7. 对象列表(ConfiguredJdbcAgent 路径,objectSource 关闭不影响对象浏览) + List objects = agent.listObjects(schema); + Assertions.assertTrue( + objects.stream().anyMatch(object -> object.getName().equals(parent)), + objects.toString() + ); + + // 8. 数据网格的写入路径:capabilities.tableDataEdit 走 executeTransaction + // (显式事务 + Statement.executeUpdate)。实测驱动 supportsTransactions()=true, + // 三条 DML 共用一次提交,返回累计受影响行数(UPDATE 1 + DELETE 0 + INSERT 1)。 + QueryResult write = agent.executeTransaction( + List.of( + "UPDATE " + schema + "." + parent + " SET name = 'one-updated' WHERE id = 1", + "DELETE FROM " + schema + "." + child + " WHERE cid = 99", + "INSERT INTO " + schema + "." + child + " VALUES (10, 1)" + ), + schema + ); + Assertions.assertEquals(2, write.getAffected_rows(), "update + insert should report 2 affected rows"); + + QueryResult reread = agent.executeQuery( + "SELECT count(*) FROM " + schema + "." + child, + null, + new ExecuteQueryOptions() + ); + Assertions.assertEquals("1", String.valueOf(reread.getRows().get(0).get(0)), reread.getRows().toString()); + } finally { + Collections.reverse(created); + for (String name : created) { + try (Statement cleanup = agent.getConnection().createStatement()) { + cleanup.execute("DROP TABLE IF EXISTS " + schema + "." + name + " CASCADE"); + } catch (Exception ignored) { + // Best-effort cleanup: the smoke database is disposable. + } + } + } + } finally { + agent.disconnect(); + } + } + + private static String envOr(String name, String fallback) { + String value = System.getenv(name); + return value == null || value.isBlank() ? fallback : value; + } +} diff --git a/agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentMetadataTest.java b/agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentMetadataTest.java new file mode 100644 index 0000000000..b463cd03e8 --- /dev/null +++ b/agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentMetadataTest.java @@ -0,0 +1,380 @@ +package com.dbx.agent.gbase8c; + +import com.dbx.agent.ConfiguredJdbcAgent; +import com.dbx.agent.ConnectParams; +import com.dbx.agent.DatabaseInfo; +import com.dbx.agent.TableInfo; +import com.dbx.agent.test.TestSupport; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * GBase 8c 的元数据行为契约。 + * + *

三条断言各自对应一次真机实测,注释里写了依据,改动前请先看 + * {@code docs/pips/plans/2026-10-16-gbase8c-support.md} 的附录 A。 + */ +class Gbase8cAgentMetadataTest { + /** 实测 getSchemas() 返回 19 个,其中 17 个是 openGauss/GaussDB 内部 schema。 */ + private static final List VENDOR_SCHEMAS = List.of( + "auditadmin", "audituser", "blockchain", "coverage", "cstore", "db4ai", + "dbe_perf", "dbe_pldebugger", "dbe_pldeveloper", "dbe_sql_util", + "information_schema", "pg_catalog", "pkg_service", "securityadmin", + "snapshot", "sqladvisor", "sysadmin", "dbx_smoke", "public" + ); + + /** 实测 getTableTypes() 里含非标准项,其中 INDEX 会让索引变成「表」。 */ + private static final List VENDOR_TABLE_TYPES = List.of( + "FOREIGN TABLE", "INDEX", "LOCAL TEMPORARY", "SEQUENCE", "SYSTEM INDEX", + "SYSTEM TABLE", "SYSTEM TOAST INDEX", "SYSTEM TOAST TABLE", "SYSTEM VIEW", + "TABLE", "TEMPORARY TABLE", "TYPE", "VIEW" + ); + + @Test + void listsEveryDatabaseFromPgDatabaseInsteadOfTheSingleGetCatalogsRow() { + // 实测 getCatalogs() 只返回当前连接的库(1 行),而服务器上有 7 个库。 + List statements = new ArrayList<>(); + Gbase8cAgent agent = new Gbase8cAgent(); + TestSupport.setPrivateConnection( + agent, + connection( + statements, + metadata(emptyRows(), emptyRows(), emptyRows(), emptyRows(), new ArrayList<>()), + rows(new String[] {"datname"}, new Object[][] { + {"dbx_a"}, {"dbx_b"}, {"dbx_c"}, {"dbx_pg"}, {"postgres"} + }), + "dbx_pg", + "public" + ) + ); + setConfiguredDatabase(agent, "dbx_pg"); + + List databases = agent.listDatabases().stream().map(DatabaseInfo::getName).toList(); + + Assertions.assertEquals( + List.of("dbx_a", "dbx_b", "dbx_c", "dbx_pg", "postgres"), + databases + ); + Assertions.assertEquals(List.of(Gbase8cAgent.LIST_DATABASES_SQL), statements); + } + + @Test + void hidesOpenGaussSystemSchemasFromTheSchemaList() { + List metadataRequests = new ArrayList<>(); + Gbase8cAgent agent = new Gbase8cAgent(); + TestSupport.setPrivateConnection( + agent, + connection( + new ArrayList<>(), + metadata(emptyRows(), schemas(VENDOR_SCHEMAS), emptyRows(), emptyRows(), metadataRequests), + emptyRows(), + "dbx_pg", + "public" + ) + ); + setConfiguredDatabase(agent, "dbx_pg"); + + List visible = agent.listSchemas(); + + Assertions.assertEquals(List.of("dbx_smoke", "public"), visible); + // 驱动拿到的 catalog 是连接上的库名;回退路径不应被触发。 + Assertions.assertEquals(List.of("getSchemas:dbx_pg"), metadataRequests); + } + + @Test + void asksTheDriverOnlyForTablesAndViews() { + // 默认白名单含 SYSTEM TABLE / SYSTEM VIEW,实测跨 schema 会多出 331 个系统对象。 + List requestedTypes = new ArrayList<>(); + List statements = new ArrayList<>(); + Gbase8cAgent agent = new Gbase8cAgent(); + TestSupport.setPrivateConnection( + agent, + connection( + statements, + metadata( + emptyRows(), + emptyRows(), + rows(new String[] {"TABLE_TYPE"}, toRows(VENDOR_TABLE_TYPES)), + rows( + new String[] {"TABLE_NAME", "TABLE_TYPE", "REMARKS"}, + new Object[][] {{"t1", "TABLE", "smoke table"}, {"v1", "VIEW", null}} + ), + new ArrayList<>(), + requestedTypes + ), + emptyRows(), + "dbx_pg", + "public" + ) + ); + setConfiguredDatabase(agent, "dbx_pg"); + + List tables = agent.listTables("dbx_smoke"); + + Assertions.assertEquals(List.of("TABLE,VIEW"), requestedTypes); + Assertions.assertEquals(List.of("t1", "v1"), tables.stream().map(TableInfo::getName).toList()); + Assertions.assertEquals("smoke table", tables.get(0).getComment()); + } + + @Test + void switchesSchemaWithSetSearchPath() { + Gbase8cAgent agent = new Gbase8cAgent(); + TestSupport.setPrivateConnection( + agent, + connection( + new ArrayList<>(), + metadata(emptyRows(), emptyRows(), emptyRows(), emptyRows(), new ArrayList<>()), + emptyRows(), + "dbx_pg", + "public" + ) + ); + + Assertions.assertEquals("SET search_path TO \"dbx_smoke\"", agent.setSchemaSQL("dbx_smoke")); + } + + @Test + void declaresTheVendorJdbcProfileAndExcludesEverySystemSchema() { + Assertions.assertEquals("com.gbase8c.Driver", Gbase8cAgent.GBASE8C_PROFILE.getDriverClass()); + Assertions.assertEquals(5432, Gbase8cAgent.GBASE8C_PROFILE.getDefaultPort()); + Assertions.assertEquals( + "jdbc:gbase8c://{host}:{port}/{database}", + Gbase8cAgent.GBASE8C_PROFILE.getUrlTemplate() + ); + // 实测四种兼容模式的 getIdentifierQuoteString() 都是双引号,且双引号确实可用。 + Assertions.assertEquals("\"", Gbase8cAgent.GBASE8C_PROFILE.getIdentifierQuote()); + Assertions.assertEquals("SET search_path TO", Gbase8cAgent.GBASE8C_PROFILE.getSchemaSwitchPrefix()); + Assertions.assertFalse(Gbase8cAgent.GBASE8C_PROFILE.getSkipExecutionContext()); + Assertions.assertFalse(Gbase8cAgent.GBASE8C_PROFILE.getNativeTableDdlSupported()); + Assertions.assertFalse(Gbase8cAgent.GBASE8C_PROFILE.getObjectSourceSupported()); + Assertions.assertFalse(Gbase8cAgent.GBASE8C_PROFILE.getTriggersSupported()); + // 驱动行的 TABLE_CAT=null,schema 查询需要 catalog 回退兜底。 + Assertions.assertTrue(Gbase8cAgent.GBASE8C_PROFILE.getCatalogFallbackEnabled()); + Assertions.assertEquals(List.of("TABLE", "VIEW"), Gbase8cAgent.GBASE8C_PROFILE.getTableTypes()); + Assertions.assertEquals(Gbase8cAgent.GBASE8C_EXCLUDED_SCHEMAS, Gbase8cAgent.GBASE8C_PROFILE.getExcludedSchemas()); + Assertions.assertTrue(Gbase8cAgent.GBASE8C_EXCLUDED_SCHEMAS.contains("DBE_PERF")); + Assertions.assertTrue(Gbase8cAgent.GBASE8C_EXCLUDED_SCHEMAS.contains("PG_CATALOG")); + Assertions.assertEquals(17, Gbase8cAgent.GBASE8C_EXCLUDED_SCHEMAS.size()); + + ConnectParams params = new ConnectParams(); + params.setHost("10.1.2.3"); + params.setPort(5432); + params.setDatabase("dbx_pg"); + + Assertions.assertEquals( + "jdbc:gbase8c://10.1.2.3:5432/dbx_pg", + Gbase8cAgent.GBASE8C_PROFILE.buildUrl(params) + ); + } + + private static void setConfiguredDatabase(Gbase8cAgent agent, String database) { + try { + Field field = ConfiguredJdbcAgent.class.getDeclaredField("configuredDatabase"); + field.setAccessible(true); + field.set(agent, database); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + private static DatabaseMetaData metadata( + ResultSet catalogs, + ResultSet schemas, + ResultSet tableTypes, + ResultSet tables, + List metadataRequests + ) { + return metadata(catalogs, schemas, tableTypes, tables, metadataRequests, new ArrayList<>()); + } + + private static DatabaseMetaData metadata( + ResultSet catalogs, + ResultSet schemas, + ResultSet tableTypes, + ResultSet tables, + List metadataRequests, + List requestedTypes + ) { + return proxy(DatabaseMetaData.class, (proxy, method, args) -> { + switch (method.getName()) { + case "getCatalogs": + return catalogs; + case "getSchemas": + metadataRequests.add("getSchemas:" + args[0]); + return schemas; + case "getTableTypes": + return tableTypes; + case "getTables": + requestedTypes.add(String.join(",", (String[]) args[3])); + return tables; + case "getIdentifierQuoteString": + // 厂商驱动在 A/B/C/PG 四种兼容模式下都返回这个值。 + return "\""; + default: + return defaultValue(method.getReturnType()); + } + }); + } + + private static Connection connection( + List statements, + DatabaseMetaData metadata, + ResultSet preparedRows, + String catalog, + String schema + ) { + return proxy(Connection.class, (proxy, method, args) -> { + switch (method.getName()) { + case "createStatement": + return statement(statements); + case "prepareStatement": + statements.add((String) args[0]); + return preparedStatement(preparedRows); + case "getMetaData": + return metadata; + case "getCatalog": + return catalog; + case "getSchema": + return schema; + case "getAutoCommit": + return true; + case "isClosed": + return false; + case "close": + return null; + default: + return defaultValue(method.getReturnType()); + } + }); + } + + private static Statement statement(List statements) { + ResultSet resultSet = emptyRows(); + return proxy(Statement.class, (proxy, method, args) -> { + switch (method.getName()) { + case "execute": + statements.add((String) args[0]); + return false; + case "executeQuery": + statements.add((String) args[0]); + return resultSet; + case "getResultSet": + return resultSet; + case "getUpdateCount": + return 0; + case "close": + return null; + default: + return defaultValue(method.getReturnType()); + } + }); + } + + private static PreparedStatement preparedStatement(ResultSet resultSet) { + return proxy(PreparedStatement.class, (proxy, method, args) -> { + switch (method.getName()) { + case "executeQuery": + return resultSet; + case "getResultSet": + return resultSet; + case "getUpdateCount": + return 0; + case "close": + return null; + default: + return defaultValue(method.getReturnType()); + } + }); + } + + private static ResultSet schemas(List names) { + Object[][] data = new Object[names.size()][]; + for (int i = 0; i < names.size(); i++) { + data[i] = new Object[] {names.get(i)}; + } + return rows(new String[] {"TABLE_SCHEM"}, data); + } + + private static Object[][] toRows(List values) { + Object[][] data = new Object[values.size()][]; + for (int i = 0; i < values.size(); i++) { + data[i] = new Object[] {values.get(i)}; + } + return data; + } + + private static ResultSet rows(String[] columns, Object[][] data) { + final int[] index = {-1}; + return proxy(ResultSet.class, (proxy, method, args) -> { + switch (method.getName()) { + case "next": + index[0]++; + return index[0] < data.length; + case "getString": { + Object cell = cell(data[index[0]], columns, args[0]); + return cell == null ? null : cell.toString(); + } + case "wasNull": + return false; + case "close": + return null; + default: + return defaultValue(method.getReturnType()); + } + }); + } + + private static Object cell(Object[] row, String[] columns, Object selector) { + int position = selector instanceof Number + ? ((Number) selector).intValue() - 1 + : Arrays.asList(columns).indexOf((String) selector); + return row[position]; + } + + private static ResultSet emptyRows() { + return rows(new String[0], new Object[0][]); + } + + private static T proxy(Class type, InvocationHandler handler) { + return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, handler)); + } + + private static Object defaultValue(Class type) { + if (Boolean.TYPE.equals(type)) { + return false; + } + if (Byte.TYPE.equals(type)) { + return (byte) 0; + } + if (Short.TYPE.equals(type)) { + return (short) 0; + } + if (Integer.TYPE.equals(type)) { + return 0; + } + if (Long.TYPE.equals(type)) { + return 0L; + } + if (Float.TYPE.equals(type)) { + return 0f; + } + if (Double.TYPE.equals(type)) { + return 0.0d; + } + if (Character.TYPE.equals(type)) { + return '\0'; + } + return null; + } +} diff --git a/agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentTest.java b/agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentTest.java new file mode 100644 index 0000000000..bb5f2171f6 --- /dev/null +++ b/agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentTest.java @@ -0,0 +1,29 @@ +package com.dbx.agent.gbase8c; + +import com.dbx.agent.ConnectParams; +import com.dbx.agent.DatabaseAgent; +import com.dbx.agent.test.JdbcFakeExecutionBehaviorTest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class Gbase8cAgentTest extends JdbcFakeExecutionBehaviorTest { + @Override + protected DatabaseAgent createAgent() { + return new Gbase8cAgent(); + } + + @Override + protected String resultSetSql() { + return "CALL sample_proc()"; + } + + @Test + void buildsTheVendorJdbcUrlFromTheConnectionFields() { + // 厂商驱动只认 jdbc:gbase8c:// 与 com.gbase8c.Driver(实测 jdbc:postgresql:// 会被 + // 拒绝、jar 内也没有 Maven 坐标),所以 URL 模板与驱动类是这套集成的对外契约。 + ConnectParams params = new ConnectParams("10.1.2.3", 5432, "warehouse", "gbase", "secret", "", "", false); + + Assertions.assertEquals("jdbc:gbase8c://10.1.2.3:5432/warehouse", Gbase8cAgent.GBASE8C_PROFILE.buildUrl(params)); + Assertions.assertEquals("com.gbase8c.Driver", Gbase8cAgent.GBASE8C_PROFILE.getDriverClass()); + } +} diff --git a/agents/settings.gradle b/agents/settings.gradle index a2a8dbeec4..9653ed95b4 100644 --- a/agents/settings.gradle +++ b/agents/settings.gradle @@ -3,7 +3,7 @@ rootProject.name = 'dbx-agents' def infrastructureModules = ['common', 'test-support'] def driverModules = [ 'access', 'dameng', 'goldendb', 'databend', 'databricks', 'saphana', - 'teradata', 'vertica', 'firebird', 'exasol', 'oceanbase-oracle', 'gbase8a', 'gbase8s', + 'teradata', 'vertica', 'firebird', 'exasol', 'oceanbase-oracle', 'gbase8a', 'gbase8c', 'gbase8s', 'bigquery', 'kylin', 'ignite', 'ignite3', 'spanner', 'sundb', 'h2', 'h2-legacy', 'snowflake', 'trino', 'spark', 'db2', 'informix', 'mongodb', 'highgo', 'uxdb', 'yashandb', 'oscar', 'iris', 'cache', 'kafka', 'sqlserver-legacy', 'transwarp' diff --git a/agents/versions.json b/agents/versions.json index 4f78fede12..9aba1a12e6 100644 --- a/agents/versions.json +++ b/agents/versions.json @@ -11,6 +11,7 @@ "exasol": "0.1.77", "firebird": "0.1.79", "gbase8a": "0.1.76", + "gbase8c": "0.1.0", "gbase8s": "0.1.71", "goldendb": "0.1.82", "h2": "0.1.82", diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 89311d9630..e21840a71e 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -76,6 +76,7 @@ import type { PluginCenterFocus } from "@/lib/plugins/pluginCenterNavigation"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { isWindows } from "@/lib/backend/platform"; import { applyMeilisearchBasePathToExternalConfig, applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl"; +import { hasGbase8cConnectionDatabase } from "@/lib/connection/gbase8cDatabase"; import { loadVisibleDatabasesPickerNames } from "@/lib/connection/visibleDatabasesPickerLoad"; import { hasXuguConnectionDatabase } from "@/lib/connection/xuguDatabase"; import { @@ -3359,6 +3360,7 @@ const databasePlaceholder = computed(() => { if (form.value.db_type === "oracle" && form.value.oracle_connection_type === "tns") return t("connection.oracleTnsAliasPlaceholder"); if (form.value.db_type === "xugu") return t("connection.databasePlaceholderRequired"); if (form.value.db_type === "kingbase") return t("connection.databasePlaceholderRequired"); + if (form.value.db_type === "gbase8c") return t("connection.databasePlaceholderRequired"); const fallback = defaultDatabaseForProfile(); if (!fallback) return t("connection.databasePlaceholder"); return t("connection.databasePlaceholderWithDefault", { database: fallback }); @@ -3811,7 +3813,7 @@ function dbCategoryForOption(value: string): DbCategoryKey | undefined { const selectedDbIcon = computed(() => (isPluginConnection.value ? "plugin" : iconTypeMap[selectedType.value] || selectedProfile().icon || selectedType.value)); function supportsNativeAgentJdbcDriverConfigType(dbType: DatabaseType): boolean { - return dbType === "prestosql" || dbType === "bigquery" || dbType === "dameng" || dbType === "sundb"; + return dbType === "prestosql" || dbType === "bigquery" || dbType === "dameng" || dbType === "sundb" || dbType === "gbase8c"; } const jdbcBackedDatabaseTypes = new Set(["jdbc", "prestosql", "bigquery"]); @@ -3830,6 +3832,7 @@ const supportsNativeAgentJdbcDriverConfig = computed(() => supportsNativeAgentJd const nativeAgentJdbcDriverHint = computed(() => { if (form.value.db_type === "dameng") return t("connection.damengCustomDriverHint"); if (form.value.db_type === "sundb") return t("connection.sundbCustomDriverHint"); + if (form.value.db_type === "gbase8c") return t("connection.gbase8cCustomDriverHint"); return t("connection.jdbcPluginHint"); }); const isH2FileMode = computed(() => form.value.db_type === "h2" && h2ConnectionMode.value === "file"); @@ -4914,6 +4917,12 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi throw new Error(t("connection.kingbaseDatabaseRequired")); } } + if (config.db_type === "gbase8c") { + config.database = config.database?.trim() || undefined; + if (!hasGbase8cConnectionDatabase(config.database, config.connection_string)) { + throw new Error(t("connection.gbase8cDatabaseRequired")); + } + } if (config.db_type === "dynamodb") { config.database = config.database?.trim() || "us-east-1"; config.username = config.username.trim(); diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue index 4f5a635c8e..a0c29c0a56 100644 --- a/apps/desktop/src/components/icons/DatabaseIcon.vue +++ b/apps/desktop/src/components/icons/DatabaseIcon.vue @@ -98,6 +98,7 @@ const assetIcons: Record = { exasol: "exasol", gbase: "gbase.png", gbase8a: "gbase.png", + gbase8c: "gbase.png", gbase8s: "gbase.png", tdsql: "tdsql", polardb: "polardb.webp", diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index bb036238d5..48ed39d2ba 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -641,6 +641,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "Tələb olunur, mövcud verilənlər bazasını daxil edin", databasePlaceholderWithDefault: "İstəyə bağlı, standart olaraq {database}", kingbaseDatabaseRequired: "KingbaseES əlaqə qurmazdan və ya verilənlər bazası siyahısını yükləməzdən əvvəl mövcud verilənlər bazası tələb edir.", + gbase8cDatabaseRequired: "GBase 8c üçün qoşulmadan əvvəl mövcud verilənlər bazası tələb olunur.", xuguDatabaseRequired: "XuguDB-yə qoşulmaq üçün əvvəlcə mövcud verilənlər bazası daxil edilməlidir.", defaultDatabase: "Standart verilənlər bazası", authDatabase: "Autentifikasiya verilənlər bazası", @@ -768,6 +769,8 @@ export default withEnglishFallback({ damengDriverModeHint: "DM8 üçün daxil edilmiş drayverdən istifadə edin. DM6 və ya başqa uyğun versiya üçün onun rəsmi JDBC JAR faylını seçin.", damengCustomDriverHint: "Lisenziyalı mənbədən Dameng JDBC JAR faylı seçin. Fərdi drayver Dameng agenti daxilində təcrid olunur və ümumi JDBC plaginini tələb etmir.", damengCustomDriverRequired: "Dameng JDBC drayverinin JAR faylını seçin.", + gbase8cCustomDriverHint: + "GBase 8c üçün Agent ilə birlikdə gəlməyən vendor JDBC drayveri (com.gbase8c.Driver) tələb olunur. gbase.cn saytından endirin, «Əl ilə təyin olunan classpath» açarını yandırın və gbase8c-jdbc-*.jar faylını göstərin. Quraşdırılmış drayver paketi bu drayveri artıq ehtiva edirsə, heç nə doldurmaq lazım deyil.", sundbCustomDriverHint: "SunDB agenti vendor JDBC drayverini özü ilə gətirir, ona görə JAR tələb olunmur (drayver sinfi csii.sundb.jdbc.SundbDriver). Yalnız daha yeni vendor drayveri yükləmək üçün burada JAR göstərin; fərdi drayver agent daxilində təcrid olunaraq yüklənir.", redisTlsInsecure: "Sertifikat yoxlamasını ötür", redisTlsInsecureHint: "Öz imzalı sertifikatlar və ya özəl CA-lar üçün redis-cli --tls --insecure əmrinə bərabərdir.", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 917615a20b..6241041ebe 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1215,6 +1215,7 @@ export default { databasePlaceholderRequired: "Required, enter an existing database", databasePlaceholderWithDefault: "Optional, defaults to {database}", kingbaseDatabaseRequired: "KingbaseES requires an existing database before connecting or loading the database list.", + gbase8cDatabaseRequired: "GBase 8c requires an existing database before connecting.", xuguDatabaseRequired: "XuguDB requires an existing database before connecting.", defaultDatabase: "Default DB", authDatabase: "Auth DB", @@ -1397,6 +1398,7 @@ export default { damengDriverModeHint: "Use the bundled driver for DM8. For DM6 or another compatible version, select its official JDBC JAR.", damengCustomDriverHint: "Select a Dameng JDBC JAR from a licensed source. The custom driver is isolated inside the Dameng agent and does not require the generic JDBC plugin.", damengCustomDriverRequired: "Select a Dameng JDBC driver JAR.", + gbase8cCustomDriverHint: "GBase 8c needs the vendor JDBC driver (com.gbase8c.Driver), which the Agent package does not include. Download it from gbase.cn, turn on «Manual classpath», and point it at gbase8c-jdbc-*.jar. If the driver package you installed already carries it, leave this empty.", sundbCustomDriverHint: "The SunDB Agent ships the vendor JDBC driver, so no JAR is needed (driver class csii.sundb.jdbc.SundbDriver). Provide a JAR here only to load a newer vendor driver; custom drivers are loaded in isolation inside the Agent.", redisTlsInsecure: "Skip certificate verification", redisTlsInsecureHint: "Equivalent to redis-cli --tls --insecure, for self-signed certificates or private CAs.", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index d752b3fee6..388745337e 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1065,6 +1065,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "Obligatorio, ingresa una base de datos existente", databasePlaceholderWithDefault: "Opcional, por defecto {database}", kingbaseDatabaseRequired: "KingbaseES requiere una base de datos existente antes de conectar o cargar la lista de bases de datos.", + gbase8cDatabaseRequired: "GBase 8c requiere una base de datos existente antes de conectarse.", xuguDatabaseRequired: "XuguDB requiere una base de datos existente antes de conectarse.", defaultDatabase: "BD predeterminada", authDatabase: "BD de autenticación", @@ -1887,6 +1888,7 @@ export default withEnglishFallback({ damengDriverModeHint: "Use el controlador incluido para DM8. Para DM6 u otra versión compatible, seleccione su JAR JDBC oficial.", damengCustomDriverHint: "Seleccione un JAR JDBC de Dameng de una fuente con licencia. El controlador se carga de forma aislada en el agente de Dameng y no requiere el complemento JDBC genérico.", damengCustomDriverRequired: "Seleccione un JAR del controlador JDBC de Dameng.", + gbase8cCustomDriverHint: "GBase 8c requiere el controlador JDBC del fabricante (com.gbase8c.Driver), que el paquete del Agent no incluye. Descárguelo desde gbase.cn, active «Classpath manual» y apúntelo a gbase8c-jdbc-*.jar. Si el paquete de controlador instalado ya lo incluye, déjelo vacío.", sundbCustomDriverHint: "El agente de SunDB ya incluye el controlador JDBC del fabricante, por lo que no se necesita ningún JAR (clase de controlador csii.sundb.jdbc.SundbDriver). Indique aquí un JAR solo para cargar un controlador del fabricante más reciente; el controlador personalizado se carga de forma aislada dentro del agente.", zookeeperClusterInputHint: "El clúster ZooKeeper puede completarse con todos los nodos en la URL superior (por ejemplo, zookeeper://zk-1:2181,zk-2:2181/app), o en el Connect String a continuación.", diff --git a/apps/desktop/src/i18n/locales/id.ts b/apps/desktop/src/i18n/locales/id.ts index f03ff1e536..c60d325ab9 100644 --- a/apps/desktop/src/i18n/locales/id.ts +++ b/apps/desktop/src/i18n/locales/id.ts @@ -842,6 +842,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "Wajib diisi, masukkan database yang sudah ada", databasePlaceholderWithDefault: "Opsional, default ke {database}", kingbaseDatabaseRequired: "KingbaseES memerlukan database yang sudah ada sebelum menghubungkan atau memuat daftar database.", + gbase8cDatabaseRequired: "GBase 8c memerlukan database yang sudah ada sebelum menghubungkan.", xuguDatabaseRequired: "XuguDB memerlukan database yang sudah ada sebelum menghubungkan.", defaultDatabase: "DB Default", authDatabase: "DB Auth", @@ -1024,6 +1025,7 @@ export default withEnglishFallback({ damengDriverModeHint: "Gunakan driver bawaan untuk DM8. Untuk DM6 atau versi kompatibel lainnya, pilih JAR JDBC resminya.", damengCustomDriverHint: "Pilih JAR JDBC Dameng dari sumber berlisensi. Driver kustom diisolasi di dalam agent Dameng dan tidak memerlukan plugin JDBC generik.", damengCustomDriverRequired: "Pilih JAR driver JDBC Dameng.", + gbase8cCustomDriverHint: "GBase 8c memerlukan driver JDBC vendor (com.gbase8c.Driver) yang tidak disertakan paket Agent. Unduh dari gbase.cn, aktifkan «Classpath manual», lalu arahkan ke gbase8c-jdbc-*.jar. Jika paket driver yang Anda pasang sudah membawanya, biarkan kosong.", sundbCustomDriverHint: "SunDB Agent sudah menyertakan driver JDBC vendor, jadi tidak diperlukan JAR (driver class csii.sundb.jdbc.SundbDriver). Sediakan JAR di sini hanya untuk memuat driver vendor yang lebih baru; driver kustom dimuat secara terisolasi di dalam Agent.", redisTlsInsecure: "Lewati verifikasi sertifikat", redisTlsInsecureHint: "Setara dengan redis-cli --tls --insecure, untuk sertifikat self-signed atau CA privat.", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 24d22b4863..cd3007cbcf 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1054,6 +1054,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "Obbligatorio, inserisci un database esistente", databasePlaceholderWithDefault: "Opzionale, predefinito {database}", kingbaseDatabaseRequired: "KingbaseES richiede un database esistente prima di connettersi o caricare l'elenco dei database.", + gbase8cDatabaseRequired: "GBase 8c richiede un database esistente prima di connettersi.", xuguDatabaseRequired: "XuguDB richiede un database esistente prima di connettersi.", defaultDatabase: "DB Predefinito", authDatabase: "DB di Autenticazione", @@ -1796,6 +1797,7 @@ export default withEnglishFallback({ damengDriverModeHint: "Usa il driver integrato per DM8. Per DM6 o un'altra versione compatibile, seleziona il relativo JAR JDBC ufficiale.", damengCustomDriverHint: "Seleziona un JAR JDBC Dameng da una fonte autorizzata. Il driver viene caricato in modo isolato nell'agente Dameng e non richiede il plugin JDBC generico.", damengCustomDriverRequired: "Seleziona un JAR del driver JDBC Dameng.", + gbase8cCustomDriverHint: "GBase 8c richiede il driver JDBC del fornitore (com.gbase8c.Driver), che il pacchetto Agent non include. Scaricalo da gbase.cn, attiva «Classpath manuale» e indicalo su gbase8c-jdbc-*.jar. Se il pacchetto driver installato lo contiene già, lascia vuoto.", sundbCustomDriverHint: "L'agente SunDB include il driver JDBC del fornitore, quindi non serve alcun JAR (classe driver csii.sundb.jdbc.SundbDriver). Indica qui un JAR solo per caricare un driver del fornitore più recente; il driver personalizzato viene caricato in modo isolato all'interno dell'agente.", zookeeperClusterInputHint: "Il cluster ZooKeeper può essere specificato inserendo tutti i nodi nell'URL superiore (ad esempio zookeeper://zk-1:2181,zk-2:2181/app), oppure nella stringa di connessione sottostante.", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 596a7da2c8..973fcb8bf1 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1054,6 +1054,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "必須、既存のデータベースを入力", databasePlaceholderWithDefault: "任意、デフォルトは {database}", kingbaseDatabaseRequired: "KingbaseES は接続またはデータベース一覧の読み込み前に、既存のデータベース名が必要です。", + gbase8cDatabaseRequired: "GBase 8c に接続するには、既存のデータベース名が必要です。", xuguDatabaseRequired: "XuguDB に接続するには、既存のデータベース名が必要です。", defaultDatabase: "デフォルトDB", authDatabase: "認証DB", @@ -1796,6 +1797,8 @@ export default withEnglishFallback({ damengDriverModeHint: "DM8 では内蔵ドライバーを使用します。DM6 またはその他の互換バージョンでは、対応する公式 JDBC JAR を選択してください。", damengCustomDriverHint: "正規の提供元から入手した Dameng JDBC JAR を選択してください。カスタムドライバーは Dameng エージェント内で分離して読み込まれ、汎用 JDBC プラグインは不要です。", damengCustomDriverRequired: "Dameng JDBC ドライバー JAR を選択してください。", + gbase8cCustomDriverHint: + "GBase 8c はベンダー提供の JDBC ドライバー(com.gbase8c.Driver)が必要で、Agent のパッケージには含まれていません。gbase.cn からダウンロードし、「手動クラスパス」をオンにして gbase8c-jdbc-*.jar を指定してください。インストール済みのドライバーパッケージに同梱されている場合は入力不要です。", sundbCustomDriverHint: "SunDB エージェントにはベンダー製 JDBC ドライバーが同梱されているため、JAR の指定は不要です(ドライバークラス csii.sundb.jdbc.SundbDriver)。より新しいベンダー製ドライバーを読み込む場合にのみ、ここで JAR を指定してください。カスタムドライバーはエージェント内で分離して読み込まれます。", localInfilePathHint: "LOAD DATA LOCAL INFILE を使用する場合、localInfilePath=/絶対パス/file.csv で各ファイルを明示的に許可してください。複数のファイルがある場合はこのパラメーターを繰り返します。", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 69f258abfc..91c1b1b099 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1055,6 +1055,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "필수, 기존 데이터베이스 입력", databasePlaceholderWithDefault: "선택, 기본값 {database}", kingbaseDatabaseRequired: "KingbaseES는 연결이나 데이터베이스 목록을 불러오기 전에 기존 데이터베이스가 필요합니다.", + gbase8cDatabaseRequired: "GBase 8c에 연결하려면 기존 데이터베이스가 필요합니다.", xuguDatabaseRequired: "XuguDB에 연결하려면 기존 데이터베이스가 필요합니다.", defaultDatabase: "기본 DB", authDatabase: "인증 DB", @@ -1182,6 +1183,7 @@ export default withEnglishFallback({ damengDriverModeHint: "DM8은 내장 드라이버를 사용합니다. DM6 또는 다른 호환 버전은 해당 공식 JDBC JAR을 선택하세요.", damengCustomDriverHint: "정식 출처의 Dameng JDBC JAR을 선택하세요. 사용자 지정 드라이버는 Dameng 에이전트에서 격리되어 로드되며 일반 JDBC 플러그인이 필요하지 않습니다.", damengCustomDriverRequired: "Dameng JDBC 드라이버 JAR을 선택하세요.", + gbase8cCustomDriverHint: "GBase 8c는 Agent 패키지에 포함되지 않은 벤더 JDBC 드라이버(com.gbase8c.Driver)가 필요합니다. gbase.cn에서 내려받은 뒤 «수동 클래스패스»를 켜고 gbase8c-jdbc-*.jar를 지정하세요. 설치한 드라이버 패키지에 이미 들어 있다면 비워 두세요.", sundbCustomDriverHint: "SunDB 에이전트에는 벤더 JDBC 드라이버가 포함되어 있으므로 JAR을 지정할 필요가 없습니다(드라이버 클래스 csii.sundb.jdbc.SundbDriver). 더 새로운 벤더 드라이버를 로드할 때만 여기에서 JAR을 지정하세요. 사용자 지정 드라이버는 에이전트 내에서 격리되어 로드됩니다.", redisTlsInsecure: "인증서 검증 건너뛰기", redisTlsInsecureHint: "redis-cli --tls --insecure과 동일하며, 자체 서명 인증서나 사설 CA용입니다.", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 81abe95c1f..71181fbba6 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1055,6 +1055,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "Obrigatório, informe um banco de dados existente", databasePlaceholderWithDefault: "Opcional, padrão é {database}", kingbaseDatabaseRequired: "KingbaseES exige um banco de dados existente antes de conectar ou carregar a lista de bancos de dados.", + gbase8cDatabaseRequired: "O GBase 8c exige um banco de dados existente antes de conectar.", xuguDatabaseRequired: "XuguDB exige um banco de dados existente antes de conectar.", defaultDatabase: "BD padrão", authDatabase: "BD de autenticação", @@ -1797,6 +1798,7 @@ export default withEnglishFallback({ damengDriverModeHint: "Use o driver integrado para DM8. Para DM6 ou outra versão compatível, selecione o JAR JDBC oficial correspondente.", damengCustomDriverHint: "Selecione um JAR JDBC do Dameng de uma fonte licenciada. O driver é carregado de forma isolada no agente Dameng e não requer o plugin JDBC genérico.", damengCustomDriverRequired: "Selecione um JAR do driver JDBC do Dameng.", + gbase8cCustomDriverHint: "O GBase 8c requer o driver JDBC do fabricante (com.gbase8c.Driver), que o pacote do Agent não inclui. Baixe-o em gbase.cn, ative «Classpath manual» e aponte para gbase8c-jdbc-*.jar. Se o pacote de driver instalado já o inclui, deixe vazio.", sundbCustomDriverHint: "O agente SunDB já inclui o driver JDBC do fabricante, portanto nenhum JAR é necessário (classe do driver csii.sundb.jdbc.SundbDriver). Indique um JAR aqui apenas para carregar um driver do fabricante mais recente; o driver personalizado é carregado de forma isolada dentro do agente.", zookeeperClusterInputHint: "O cluster ZooKeeper pode ser preenchido com todos os nós na URL acima (por exemplo, zookeeper://zk-1:2181,zk-2:2181/app), ou também na Connect String abaixo.", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index 0448fe6cda..040836c1b3 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -1034,6 +1034,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "Обязательно, укажите существующую базу данных", databasePlaceholderWithDefault: "Необязательно, по умолчанию {database}", kingbaseDatabaseRequired: "KingbaseES требует существующую базу данных перед подключением или загрузкой списка баз данных.", + gbase8cDatabaseRequired: "GBase 8c требует существующую базу данных перед подключением.", xuguDatabaseRequired: "XuguDB требует существующую базу данных перед подключением.", defaultDatabase: "БД по умолчанию", authDatabase: "БД аутентификации", @@ -1161,6 +1162,7 @@ export default withEnglishFallback({ damengDriverModeHint: "Используйте встроенный драйвер для DM8. Для DM6 или другой совместимой версии выберите её официальный JDBC JAR.", damengCustomDriverHint: "Выберите Dameng JDBC JAR из лицензированного источника. Пользовательский драйвер изолирован внутри агента Dameng и не требует универсального плагина JDBC.", damengCustomDriverRequired: "Выберите JAR-файл драйвера Dameng JDBC.", + gbase8cCustomDriverHint: "Для GBase 8c нужен JDBC-драйвер производителя (com.gbase8c.Driver), которого нет в пакете Agent. Скачайте его с gbase.cn, включите «Ручной classpath» и укажите gbase8c-jdbc-*.jar. Если в установленном пакете драйвера он уже есть, оставьте поле пустым.", sundbCustomDriverHint: "Агент SunDB уже включает драйвер JDBC от производителя, поэтому JAR указывать не нужно (класс драйвера csii.sundb.jdbc.SundbDriver). Укажите JAR здесь, только чтобы загрузить более новую версию драйвера производителя; пользовательский драйвер загружается изолированно внутри агента.", redisTlsInsecure: "Пропускать проверку сертификата", diff --git a/apps/desktop/src/i18n/locales/th.ts b/apps/desktop/src/i18n/locales/th.ts index 3371a48d5e..197c501694 100644 --- a/apps/desktop/src/i18n/locales/th.ts +++ b/apps/desktop/src/i18n/locales/th.ts @@ -1181,6 +1181,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "จำเป็นต้องมี ป้อนฐานข้อมูลที่มีอยู่", databasePlaceholderWithDefault: "ไม่บังคับ ค่าเริ่มต้นคือ {database}", kingbaseDatabaseRequired: "KingbaseES ต้องมีฐานข้อมูลที่มีอยู่ก่อนเชื่อมต่อหรือโหลดรายการฐานข้อมูล", + gbase8cDatabaseRequired: "GBase 8c ต้องมีฐานข้อมูลที่มีอยู่ก่อนเชื่อมต่อ", xuguDatabaseRequired: "XuguDB ต้องมีฐานข้อมูลที่มีอยู่ก่อนเชื่อมต่อ", defaultDatabase: "DB เริ่มต้น", authDatabase: "DB สำหรับ Auth", @@ -1362,6 +1363,7 @@ export default withEnglishFallback({ damengDriverModeHint: "ใช้ Driver ที่ Bundled สำหรับ DM8 สำหรับ DM6 หรือเวอร์ชันอื่นที่เข้ากันได้ ให้เลือก JDBC JAR อย่างเป็นทางการ", damengCustomDriverHint: "เลือก Dameng JDBC JAR จาก Source ที่มี License Driver กำหนดเองจะถูกแยกไว้ภายใน Agent ของ Dameng และไม่ต้องใช้ Plugin JDBC ทั่วไป", damengCustomDriverRequired: "เลือก Dameng JDBC Driver JAR", + gbase8cCustomDriverHint: "GBase 8c ต้องใช้ไดรเวอร์ JDBC ของผู้จำหน่าย (com.gbase8c.Driver) ซึ่งไม่ได้อยู่ในแพ็กเกจ Agent โปรดดาวน์โหลดจาก gbase.cn เปิด «Classpath ด้วยตนเอง» แล้วระบุ gbase8c-jdbc-*.jar หากแพ็กเกจไดรเวอร์ที่ติดตั้งมีไดรเวอร์นี้อยู่แล้ว ไม่ต้องกรอก", sundbCustomDriverHint: "SunDB Agent มาพร้อม Driver JDBC ของผู้จำหน่าย จึงไม่ต้องใช้ JAR (คลาส Driver csii.sundb.jdbc.SundbDriver) ระบุ JAR ที่นี่เฉพาะเมื่อต้องการโหลด Driver ของผู้จำหน่ายเวอร์ชันใหม่กว่า Driver กำหนดเองจะโหลดแบบแยกภายใน Agent", redisTlsInsecure: "ข้ามการตรวจสอบใบรับรอง", redisTlsInsecureHint: "เทียบเท่ากับ redis-cli --tls --insecure สำหรับใบรับรอง Self-signed หรือ CA ส่วนตัว", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 490b132ad1..4306b6236b 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -641,6 +641,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "Zorunlu, mevcut bir veritabanı girin", databasePlaceholderWithDefault: "İsteğe bağlı, varsayılan {database}", kingbaseDatabaseRequired: "KingbaseES bağlanmadan veya veritabanı listesini yüklemeden önce mevcut bir veritabanı ister.", + gbase8cDatabaseRequired: "GBase 8c'ye bağlanmadan önce mevcut bir veritabanı gerekir.", xuguDatabaseRequired: "XuguDB'ye bağlanmadan önce mevcut bir veritabanı gerekir.", defaultDatabase: "Varsayılan VT", authDatabase: "Kimlik Doğrulama VT", @@ -768,6 +769,7 @@ export default withEnglishFallback({ damengDriverModeHint: "DM8 için pakete gömülü sürücüyü kullanın. DM6 veya başka bir uyumlu sürüm için resmî JDBC JAR dosyasını seçin.", damengCustomDriverHint: "Lisanslı bir kaynaktan Dameng JDBC JAR dosyası seçin. Özel sürücü Dameng agent'ı içinde yalıtılır ve genel JDBC eklentisini gerektirmez.", damengCustomDriverRequired: "Bir Dameng JDBC sürücü JAR dosyası seçin.", + gbase8cCustomDriverHint: "GBase 8c, Agent paketinde bulunmayan üretici JDBC sürücüsünü (com.gbase8c.Driver) gerektirir. gbase.cn üzerinden indirin, «Elle classpath» anahtarını açın ve gbase8c-jdbc-*.jar dosyasını gösterin. Kurduğunuz sürücü paketi bunu zaten içeriyorsa boş bırakın.", sundbCustomDriverHint: "SunDB agent'i üretici JDBC sürücüsünü içerir, bu yüzden JAR gerekmez (sürücü sınıfı csii.sundb.jdbc.SundbDriver). Yalnızca daha yeni bir üretici sürücüsü yüklemek için burada JAR belirtin; özel sürücü agent içinde yalıtılarak yüklenir.", redisTlsInsecure: "Sertifika doğrulamasını atla", redisTlsInsecureHint: "redis-cli --tls --insecure ile eşdeğerdir; kendinden imzalı sertifikalar veya özel CA'lar içindir.", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 243d310fd6..3cd39aa238 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1151,6 +1151,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "必填,请填写已存在的数据库", databasePlaceholderWithDefault: "可选,默认 {database}", kingbaseDatabaseRequired: "KingbaseES 需要先填写一个已存在的数据库后才能连接或加载数据库列表。", + gbase8cDatabaseRequired: "南大通用 GBase 8c 需要先填写一个已存在的数据库才能连接。", xuguDatabaseRequired: "虚谷 XuguDB 需要先填写一个已存在的数据库才能连接。", defaultDatabase: "默认库", authDatabase: "认证库", @@ -1332,6 +1333,7 @@ export default withEnglishFallback({ damengDriverModeHint: "DM8 使用内置驱动;DM6 或其他兼容版本请选择对应的官方 JDBC JAR。", damengCustomDriverHint: "请选择合法来源的达梦 JDBC JAR。自定义驱动在达梦 Agent 内隔离加载,无需安装通用 JDBC 插件。", damengCustomDriverRequired: "请选择达梦 JDBC 驱动 JAR。", + gbase8cCustomDriverHint: "GBase 8c 需要厂商 JDBC 驱动(com.gbase8c.Driver),Agent 包不含它。请从 gbase.cn 下载后打开「手动 classpath」,指向 gbase8c-jdbc-*.jar;若你安装的驱动包已带该驱动,则留空即可。", sundbCustomDriverHint: "科蓝 SUNDB Agent 已内置厂商 JDBC 驱动,无需指定 JAR(驱动类 csii.sundb.jdbc.SundbDriver)。仅在需要加载更新版厂商驱动时在此指定 JAR;自定义驱动在 Agent 内隔离加载。", redisTlsInsecure: "跳过证书验证", redisTlsInsecureHint: "等价于 redis-cli --tls --insecure,适用于自签名证书或私有 CA。", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 851849c2f2..ac8a6f088b 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1059,6 +1059,7 @@ export default withEnglishFallback({ databasePlaceholderRequired: "必填,請填寫已存在的資料庫", databasePlaceholderWithDefault: "可選,預設 {database}", kingbaseDatabaseRequired: "KingbaseES 需要先填寫一個已存在的資料庫後才能連線或載入資料庫清單。", + gbase8cDatabaseRequired: "南大通用 GBase 8c 需要先填寫一個已存在的資料庫才能連線。", xuguDatabaseRequired: "虛谷 XuguDB 需要先填寫一個已存在的資料庫才能連線。", defaultDatabase: "預設 DB", authDatabase: "驗證 DB", @@ -1808,6 +1809,7 @@ export default withEnglishFallback({ damengDriverModeHint: "DM8 使用內建驅動;DM6 或其他相容版本請選擇對應的官方 JDBC JAR。", damengCustomDriverHint: "請選擇合法來源的達夢 JDBC JAR。自訂驅動會在達夢 Agent 內隔離載入,不需要安裝通用 JDBC 外掛。", damengCustomDriverRequired: "請選擇達夢 JDBC 驅動 JAR。", + gbase8cCustomDriverHint: "GBase 8c 需要廠商 JDBC 驅動(com.gbase8c.Driver),Agent 套件不含它。請從 gbase.cn 下載後開啟「手動 classpath」,指向 gbase8c-jdbc-*.jar;若你安裝的驅動套件已帶該驅動,留空即可。", sundbCustomDriverHint: "科藍 SUNDB Agent 已內建廠商 JDBC 驅動,無需指定 JAR(驅動類別 csii.sundb.jdbc.SundbDriver)。僅在需要載入更新版廠商驅動時在此指定 JAR;自訂驅動會在 Agent 內隔離載入。", zookeeperClusterInputHint: "ZooKeeper 叢集可在上方 URL 中填寫全部節點(例如 zookeeper://zk-1:2181,zk-2:2181/app),也可在下方 Connect String 中填寫。", zookeeperAuthMethod: "認證方式", diff --git a/apps/desktop/src/lib/__tests__/connection/gbase8cDatabase.spec.ts b/apps/desktop/src/lib/__tests__/connection/gbase8cDatabase.spec.ts new file mode 100644 index 0000000000..956ccdc9d1 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/connection/gbase8cDatabase.spec.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { hasGbase8cConnectionDatabase } from "@/lib/connection/gbase8cDatabase"; + +describe("hasGbase8cConnectionDatabase", () => { + it("accepts a filled database name, trimming it", () => { + expect(hasGbase8cConnectionDatabase(" dbx_pg ", undefined)).toBe(true); + }); + + it("rejects a missing or blank database", () => { + expect(hasGbase8cConnectionDatabase(undefined, undefined)).toBe(false); + expect(hasGbase8cConnectionDatabase("", undefined)).toBe(false); + expect(hasGbase8cConnectionDatabase(" \t ", undefined)).toBe(false); + }); + + it("accepts a full connection string, which the agent prefers over host/port/database", () => { + expect(hasGbase8cConnectionDatabase(undefined, "jdbc:gbase8c://db.example:5432/warehouse")).toBe(true); + }); + + it("still rejects a blank connection string", () => { + expect(hasGbase8cConnectionDatabase(undefined, " ")).toBe(false); + }); +}); diff --git a/apps/desktop/src/lib/connection/driver-category-definitions.ts b/apps/desktop/src/lib/connection/driver-category-definitions.ts index 0ac71cd057..cb5e000586 100644 --- a/apps/desktop/src/lib/connection/driver-category-definitions.ts +++ b/apps/desktop/src/lib/connection/driver-category-definitions.ts @@ -33,6 +33,7 @@ export const AGENT_DRIVER_CATEGORY_MAP: Readonly String { | DatabaseType::Highgo | DatabaseType::Vastbase | DatabaseType::Uxdb + | DatabaseType::Gbase8c | DatabaseType::Redshift => { let (schema_name, table_name) = postgres_table_identity(table_identifier); let schema_predicate = schema_name.map_or_else( diff --git a/crates/dbx-core/src/safety/production_safety.rs b/crates/dbx-core/src/safety/production_safety.rs index 7c2b437c14..689d1b38de 100644 --- a/crates/dbx-core/src/safety/production_safety.rs +++ b/crates/dbx-core/src/safety/production_safety.rs @@ -518,6 +518,7 @@ fn schema_first_qualifier_type(db_type: &DatabaseType) -> bool { | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase + | DatabaseType::Gbase8c | DatabaseType::Yashandb | DatabaseType::Oracle | DatabaseType::OceanbaseOracle @@ -612,6 +613,7 @@ impl SqlScanLexerRules { | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb + | DatabaseType::Gbase8c | DatabaseType::Kwdb ); let oracle_family = matches!(db_type, DatabaseType::Oracle | DatabaseType::OceanbaseOracle); diff --git a/crates/dbx-core/tests/database_capabilities.rs b/crates/dbx-core/tests/database_capabilities.rs index 82cf85a32c..6ee43a7f66 100644 --- a/crates/dbx-core/tests/database_capabilities.rs +++ b/crates/dbx-core/tests/database_capabilities.rs @@ -118,6 +118,7 @@ fn maps_agent_database_types_to_driver_keys() { assert_eq!(agent_key(&DatabaseType::Databend, None), Some("databend")); assert_eq!(agent_key(&DatabaseType::InfluxDb, None), Some("influxdb")); assert_eq!(agent_key(&DatabaseType::Uxdb, None), Some("uxdb")); + assert_eq!(agent_key(&DatabaseType::Gbase8c, None), Some("gbase8c")); assert_eq!(agent_key(&DatabaseType::ZooKeeper, None), Some("zookeeper")); assert_eq!(agent_key(&DatabaseType::Spanner, None), Some("spanner")); assert_eq!(agent_key(&DatabaseType::Oracle, Some("oracle-legacy")), Some("oracle")); @@ -159,6 +160,7 @@ fn classifies_agent_database_types() { assert!(is_agent_type(&DatabaseType::Exasol)); assert!(is_agent_type(&DatabaseType::OceanbaseOracle)); assert!(is_agent_type(&DatabaseType::Gbase)); + assert!(is_agent_type(&DatabaseType::Gbase8c)); assert!(is_agent_type(&DatabaseType::Access)); assert!(is_agent_type(&DatabaseType::Databend)); assert!(is_agent_type(&DatabaseType::InfluxDb)); @@ -364,6 +366,26 @@ fn driver_manifest_declares_expected_product_capabilities() { assert!(!uxdb.capabilities.data_transfer); assert!(!uxdb.capabilities.user_admin); + let gbase8c = find_driver(DatabaseType::Gbase8c); + assert_eq!(gbase8c.label, "南大通用 GBase 8c"); + assert_eq!(gbase8c.runtime_mode, "agent"); + assert_eq!(gbase8c.agent_key.as_deref(), Some("gbase8c")); + assert_eq!(gbase8c.support_level, "operate"); + assert!(gbase8c.capabilities.query_execution); + assert!(gbase8c.capabilities.metadata_browse); + assert!(gbase8c.capabilities.table_data_edit); + assert!(gbase8c.capabilities.driver_management); + // Generated DDL has to follow DBCOMPATIBILITY (A/B/C/PG), which is fixed at database + // creation and cannot be changed by SQL, so the same DDL can be right on one GBase 8c + // instance and wrong on another. Structure editing, table import and data transfer stay + // off until the DDL generator can honour the connected database's mode. + assert!(!gbase8c.capabilities.table_structure_edit); + assert!(!gbase8c.capabilities.table_import); + assert!(!gbase8c.capabilities.data_transfer); + // ConfiguredJdbcAgent (the base this agent uses) throws for object source. + assert!(!gbase8c.capabilities.object_source); + assert!(!gbase8c.capabilities.user_admin); + let salesforce = find_driver(DatabaseType::Salesforce); assert_eq!(salesforce.label, "Salesforce"); assert_eq!(salesforce.support_level, "operate"); diff --git a/crates/dbx-sql-core/src/query_execution_sql.rs b/crates/dbx-sql-core/src/query_execution_sql.rs index 92488ba0a7..608f43d716 100644 --- a/crates/dbx-sql-core/src/query_execution_sql.rs +++ b/crates/dbx-sql-core/src/query_execution_sql.rs @@ -834,6 +834,7 @@ fn is_postgresql_family_database(database_type: DatabaseType) -> bool { | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase + | DatabaseType::Gbase8c | DatabaseType::Kwdb ) } diff --git a/crates/dbx-sql-core/src/sql.rs b/crates/dbx-sql-core/src/sql.rs index 24334aaed9..c6aa4e2136 100644 --- a/crates/dbx-sql-core/src/sql.rs +++ b/crates/dbx-sql-core/src/sql.rs @@ -326,6 +326,7 @@ impl SqlDialectProfile { | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb + | DatabaseType::Gbase8c | DatabaseType::Kwdb ) } diff --git a/crates/dbx-sql-core/src/sql_risk.rs b/crates/dbx-sql-core/src/sql_risk.rs index e496ab03b7..a1fdbd87f3 100644 --- a/crates/dbx-sql-core/src/sql_risk.rs +++ b/crates/dbx-sql-core/src/sql_risk.rs @@ -834,6 +834,7 @@ fn supports_select_into_table_creation(database_type: DatabaseType) -> bool { | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase + | DatabaseType::Gbase8c | DatabaseType::Kwdb | DatabaseType::SqlServer ) diff --git a/crates/dbx-sql-data/src/data_grid_sql.rs b/crates/dbx-sql-data/src/data_grid_sql.rs index 2b80125e4a..7d49596039 100644 --- a/crates/dbx-sql-data/src/data_grid_sql.rs +++ b/crates/dbx-sql-data/src/data_grid_sql.rs @@ -3057,6 +3057,7 @@ fn keeps_literal_backslashes(database_type: Option) -> bool { | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase + | DatabaseType::Gbase8c | DatabaseType::Kwdb ) ) @@ -4655,6 +4656,7 @@ fn is_postgres_like_pattern_database(database_type: Option) -> boo | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase + | DatabaseType::Gbase8c | DatabaseType::OpenGauss ) ) @@ -4786,6 +4788,7 @@ fn uses_keyless_row_predicate(database_type: Option) -> bool { | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb + | DatabaseType::Gbase8c | DatabaseType::Vastbase | DatabaseType::Goldendb | DatabaseType::Yashandb diff --git a/crates/dbx-sql-data/src/database_structure_search_sql.rs b/crates/dbx-sql-data/src/database_structure_search_sql.rs index 46c19ac1d3..d6cf3a0434 100644 --- a/crates/dbx-sql-data/src/database_structure_search_sql.rs +++ b/crates/dbx-sql-data/src/database_structure_search_sql.rs @@ -57,6 +57,7 @@ pub fn build_database_structure_search_sql( | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb + | DatabaseType::Gbase8c | DatabaseType::Vastbase | DatabaseType::OpenGauss | DatabaseType::Gaussdb diff --git a/crates/dbx-sql-dialect/src/sql_dialect/capabilities.rs b/crates/dbx-sql-dialect/src/sql_dialect/capabilities.rs index 3d8d6850e6..bc8b770eea 100644 --- a/crates/dbx-sql-dialect/src/sql_dialect/capabilities.rs +++ b/crates/dbx-sql-dialect/src/sql_dialect/capabilities.rs @@ -35,6 +35,7 @@ pub fn is_schema_aware(database_type: DatabaseType) -> bool { | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb + | DatabaseType::Gbase8c | DatabaseType::Vastbase | DatabaseType::Yashandb | DatabaseType::Oscar diff --git a/crates/dbx-sql-dialect/src/sql_dialect/ddl_profile.rs b/crates/dbx-sql-dialect/src/sql_dialect/ddl_profile.rs index 1c456e3648..3cfa7ad8ae 100644 --- a/crates/dbx-sql-dialect/src/sql_dialect/ddl_profile.rs +++ b/crates/dbx-sql-dialect/src/sql_dialect/ddl_profile.rs @@ -856,7 +856,7 @@ pub fn profile_for(db_type: DatabaseType) -> DdlDialectProfile { } // PostgreSQL family - Postgres | Redshift | Gaussdb | Kingbase | Highgo | Vastbase | OpenGauss | Kwdb | Uxdb => { + Postgres | Redshift | Gaussdb | Kingbase | Highgo | Vastbase | OpenGauss | Kwdb | Uxdb | Gbase8c => { postgres_family(db_type) } diff --git a/crates/dbx-sql-schema/src/db_admin_sql.rs b/crates/dbx-sql-schema/src/db_admin_sql.rs index 1c2d01a36f..2e35d2ff48 100644 --- a/crates/dbx-sql-schema/src/db_admin_sql.rs +++ b/crates/dbx-sql-schema/src/db_admin_sql.rs @@ -660,6 +660,7 @@ fn supports_vacuum_table(database_type: Option) -> bool { | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase + | DatabaseType::Gbase8c | DatabaseType::OpenGauss ) ) @@ -709,6 +710,7 @@ fn supports_truncate_table_cascade(database_type: Option) -> bool | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase + | DatabaseType::Gbase8c | DatabaseType::OpenGauss ) ) diff --git a/crates/dbx-sql-schema/src/schema_diff.rs b/crates/dbx-sql-schema/src/schema_diff.rs index 98969fe84b..4be4ba5300 100644 --- a/crates/dbx-sql-schema/src/schema_diff.rs +++ b/crates/dbx-sql-schema/src/schema_diff.rs @@ -4607,6 +4607,7 @@ fn is_postgres_family_ddl(db_type: DatabaseType) -> bool { | DatabaseType::Vertica | DatabaseType::Exasol | DatabaseType::Uxdb + | DatabaseType::Gbase8c ) } diff --git a/crates/dbx-types/src/models/connection.rs b/crates/dbx-types/src/models/connection.rs index d11780f66c..8cec3aa576 100644 --- a/crates/dbx-types/src/models/connection.rs +++ b/crates/dbx-types/src/models/connection.rs @@ -1086,6 +1086,9 @@ impl ConnectionConfig { DatabaseType::Kingbase | DatabaseType::Vastbase => Some("postgres"), DatabaseType::Highgo => Some("highgo"), DatabaseType::Uxdb => Some("uxdb"), + // GBase 8c keeps the PostgreSQL-shaped `postgres` maintenance database + // (measured on GBase8cV6 1.0.0B20), so an empty database field resolves to it. + DatabaseType::Gbase8c => Some("postgres"), DatabaseType::Yashandb => Some("yasdb"), DatabaseType::Oracle if !self.oracle_connection_type.as_deref().is_some_and(|mode| mode.eq_ignore_ascii_case("tns")) => @@ -1290,6 +1293,7 @@ impl ConnectionConfig { DatabaseType::Highgo => format!("highgo://{host}:{port}{db_part}"), DatabaseType::Uxdb => format!("uxdb://{host}:{port}{db_part}"), DatabaseType::Vastbase => format!("vastbase://{host}:{port}{db_part}"), + DatabaseType::Gbase8c => format!("gbase8c://{host}:{port}{db_part}"), DatabaseType::Goldendb => format!("goldendb://{host}:{port}{db_part}"), DatabaseType::Gaussdb => { if is_multi_host(raw_host) { @@ -1489,6 +1493,9 @@ impl ConnectionConfig { DatabaseType::Vastbase => { format!("vastbase://{}:{}@{host}:{port}{db_part}", username, password) } + DatabaseType::Gbase8c => { + format!("gbase8c://{}:{}@{host}:{port}{db_part}", username, password) + } DatabaseType::Goldendb => { format!("goldendb://{}:{}@{host}:{port}{db_part}", username, password) } diff --git a/docs/data/databaseSupport.ts b/docs/data/databaseSupport.ts index af14e0f395..dd62da3ff8 100644 --- a/docs/data/databaseSupport.ts +++ b/docs/data/databaseSupport.ts @@ -94,6 +94,7 @@ export const databaseSupport: DatabaseSupportItem[] = [ { id: "sundb", name: "SunDB", icon: "/icons/database/sundb.svg", tone: "#f97316" }, { id: "xugu", name: "XuguDB", icon: "/icons/database/xugu.png", tone: "#84cc16" }, { id: "gbase", name: "GBase", icon: "/icons/database/gbase.png", tone: "#06b6d4" }, + { id: "gbase8c", name: "GBase 8c", icon: "/icons/database/gbase.png", tone: "#06b6d4" }, { id: "access", name: "Access", icon: "/icons/database/access.png", tone: "#a53346" }, { id: "h2", name: "H2", icon: "/icons/database/h2.svg", tone: "#f7a81b" }, { id: "etcd", name: "Etcd", icon: "/icons/database/etcd.svg", tone: "#419eda" }, diff --git a/docs/lib/agentRegistry.ts b/docs/lib/agentRegistry.ts index 268a2a7301..0dee0291e9 100644 --- a/docs/lib/agentRegistry.ts +++ b/docs/lib/agentRegistry.ts @@ -121,6 +121,7 @@ const driverLabels: Record = { exasol: "Exasol", firebird: "Firebird", gbase8a: "GBase 8a", + gbase8c: "GBase 8c", gbase8s: "GBase 8s", goldendb: "GoldenDB", h2: "H2", diff --git a/docs/pips/plans/2026-10-16-gbase8c-support.md b/docs/pips/plans/2026-10-16-gbase8c-support.md new file mode 100644 index 0000000000..da73e8d031 --- /dev/null +++ b/docs/pips/plans/2026-10-16-gbase8c-support.md @@ -0,0 +1,409 @@ +# PIP: GBase 8c 支持 + +## 状态 + +Implemented(设计已确认,实现中) + +## 摘要 + +为 DBX 新增南大通用 GBase 8c 连接类型:新 dbType `gbase8c`,`runtimeMode: agent`,由新增的 `agents/drivers/gbase8c` 模块通过**厂商 JDBC 驱动**(`com.gbase8c.Driver` + `jdbc:gbase8c://`)访问数据库。 + +GBase 8c 的内核是 openGauss(PostgreSQL 9.2 世代),因此**原生 Rust 路线在原理上也可行**,但厂商的分发与兼容性口径使它不可取: + +- 官方文档通篇不出现 `com.gbase8c.Driver` / `jdbc:gbase8c://`,而厂商 jar **明确拒绝** `jdbc:postgresql://`(实测 `SQLException: No suitable driver found`)。也就是说「官方保真」保的是 jar 里的实现,不是文档承诺的任何东西。 +- 无版本兼容矩阵、无发布说明、无 changelog;唯一口径是「新驱动前向兼容数据库」。 +- 没有官方 Docker 镜像,也没有可复现的官方安装容器路径(安装包只有 V6 单一下载项)。 + +采用厂商 JDBC 的收益是:协议、认证、兼容模式等最难的部分全部由厂商客户端承担,DBX 侧只做元数据适配与描述符注册。 + +## 背景 + +### 为什么是「新 dbType + agent」而不是别的形状 + +| 方案 | 否决理由 | +|---|---| +| 挂到既有 `gbase` dbType 下当第三个 profile | `plugins/connection-types/gbase.yaml` 的 `dialect: GBase` 指向 MySQL 方言(反引号、MySQL `MODIFY COLUMN`),而 `dialect` / `defaultPort` / `transferObjectFamily` 都按 dbType 索引,profile 改不了 → 会继承 8a 的反引号与全表扫描口径 | +| 挂到 `postgres` / `opengauss` 下当 profile | 策略块(capabilities / mcpMode / traits)只按 dbType 索引,没有按 profile 覆盖的入口;实测 postgres 会继承 `sqlExplain:true`/`tableImport:true`/`mcpMode:direct`,opengauss 会继承 `mcpMode:bridge` 与 openGauss 专属对象树 | +| 原生 Rust 直连(复用 `PoolKind::Postgres`) | 见摘要:认证在默认配置下确实是标准 MD5,但 sha256-only 部署需要 `tokio-postgres-gaussdb` fork 的 GaussDB 握手,而该 fork 的 pin 是**孤儿提交**(`ac8ad476`,无任何分支或标签包含它,可被 GC),且无法前移(master 有 GaussDB 认证但缺 `RowStream::columns()`;`dev/row-stream-columns` 有后者但删掉了认证) | + +### 为什么基类取 `ConfiguredJdbcAgent` 而不是 `PostgresLikeAgent` + +在真机实例上实测 `PostgresLikeAgent` 依赖的三项能力: + +| 依赖 | 实测 | +|---|---| +| `JOIN LATERAL`(`PostgresLikeAgent.java:495` primaryKeys、`:589` listIndexes、`:748,750` listForeignKeys) | **不支持**:`ERROR: syntax error at or near "select"`(指向 `lateral`) | +| `pg_catalog.to_regclass(text)`(`:355` 视图源码) | **不存在**:`ERROR: function pg_catalog.to_regclass(unknown) does not exist` | +| `pg_proc.prokind`(`:213,216,360,366`) | 列存在,但语义不可靠:函数 `f`、过程 `p` 可区分,而聚合的 `prokind` 仍是 `f`(`proisagg=t` 有 170 行) | + +前两项分别打死三条热路径和视图源码,且 `PostgresLikeAgent` 的兜底**只存在于 `getTableDdl` 内部**,`listObjects`/`getColumns`/`listIndexes`/`listForeignKeys` 经 RPC 直调时零兜底。故取 `ConfiguredJdbcAgent`(代价是 `objectSource: false`,`getObjectSource` 抛 `UnsupportedOperationException`)。 + +## 已确认的设计决策 + +| # | 决策 | 选择 | +|---|---|---| +| 1 | 交付门槛 | 上游质量:先落本 spec,再写代码;i18n 只新增一个键(`connection.gbase8cCustomDriverHint`,13 份 locale)——因为 `supportsNativeAgentJdbcDriverConfigType` 必须纳入 `gbase8c`,用户才看得到 `jdbc_driver_paths` 字段;8c 本身没有专属表单字段 | +| 2 | 能力矩阵 | `operate` 但按实测裁剪:`tableStructureEdit` 关闭(DDL 必须随 `DBCOMPATIBILITY` 变化,而模式建库时固定且不可 SQL 修改) | +| 3 | 覆盖面 | Desktop + Web;MCP 走 `mcpMode: bridge`;CLI 直连与 `plugins/jdbc` Java 插件为非目标 | +| 4 | SQL 方言 | `dialect: PostgreSQL`,不新建 `plugins/dialects/gbase8c.yaml`(agent 路线下方言 yaml 的元数据半边用不上,DDL 半边只在 `tableStructureEdit` 打开后有用) | +| 5 | 兼容模式 | **不改任何共享代码**:实测四种兼容模式下双引号标识符都可用、反引号全部报错,而驱动的 `getIdentifierQuoteString()` 恰好返回双引号,故**不**把 `gbase8c` 加进 `isPostgresCompatibleJdbc`(见下) | +| 6 | 厂商 jar 分发 | `Agent-External-Driver: true`(用户自备),不提交到仓库 | +| 7 | 命名与图标 | 独立选择器条目 + `南大通用 GBase 8c` + `category: domestic` + 复用 `gbase.svg` | +| 8 | 验证实例 | 用官方 V6 安装包自建一次性本地实例(见附录 B),不提交 `deploy/database/gbase8c/` 菜谱 | +| 9 | 版本声明 | 描述符不写任何版本号,支持矩阵写「GBase 8c(以厂商 JDBC 驱动为准)」 | + +### 决策 5 的证据(原方案已被实测推翻) + +**原方案**:`isPostgresCompatibleJdbc`(`agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java:534-554`)匹配 9 个关键串,`jdbc:gbase8c:` 与 `com.gbase8c.Driver` 一个都不匹配,所以打算把 `gbase8c` 加进该列表,让 `resolveIdentifierQuote` 走 `GAUSSDB_COMPATIBILITY_SQL` + `gaussdbIdentifierQuote`,理由是「B 模式(MySQL 兼容)应该用反引号,而驱动只会返回双引号」。 + +**实测推翻了这条推理**。在同一个实例上对四种兼容模式各建一张 `public.qt (id int, "na me" varchar(10))` 并执行三种写法: + +| 模式 | `datcompatibility` | 裸标识符 | 双引号 `"na me"` | 反引号 `` `na me` `` | +|---|---|---|---|---| +| dbx_a | A(Oracle) | 可用 | **可用** | `ERROR: syntax error at or near "`"` | +| dbx_b | B(MySQL) | 可用 | **可用** | `ERROR: syntax error at or near "`"` | +| dbx_c | C(Teradata) | 可用 | **可用** | `ERROR: syntax error at or near "`"` | +| dbx_pg | PG | 可用 | **可用** | `ERROR: syntax error at or near "`"` | + +也就是说 **GBase 8c 在任何兼容模式下都不接受反引号**,B 模式也照样是双引号——`gaussdbIdentifierQuote` 里 `M|B|MYSQL → 反引号` 那条映射是华为 GaussDB 的行为,不是 GBase 8c 的行为。若照原方案接线,B 模式会开始输出反引号,**把本来能用的查询改成语法错误**。 + +同时实测驱动的 `getIdentifierQuoteString()` 在四种模式下**都是双引号**,与数据库行为一致。而 `isPostgresCompatibleJdbc` 全仓只有一个消费点(`AbstractJdbcAgent.java:448`,即 `resolveIdentifierQuote`),所以「什么都不改」既正确又零风险:gbase8c 的引号走默认回落,拿到的正是正确答案。 + +`SELECT datcompatibility FROM pg_catalog.pg_database WHERE datname = current_database()` 在该实例上确实可用(返回 `PG`/`A`/`B`/`C`),只是**本类型不需要它**。 + +顺带实测:`SET search_path TO "dbx_smoke"` 在四种模式下均无报错,故 profile 的 `schemaSwitchPrefix` 取 `SET search_path TO` 正确。 + +## 驱动架构与文件清单 + +| 文件 | 动作 | 内容 | +|---|---|---| +| `plugins/connection-types/gbase8c.yaml` | 新增 | dbType `gbase8c`、`rustVariant: Gbase8c`、`dialect: PostgreSQL`、`runtimeMode: agent`、`mcpMode: bridge`、`agentKey: gbase8c`、`defaultPort: 5432`、`driverStoreOrder: 55`、`traits: {schemaAware, treeSchema, pgVacuum}`、`supportLevel: operate`,能力集见描述符草案 | +| `plugins/connection-types/profiles/catalog.yaml` | 修改 | `id: gbase8c` / `label: 南大通用 GBase 8c` / `icon: gbase` / `port: 5432` / `user: gbase` / `category: domestic` | +| `agents/drivers/gbase8c/build.gradle` | 新增 | `Agent-Label: GBase 8c`、`Agent-External-Driver: true`、`Main-Class: com.dbx.agent.gbase8c.Gbase8cAgent`、`fileTree(libs)` | +| `agents/drivers/gbase8c/src/main/java/com/dbx/agent/gbase8c/Gbase8cAgent.java` | 新增 | `extends ConfiguredJdbcAgent`;profile;`listDatabases` 覆写 | +| `agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentTest.java` | 新增 | 继承 `JdbcFakeExecutionBehaviorTest`(执行路径共享契约)+ 驱动类断言 | +| `agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentMetadataTest.java` | 新增 | profile 契约、`excludedSchemas`、`tableTypes` 交集、`listDatabases` SQL、`setSchemaSQL` | +| `agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentLiveTest.java` | 新增 | 真机冒烟(`DBX_TEST_GBASE8C_URL` 门控),结果见「实测冒烟结果」 | +| `agents/settings.gradle`、`agents/build.gradle`、`agents/versions.json`、`agents/README.md`、`agents/README.zh-CN.md` | 修改 | 模块注册的五道闸 + 两张支持表各加一行 | +| `apps/desktop/src/components/connection/ConnectionDialog.vue` | 修改 | `supportsNativeAgentJdbcDriverConfigType` 加 `gbase8c`(否则用户看不到「驱动 JAR」字段);`nativeAgentJdbcDriverHint` 加分支 | +| `apps/desktop/src/components/icons/DatabaseIcon.vue` | 修改 | `gbase8c: "gbase.png"` | +| `apps/desktop/src/i18n/locales/*.ts` | 修改 | 13 份 locale 加 `connection.gbase8cCustomDriverHint` | +| `apps/desktop/src/lib/database/databaseNamespaceCreation.ts` | 修改 | `Record` 矩阵加 `gbase8c`(deferred:建库要显式 `DBCOMPATIBILITY`) | +| `apps/desktop/src/lib/database/databasePropertyEditing.ts` | 修改 | 同一张 `Record` 矩阵加 `gbase8c`(deferred) | +| `docs/data/databaseSupport.ts` | 修改 | 加一行 `gbase8c` | +| `crates/dbx-types/src/models/connection.rs` | 修改 | `default_database` → `Some("postgres")`;脱敏 URL 与带凭据 URL 各加一支 `gbase8c://` | +| `crates/dbx-sql-dialect/src/sql_dialect/ddl_profile.rs` | 修改 | `profile_for` 的 PG 家族臂加 `Gbase8c`(**编译必需**) | +| `crates/dbx-sql-dialect/src/sql_dialect/capabilities.rs` | 修改 | `is_schema_aware` 加 `Gbase8c`(`schemaAware` 在 Rust 侧**唯一**载体:capabilities/traits 不进 `DatabaseManifestEntry`) | +| `crates/dbx-sql-core/src/sql.rs` | 修改 | `is_postgres_string_lexer_database` 加 `Gbase8c`(字面量词法 `E'…'`/嵌套注释) | +| `crates/dbx-sql-core/src/query_execution_sql.rs`、`sql_risk.rs` | 修改 | PG 家族写检测加 `Gbase8c`(缺了会让 `SELECT … INTO newtable` 漏检成只读) | +| `crates/dbx-sql-data/src/data_grid_sql.rs` | 修改 | 三处:字面量反斜杠保留、`col::text` 过滤转换、无主键行谓词 | +| `crates/dbx-sql-data/src/database_structure_search_sql.rs` | 修改 | 结构搜索的 PG 家族臂加 `Gbase8c`(缺了功能静默不可用) | +| `crates/dbx-sql-schema/src/schema_diff.rs` | 修改 | `is_postgres_family_ddl` 加 `Gbase8c`(`compareModes` 含 schema) | +| `crates/dbx-sql-schema/src/db_admin_sql.rs` | 修改 | `supports_vacuum_table`(`pgVacuum` 的唯一载体)、`supports_truncate_table_cascade` | +| `crates/dbx-core/src/connection/mod.rs` | 修改 | `agent_connection_pool_database_type!` 宏加 `Gbase8c`(**编译必需**:`:2780` 的 `match` 无通配) | +| `crates/dbx-core/src/safety/production_safety.rs` | 修改 | `schema_first_qualifier_type`(两段名归属判定)+ SQL 扫描器的 `postgres_family` | +| `crates/dbx-core/src/data/script_generator.rs` | 修改 | 幂等 `CREATE TABLE` 重写的 PG 臂 | +| `crates/dbx-core/tests/database_capabilities.rs` | 修改 | `agent_key` / `is_agent_type` / 产品能力断言 | + +### 刻意不做的改动 + +| 位置 | 为什么不改 | +|---|---| +| `agents/common/**` | 原计划要把 `gbase8c` 加进 `isPostgresCompatibleJdbc`,被实测推翻(见决策 5);本功能零共享 Java 代码改动 | +| `crates/dbx-driver-agent/src/agent_connection.rs` | `agent_connect_params_with_role` 的三段 postgres-like 分支都**不能**加 `Gbase8c`:只加 `:106/:122` 会在 `:555-561` 的 scheme `match` 撞 `unreachable!()`;两处都加则 Rust 会自己拼 URL,与「Java profile 自己拼 `jdbc:gbase8c://`」冲突。兜底臂 `:141-143` 返回空 `connection_string`,正是 Java `JdbcAgentProfile.buildUrl` 期望的输入 | +| `src-tauri/src/commands/connection.rs` | agent 家族路由由 manifest 驱动(`database_capabilities::is_agent_type` ← `database_manifest::is_agent_runtime`),`Gbase8c` 自动命中 `:1845`/`:2313` 的守卫臂 | +| `crates/dbx-core/src/schema/providers/native.rs` | 该 provider 无任何调用点(`providers/mod.rs` 只有一行 `pub(super) mod native;`,`crates/dbx-core/src` 内 grep `native::` 零命中),且 agent 类型不会进去 | +| `crates/dbx-core/src/connection/mod.rs:1066 needs_legacy_postgres_like_database_probe` | 它给「旧记录/被清空的 database」在 `postgres` 不存在时回退 `template1`。实测实例的 `postgres` 存在,`default_database()` 现在也给 `postgres`,没有 legacy 记录需要兜底;纳入会引入每次空库连接的探测成本 | +| `crates/dbx-core/src/schema/mod.rs:7765 is_agent_postgres_metadata_fallback_config` | 是否让 agent 元数据缺失时回退原生 tokio-postgres 池需要针对本类型实测(注释明确 UXDB 被排除,并非「PG 家族一律纳入」) | +| `crates/dbx-core/src/schema/mod.rs:9942 is_agent_pg_sequence_config` | 序列走 agent 的 `relkind='S'` 查询还是原路径,同样需要实测 | +| `crates/dbx-core/src/schema/mod.rs:11083 supports_custom_type_details` | 刻意收窄的「已验证五族」名单,测试 `:3513-3532` 反而断言 Highgo/Uxdb 为假 | +| `agents/metadata-constraint-coverage.tsv` | 该表只要求覆写了 `listTables`/`listObjects` 的驱动登记;本 agent 不覆写,靠收窄 `tableTypes` 白名单解决 | +| `deploy/database/gbase8c/**` | Q9 决策:不做菜谱 | + + +## 描述符草案 + +```yaml +schemaVersion: 1 +order: 455 +dbType: gbase8c +compareModes: [schema, data] +rustVariant: Gbase8c +label: 南大通用 GBase 8c +dialect: PostgreSQL +runtimeMode: agent +mcpMode: bridge +agentKey: gbase8c +driverStoreVisible: true +driverStoreOrder: 55 +singleConnectionPool: true +metadataConnectionScoped: false +skipTcpProbe: true +defaultPort: 5432 +traits: + schemaAware: true + treeSchema: true + pgVacuum: true +supportLevel: operate +capabilities: + queryExecution: true + metadataBrowse: true + objectBrowser: true + objectSource: false + schemaSearch: true + diagram: true + tableDataEdit: true + tableStructureEdit: false + tableImport: false + dataTransfer: false + sqlFileExecution: true + databaseCreate: false + fieldLineage: false + sqlExplain: false + userAdmin: false + driverManagement: true +``` + +`transferSupportLevel` / `transferObjectFamily` **刻意省略**(与 `plugins/connection-types/uxdb.yaml` 一致):两者只有在 `dataTransfer: true` 时才有消费者,而本类型 `dataTransfer: false`。 + +## Agent profile + +`agents/drivers/gbase8c` 的 profile 槽位取值及其依据: + +| 槽位 | 取值 | 依据 | +|---|---|---| +| `driverClass` | `com.gbase8c.Driver` | 实测 `Class.forName` 成功;`META-INF/services/java.sql.Driver` 内容即此类 | +| `urlTemplate` | `jdbc:gbase8c://{host}:{port}/{database}` | 实测厂商 jar 拒绝 `jdbc:postgresql://` | +| `defaultPort` | `5432` | 官方默认端口;CN 是客户端入口 | +| `excludedSchemas` | 17 个(见下) | 实测 `getSchemas()` 返回 19 个,其中 17 个是 openGauss 系统 schema | +| `tableTypes` | `{TABLE, VIEW}` | **必须收窄**:默认白名单含 `SYSTEM TABLE`/`SYSTEM VIEW`,实测跨 schema 返回 531 个对象、其中 331 个是 SYSTEM | +| `identifierQuote` | `"` | 静态回落值;实际由 `datcompatibility` 决定 | +| `schemaSwitchPrefix` | `SET search_path TO` | 默认值是 `SET SCHEMA`(Oracle 味),PG 家族必须改 | +| `catalogFallbackEnabled` | `true` | 驱动行的 `TABLE_CAT=null`,`getSchemas` 的回退路径需要它兜底 | +| `nativeTableDdlSupported` | `false` | `tableStructureEdit` 关闭 | +| `objectSourceSupported` | `false` | `ConfiguredJdbcAgent.getObjectSource` 抛异常 | +| `triggersSupported` | `false` | 继承实现是恒定空列表 | + +`excludedSchemas` 实测清单: + +``` +auditadmin, audituser, blockchain, coverage, cstore, db4ai, dbe_perf, +dbe_pldebugger, dbe_pldeveloper, dbe_sql_util, information_schema, +pg_catalog, pkg_service, securityadmin, snapshot, sqladvisor, sysadmin +``` + +注:`getExcludedSchemas()` 全仓只有两处消费(`StandardJdbcMetadata.java:84` 的 `listSchemas`、`:462` 的补全过滤),`listTables`/`listObjects` 都不看它 —— 本驱动是全仓第一个真正使用这个槽的 profile。 + +## Agent 覆写清单 + +| 方法 | 是否覆写 | 理由 | +|---|---|---| +| `listDatabases()` | **必须** | 继承路径是 `StandardJdbcMetadata.java:25-34` 的 `conn.getMetaData().getCatalogs()`,实测**只返回 1 行**(当前库),而服务器上有 7 个库。改查 `SELECT datname FROM pg_catalog.pg_database WHERE datistemplate = false ORDER BY datname` | +| `resultValueReader()` | **不覆写** | 继承的 3 参 `resultValue`(`AbstractJdbcAgent.java:637-670`)把 `OTHER`/`STRUCT`/`BLOB`/`CLOB`/`ARRAY` 全部交给 `rs.getString`,而实测 `getString` 对这些类型**全部可用**:`raw`/`json`/`inet`→`AABB`/`{"k":1}`/`10.0.0.1`(底层 `PGobject`)、`uuid`→文本、`blob`→`424C4F4231`、`clob`→`CLOB1`、`_int4`/`_text`(jdbc=2003 ARRAY,底层 `PgArray`)→`{1,2,3}`/`{x,y}`。没有任何一类需要额外处理,多加一条路径只会引入未测分支 | +| `listTables(schema, …)` | **不覆写** | 实测厂商驱动**遵守** `types` 数组:`{TABLE,VIEW}` 精确返回 2 行、`{INDEX}` 才 2 行、只有 `types=null` 才出现 `TABLE_TYPE='INDEX'` 行。收窄 `tableTypes` 白名单即可,无需后置过滤,因而也不需要登记 `agents/metadata-constraint-coverage.tsv` | +| `listTriggers` / `getTableDdl` 的 check 约束 | 不覆写 | 均不在本版能力范围内 | + +**硬约束:agent 代码不得 import 任何 `com.gbase8c.*` 类型。** 驱动会让 `getObject()` 返回 `com.gbase8c.core.types.PGClob` 与 `com.gbase8c.util.PGobject`;继承路径从不调 `getObject`,这正是它安全的原因。`resultValue` 覆写只能用 `java.sql.*` 的标准接口。 + +**两个已知的重复,是刻意的:** + +- `LIST_DATABASES_SQL` 与 `PostgresLikeAgent.java:141-142` 的 `datistemplate = false ORDER BY datname` 逐字相同。前者用 `profile.catalogRelation("database")` 拼字符串,而本 Agent 继承 `ConfiguredJdbcAgent`,拿不到 `PostgresLikeAgent` 实例;共享这段 SQL 必须改 `agents/common/**`,而那是本 spec 明确不动的地方。将来若把该查询下沉到 `JdbcAgentProfile`,这处应当一起收编。 +- `GBASE8C_PROFILE` 用 12 个位置参数构造,末尾四个裸布尔(`catalogFallbackEnabled=true`、`nativeTableDdlSupported/objectSourceSupported/triggersSupported=false`)。这是 `JdbcAgentProfile` 既有的构造器形状,同为 PG 家族的 `uxdb`/`highgo` 也这么写;可读性问题由 `Gbase8cAgent.java` 里那段点名注释承担,避免为一个驱动引入全仓没有的具名覆写风格。 + +## 需要同时改的 Rust 与前端接线点 + +| 位置 | 改动 | +|---|---| +| ~~`agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java:534-554`~~ | **不改**:实测推翻(见决策 5)。`isPostgresCompatibleJdbc` 的唯一消费点是 `:448` 的 `resolveIdentifierQuote`,默认回落给出的双引号在四种模式下都正确 | +| `crates/dbx-types/build.rs` | 无需改(读目录自动生成 `DatabaseType::Gbase8c`) | +| `crates/dbx-core/src/connection/mod.rs:2780+` | 无需改(agent 路线不发原生 pool) | +| `src-tauri/src/commands/connection.rs:1432` | 无需改(同上) | +| `crates/dbx-core/src/schema/providers/native.rs` | 无需改(同上) | +| `apps/desktop/src/components/connection/ConnectionDialog.vue:3778-3780` | `supportsNativeAgentJdbcDriverConfigType` 加 `gbase8c`,否则用户在表单里看不到 `jdbc_driver_paths` | +| `apps/desktop/src/components/connection/ConnectionDialog.vue:3795-3799` | `nativeAgentJdbcDriverHint` 加 `gbase8c` 分支 | +| ~~`apps/desktop/src/lib/database/gbase8cDriverOptions.ts`~~ | **不建**:`AbstractJdbcAgent.loadDriver`(`:127-144`)在 `jdbc_driver_class` 为空时回落到 profile 的 `driverClass()`,所以只给 `jdbc_driver_paths` 即可;唯一先例 `bigquery`(同为 `Agent-External-Driver`)也没有 `*DriverOptions.ts` | +| `crates/dbx-core/assets/database-drivers.manifest.json`(生成物) | 由 `pnpm generate:connection-types` 重生成 | + +## 非目标 + +- `plugins/jdbc` Java 插件路线(`DbxJdbcPlugin.java` 有独立的方言映射表) +- CLI 直连 +- `tableStructureEdit` / DDL 生成(`DBCOMPATIBILITY` A 模式下空串即 NULL、`DATE`→`TIMESTAMP(0)`、CHAR 按字节计;同一份 DDL 在一个 8c 实例上对、在另一个上错,而模式建库时固定且不可 SQL 修改) +- 数据传输与表导入(依赖上面的 DDL 生成) +- `deploy/database/gbase8c/` 菜谱 +- sm3 认证(本构建的 `postgresql.conf` 注释只列出 `0=md5 / 1=sha256+md5 / 2=sha256`) + +## 附录 A:真机实测事实表 + +观测实例:官方 `GBase8cV6_S1.0.0B20_centos7.8_x86_64.tar.gz`,容器 `centos:7.9.2009`,`simpleInstall` 单节点。 + +### A.1 认证与连接 + +| 探针 | 结果 | +|---|---| +| `password_encryption_type=1`(分布式默认) | 服务端索要 **auth code 5 = 标准 MD5**,salt `1446da93` | +| `password_encryption_type=2`(仅 sha256) | 服务端索要 **auth code 10 + int32 标签 2**,载荷 136 字符十六进制 | +| 初始用户远端连接 | **被拒**:`SFATAL MForbid remote connection with initial user.`(SQLSTATE 28000) | +| `ssl` 默认值 | `off`;SSLRequest 回 `N` | +| SSLRequest(8 字节 `80877103`) | 服务端回 `N` | +| 厂商驱动连接耗时 | 614 ms(`dbx` 用户,sha256-only) | + +### A.2 版本与兼容模式 + +| 探针 | 结果 | +|---|---| +| `select version()` | `(GBase8cV6 1.0.0B20 build 80563724) compiled at 2026-05-13 03:09:52 commit 0 last mr 1443 on x86_64-unknown-linux-gnu, compiled by g++ (GCC) 10.3.0, 64-bit` —— **不含 `PostgreSQL` 子串** | +| `JDBC getDatabaseProductName/Version` | `GBase8c` / `9.2.4` | +| `show server_version_num` | `90204` | +| `datcompatibility` | `postgres = PG`、`template0 = A`、`template1 = A` | +| 四模式建库 | `CREATE DATABASE dbx_a/dbx_b/dbx_c/dbx_pg DBCOMPATIBILITY 'A'/'B'/'C'/'PG'` 全部成功,同实例共存 | +| `dolphin%` schema(B 模式重写目标) | `dbx_b` 中计数为 0 | +| `current_schema()` | 用户 `dbx` 在 `postgres` 里是 `dbx`,在 `dbx_pg`/`dbx_a`/`dbx_b`/`dbx_c` 里是 `public`;同名用户 schema 只在 `postgres` 存在 | + +### A.3 目录面 + +| 探针 | 结果 | +|---|---| +| `information_schema` 视图数 | 66 | +| `pg_catalog` 普通表数 | 130 | +| `pg_proc.prokind` | 列存在;函数 `f`、过程 `p`;但分布只见 `f`,而 `proisagg=t` 有 170 行 | +| `JOIN LATERAL` | **不支持** | +| `pg_catalog.to_regclass(text)` | **不存在** | +| `array_position` | **不存在**(`pg_proc` 命中 0) | +| 双参 `pg_get_viewdef(regclass, bool)` | 存在(对表报 `Not a view`,是语义错而非缺函数) | +| `generate_series` | 可用 | +| `array_length(int2vector, 1)` | 可用(返回 2) | +| 例程创建 | `CREATE FUNCTION … AS $$ … $$ LANGUAGE plpgsql` 成功;`CREATE PROCEDURE … AS $$ … $$ LANGUAGE plpgsql` **语法报错**,必须用 openGauss 的 `AS BEGIN … END; /` 经 `gsql -f` 提交 | + +### A.4 JDBC 驱动怪癖 + +| 探针 | 结果 | +|---|---| +| `getCatalogs()` | **1 行**(当前库);`pg_database` 有 7 行 | +| `getTableTypes()` | 含非标准项 `INDEX`、`SYSTEM INDEX`、`SYSTEM TABLE`、`SYSTEM VIEW`、`SYSTEM TOAST INDEX`、`SYSTEM TOAST TABLE`、`TYPE`、`LOCAL TEMPORARY`、`TEMPORARY *` | +| `getTables(types=null)` | 返回 `TABLE_TYPE='INDEX'` 行(`t1_pkey`、`idx_t1_name`) | +| `getTables(types={TABLE,VIEW})` | 精确 2 行 | +| `getTables(types={TABLE,VIEW,SYSTEM TABLE,SYSTEM VIEW})` 跨全 schema | **531 行,其中 331 行是 SYSTEM**(`pg_catalog.gs_*`、`information_schema.sql_*`) | +| `getSchemas()` | 19 个,含 `dbe_*`/`cstore`/`db4ai`/`blockchain`/`securityadmin`/`sysadmin`/`snapshot`/`sqladvisor`/`pkg_service`/`auditadmin`/`audituser`/`coverage`;不含 `dbx` | +| `getTables` 行的 catalog | `TABLE_CAT=null`、`TABLE_SCHEM=dbx_smoke` | +| `getProcedures` / `getFunctions` | 函数出现在 `getProcedures`(`PROCEDURE_TYPE=2`),`getFunctions` 返回 0 行 | +| `getImportedKeys` | 可用:`fk=child_pid_fkey pid -> t1.id updateRule=3 deleteRule=3` | +| 表注释 | 经 `getTables` 的 `REMARKS` 返回(实测 `remarks=smoke table`) | +| `getDriverName()` | `GBASE JDBC Driver /*$mvn.project.property.parsedversion.osgiversion$*/` —— 厂商漏替换的 Maven 占位符 | +| `getIdentifierQuoteString()` | PG/A/B/C **四模式全是双引号**,且与数据库行为一致(双引号标识符全模式可用,反引号全模式 `syntax error`) | +| `getTypeInfo` | 每行 `precision=0`;`getColumns.COLUMN_SIZE` 常为 `2147483647` | + +### A.5 非标准类型的往返 + +| 类型 | `getColumns` | `getTypeInfo` | `getObject()` | `getString()` | +|---|---|---|---|---| +| `int1` | `int1` / −6 | −6 | `java.lang.Integer` | `200` | +| `int16` | — | 1111 | **列不可创建**:`ERROR: It's not supported to create int16 column` | — | +| `raw` | `raw` / 1111 | 1111 | `com.gbase8c.util.PGobject` | `AABB` | +| `blob` | `blob` / 2004 | 2004 | `byte[]` | `424C4F4231` | +| `clob` | `clob` / 2005 | 2005 | `com.gbase8c.core.types.PGClob` | `CLOB1` | +| `money` | `money` / 8 | 8 | `java.lang.Double = 12.34` | `$12.34` | +| `bytea` | `bytea` / −2 | −2 | `byte[]` | 十六进制文本 | +| `json` | `json` / 1111 | 1111 | `com.gbase8c.util.PGobject` | `{"k":1}` | +| `uuid` | `uuid` / 1111 | 1111 | `java.util.UUID` | 文本 | +| `inet` | `inet` / 1111 | 1111 | `com.gbase8c.util.PGobject` | `10.0.0.1` | +| `int[]` / `text[]` | `_int4`/`_text` / 2003 | 2003 | `com.gbase8c.jdbc.PgArray` | `{1,2,3}` / `{x,y}` | +| `jsonb` | — | 1111 | — | — | +| `abstime`/`reltime`/`tinterval`/`xid32`/`smgr`/`unknown` | — | 1111 | — | — | +| `pg_class`/`pg_proc`/`pg_type`/`gs_package` | — | **2002 (STRUCT)** | — | — | + +`information_schema.table_constraints` 会把自动派生 NOT NULL 约束报成 `CHECK`(形如 `16393_16451_1_not_null`),而 `pg_constraint` 只列真约束(`contype='c'`)。若日后实现 check 约束列举,应读 `pg_constraint`。 + +## 实测冒烟结果(`Gbase8cAgentLiveTest`) + +`agents/drivers/gbase8c/src/test/java/com/dbx/agent/gbase8c/Gbase8cAgentLiveTest.java` 对附录 B 建起来的真机实例跑通过(2026-10-16),是本功能唯一的端到端证据。跑法:把 `gbase8c-jdbc-3.0.0B02.jar` 放进 `agents/drivers/gbase8c/libs/`(被 `agents/.gitignore` 的 `**/libs/*.jar` 忽略),设 `DBX_TEST_GBASE8C_URL` / `_USER` / `_PASSWORD`(可选 `_SCHEMA`、`_DATABASE`)后 `./gradlew :gbase8c:test --tests '*Gbase8cAgentLiveTest*'`;环境变量缺失时整个用例被跳过。 + +| 断言 | 结果 | +|---|---| +| 用 `jdbc:gbase8c://…` + `com.gbase8c.Driver` 连上(走 `GBASE8C_PROFILE` 的 URL 模板) | ✅ | +| `listDatabases()` 返回**多个**库且含 `postgres`(覆写生效:驱动的 `getCatalogs()` 只给 1 行) | ✅ | +| `listSchemas()` 过滤掉 `dbe_perf` / `cstore` / `db4ai` / `sqladvisor` / `pkg_service` / `snapshot` | ✅ | +| `listTables(schema)` 的表类型 ⊆ `{TABLE, VIEW}`,无 `gs_` / `sql_` 前缀的系统对象 | ✅ | +| 表注释经 `REMARKS` 返回 | ✅ | +| `getColumns()` 报出主键列(`getIs_primary_key()`) | ✅ | +| `listIndexes()` 列出主键索引与它的列 | ✅ | +| `listForeignKeys()` 解出 `ref_table` | ✅ | +| `setSchemaSQL()` = `SET search_path TO "public"`,`getIdentifierQuote()` = `"` | ✅ | +| `executeQuery()` 返回列名与 2 行数据 | ✅ | +| `listObjects()` 列出新建的表 | ✅ | +| `executeTransaction()` 三条 DML 一次提交:`UPDATE` 1 行 + `DELETE` 0 行 + `INSERT` 1 行 → `getAffected_rows() == 2`,回读 `SELECT count(*)` == 1 | ✅ | + +`tableDataEdit` 的证据是最后一行(数据网格的保存走 `executeTransaction` → `Statement.executeUpdate` + 显式提交)。`schemaSearch` 的证据见下。 + +### `schemaSearch` 的证据 + +结构搜索的 SQL 由 Rust 侧的 `postgres_family_structure_search_sql`(`crates/dbx-sql-data/src/database_structure_search_sql.rs:179-222`)生成,Java 测试拿不到它,因此把生成的字符串按 `schema=None` / `schema='public'`、`term='probe'`、`limit=200` 复刻后用容器内 `gsql` 对同一实例执行,两种过滤都返回同样的两行: + +| object_schema | object_name | column_name | column_comment | table_comment | +|---|---|---|---|---| +| public | g8c_search_probe | probe_col_abc | probe column comment | | +| public | g8c_search_probe | | | probe table comment | + +即该查询只用 `pg_attribute` / `pg_class` / `pg_namespace` / `pg_description`,不碰 `JOIN LATERAL` 与 `to_regclass`,在 8c 内核上可用(字段命中与表注释命中两个分支都命中)。 + +**冒烟发现的既有特性(不是本功能的缺陷,已写进用例注释)**:厂商驱动的 `getIndexInfo` 把主键索引报成普通唯一索引(`is_primary=false`、`constraint_backed=false`),与 `getPrimaryKeys` 的 `PK_NAME` 对不上;主键本身由 `getColumns()` 的 `is_primary_key` 正确报告。`StandardJdbcMetadata` 对所有 `ConfiguredJdbcAgent` 驱动共用这条路径,所以不是 GBase 8c 特有,修它属于另一个改动。 + +### 「数据库」是必填项(真机实测) + +Agent 用 `jdbc:gbase8c://{host}:{port}/{database}` 拼 URL。库名留空时 URL 退化成 `jdbc:gbase8c://host:port/`,实测厂商驱动会拿**用户名**当库名去连: + +| URL | 结果 | +|---|---| +| `jdbc:gbase8c://127.0.0.1:15432/dbx_pg` | 连上,`current_database=dbx_pg`、`current_schema=public` | +| `jdbc:gbase8c://127.0.0.1:15432/` | `FATAL: database "dbx" does not exist`(驱动拿用户名当库名) | +| `jdbc:gbase8c://127.0.0.1:15432` | `No suitable driver found`(URL 必须在 host/port 后带 `/`) | + +因此前端把 GBase 8c 并入「库名必填」一族(与 `kingbase` / `xugu` 同列):占位文案走 `connection.databasePlaceholderRequired`,提交校验走 `hasGbase8cConnectionDatabase`(`apps/desktop/src/lib/connection/gbase8cDatabase.ts`,配套 `apps/desktop/src/lib/__tests__/connection/gbase8cDatabase.spec.ts`)。填了完整 `connection_string` 时不要求库名——`JdbcAgentProfile.buildUrl` 在它非空时原样使用。 + +### 本地 dev 驱动包与发布包不一样(本次验证的重要前提) + +`agents/drivers/gbase8c/build.gradle` 用 `fileTree(dir: 'libs', include: ['*.jar'])` 收集依赖,而 `agents/.gitignore:1` 的 `**/libs/*.jar` 只允许厂商 jar 存在于本地。于是: + +- **本地**:`libs/gbase8c-jdbc-3.0.0B02.jar` 在 → Gradle 打出的 `dbx-agent-gbase8c.jar` **内含** `com/gbase8c/Driver.class`(本次实测该包 1699797 B,内部确认有 `com/gbase8c/**`),驱动管理装的就是它,所以本机连接不再需要填 jar;跑 `Gbase8cAgentLiveTest` 也依赖这个 jar 在 `libs/` 里。 +- **发布**:CI 从干净检出构建,`libs/` 为空 → 包内没有厂商驱动,`Agent-External-Driver: true` 对应的 `external_driver_required: true` 才是真的,用户必须自备 jar。 + +这个差异本身是设计允许的(同一份源码在两种环境下各取所需),但**在带着 `libs/` 的机器上跑 `release:agents`,会打出一个内含厂商 jar 却声明「需要外部驱动」的包**,等于在没有再分发许可的情况下分发了该 jar。`agents/scripts/validate_agents.py` 目前没有这条检查(全仓唯一相关断言是 `agents/drivers/transwarp/src/test/java/com/dbx/agent/transwarp/TranswarpAgentTest.java:179` 的 `assertFalse(...)`,方向正好相反)。建议在发布流程里补一条「声明外部驱动的模块,其 shadow jar 不得包含该驱动类」的校验;本 spec 不含这项改动。 + +## 附录 B:临时验证实例的构建方式 + +不提交为 `deploy/database/` 菜谱,仅作本地验证。完整步骤: + +1. 取包:`https://www.gbase.cn/download/gbase-8c?category=INSTALL_PACKAGE`(需登录)→ `GBase8cV6_S1.0.0B20_centos7.8_x86_64.tar.gz`(306043750 B,sha256 `7C69D7446D0E8EB9B60F188F1532C2DB826865447BCE4C1650CF5537DF2CAFCB`) +2. 解包:内含 `…_CentOS_x86_64.tar.bz2`(server 树,1559 项)、`…_om.tar.gz`(OM 工具)、`upgrade_sql.tar.gz` +3. 基座:`docker run -d --name dbx-gbase8c-test -p 15432:5432 centos:7.9.2009 sleep infinity` +4. 依赖:把 `CentOS-*.repo` 移走,写 vault 源(`http://vault.centos.org/7.9.2009/os/x86_64/` + `/updates/x86_64/`,`gpgcheck=0`),装 `libaio-0.3.109-13.el7` +5. 安装:建非 root 用户 `gbase`,server 树放 `/opt/gbase`,`sh simpleInstall/install.sh -w '<强密码>'`(stdin 喂 `no` 跳过 demo 库;注意答 `no` 时脚本退出码为 1 但安装成功) +6. 远端接入:`postgresql.conf` 改 `listen_addresses = '*'`、`password_encryption_type = 1`(或 2),`pg_hba.conf` 追加 `host all all 0.0.0.0/0 sha256`,`gs_ctl stop/start -Z single_node` +7. 非初始用户:`CREATE USER dbx WITH PASSWORD '…'; ALTER USER dbx SYSADMIN;` —— **初始用户不能远端连接** +8. 客户端:厂商驱动 `GBase-JDBC-x86_64.zip`(796565 B,页面无需登录)→ `gbase8c-jdbc-3.0.0B02.jar`(842978 B) + +已知非致命噪音:`[Alarm Module]` 系列、`failed to open feature control file … gbased.version`、`could not create any HA TCP/IP sockets`、`Failed to obtain environment value $GAUSSLOG!`、`ERROR: gtm proxy: ProxyEventListener: socket broken!`(单节点无 GTM,连 `localhost:6666` 失败属预期)。 + +工具的坑:`gs_guc` 的 `-Z` 只接受 `coordinator`/`datanode`/`gtm`(`single_node` 会报 `unrecognized node type`),因此单节点下直接编辑 `postgresql.conf`/`pg_hba.conf`;`gs_ctl restart` 在本环境不可靠(报 `stopped waiting` + `could not start server`),改用 `stop` → sleep → `start`。 + +## 决策日志 + +| 日期 | 决策 | 变更原因 | +|---|---|---| +| 2026-10-16 | 运行时路线从「原生 Rust 复用 `PoolKind::Postgres`」改为「新 dbType + agent + 厂商 JDBC」 | 用户指出保持与官方一致的兼容性的唯一方式是使用官方发布的 JDBC;实测证实厂商 jar 拒绝 `jdbc:postgresql://` | +| 2026-10-16 | 基类从 `PostgresLikeAgent` 改为 `ConfiguredJdbcAgent` | 真机实测 `JOIN LATERAL` 不支持、`to_regclass` 不存在,打死三条热路径与视图源码 | +| 2026-10-16 | `tableStructureEdit` 保持关闭 | DDL 必须随 `DBCOMPATIBILITY` 变化,模式建库时固定且不可 SQL 修改 | +| 2026-10-16 | 兼容模式只做标识符引号一层 | 实测四种模式的 `getIdentifierQuoteString()` 全是双引号,B 模式必须靠 `datcompatibility` 才能拿到反引号 | +| 2026-10-16 | 兼容模式**不改共享代码**(原计划给 `isPostgresCompatibleJdbc` 加 `gbase8c`) | 实测四种兼容模式下反引号**全部报错**、双引号全部可用,驱动的默认回答就是双引号;原计划会让 B 模式输出反引号从而弄坏查询 | +| 2026-10-16 | `resultValueReader` **不覆写**(原列为建议项) | 实测 `getString` 对 `raw`/`json`/`inet`/`uuid`/`blob`/`clob`/`_int4`/`_text` 全部可用,多一条路径只增加未测分支 | +| 2026-10-16 | 厂商 jar 走 `Agent-External-Driver: true` | jar 内只有 BSD-2/PostgreSQL License 文本,厂商未声明再分发权,Maven Central 无 `cn.gbase` 坐标 | +| 2026-10-16 | `driverStoreOrder` 取 **55** 而不是 17 | `driverStoreOrder` 与可见 profile 的 `storeOrder` 共用一个唯一序列,实测 1–54 全被占用(gbase 15、gbase8s profile 16、access 17…iris 42、duckdb/mq/sqlserver/sqlite/etcd 43–49…oracle 54);README 要求这些值「stable」,所以新条目取下一个空位而不是重排既有 40 项 | +| 2026-10-16 | `agent_connection.rs` 的三段 postgres-like 分支**不**纳入 `Gbase8c` | 只加 `:106/:122` 会在 `:555-561` 的 scheme `match` 撞 `unreachable!()`;两处都加则 Rust 自己拼 URL,与「Java profile 拼 `jdbc:gbase8c://`」冲突。兜底臂返回空 `connection_string`,正是 Java 侧期望的输入 | +| 2026-10-16 | `needs_legacy_postgres_like_database_probe` **不**纳入 `Gbase8c` | 该探测给「旧记录/被清空的 database」在 `postgres` 不存在时回退 `template1`;实测实例有 `postgres`,且本类型没有 legacy 记录,纳入只会给每次空库连接加一次探测 | +| 2026-10-16 | 前端两张 `Record` 矩阵给 `gbase8c` 填 `deferred` | `databaseNamespaceCreation` 的理由是 `databaseCreate: false` 且建库要显式 `DBCOMPATIBILITY`(建库时固定、不可 SQL 修改);`databasePropertyEditing` 的理由是库/模式注释编辑尚未实测。两条都与父 `gbase` 条目的保守处理一致 | +| 2026-10-16 | 不改 `schema/mod.rs:7765` / `:9942` / `:11083` | 分别是「agent 元数据缺失时是否回退原生 PG 池」「序列是否走 agent 的 `relkind='S'` 查询」「自定义类型详情」,都需要针对本类型单独实测,注释本身也说明它们不是「PG 家族一律纳入」 | +| 2026-10-16 | 补 `apps/desktop/src/lib/connection/driver-category-definitions.ts` 的 `gbase8c: "domestic"` | `/code-review` 之后的 `pnpm test` 抓到:`driverCategoryDefinitions.spec.ts` 的「covers every driver the store can list」是从 manifest 派生的 store-visible agent key 全集,漏一个键会让驱动商店把该驱动渲染成未分组(注释里写着 `oracle-oci` 就这么漏过一次) | +| 2026-10-16 | 补 `docs/lib/agentRegistry.ts` 的 `driverLabels` 与 `.github/scripts/{label-pull-request,database-issue-catalog}.mjs` 的别名 | code review 发现:文档的驱动表会退回 `key.replace(/-/g, " ")` 显示裸 `gbase8c`,PR 也拿不到 `database: gbase` 标签 | +| 2026-10-16 | `schemaSearch` / `tableDataEdit` 从「声明但未实测」补成有证据 | code review 指出这两项与「只声明实测通过的项」冲突:`tableDataEdit` 由 `Gbase8cAgentLiveTest` 新增的 `executeTransaction()` 断言补齐,`schemaSearch` 用容器内 `gsql` 原样执行 `postgres_family_structure_search_sql` 生成的 SQL 补齐 | +| 2026-10-16 | 前端「数据库」对 GBase 8c 由选填改为必填 | 试用反馈:界面显示「可选」,但库名留空时 URL 退化成 `jdbc:gbase8c://host:port/`,厂商驱动拿用户名当库名去连,报 `FATAL: database "dbx" does not exist`。改为与 `kingbase`/`xugu` 相同的处理:占位走 `databasePlaceholderRequired`,提交校验走新模块 `apps/desktop/src/lib/connection/gbase8cDatabase.ts`(带 spec) | +| 2026-10-16 | 驱动提示文案改为点名「手动 classpath」开关 | 原文案写「在『驱动 JAR』里指向 …」,但该区块里真正要打开的是默认收起的「手动 classpath」开关,试用时按原文案找不到入口 | diff --git a/plugins/connection-types/gbase8c.yaml b/plugins/connection-types/gbase8c.yaml new file mode 100644 index 0000000000..f22699b888 --- /dev/null +++ b/plugins/connection-types/gbase8c.yaml @@ -0,0 +1,38 @@ +schemaVersion: 1 +order: 455 +dbType: gbase8c +compareModes: [schema, data] +rustVariant: Gbase8c +label: 南大通用 GBase 8c +dialect: PostgreSQL +runtimeMode: agent +mcpMode: bridge +agentKey: gbase8c +driverStoreVisible: true +driverStoreOrder: 55 +singleConnectionPool: true +metadataConnectionScoped: false +skipTcpProbe: true +defaultPort: 5432 +traits: + schemaAware: true + treeSchema: true + pgVacuum: true +supportLevel: operate +capabilities: + queryExecution: true + metadataBrowse: true + objectBrowser: true + objectSource: false + schemaSearch: true + diagram: true + tableDataEdit: true + tableStructureEdit: false + tableImport: false + dataTransfer: false + sqlFileExecution: true + databaseCreate: false + fieldLineage: false + sqlExplain: false + userAdmin: false + driverManagement: true diff --git a/plugins/connection-types/profiles/catalog.yaml b/plugins/connection-types/profiles/catalog.yaml index dd2070ec2b..0f428e1982 100644 --- a/plugins/connection-types/profiles/catalog.yaml +++ b/plugins/connection-types/profiles/catalog.yaml @@ -317,6 +317,13 @@ profiles: icon: gbase port: 9088 user: gbasedbt + - id: gbase8c + dbType: gbase8c + label: 南大通用 GBase 8c + icon: gbase + port: 5432 + user: gbase + category: domestic - id: opengauss dbType: opengauss label: openGauss