diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 55525adc2c..11b37ecf08 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -9241,7 +9241,7 @@ function openExternalUrl(url: string) { -
+
diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index db21f50ff9..48bbd936cf 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -4440,7 +4440,7 @@ const canSetCreateDatabaseCharset = computed(() => { const canSetCreateDatabaseLocale = computed(() => { const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined; - return connectionNamespaceCreationTarget(config) === "database" && supportsCreateDatabaseLocale(config?.db_type, config?.driver_profile); + return connectionNamespaceCreationTarget(config) === "database" && supportsCreateDatabaseLocale(config?.db_type); }); const canDropDatabase = computed(() => { diff --git a/apps/desktop/src/composables/__tests__/useSidebarTableMutationRuntime.saphana.spec.ts b/apps/desktop/src/composables/__tests__/useSidebarTableMutationRuntime.saphana.spec.ts index a00243e63f..97ae0e2ff3 100644 --- a/apps/desktop/src/composables/__tests__/useSidebarTableMutationRuntime.saphana.spec.ts +++ b/apps/desktop/src/composables/__tests__/useSidebarTableMutationRuntime.saphana.spec.ts @@ -124,7 +124,7 @@ describe("useSidebarTableMutationRuntime SAP HANA schema-scoped actions", () => }); it("omits the GBase 8s metadata owner from generated table SQL", async () => { - const { feature } = runtime("tenant", { db_type: "gbase", driver_profile: "gbase8s" }, "informix"); + const { feature } = runtime("tenant", { db_type: "gbase8s", driver_profile: "gbase8s" }, "informix"); await feature.confirmDropTable(); diff --git a/apps/desktop/src/lib/__tests__/connection/connectionUrlBuilder.spec.ts b/apps/desktop/src/lib/__tests__/connection/connectionUrlBuilder.spec.ts index 92d223e528..94520da364 100644 --- a/apps/desktop/src/lib/__tests__/connection/connectionUrlBuilder.spec.ts +++ b/apps/desktop/src/lib/__tests__/connection/connectionUrlBuilder.spec.ts @@ -302,7 +302,7 @@ describe("buildConnectionUrlCopy JDBC URL", () => { it("selects dialects by driver profile", () => { expect(buildConnectionUrlCopy(config({ db_type: "gaussdb", driver_profile: "gaussdb-m" }), "jdbcUrl")).toBe("jdbc:gaussdb://db.example.com:5432/appdb"); expect(buildConnectionUrlCopy(config({ db_type: "gaussdb" }), "jdbcUrl")).toBe("jdbc:postgresql://db.example.com:5432/appdb"); - expect(buildConnectionUrlCopy(config({ db_type: "gbase", port: 9088, driver_profile: "gbase8s" }), "jdbcUrl")).toBe("jdbc:gbasedbt-sqli://db.example.com:9088/appdb"); + expect(buildConnectionUrlCopy(config({ db_type: "gbase8s", port: 9088, driver_profile: "gbase8s" }), "jdbcUrl")).toBe("jdbc:gbasedbt-sqli://db.example.com:9088/appdb"); expect(buildConnectionUrlCopy(config({ db_type: "gbase", port: 5258, driver_profile: "gbase8a" }), "jdbcUrl")).toBeNull(); expect(buildConnectionUrlCopy(config({ db_type: "tdengine", port: 6041 }), "jdbcUrl")).toBe("jdbc:TAOS-RS://db.example.com:6041/appdb"); expect(buildConnectionUrlCopy(config({ db_type: "tdengine", port: 6030 }), "jdbcUrl")).toBe("jdbc:TAOS://db.example.com:6030/appdb"); diff --git a/apps/desktop/src/lib/__tests__/connection/gbase8sConnectionDialog.spec.ts b/apps/desktop/src/lib/__tests__/connection/gbase8sConnectionDialog.spec.ts index cc4ddaf267..abd7547dc3 100644 --- a/apps/desktop/src/lib/__tests__/connection/gbase8sConnectionDialog.spec.ts +++ b/apps/desktop/src/lib/__tests__/connection/gbase8sConnectionDialog.spec.ts @@ -28,7 +28,7 @@ function selectedProfileHarness(selectedTypeValue: string, driverProfile: string gbase8c: { label: "南大通用 GBase 8c" }, }, { value: selectedTypeValue }, - { value: { db_type: driverProfile === "gbase8c" ? "gbase8c" : "gbase", driver_profile: driverProfile } }, + { value: { db_type: driverProfile === "gbase8c" ? "gbase8c" : driverProfile === "gbase8s" ? "gbase8s" : "gbase", driver_profile: driverProfile } }, () => null, { value: null }, gbaseProfileFromConfig, diff --git a/apps/desktop/src/lib/__tests__/connection/gbaseProfiles.spec.ts b/apps/desktop/src/lib/__tests__/connection/gbaseProfiles.spec.ts index d191b9a838..cb9b5a79ac 100644 --- a/apps/desktop/src/lib/__tests__/connection/gbaseProfiles.spec.ts +++ b/apps/desktop/src/lib/__tests__/connection/gbaseProfiles.spec.ts @@ -11,6 +11,9 @@ describe("gbaseProfiles", () => { it("resolves the toggle profile from a saved connection", () => { expect(gbaseProfileFromConfig({ db_type: "gbase8c", driver_profile: "gbase8c" })).toBe("gbase8c"); expect(gbaseProfileFromConfig({ db_type: "gbase8c" })).toBe("gbase8c"); + expect(gbaseProfileFromConfig({ db_type: "gbase8s", driver_profile: "gbase8s" })).toBe("gbase8s"); + expect(gbaseProfileFromConfig({ db_type: "gbase8s" })).toBe("gbase8s"); + // A not-yet-migrated in-memory config still resolves to the 8s toggle. expect(gbaseProfileFromConfig({ db_type: "gbase", driver_profile: "gbase8s" })).toBe("gbase8s"); expect(gbaseProfileFromConfig({ db_type: "gbase", driver_profile: "gbase8a" })).toBe("gbase8a"); expect(gbaseProfileFromConfig({ db_type: "gbase", driver_profile: "gbase" })).toBe("gbase8a"); diff --git a/apps/desktop/src/lib/__tests__/database/createDatabaseLocale.spec.ts b/apps/desktop/src/lib/__tests__/database/createDatabaseLocale.spec.ts index 6bea85ccbe..1e0d5b832c 100644 --- a/apps/desktop/src/lib/__tests__/database/createDatabaseLocale.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/createDatabaseLocale.spec.ts @@ -3,19 +3,19 @@ import { supportsCreateDatabaseCharset, supportsCreateDatabaseLocale } from "@/l import { DEFAULT_GBASE8S_DATABASE_LOCALE, defaultGbase8sDatabaseLocale, GBASE8S_DATABASE_LOCALES } from "@/lib/database/createDatabaseCharsetOptions"; describe("GBase 8s / Informix create-database locale support", () => { - it("offers a locale picker only for the gbase8s profile", () => { - expect(supportsCreateDatabaseLocale("gbase", "gbase8s")).toBe(true); + it("offers a locale picker only for the gbase8s type", () => { + expect(supportsCreateDatabaseLocale("gbase8s")).toBe(true); // Plain Informix's agent has no directive handling, so the locale picker must stay hidden // there until InformixAgent implements it; showing it would silently ignore the choice. - expect(supportsCreateDatabaseLocale("informix", undefined)).toBe(false); + expect(supportsCreateDatabaseLocale("informix")).toBe(false); }); it("does not treat GBase 8a or MySQL as locale-picking", () => { - expect(supportsCreateDatabaseLocale("gbase", "gbase8a")).toBe(false); - expect(supportsCreateDatabaseLocale("mysql", "mysql")).toBe(false); + expect(supportsCreateDatabaseLocale("gbase")).toBe(false); + expect(supportsCreateDatabaseLocale("mysql")).toBe(false); // The MySQL charset path stays independent of the Informix locale path. expect(supportsCreateDatabaseCharset("mysql", "mysql")).toBe(true); - expect(supportsCreateDatabaseCharset("gbase", "gbase8s")).toBe(false); + expect(supportsCreateDatabaseCharset("gbase8s", "gbase8s")).toBe(false); }); it("defaults to a Chinese-capable UTF-8 locale", () => { diff --git a/apps/desktop/src/lib/__tests__/database/databaseNamespaceCreation.spec.ts b/apps/desktop/src/lib/__tests__/database/databaseNamespaceCreation.spec.ts index 7eb2fff0cd..07d6d98cd4 100644 --- a/apps/desktop/src/lib/__tests__/database/databaseNamespaceCreation.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/databaseNamespaceCreation.spec.ts @@ -7,7 +7,7 @@ const databaseNode = { type: "database" as const, database: "dcss" }; describe("database namespace creation targets", () => { it("offers Create Database (not Create Schema) for GBase 8s", () => { - const gbase8s = { db_type: "gbase", driver_profile: "gbase8s" } as Conn; + const gbase8s = { db_type: "gbase8s", driver_profile: "gbase8s" } as Conn; expect(connectionNamespaceCreationTarget(gbase8s)).toBe("database"); expect(databaseNodeNamespaceCreationTarget(gbase8s, databaseNode)).toBeNull(); diff --git a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts index 0a2689a087..3014ef4a21 100644 --- a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts @@ -159,7 +159,7 @@ describe("jdbc dialect inference", () => { expect(connectionShouldLoadIdentifierQuote({ db_type: "kingbase" })).toBe(true); expect(connectionShouldLoadIdentifierQuote({ db_type: "kyuubi" })).toBe(true); expect(connectionShouldLoadIdentifierQuote({ db_type: "gaussdb" })).toBe(true); - expect(connectionShouldLoadIdentifierQuote({ db_type: "gbase", driver_profile: "gbase8s" })).toBe(true); + expect(connectionShouldLoadIdentifierQuote({ db_type: "gbase8s", driver_profile: "gbase8s" })).toBe(true); expect(connectionShouldLoadIdentifierQuote({ db_type: "gbase", driver_profile: "gbase8a" })).toBe(false); // Cloud Spanner reports a backtick for GoogleSQL and a double quote for the // PostgreSQL dialect; both are only known from the connection. @@ -175,12 +175,12 @@ describe("jdbc dialect inference", () => { }); it("falls back to a flat table tree when GBase 8s reports no schemas", () => { - expect(connectionShouldDiscoverJdbcSchemas({ db_type: "gbase", driver_profile: "gbase8s" })).toBe(true); + expect(connectionShouldDiscoverJdbcSchemas({ db_type: "gbase8s", driver_profile: "gbase8s" })).toBe(true); expect(connectionShouldDiscoverJdbcSchemas({ db_type: "gbase", driver_profile: "gbase8a" })).toBe(false); }); it("omits the metadata owner from GBase 8s table SQL", () => { - expect(connectionTableSqlSchema({ db_type: "gbase", driver_profile: "gbase8s" }, "gbasedbt")).toBeUndefined(); + expect(connectionTableSqlSchema({ db_type: "gbase8s", driver_profile: "gbase8s" }, "gbasedbt")).toBeUndefined(); expect(connectionTableSqlSchema({ db_type: "gbase", driver_profile: "gbase8a" }, "analytics")).toBe("analytics"); expect(connectionTableSqlSchema({ db_type: "informix", driver_profile: "informix" }, "informix")).toBe("informix"); }); @@ -485,7 +485,7 @@ describe("object tree node schema", () => { it.each([ { db_type: "jdbc" as const, connection_string: "jdbc:informix-sqli://localhost:9088/prulife" }, - { db_type: "gbase" as const, driver_profile: "gbase8s" }, + { db_type: "gbase8s" as const, driver_profile: "gbase8s" }, ])("keeps compatible Informix metadata on the login owner", (connection) => { expect(connectionObjectTreeQuerySchema(connection, "prulife")).toBe(""); expect(connectionObjectTreeNodeSchema(connection, "prulife")).toBeUndefined(); diff --git a/apps/desktop/src/lib/__tests__/sidebar/sidebarDatabaseStorage.spec.ts b/apps/desktop/src/lib/__tests__/sidebar/sidebarDatabaseStorage.spec.ts index b8be48b763..e1e5c57457 100644 --- a/apps/desktop/src/lib/__tests__/sidebar/sidebarDatabaseStorage.spec.ts +++ b/apps/desktop/src/lib/__tests__/sidebar/sidebarDatabaseStorage.spec.ts @@ -17,7 +17,7 @@ describe("sidebar database storage", () => { } expect(supportsSidebarTableStorage(config("jdbc"))).toBe(false); expect(supportsSidebarTableStorage({ ...config("postgres"), driver_profile: "cockroachdb" })).toBe(false); - expect(supportsSidebarTableStorage({ ...config("gbase"), driver_profile: "gbase8s" })).toBe(false); + expect(supportsSidebarTableStorage({ ...config("gbase8s"), driver_profile: "gbase8s" })).toBe(false); }); it("requests and applies only visible database nodes", () => { diff --git a/apps/desktop/src/lib/connection/agentDriverInstallHint.ts b/apps/desktop/src/lib/connection/agentDriverInstallHint.ts index bec37b7e8a..ef04950def 100644 --- a/apps/desktop/src/lib/connection/agentDriverInstallHint.ts +++ b/apps/desktop/src/lib/connection/agentDriverInstallHint.ts @@ -50,7 +50,7 @@ export function agentDriverInstallKey(dbType: DatabaseType | undefined, driverPr } if (dbType === "mongodb") return "mongodb"; if (dbType === "dameng") return "dameng"; - if (dbType === "gbase") return driverProfile === "gbase8s" ? "gbase8s" : "gbase8a"; + if (dbType === "gbase") return "gbase8a"; if (dbType === "mq") { if (driverProfile === "kafka") return "kafka"; if (driverProfile === "rocketmq") return "rocketmq"; diff --git a/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts b/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts index 01596de674..0b24af1749 100644 --- a/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts +++ b/apps/desktop/src/lib/connection/connectionAttemptTimeout.ts @@ -24,6 +24,7 @@ const DRIVER_STARTUP_FLOOR_TYPES = new Set([ "exasol", "oceanbase-oracle", "gbase", + "gbase8s", "access", "oracle", "h2", diff --git a/apps/desktop/src/lib/connection/connectionUrl.ts b/apps/desktop/src/lib/connection/connectionUrl.ts index 3953efdeee..558e22dc64 100644 --- a/apps/desktop/src/lib/connection/connectionUrl.ts +++ b/apps/desktop/src/lib/connection/connectionUrl.ts @@ -74,7 +74,7 @@ const SCHEME_PROFILES: Record = { gaussdb: { type: "gaussdb", profile: "gaussdb", label: "GaussDB", defaultPort: 5432 }, kwdb: { type: "kwdb", profile: "kwdb", label: "KWDB", defaultPort: 26257 }, gbase: { type: "gbase", profile: "gbase", label: "南大通用 GBase", defaultPort: 5258 }, - "gbasedbt-sqli": { type: "gbase", profile: "gbase8s", label: "南大通用 GBase 8s", defaultPort: 9088 }, + "gbasedbt-sqli": { type: "gbase8s", profile: "gbase8s", label: "南大通用 GBase 8s", defaultPort: 9088 }, "informix-sqli": { type: "informix", profile: "informix", label: "Informix", defaultPort: 9088 }, yashandb: { type: "yashandb", profile: "yashandb", label: "YashanDB", defaultPort: 1688 }, opengauss: { type: "gaussdb", profile: "opengauss", label: "openGauss", defaultPort: 5432 }, @@ -599,7 +599,7 @@ function parseJdbcGbase8sUrl(source: string): ParsedConnectionUrl | null { const host = match.groups.host.replace(/^\[/, "").replace(/\]$/, ""); return { - dbType: "gbase", + dbType: "gbase8s", driverProfile: "gbase8s", driverLabel: "南大通用 GBase 8s", host, diff --git a/apps/desktop/src/lib/connection/connectionUrlBuilder.ts b/apps/desktop/src/lib/connection/connectionUrlBuilder.ts index b0ec9cd9f9..1c611964af 100644 --- a/apps/desktop/src/lib/connection/connectionUrlBuilder.ts +++ b/apps/desktop/src/lib/connection/connectionUrlBuilder.ts @@ -301,8 +301,10 @@ function jdbcPrefixFor(config: ConnectionUrlCopyConfig): string | null { return profile === GAUSSDB_M_JDBC_DRIVER_PROFILE ? "jdbc:gaussdb" : "jdbc:postgresql"; case "iris": return profile === "cache" ? "jdbc:Cache" : "jdbc:IRIS"; + case "gbase8s": + return "jdbc:gbasedbt-sqli"; case "gbase": - return profile === "gbase8s" ? "jdbc:gbasedbt-sqli" : null; + return null; case "tdengine": return Number(config.port) === 6030 ? "jdbc:TAOS" : "jdbc:TAOS-RS"; default: @@ -426,6 +428,7 @@ function buildSpecialJdbcUrl(config: ConnectionUrlCopyConfig, database: string, return `jdbc:inceptor2://${formatHostForUrl(host)}${portPart}/${database || "default"}${props ? `;${props}` : ""}${ssl}`; } case "informix": + case "gbase8s": case "gbase": { const prefix = config.db_type === "informix" ? "jdbc:informix-sqli" : jdbcPrefixFor(config); if (!prefix) return config.db_type === "gbase" ? null : undefined; diff --git a/apps/desktop/src/lib/connection/gbaseProfiles.ts b/apps/desktop/src/lib/connection/gbaseProfiles.ts index 295a5ff729..d4d592bb11 100644 --- a/apps/desktop/src/lib/connection/gbaseProfiles.ts +++ b/apps/desktop/src/lib/connection/gbaseProfiles.ts @@ -2,9 +2,9 @@ import type { ConnectionConfig } from "@/types/database"; /** * GBase 8a / 8s / 8c share one picker entry and one version toggle, but they are - * different engines: 8a and 8s are profiles of the `gbase` db type, while 8c is - * PostgreSQL-shaped and keeps its own `gbase8c` db type (the same split as - * OceanBase MySQL/Oracle mode). The toggle therefore switches `db_type` too. + * different engines with different SQL dialects: 8a is the MySQL-based `gbase` type, + * 8s is Informix-derived (`gbase8s`) and 8c is PostgreSQL-shaped (`gbase8c`), the same + * split as OceanBase MySQL/Oracle mode. The toggle therefore switches `db_type` too. */ export const GBASE_PROFILES = ["gbase8a", "gbase8s", "gbase8c"] as const; @@ -16,5 +16,6 @@ export function isGbaseProfile(value: string | undefined | null): value is Gbase export function gbaseProfileFromConfig(config: Pick): GbaseProfile { if (config.db_type === "gbase8c" || config.driver_profile === "gbase8c") return "gbase8c"; - return config.driver_profile === "gbase8s" ? "gbase8s" : "gbase8a"; + if (config.db_type === "gbase8s" || config.driver_profile === "gbase8s") return "gbase8s"; + return "gbase8a"; } diff --git a/apps/desktop/src/lib/database/createDatabaseSql.ts b/apps/desktop/src/lib/database/createDatabaseSql.ts index c2e4afb874..821b5f6c0f 100644 --- a/apps/desktop/src/lib/database/createDatabaseSql.ts +++ b/apps/desktop/src/lib/database/createDatabaseSql.ts @@ -22,8 +22,8 @@ export function supportsCreateDatabaseCharset(databaseType?: DatabaseType, drive // no charset clause in the SQL. The create dialog still offers a locale picker, and the chosen // value is applied by the gbase8s agent opening the CREATE DATABASE session with that DB_LOCALE. // Plain Informix is excluded until InformixAgent learns to honor the locale directive. -export function supportsCreateDatabaseLocale(databaseType?: DatabaseType, driverProfile?: string | null): boolean { - return databaseType === "gbase" && driverProfile === "gbase8s"; +export function supportsCreateDatabaseLocale(databaseType?: DatabaseType): boolean { + return databaseType === "gbase8s"; } export function buildCreateDatabaseSql(options: CreateDatabaseSqlOptions): Promise { diff --git a/apps/desktop/src/lib/database/databaseBrowserActions.ts b/apps/desktop/src/lib/database/databaseBrowserActions.ts index 9bb10a8ce4..3c8cf8418b 100644 --- a/apps/desktop/src/lib/database/databaseBrowserActions.ts +++ b/apps/desktop/src/lib/database/databaseBrowserActions.ts @@ -6,9 +6,7 @@ import { isSqlServerLinkedNode } from "@/lib/database/sqlServerLinkedServers"; import type { ConnectionConfig, TreeNode } from "@/types/database"; export function canDropDatabaseNode(node: TreeNode, connection?: ConnectionConfig): boolean { - return ( - node.type === "database" && !!node.connectionId && !!node.database && !node.catalog && !isSqlServerLinkedNode(node) && !connectionIsEffectivelyReadOnly(connection) && (supportsDatabaseCreation(connection?.db_type) || supportsCreateDatabaseLocale(connection?.db_type, connection?.driver_profile)) - ); + return node.type === "database" && !!node.connectionId && !!node.database && !node.catalog && !isSqlServerLinkedNode(node) && !connectionIsEffectivelyReadOnly(connection) && (supportsDatabaseCreation(connection?.db_type) || supportsCreateDatabaseLocale(connection?.db_type)); } export interface DatabaseBrowserMutation { diff --git a/apps/desktop/src/lib/database/databaseNamespaceCreation.ts b/apps/desktop/src/lib/database/databaseNamespaceCreation.ts index 5b405d35e0..271eda98f6 100644 --- a/apps/desktop/src/lib/database/databaseNamespaceCreation.ts +++ b/apps/desktop/src/lib/database/databaseNamespaceCreation.ts @@ -64,6 +64,8 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = { "oceanbase-oracle": { deferred: "Oracle-mode schemas are users; use a dedicated user workflow" }, questdb: { deferred: "single database model in DBX" }, gbase: { database: "schema" }, + // Informix-derived: no `CREATE SCHEMA ` (a schema is the table owner), but `CREATE DATABASE` works. + gbase8s: { connection: "database" }, // GBase 8c creates databases with an explicit DBCOMPATIBILITY mode that is fixed at // creation time, so the generic PostgreSQL-shaped create path cannot pick a correct mode. gbase8c: { deferred: "database creation needs an explicit DBCOMPATIBILITY mode fixed at creation time" }, @@ -108,13 +110,6 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = { } satisfies Record; function namespaceCreationMatrixEntry(connection: NonNullable): DatabaseNamespaceCreationMatrixEntry { - // GBase 8s shares `db_type: "gbase"` with the MySQL-based GBase 8a but is Informix-derived: - // it has no `CREATE SCHEMA ` (a schema is the table owner) yet does support - // `CREATE DATABASE`. Route it to the Informix-family semantics instead of the shared `gbase` - // entry, which is written for GBase 8a. - if (connection.db_type === "gbase" && connection.driver_profile === "gbase8s") { - return { connection: "database" }; - } return DATABASE_NAMESPACE_CREATION_MATRIX[connection.db_type]; } diff --git a/apps/desktop/src/lib/database/databasePropertyEditing.ts b/apps/desktop/src/lib/database/databasePropertyEditing.ts index ade26ef77a..3427bfab7f 100644 --- a/apps/desktop/src/lib/database/databasePropertyEditing.ts +++ b/apps/desktop/src/lib/database/databasePropertyEditing.ts @@ -65,6 +65,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = { "oceanbase-oracle": { deferred: "Oracle-mode schemas are users; use a dedicated user workflow" }, questdb: { deferred: "single database model in DBX" }, gbase: { deferred: "schema properties need product-specific handling" }, + gbase8s: { deferred: "schema properties need product-specific handling" }, gbase8c: { deferred: "database and schema comment editing not verified for first pass" }, access: { deferred: "file-backed database properties are not edited in-place" }, h2: { deferred: "schema properties need product-specific handling" }, diff --git a/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts b/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts index 628be9c0e8..7e81c23bba 100644 --- a/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts +++ b/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts @@ -78,6 +78,7 @@ const NAVICAT_STYLE_TABLE_DATA_TYPES = new Set([ "questdb", "oceanbase-oracle", "gbase", + "gbase8s", "access", "h2", "snowflake", diff --git a/apps/desktop/src/lib/database/jdbcDialect.ts b/apps/desktop/src/lib/database/jdbcDialect.ts index 71cea7a13d..84084c2b13 100644 --- a/apps/desktop/src/lib/database/jdbcDialect.ts +++ b/apps/desktop/src/lib/database/jdbcDialect.ts @@ -92,7 +92,7 @@ export function jdbcConnectionUsesDriverRowOffset(connection: JdbcDialectConnect export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined { if (!connection) return undefined; - if (connection.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return "informix"; + if (connection.db_type === "gbase8s") return "informix"; if (connection.db_type === "gbase") return "mysql"; // MySQL-protocol connections to Doris/StarRocks (db_type=mysql with a // starrocks/doris driver_profile) must use the Doris/StarRocks SQL dialect so @@ -123,7 +123,7 @@ export function transferDatabaseTypeForConnection(connection?: JdbcDialectConnec // the GBase driver manifest explicitly disables data transfer. Keep that // capability restriction attached to the raw connection type instead of // exposing the mapped MySQL type to transfer callers. - if (connection?.db_type === "gbase") return undefined; + if (connection?.db_type === "gbase" || connection?.db_type === "gbase8s") return undefined; const effective = effectiveDatabaseTypeForConnection(connection); if (effective === "doris" || effective === "starrocks") return connection?.db_type; return effective; @@ -136,7 +136,7 @@ export function connectionUsesConnectionRootSchemaMode(connection?: JdbcDialectC export function connectionShouldLoadIdentifierQuote(connection: JdbcDialectConnection | undefined): boolean { if (!connection) return false; - if (connection.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return true; + if (connection.db_type === "gbase8s") return true; if (connection.db_type === "kingbase") return true; if (connection.db_type === "kyuubi") return true; // Cloud Spanner is dual-dialect: the agent reports a backtick for GoogleSQL and @@ -233,7 +233,7 @@ export function sqlSnippetDatabaseTypeForConnection(connection?: JdbcDialectConn export function tableStructureDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined { if (!connection) return undefined; - if (connection.db_type === "gbase" && !isGbase8sProfile(connection.driver_profile)) return "gbase"; + if (connection.db_type === "gbase") return "gbase"; return effectiveDatabaseTypeForConnection(connection); } @@ -253,7 +253,7 @@ export function connectionUsesDatabaseObjectTreeMode(connection?: JdbcDialectCon export function connectionShouldDiscoverJdbcSchemas(connection?: JdbcDialectConnection): boolean { // GBase 8s exposes owner schemas only when the current database can use them // in DML; non-ANSI databases fall back to the flat table tree. - if (connection?.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return true; + if (connection?.db_type === "gbase8s") return true; return connection?.db_type === "jdbc" && !inferJdbcDialect(connection); } @@ -348,7 +348,7 @@ export function connectionObjectTreeNodeSchema(connection: JdbcDialectConnection /** GBase 8s reports the table owner as a schema, but does not accept it in table DML/DDL names. */ export function connectionTableSqlSchema(connection: JdbcDialectConnection | undefined, schema?: string): string | undefined { - if (connection?.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return undefined; + if (connection?.db_type === "gbase8s") return undefined; return schema; } diff --git a/apps/desktop/src/lib/database/productionSafety.ts b/apps/desktop/src/lib/database/productionSafety.ts index 58fa8400f9..ef1f51285a 100644 --- a/apps/desktop/src/lib/database/productionSafety.ts +++ b/apps/desktop/src/lib/database/productionSafety.ts @@ -61,6 +61,7 @@ const SCHEMA_FIRST_QUALIFIER_TYPES = new Set([ "xugu", "oscar", "gbase", + "gbase8s", "saphana", "sqlserver", "snowflake", diff --git a/apps/desktop/src/lib/database/visibleDatabases.ts b/apps/desktop/src/lib/database/visibleDatabases.ts index ea914482f6..b56c15561f 100644 --- a/apps/desktop/src/lib/database/visibleDatabases.ts +++ b/apps/desktop/src/lib/database/visibleDatabases.ts @@ -238,7 +238,7 @@ export function filterDatabaseNamesForConnection(databaseNames: string[], connec } export function filterDatabaseNamesForVisiblePicker(databaseNames: string[], connection: VisibleDatabaseConnection | undefined): string[] { - if (connection?.db_type === "gbase" && connection.driver_profile === "gbase8s") { + if (connection?.db_type === "gbase8s") { return databaseNames; } return databaseNames.filter((name) => !isSystemDatabaseName(effectiveDatabaseTypeForConnection(connection), name)); diff --git a/apps/desktop/src/lib/sidebar/sidebarDatabaseStorage.ts b/apps/desktop/src/lib/sidebar/sidebarDatabaseStorage.ts index 5541d48146..c2d284c48a 100644 --- a/apps/desktop/src/lib/sidebar/sidebarDatabaseStorage.ts +++ b/apps/desktop/src/lib/sidebar/sidebarDatabaseStorage.ts @@ -8,7 +8,6 @@ export function supportsSidebarDatabaseStorage(connection: ConnectionConfig | un export function supportsSidebarTableStorage(connection: ConnectionConfig | undefined): boolean { if (!connection || !sidebarTableStorageTypes.has(connection.db_type)) return false; - if (connection.db_type === "gbase" && connection.driver_profile === "gbase8s") return false; return connection.db_type !== "postgres" || connection.driver_profile !== "cockroachdb"; } diff --git a/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts index d47a217b32..fb69bb26bc 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.metadataLoading.spec.ts @@ -149,7 +149,7 @@ function gbase8sConnection(): ConnectionConfig { ...informixConnection(), id: "gbase8s-1", name: "GBase 8s", - db_type: "gbase", + db_type: "gbase8s", driver_profile: "gbase8s", database: "dbx_test", } as ConnectionConfig; diff --git a/apps/desktop/src/types/generated/connectionProfiles.ts b/apps/desktop/src/types/generated/connectionProfiles.ts index fbb378348f..7b72c389d3 100644 --- a/apps/desktop/src/types/generated/connectionProfiles.ts +++ b/apps/desktop/src/types/generated/connectionProfiles.ts @@ -64,7 +64,7 @@ export const CONNECTION_PROFILES = { exasol: { type: "exasol", port: 8563, user: "sys", label: "Exasol", icon: "exasol" }, gbase: { type: "gbase", port: 5258, user: "gbasedbt", label: "南大通用 GBase 8a", icon: "gbase" }, gbase8a: { type: "gbase", port: 5258, user: "gbasedbt", label: "南大通用 GBase 8a", icon: "gbase" }, - gbase8s: { type: "gbase", port: 9088, user: "gbasedbt", label: "南大通用 GBase 8s", icon: "gbase" }, + gbase8s: { type: "gbase8s", port: 9088, user: "gbasedbt", label: "南大通用 GBase 8s", icon: "gbase" }, gbase8c: { type: "gbase8c", port: 5432, user: "gbase", label: "南大通用 GBase 8c", icon: "gbase" }, opengauss: { type: "opengauss", port: 5432, user: "gaussdb", label: "openGauss", icon: "opengauss" }, gaussdb: { type: "gaussdb", port: 5432, user: "gaussdb", label: "GaussDB", icon: "gaussdb" }, diff --git a/apps/desktop/src/types/generated/databaseTypes.ts b/apps/desktop/src/types/generated/databaseTypes.ts index 2352b61796..226afcb013 100644 --- a/apps/desktop/src/types/generated/databaseTypes.ts +++ b/apps/desktop/src/types/generated/databaseTypes.ts @@ -47,6 +47,7 @@ export const DATABASE_TYPES = [ "questdb", "oceanbase-oracle", "gbase", + "gbase8s", "gbase8c", "access", "h2", diff --git a/crates/dbx-core/assets/database-drivers.manifest.json b/crates/dbx-core/assets/database-drivers.manifest.json index 1009b5695b..daf60306f9 100644 --- a/crates/dbx-core/assets/database-drivers.manifest.json +++ b/crates/dbx-core/assets/database-drivers.manifest.json @@ -1785,13 +1785,6 @@ "driverStoreVisible": true, "driverStoreOrder": 15, "driverProfiles": [ - { - "profile": "gbase8s", - "agentKey": "gbase8s", - "label": "南大通用 GBase 8s", - "storeVisible": true, - "storeOrder": 16 - }, { "profile": "gbase8a", "agentKey": "gbase8a", @@ -1828,6 +1821,47 @@ "driverManagement": true } }, + { + "dbType": "gbase8s", + "compareModes": [ + "schema", + "data" + ], + "label": "南大通用 GBase 8s", + "dialect": "Informix", + "runtimeMode": "agent", + "mcpMode": "bridge", + "agentKey": "gbase8s", + "driverStoreVisible": true, + "driverStoreOrder": 16, + "singleConnectionPool": false, + "metadataConnectionScoped": false, + "skipTcpProbe": true, + "defaultPort": 9088, + "traits": { + "schemaAware": true, + "treeSchema": true + }, + "supportLevel": "operate", + "capabilities": { + "queryExecution": true, + "metadataBrowse": true, + "objectBrowser": true, + "objectSource": false, + "schemaSearch": true, + "diagram": true, + "tableDataEdit": true, + "tableStructureEdit": true, + "tableImport": false, + "dataTransfer": false, + "sqlFileExecution": true, + "databaseCreate": false, + "fieldLineage": false, + "sqlExplain": false, + "userAdmin": false, + "driverManagement": true + } + }, { "dbType": "gbase8c", "compareModes": [ diff --git a/crates/dbx-core/src/connection/mod.rs b/crates/dbx-core/src/connection/mod.rs index 33d31bf7ee..cdbe47e59c 100644 --- a/crates/dbx-core/src/connection/mod.rs +++ b/crates/dbx-core/src/connection/mod.rs @@ -332,6 +332,7 @@ macro_rules! agent_connection_pool_database_type { | DatabaseType::Exasol | DatabaseType::OceanbaseOracle | DatabaseType::Gbase + | DatabaseType::Gbase8s | DatabaseType::Oracle | DatabaseType::H2 | DatabaseType::Snowflake diff --git a/crates/dbx-core/src/data/table_export.rs b/crates/dbx-core/src/data/table_export.rs index 5daf0dbc96..ccffb4ec06 100644 --- a/crates/dbx-core/src/data/table_export.rs +++ b/crates/dbx-core/src/data/table_export.rs @@ -373,16 +373,10 @@ struct TableExportSqlContext<'a> { schema: Option<&'a str>, } -fn table_export_sql_context<'a>( - database_type: DatabaseType, - driver_profile: Option<&str>, - schema: Option<&'a str>, -) -> TableExportSqlContext<'a> { +fn table_export_sql_context<'a>(database_type: DatabaseType, schema: Option<&'a str>) -> TableExportSqlContext<'a> { // GBase8s selects from the active database and rejects the owner-qualified - // names produced for MySQL-family GBase connections. - if database_type == DatabaseType::Gbase - && driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case("gbase8s")) - { + // names produced for other connection types. + if database_type == DatabaseType::Gbase8s { TableExportSqlContext { database_type: DatabaseType::Informix, schema: None } } else { TableExportSqlContext { database_type, schema } @@ -1612,15 +1606,15 @@ async fn export_table_data_core_inner( cancelled: Arc, cancel_token: CancellationToken, ) -> Result<(), String> { - // 1. Get database type and the profile that can refine its SQL dialect. - let (db_type, driver_profile) = { + // 1. Get database type + let db_type = { let configs = state.configs.read().await; let config = configs .get(&request.connection_id) .ok_or_else(|| format!("Connection config not found: {}", request.connection_id))?; - (config.db_type, config.driver_profile.clone()) + config.db_type }; - let sql_context = table_export_sql_context(db_type, driver_profile.as_deref(), request.schema.as_deref()); + let sql_context = table_export_sql_context(db_type, request.schema.as_deref()); // 2. Get pool let client_session_id = table_export_client_session_id(&request.export_id); @@ -3059,7 +3053,7 @@ mod tests { split_max_mb: None, omit_database_qualifier: None, }; - let context = table_export_sql_context(DatabaseType::Iotdb, None, request.schema.as_deref()); + let context = table_export_sql_context(DatabaseType::Iotdb, request.schema.as_deref()); let columns = vec!["Time".to_string(), "root.test.device2.temperature".to_string()]; let query_columns = table_export_query_columns(&request, &context, &columns).unwrap(); assert_eq!(query_columns.as_ref(), &["temperature".to_string()]); @@ -3127,7 +3121,7 @@ mod tests { split_max_mb: None, omit_database_qualifier: None, }; - let context = table_export_sql_context(DatabaseType::Iotdb, None, request.schema.as_deref()); + let context = table_export_sql_context(DatabaseType::Iotdb, request.schema.as_deref()); let columns = vec!["tImE".to_string(), "temperature".to_string()]; let query_columns = table_export_query_columns(&request, &context, &columns).unwrap(); @@ -3171,7 +3165,7 @@ mod tests { split_max_mb: None, omit_database_qualifier: None, }; - let context = table_export_sql_context(DatabaseType::Iotdb, None, request.schema.as_deref()); + let context = table_export_sql_context(DatabaseType::Iotdb, request.schema.as_deref()); let error = table_export_query_columns(&request, &context, &["TIME".to_string()]).unwrap_err(); assert_eq!(error, "IoTDB table export requires at least one non-Time column"); } @@ -3210,7 +3204,7 @@ mod tests { split_max_mb: None, omit_database_qualifier: None, }; - let context = table_export_sql_context(DatabaseType::Iotdb, None, request.schema.as_deref()); + let context = table_export_sql_context(DatabaseType::Iotdb, request.schema.as_deref()); let columns = vec![ "Time".to_string(), "root.test.device2.temperature".to_string(), @@ -3271,7 +3265,7 @@ mod tests { "SELECT \"Time\", \"value\" FROM \"samples\" LIMIT 25 OFFSET 10", ), ] { - let context = table_export_sql_context(database_type, None, None); + let context = table_export_sql_context(database_type, None); let query_columns = table_export_query_columns(&request, &context, &columns).unwrap(); assert!(matches!(query_columns, Cow::Borrowed(_))); assert_eq!(table_cursor_sql(&request, &context, query_columns.as_ref(), &[], &[]), cursor_sql); @@ -3316,7 +3310,7 @@ mod tests { split_max_mb: None, omit_database_qualifier: None, }; - let context = table_export_sql_context(DatabaseType::Oracle, None, request.schema.as_deref()); + let context = table_export_sql_context(DatabaseType::Oracle, request.schema.as_deref()); let sql = table_cursor_sql( &request, @@ -3371,7 +3365,7 @@ mod tests { }; let columns = vec!["id".to_string(), "payload".to_string()]; let primary_keys = vec!["id".to_string()]; - let context = table_export_sql_context(DatabaseType::Gbase, Some("gbase8s"), request.schema.as_deref()); + let context = table_export_sql_context(DatabaseType::Gbase8s, request.schema.as_deref()); assert_eq!(context.database_type, DatabaseType::Informix); assert_eq!(context.schema, None); @@ -3399,7 +3393,7 @@ mod tests { "SELECT COUNT(*) FROM orders" ); - let regular_gbase = table_export_sql_context(DatabaseType::Gbase, Some("gbase8a"), request.schema.as_deref()); + let regular_gbase = table_export_sql_context(DatabaseType::Gbase, request.schema.as_deref()); assert_eq!(regular_gbase.database_type, DatabaseType::Gbase); assert_eq!(regular_gbase.schema, Some("gbasedbt")); assert_eq!( @@ -3407,7 +3401,7 @@ mod tests { "SELECT \"id\", \"payload\" FROM \"gbasedbt\".\"orders\" ORDER BY \"id\" LIMIT 50 OFFSET 100" ); - let informix = table_export_sql_context(DatabaseType::Informix, None, request.schema.as_deref()); + let informix = table_export_sql_context(DatabaseType::Informix, request.schema.as_deref()); assert_eq!(informix.database_type, DatabaseType::Informix); assert_eq!(informix.schema, Some("gbasedbt")); assert_eq!( @@ -3452,7 +3446,7 @@ mod tests { }; let columns = vec!["id".to_string(), "DisplayName".to_string()]; let primary_keys = vec!["id".to_string()]; - let context = table_export_sql_context(DatabaseType::Gaussdb, None, request.schema.as_deref()); + let context = table_export_sql_context(DatabaseType::Gaussdb, request.schema.as_deref()); assert_eq!( table_cursor_sql(&request, &context, &columns, &[], &primary_keys), @@ -3516,7 +3510,7 @@ mod tests { let columns = vec!["id".to_string(), "geom".to_string(), "name".to_string()]; let column_types = vec![Some("int".to_string()), Some("geometry".to_string()), Some("varchar".to_string())]; let primary_keys = vec!["id".to_string()]; - let context = table_export_sql_context(DatabaseType::Mysql, None, None); + let context = table_export_sql_context(DatabaseType::Mysql, None); let cursor_sql = table_cursor_sql(&request, &context, &columns, &column_types, &primary_keys); assert!(cursor_sql.contains("ST_SRID(`geom`), ':', HEX(ST_AsWKB(`geom`))")); @@ -3578,7 +3572,7 @@ mod tests { split_max_mb: None, omit_database_qualifier: None, }; - let context = table_export_sql_context(DatabaseType::Oracle, None, request.schema.as_deref()); + let context = table_export_sql_context(DatabaseType::Oracle, request.schema.as_deref()); let sql = table_cursor_sql(&request, &context, &columns, &[], &primary_keys); assert_eq!(sql, "SELECT \"ID\", \"NAME\" FROM \"APP\".\"USERS\""); diff --git a/crates/dbx-core/src/safety/production_safety.rs b/crates/dbx-core/src/safety/production_safety.rs index 689d1b38de..c59c7d412b 100644 --- a/crates/dbx-core/src/safety/production_safety.rs +++ b/crates/dbx-core/src/safety/production_safety.rs @@ -534,6 +534,7 @@ fn schema_first_qualifier_type(db_type: &DatabaseType) -> bool { | DatabaseType::Xugu | DatabaseType::Oscar | DatabaseType::Gbase + | DatabaseType::Gbase8s | DatabaseType::SapHana | DatabaseType::SqlServer | DatabaseType::Snowflake diff --git a/crates/dbx-core/src/schema/mod.rs b/crates/dbx-core/src/schema/mod.rs index 8cd9d357c0..4555e6be0a 100644 --- a/crates/dbx-core/src/schema/mod.rs +++ b/crates/dbx-core/src/schema/mod.rs @@ -7298,9 +7298,7 @@ async fn list_object_statistics_once( ) .await; } - if db_config.as_ref().is_some_and(|config| { - config.db_type == DatabaseType::Gbase && config.driver_profile.as_deref() != Some("gbase8s") - }) { + if db_config.as_ref().is_some_and(|config| config.db_type == DatabaseType::Gbase) { let sql = gbase8a_object_statistics_sql(database); return agent_list_object_statistics( client, diff --git a/crates/dbx-core/tests/database_capabilities.rs b/crates/dbx-core/tests/database_capabilities.rs index 6ee43a7f66..27a7269777 100644 --- a/crates/dbx-core/tests/database_capabilities.rs +++ b/crates/dbx-core/tests/database_capabilities.rs @@ -113,6 +113,7 @@ fn maps_agent_database_types_to_driver_keys() { assert_eq!(agent_key(&DatabaseType::Exasol, None), Some("exasol")); assert_eq!(agent_key(&DatabaseType::OceanbaseOracle, None), Some("oceanbase-oracle")); assert_eq!(agent_key(&DatabaseType::Gbase, None), Some("gbase8a")); + assert_eq!(agent_key(&DatabaseType::Gbase8s, None), Some("gbase8s")); assert_eq!(agent_key(&DatabaseType::Access, None), Some("access")); assert_eq!(agent_key(&DatabaseType::Oracle, None), Some("oracle")); assert_eq!(agent_key(&DatabaseType::Databend, None), Some("databend")); diff --git a/crates/dbx-driver-agent/src/agent_manager.rs b/crates/dbx-driver-agent/src/agent_manager.rs index 5848b189f0..99e360549c 100644 --- a/crates/dbx-driver-agent/src/agent_manager.rs +++ b/crates/dbx-driver-agent/src/agent_manager.rs @@ -551,7 +551,7 @@ mod tests { assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Oracle, Some("oracle-10g")), Some("oracle")); assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Oracle, Some("oracle-legacy")), Some("oracle")); assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Oracle, None), Some("oracle")); - assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Gbase, Some("gbase8s")), Some("gbase8s")); + assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Gbase8s, None), Some("gbase8s")); assert_eq!(AgentManager::db_type_to_agent_key(&DatabaseType::Gbase, None), Some("gbase8a")); manager.stop_daemon_by_key("oracle").await; manager.stop_daemon_by_key("gbase8s").await; diff --git a/crates/dbx-sql-data/src/data_grid_sql.rs b/crates/dbx-sql-data/src/data_grid_sql.rs index 7d49596039..8fc5921302 100644 --- a/crates/dbx-sql-data/src/data_grid_sql.rs +++ b/crates/dbx-sql-data/src/data_grid_sql.rs @@ -4803,6 +4803,7 @@ fn uses_keyless_row_predicate(database_type: Option) -> bool { | DatabaseType::Questdb | DatabaseType::OceanbaseOracle | DatabaseType::Gbase + | DatabaseType::Gbase8s | DatabaseType::Access | DatabaseType::H2 | DatabaseType::Snowflake 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 d6cf3a0434..41da857d6f 100644 --- a/crates/dbx-sql-data/src/database_structure_search_sql.rs +++ b/crates/dbx-sql-data/src/database_structure_search_sql.rs @@ -82,9 +82,9 @@ pub fn build_database_structure_search_sql( tdengine_structure_search_sql(&options.database, &tdengine_like_pattern_literal(&term), limit) } DatabaseType::Snowflake => snowflake_structure_search_sql(options.schema.as_deref(), &pattern_literal, limit), - // gbase8s is Informix-compatible (see the table_structure_sql dialects) - // and has no information_schema; only MySQL-dialect Gbase 8a takes this path. - DatabaseType::Gbase if !driver_profile_is_gbase8s(options.driver_profile.as_deref()) => { + // GBase 8s is its own `Gbase8s` type (Informix-compatible, no information_schema) + // and falls through to the data search; only MySQL-dialect Gbase 8a takes this path. + DatabaseType::Gbase => { gbase_structure_search_sql(&options.database, options.schema.as_deref(), &pattern_literal, limit) } DatabaseType::Jdbc if driver_profile_is_mysql(options.driver_profile.as_deref()) => { @@ -117,10 +117,6 @@ fn driver_profile_is_postgres(profile: Option<&str>) -> bool { profile.contains("postgres") || profile.contains("kingbase") || profile.contains("opengauss") } -fn driver_profile_is_gbase8s(profile: Option<&str>) -> bool { - profile.map(str::trim).is_some_and(|profile| profile.to_ascii_lowercase().contains("gbase8s")) -} - fn driver_profile_is_oracle(profile: Option<&str>) -> bool { let profile = profile.map(str::trim).unwrap_or("").to_ascii_lowercase(); profile.contains("oracle") || profile.contains("dameng") || profile.contains("dm") @@ -596,15 +592,15 @@ mod tests { assert_eq!( build_database_structure_search_sql(DatabaseStructureSearchSqlOptions { - database_type: Some(DatabaseType::Gbase), - driver_profile: Some("gbase8s".to_string()), + database_type: Some(DatabaseType::Gbase8s), + driver_profile: None, database: "app".to_string(), schema: None, term: "email".to_string(), limit: Some(10), }), None, - "gbase8s is Informix-compatible and must fall back to data search" + "GBase 8s is Informix-compatible and must fall back to data search" ); } diff --git a/crates/dbx-sql-dialect/src/sql_dialect/capabilities.rs b/crates/dbx-sql-dialect/src/sql_dialect/capabilities.rs index bc8b770eea..29b3f259d2 100644 --- a/crates/dbx-sql-dialect/src/sql_dialect/capabilities.rs +++ b/crates/dbx-sql-dialect/src/sql_dialect/capabilities.rs @@ -47,6 +47,7 @@ pub fn is_schema_aware(database_type: DatabaseType) -> bool { | DatabaseType::OpenGauss | DatabaseType::OceanbaseOracle | DatabaseType::Gbase + | DatabaseType::Gbase8s | DatabaseType::Databend | DatabaseType::Jdbc | DatabaseType::H2 @@ -134,7 +135,7 @@ pub fn pagination_strategy(database_type: Option, context: Paginat Some(DatabaseType::Db2) => TablePaginationStrategy::Db2FetchFirst, Some(DatabaseType::SqlServer) => TablePaginationStrategy::SqlServerTop, Some(DatabaseType::Iris) => TablePaginationStrategy::IrisTop, - Some(DatabaseType::Informix) => TablePaginationStrategy::InformixFirst, + Some(DatabaseType::Informix | DatabaseType::Gbase8s) => TablePaginationStrategy::InformixFirst, Some(DatabaseType::Firebird) => TablePaginationStrategy::FirebirdRows, Some(DatabaseType::OceanbaseOracle) => TablePaginationStrategy::Rownum, Some(DatabaseType::Questdb) => TablePaginationStrategy::QuestDbLimit, 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 3cfa7ad8ae..7bf77c9401 100644 --- a/crates/dbx-sql-dialect/src/sql_dialect/ddl_profile.rs +++ b/crates/dbx-sql-dialect/src/sql_dialect/ddl_profile.rs @@ -898,7 +898,7 @@ pub fn profile_for(db_type: DatabaseType) -> DdlDialectProfile { ClickHouse => clickhouse_profile(), // Engines whose DROP TABLE grammar has no `IF EXISTS`. - Db2 | Informix | SapHana | Teradata => { + Db2 | Informix | Gbase8s | SapHana | Teradata => { let mut profile = conservative_ansi(db_type); profile.drop_table_supports_if_exists = false; profile diff --git a/crates/dbx-sql-dialect/src/sql_dialect/identifiers.rs b/crates/dbx-sql-dialect/src/sql_dialect/identifiers.rs index efe14c1c2e..4c2035b4fd 100644 --- a/crates/dbx-sql-dialect/src/sql_dialect/identifiers.rs +++ b/crates/dbx-sql-dialect/src/sql_dialect/identifiers.rs @@ -189,7 +189,7 @@ pub fn quote_table_identifier(database_type: Option, name: &str) - Some(db_type) if uses_backtick_identifier_quoting(Some(db_type)) => { format!("`{}`", name.replace('`', "``")) } - Some(DatabaseType::Informix) if is_simple_informix_identifier(name) => name.to_string(), + Some(DatabaseType::Informix | DatabaseType::Gbase8s) if is_simple_informix_identifier(name) => name.to_string(), // SUNDB folds unquoted identifiers to uppercase (its JDBC driver reports // storesUpperCaseIdentifiers=true / supportsMixedCaseIdentifiers=false), so // `select * from rs_salest_budget` resolves to RS_SALEST_BUDGET. Quoting the diff --git a/crates/dbx-sql-dialect/src/sql_dialect/table_select.rs b/crates/dbx-sql-dialect/src/sql_dialect/table_select.rs index 4da94c5f30..16939cf223 100644 --- a/crates/dbx-sql-dialect/src/sql_dialect/table_select.rs +++ b/crates/dbx-sql-dialect/src/sql_dialect/table_select.rs @@ -201,8 +201,11 @@ pub fn table_data_schema<'a>( driver_profile: Option<&str>, schema: Option<&'a str>, ) -> Option<&'a str> { - if database_type == Some(DatabaseType::Informix) - && driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case("gbase8s")) + // The frontend reports GBase 8s through the Informix dialect (see `connectionSqlDialect`), so + // the owner-free rule applies to both the dialect-mapped and the native `gbase8s` type. + if database_type == Some(DatabaseType::Gbase8s) + || (database_type == Some(DatabaseType::Informix) + && driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case("gbase8s"))) { None } else { @@ -599,7 +602,7 @@ pub fn uses_connection_identifier_quote(database_type: Option, ide // Kyuubi normally uses Hive-family backticks, but a Trino-backed // session reports the ANSI double quote through connection info. || (database_type == Some(DatabaseType::Kyuubi) && identifier_quote.is_some()) - || (database_type == Some(DatabaseType::Informix) && identifier_quote.is_some()) + || (matches!(database_type, Some(DatabaseType::Informix | DatabaseType::Gbase8s)) && identifier_quote.is_some()) || (matches!(database_type, Some(DatabaseType::Gaussdb | DatabaseType::OpenGauss | DatabaseType::Postgres)) && identifier_quote.is_some()) } diff --git a/crates/dbx-sql-schema/src/db_admin_sql.rs b/crates/dbx-sql-schema/src/db_admin_sql.rs index 2e35d2ff48..a16289d07e 100644 --- a/crates/dbx-sql-schema/src/db_admin_sql.rs +++ b/crates/dbx-sql-schema/src/db_admin_sql.rs @@ -298,12 +298,12 @@ fn build_create_database_statement(options: &CreateDatabaseSqlOptions) -> Result quote_table_identifier(options.database_type, &options.name) )); } - if is_informix_family(options.database_type, options.driver_profile.as_deref()) { + if is_informix_family(options.database_type) { // Informix / GBase 8s accept only a bare `CREATE DATABASE `. The new database // inherits the instance default locale, and the MySQL `CHARACTER SET`/`COLLATE` // clauses are invalid syntax here. Database names are ordinary identifiers, so quote // them with the Informix rule (unquoted for simple identifiers) rather than the - // default double-quote path that `DatabaseType::Gbase` would otherwise take. + // default double-quote path that other types would otherwise take. let name = quote_table_identifier(Some(DatabaseType::Informix), &options.name); let locale = clean_sql_option(options.charset.as_deref()); if locale.is_empty() { @@ -325,23 +325,18 @@ fn build_create_database_statement(options: &CreateDatabaseSqlOptions) -> Result Ok(format!("CREATE DATABASE {name} CHARACTER SET {charset}{collate_clause};")) } -/// Whether the connection belongs to the Informix family: standalone Informix, or GBase 8s -/// (which shares `DatabaseType::Gbase` with the MySQL-based GBase 8a and is only distinguishable -/// through the `gbase8s` driver profile). Informix-family servers accept `CREATE DATABASE` but -/// have no `CREATE SCHEMA ` statement (a "schema" is the table owner), so the create -/// targets differ from the rest of the `Gbase` family. -fn is_informix_family(database_type: Option, driver_profile: Option<&str>) -> bool { - match database_type { - Some(DatabaseType::Informix) => true, - Some(DatabaseType::Gbase) => driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case("gbase8s")), - _ => false, - } +/// Whether the connection belongs to the Informix family: standalone Informix, or GBase 8s. +/// Informix-family servers accept `CREATE DATABASE` but have no `CREATE SCHEMA ` +/// statement (a "schema" is the table owner), so the create targets differ from the MySQL-based +/// GBase 8a (`DatabaseType::Gbase`). +fn is_informix_family(database_type: Option) -> bool { + matches!(database_type, Some(DatabaseType::Informix | DatabaseType::Gbase8s)) } pub fn supports_create_database_target(database_type: Option, driver_profile: Option<&str>) -> bool { // Informix / GBase 8s create namespaces with `CREATE DATABASE`, so they are valid targets // even though they are absent from the explicit list below. - if is_informix_family(database_type, driver_profile) { + if is_informix_family(database_type) { return true; } matches!( @@ -1559,12 +1554,11 @@ mod tests { #[test] fn builds_informix_family_create_database_without_mysql_clause() { - // GBase 8s shares DatabaseType::Gbase with GBase 8a and is only identified by the - // gbase8s driver profile; with no locale chosen it emits a bare, unquoted CREATE DATABASE. + // With no locale chosen, GBase 8s emits a bare, unquoted CREATE DATABASE. assert_eq!( build_create_database_sql(CreateDatabaseSqlOptions { - database_type: Some(DatabaseType::Gbase), - driver_profile: Some("gbase8s".to_string()), + database_type: Some(DatabaseType::Gbase8s), + driver_profile: None, target: None, parent: None, name: "app_db".to_string(), @@ -1596,8 +1590,8 @@ mod tests { // because Informix cannot express a codeset in CREATE DATABASE. assert_eq!( build_create_database_sql(CreateDatabaseSqlOptions { - database_type: Some(DatabaseType::Gbase), - driver_profile: Some("gbase8s".to_string()), + database_type: Some(DatabaseType::Gbase8s), + driver_profile: None, target: None, parent: None, name: "app_db".to_string(), @@ -1637,8 +1631,8 @@ mod tests { } #[test] - fn create_database_target_distinguishes_gbase_profiles() { - assert!(supports_create_database_target(Some(DatabaseType::Gbase), Some("gbase8s"))); + fn create_database_target_distinguishes_gbase_types() { + assert!(supports_create_database_target(Some(DatabaseType::Gbase8s), None)); assert!(!supports_create_database_target(Some(DatabaseType::Gbase), Some("gbase8a"))); assert!(!supports_create_database_target(Some(DatabaseType::Gbase), None)); assert!(supports_create_database_target(Some(DatabaseType::Informix), None)); diff --git a/crates/dbx-sql-schema/src/table_structure_sql/dialect.rs b/crates/dbx-sql-schema/src/table_structure_sql/dialect.rs index 9d6627cdbc..0b7accfa67 100644 --- a/crates/dbx-sql-schema/src/table_structure_sql/dialect.rs +++ b/crates/dbx-sql-schema/src/table_structure_sql/dialect.rs @@ -147,11 +147,9 @@ pub(super) fn capabilities_for( comment: true, ..base }, - // GBase 8s is Informix-compatible, unlike the rest of the `Gbase` - // family (GBase 8a and friends) which speak MySQL wire protocol and - // SQL dialect. `driver_profile` is the only signal that distinguishes - // them (see the same check in `table_export.rs` / `table_select.rs`). - Some(DatabaseType::Gbase) if driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case("gbase8s")) => { + // GBase 8s is Informix-compatible, unlike GBase 8a (`Gbase`), which speaks + // the MySQL wire protocol and SQL dialect. + Some(DatabaseType::Gbase8s) => { TableStructureCapabilities { dialect: StructureDialect::Informix, add_column: true, diff --git a/crates/dbx-sql-schema/src/table_structure_sql/tests.rs b/crates/dbx-sql-schema/src/table_structure_sql/tests.rs index 518694bc68..abe452265a 100644 --- a/crates/dbx-sql-schema/src/table_structure_sql/tests.rs +++ b/crates/dbx-sql-schema/src/table_structure_sql/tests.rs @@ -2809,8 +2809,8 @@ fn gbase8a_allows_mysql_style_column_reorder() { #[test] fn gbase8s_uses_informix_ddl_not_mysql() { let result = build_create_table_sql(TableStructureSqlOptions { - database_type: Some(DatabaseType::Gbase), - driver_profile: Some("gbase8s".to_string()), + database_type: Some(DatabaseType::Gbase8s), + driver_profile: None, schema: Some("gbasedbt".to_string()), table_name: "issue8003_repro".to_string(), columns: vec![ diff --git a/crates/dbx-types/src/models/connection.rs b/crates/dbx-types/src/models/connection.rs index 8cec3aa576..125a6c0f5c 100644 --- a/crates/dbx-types/src/models/connection.rs +++ b/crates/dbx-types/src/models/connection.rs @@ -887,6 +887,7 @@ impl<'de> Deserialize<'de> for ConnectionConfig { { let mut value = Value::deserialize(deserializer)?; migrate_legacy_transport_layers(&mut value); + migrate_legacy_gbase8s_profile(&mut value); let data = ConnectionConfigData::deserialize(value).map_err(serde::de::Error::custom)?; if let Some(grouping) = &data.redis_key_grouping { let mut ids = std::collections::HashSet::new(); @@ -909,6 +910,20 @@ impl<'de> Deserialize<'de> for ConnectionConfig { } } +/// GBase 8s used to be the `gbase8s` driver profile of the MySQL-based `gbase` type. It is its own +/// `gbase8s` type now (Informix dialect), so saved and in-flight configs are rewritten on load. +fn migrate_legacy_gbase8s_profile(value: &mut Value) { + let Some(object) = value.as_object_mut() else { + return; + }; + let is_gbase = object.get("db_type").and_then(Value::as_str) == Some("gbase"); + let is_8s_profile = + object.get("driver_profile").and_then(Value::as_str).is_some_and(|p| p.eq_ignore_ascii_case("gbase8s")); + if is_gbase && is_8s_profile { + object.insert("db_type".to_string(), Value::String("gbase8s".to_string())); + } +} + fn migrate_legacy_transport_layers(value: &mut Value) { let Some(object) = value.as_object_mut() else { return; @@ -1324,6 +1339,7 @@ impl ConnectionConfig { DatabaseType::Ignite => format!("ignite://{host}:{port}{db_part}"), DatabaseType::Ignite3 => format!("ignite3://{host}:{port}{db_part}"), DatabaseType::Gbase => format!("gbase://{host}:{port}{db_part}"), + DatabaseType::Gbase8s => format!("gbase8s://{host}:{port}{db_part}"), DatabaseType::H2 => format!("h2://{host}:{port}{db_part}"), DatabaseType::Snowflake => format!("snowflake://{host}/{db_part}"), DatabaseType::Trino => format!("trino://{host}:{port}{db_part}"), @@ -1555,6 +1571,9 @@ impl ConnectionConfig { DatabaseType::Gbase => { format!("gbase://{}:{}@{host}:{port}{db_part}", username, password) } + DatabaseType::Gbase8s => { + format!("gbase8s://{}:{}@{host}:{port}{db_part}", username, password) + } DatabaseType::H2 => { format!("h2://{}:{}@{host}:{port}{db_part}", username, password) } @@ -3131,6 +3150,33 @@ mod tests { assert_eq!(database_info_from_protocol_value(&serde_json::json!({ "ok": true })), None); } + #[test] + fn legacy_gbase8s_profile_connections_load_as_the_gbase8s_type() { + let config = |db_type: &str, driver_profile: Option<&str>| { + let mut value = serde_json::json!({ + "id": "id", + "name": "GBase", + "db_type": db_type, + "host": "localhost", + "port": 9088, + "username": "gbasedbt", + "password": "", + "database": "app" + }); + if let Some(profile) = driver_profile { + value["driver_profile"] = serde_json::json!(profile); + } + serde_json::from_value::(value).unwrap() + }; + + assert_eq!(config("gbase", Some("gbase8s")).db_type, DatabaseType::Gbase8s); + assert_eq!(config("gbase", Some("GBASE8S")).db_type, DatabaseType::Gbase8s); + assert_eq!(config("gbase8s", Some("gbase8s")).db_type, DatabaseType::Gbase8s); + assert_eq!(config("gbase", Some("gbase8a")).db_type, DatabaseType::Gbase); + assert_eq!(config("gbase", None).db_type, DatabaseType::Gbase); + assert_eq!(config("informix", Some("gbase8s")).db_type, DatabaseType::Informix); + } + #[test] fn default_schema_is_optional_and_round_trips() { let base = serde_json::json!({ diff --git a/docs/data/databaseSupport.ts b/docs/data/databaseSupport.ts index dd62da3ff8..bbff8fb85e 100644 --- a/docs/data/databaseSupport.ts +++ b/docs/data/databaseSupport.ts @@ -95,6 +95,7 @@ export const databaseSupport: DatabaseSupportItem[] = [ { 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: "gbase8s", name: "GBase 8s", 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/packages/app-tests/agentDriverInstallHint.test.ts b/packages/app-tests/agentDriverInstallHint.test.ts index 5727dd722f..fed35ea67e 100644 --- a/packages/app-tests/agentDriverInstallHint.test.ts +++ b/packages/app-tests/agentDriverInstallHint.test.ts @@ -51,13 +51,14 @@ test("maps GBase profiles to their matching agent driver keys", () => { assert.equal(agentDriverInstallKey("gbase"), "gbase8a"); assert.equal(agentDriverInstallKey("gbase", "gbase"), "gbase8a"); assert.equal(agentDriverInstallKey("gbase", "gbase8a"), "gbase8a"); - assert.equal(agentDriverInstallKey("gbase", "gbase8s"), "gbase8s"); + assert.equal(agentDriverInstallKey("gbase8s"), "gbase8s"); + assert.equal(agentDriverInstallKey("gbase8s", "gbase8s"), "gbase8s"); }); test("uses the selected GBase 8s agent for install hints", () => { assert.equal( showAgentDriverInstallHint( - "gbase", + "gbase8s", [ { db_type: "gbase", installed: true }, { db_type: "gbase8s", installed: false }, @@ -68,7 +69,7 @@ test("uses the selected GBase 8s agent for install hints", () => { ); assert.equal( showAgentDriverInstallHint( - "gbase", + "gbase8s", [ { db_type: "gbase", installed: false }, { db_type: "gbase8s", installed: true }, @@ -89,7 +90,7 @@ test("uses unified Oracle and selected profile keys for update hints", () => { assert.equal(hasAgentDriverUpdate("oracle", [{ db_type: "oracle", installed: true, update_available: true }], "oracle-10g"), true); assert.equal( hasAgentDriverUpdate( - "gbase", + "gbase8s", [ { db_type: "gbase", installed: true, update_available: false }, { db_type: "gbase8s", installed: true, update_available: true }, diff --git a/packages/app-tests/connectionUrl.test.ts b/packages/app-tests/connectionUrl.test.ts index 19da035bf7..5db61c33ad 100644 --- a/packages/app-tests/connectionUrl.test.ts +++ b/packages/app-tests/connectionUrl.test.ts @@ -416,7 +416,7 @@ test("parses Apache IoTDB JDBC URLs", () => { test("parses GBase 8s JDBC URLs", () => { const parsed = parseConnectionUrl("jdbc:gbasedbt-sqli://gbasedbt:secret@gbase.example.com:20013/testdb:GBASEDBTSERVER=gbase01;CLIENT_LOCALE=zh_cn.utf8"); - assert.equal(parsed.dbType, "gbase"); + assert.equal(parsed.dbType, "gbase8s"); assert.equal(parsed.driverProfile, "gbase8s"); assert.equal(parsed.driverLabel, "南大通用 GBase 8s"); assert.equal(parsed.host, "gbase.example.com"); diff --git a/plugins/connection-types/gbase.yaml b/plugins/connection-types/gbase.yaml index 9fd8958637..157c4d7903 100644 --- a/plugins/connection-types/gbase.yaml +++ b/plugins/connection-types/gbase.yaml @@ -12,11 +12,6 @@ agentKey: gbase8a driverStoreVisible: true driverStoreOrder: 15 driverProfiles: - - profile: gbase8s - agentKey: gbase8s - label: 南大通用 GBase 8s - storeVisible: true - storeOrder: 16 - profile: gbase8a agentKey: gbase8a label: 南大通用 GBase 8a diff --git a/plugins/connection-types/gbase8s.yaml b/plugins/connection-types/gbase8s.yaml new file mode 100644 index 0000000000..1a50039bdf --- /dev/null +++ b/plugins/connection-types/gbase8s.yaml @@ -0,0 +1,37 @@ +schemaVersion: 1 +order: 452 +dbType: gbase8s +compareModes: [schema, data] +rustVariant: Gbase8s +label: 南大通用 GBase 8s +dialect: Informix +runtimeMode: agent +mcpMode: bridge +agentKey: gbase8s +driverStoreVisible: true +driverStoreOrder: 16 +singleConnectionPool: false +metadataConnectionScoped: false +skipTcpProbe: true +defaultPort: 9088 +traits: + schemaAware: true + treeSchema: true +supportLevel: operate +capabilities: + queryExecution: true + metadataBrowse: true + objectBrowser: true + objectSource: false + schemaSearch: true + diagram: true + tableDataEdit: true + tableStructureEdit: true + 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 2d3a4443ee..bcf9a9a15f 100644 --- a/plugins/connection-types/profiles/catalog.yaml +++ b/plugins/connection-types/profiles/catalog.yaml @@ -312,7 +312,7 @@ profiles: port: 5258 user: gbasedbt - id: gbase8s - dbType: gbase + dbType: gbase8s label: 南大通用 GBase 8s icon: gbase port: 9088