From cbc7f9b0db019db5897a545a0f1668bd250f4ba0 Mon Sep 17 00:00:00 2001
From: t8y2 <1156263951@qq.com>
Date: Sun, 14 Jun 2026 10:16:24 +0800
Subject: [PATCH] fix(gbase): support 8a and 8s profiles
---
.../connection/ConnectionDialog.vue | 17 ++-
.../src/components/icons/DatabaseIcon.vue | 1 +
.../src/components/layout/ContentArea.vue | 5 +
.../src/components/objects/ObjectBrowser.vue | 5 +-
.../src/components/sidebar/ConnectionTree.vue | 6 +-
.../src/components/sidebar/TreeItem.vue | 48 +++++--
.../structure/TableStructureEditor.vue | 4 +-
apps/desktop/src/i18n/locales/en.ts | 1 +
apps/desktop/src/i18n/locales/zh-CN.ts | 1 +
.../desktop/src/lib/databaseCapabilitySets.ts | 2 +
apps/desktop/src/lib/jdbcDialect.ts | 26 +++-
.../src/lib/tableStructureCapabilities.ts | 28 +++-
.../src/lib/tableStructureEditorState.ts | 31 ++++
apps/desktop/src/stores/connectionStore.ts | 6 +-
apps/desktop/src/stores/queryStore.ts | 21 ++-
apps/desktop/src/types/database.ts | 1 +
.../assets/database-drivers.manifest.json | 11 +-
crates/dbx-core/src/agent_catalog.rs | 10 +-
crates/dbx-core/src/agent_connection.rs | 2 +
crates/dbx-core/src/cloud_sync.rs | 1 +
crates/dbx-core/src/connection.rs | 1 +
crates/dbx-core/src/connection_secrets.rs | 1 +
crates/dbx-core/src/db/redis_driver.rs | 1 +
crates/dbx-core/src/models/connection.rs | 6 +
crates/dbx-core/src/query.rs | 1 +
crates/dbx-core/src/query_result_sql.rs | 66 +++++++++
crates/dbx-core/src/schema.rs | 1 +
crates/dbx-core/src/schema/normalization.rs | 1 +
.../dbx-core/src/sql_dialect/table_select.rs | 17 +++
crates/dbx-core/src/sql_dialect/tests.rs | 32 +++++
.../src/table_structure_sql/column_alter.rs | 40 +++++-
.../src/table_structure_sql/columns.rs | 21 ++-
.../src/table_structure_sql/dialect.rs | 24 +++-
.../src/table_structure_sql/indexes.rs | 2 +-
.../dbx-core/src/table_structure_sql/tests.rs | 132 ++++++++++++++++++
.../dbx-core/src/table_structure_sql/util.rs | 17 ++-
crates/dbx-core/src/transfer.rs | 1 +
.../dbx-core/tests/live_postgres_transfer.rs | 1 +
.../app-tests/databaseCapabilities.test.ts | 2 +
packages/app-tests/jdbcDialect.test.ts | 20 ++-
.../tableStructureCapabilities.test.ts | 32 +++++
41 files changed, 596 insertions(+), 50 deletions(-)
diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue
index b404966af8..463a347bc7 100644
--- a/apps/desktop/src/components/connection/ConnectionDialog.vue
+++ b/apps/desktop/src/components/connection/ConnectionDialog.vue
@@ -129,6 +129,7 @@ const defaultForm = (): ConnectionForm => ({
redis_cluster_nodes: "",
redis_key_separator: ":",
etcd_endpoints: "",
+ gbase_server: "",
read_only: false,
visible_databases: undefined,
});
@@ -388,7 +389,8 @@ const driverProfiles: Record<
vertica: { type: "vertica", port: 5433, user: "dbadmin", label: "Vertica", icon: "vertica" },
firebird: { type: "firebird", port: 3050, user: "SYSDBA", label: "Firebird", icon: "firebird" },
exasol: { type: "exasol", port: 8563, user: "sys", label: "Exasol", icon: "exasol" },
- gbase: { type: "gbase", port: 5258, user: "gbasedbt", label: "GBase", icon: "gbase" },
+ 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" },
opengauss: {
type: "opengauss",
@@ -545,7 +547,7 @@ function switchOceanbaseMode(mode: "mysql" | "oracle") {
resetTestState();
}
-function switchGbaseProfile(profile: "gbase" | "gbase8s") {
+function switchGbaseProfile(profile: "gbase8a" | "gbase8s") {
applyProfile(profile, false);
selectedType.value = "gbase";
resetTestState();
@@ -603,7 +605,7 @@ watch(
if (profile === "oceanbase") {
oceanbaseSubMode.value = config.driver_profile === "oceanbase-oracle" ? "oracle" : "mysql";
}
- if (profile === "gbase8s") {
+ if (profile === "gbase8a" || profile === "gbase8s") {
selectedType.value = "gbase";
}
mongoUseUrl.value = !!config.connection_string;
@@ -1590,7 +1592,7 @@ function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) {
oceanbaseSubMode.value = "oracle";
selectedType.value = "oceanbase";
}
- if (draft.driverProfile === "gbase8s") {
+ if (draft.driverProfile === "gbase8a" || draft.driverProfile === "gbase8s") {
selectedType.value = "gbase";
}
customDriverName.value = isCustomCompatibleProfile() ? draft.driverLabel : "";
@@ -2184,7 +2186,7 @@ function openExternalUrl(url: string) {
-
+
@@ -2595,6 +2597,11 @@ function openExternalUrl(url: string) {
+
+
+
+
+
diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue
index 650f23f78c..469593e6c6 100644
--- a/apps/desktop/src/components/icons/DatabaseIcon.vue
+++ b/apps/desktop/src/components/icons/DatabaseIcon.vue
@@ -66,6 +66,7 @@ const assetIcons: Record = {
firebird: "firebird.webp",
exasol: "exasol.webp",
gbase: "gbase.webp",
+ gbase8a: "gbase.webp",
gbase8s: "gbase.webp",
tdsql: "tdsql.webp",
polardb: "polardb.webp",
diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue
index a04bb7aa03..1c042bba4f 100644
--- a/apps/desktop/src/components/layout/ContentArea.vue
+++ b/apps/desktop/src/components/layout/ContentArea.vue
@@ -643,6 +643,11 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
{{ t(queryExecutionLabelKey(activeTab)) }}
· {{ queryRunningElapsedSeconds }}s
+
{{ t("editor.pressToExecute", { mod: shortcutModifier }) }}
diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue
index a1b89739e5..9a713e666a 100644
--- a/apps/desktop/src/components/objects/ObjectBrowser.vue
+++ b/apps/desktop/src/components/objects/ObjectBrowser.vue
@@ -47,7 +47,7 @@ import * as api from "@/lib/api";
import type { ConnectionConfig, ObjectInfo, ObjectSourceKind } from "@/types/database";
import { isSchemaAware } from "@/lib/databaseCapabilities";
import { supportsSchemaDiagram, supportsTableImport, supportsTableStructureEditing, supportsTableTruncate } from "@/lib/databaseFeatureSupport";
-import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
+import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/jdbcDialect";
import { buildTableSelectSql } from "@/lib/tableSelectSql";
import { buildDropObjectSql, buildDuplicateTableStructureSql, buildEmptyTableSql, buildTruncateTableSql, type TableAdminSqlOptions } from "@/lib/dbAdminSql";
import { useToast } from "@/composables/useToast";
@@ -106,6 +106,7 @@ const sourceError = ref("");
const sourceRow = ref
(null);
const sourceEditing = ref(false);
const effectiveDatabaseType = computed(() => effectiveDatabaseTypeForConnection(props.connection) ?? props.connection.db_type);
+const tableStructureDatabaseType = computed(() => tableStructureDatabaseTypeForConnection(props.connection) ?? props.connection.db_type);
const sourceDraft = ref("");
const sourceSaving = ref(false);
const sourceSaveError = ref("");
@@ -144,7 +145,7 @@ const procedureCount = computed(() => rows.value.filter((row) => row.type === "P
const functionCount = computed(() => rows.value.filter((row) => row.type === "FUNCTION").length);
const sequenceCount = computed(() => rows.value.filter((row) => row.type === "SEQUENCE").length);
const packageCount = computed(() => rows.value.filter((row) => row.type === "PACKAGE" || row.type === "PACKAGE_BODY").length);
-const canOpenStructureEditor = computed(() => supportsTableStructureEditing(effectiveDatabaseType.value));
+const canOpenStructureEditor = computed(() => supportsTableStructureEditing(tableStructureDatabaseType.value));
const canOpenDiagram = computed(() => !!props.database && supportsSchemaDiagram(effectiveDatabaseType.value));
const canOpenTableImport = computed(() => !!props.database && supportsTableImport(effectiveDatabaseType.value));
const supportsTruncateTable = computed(() => supportsTableTruncate(effectiveDatabaseType.value));
diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue
index 446288ad79..1ce34a6cea 100644
--- a/apps/desktop/src/components/sidebar/ConnectionTree.vue
+++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue
@@ -326,7 +326,9 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
if (!dbNode) return;
const targetSchema = "schema" in target ? target.schema : undefined;
const databaseChildrenLoaded = !!dbNode.children && dbNode.children.length > 0;
- const shouldLoadSchemaTables = target.type === "table" && !!targetSchema && usesTreeSchemaMode(config.db_type) && !connectionUsesDatabaseObjectTreeMode(config);
+ const effectiveDbType = effectiveDatabaseTypeForConnection(config);
+ const usesSchemaTree = usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config);
+ const shouldLoadSchemaTables = target.type === "table" && !!targetSchema && usesSchemaTree;
if (!force && databaseChildrenLoaded && !shouldLoadSchemaTables) return;
// Load database contents
@@ -335,7 +337,7 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
if (force || !databaseChildrenLoaded) {
await store.loadSqlServerDatabaseObjects(connId, target.database, loadOptions);
}
- } else if (usesTreeSchemaMode(config.db_type) && !connectionUsesDatabaseObjectTreeMode(config)) {
+ } else if (usesSchemaTree) {
if (force || !databaseChildrenLoaded) {
await store.loadSchemas(connId, target.database, loadOptions);
}
diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue
index e3d3e534fe..cb4a101f08 100644
--- a/apps/desktop/src/components/sidebar/TreeItem.vue
+++ b/apps/desktop/src/components/sidebar/TreeItem.vue
@@ -91,7 +91,7 @@ import { buildRenameObjectSql, supportsObjectRename, type RenameableObjectType }
import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/objectSourceEditor";
import { buildViewDdl } from "@/lib/viewDdl";
import { getTableStructureCapabilities } from "@/lib/tableStructureCapabilities";
-import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
+import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/jdbcDialect";
import { hexToRgba } from "@/lib/color";
import { focusSidebarRenameInput } from "@/lib/sidebarRenameFocus";
import { hasTreeNodeDatabaseContext } from "@/lib/treeNodeContext";
@@ -164,6 +164,10 @@ function currentDatabaseType(): DatabaseType | undefined {
return props.node.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(props.node.connectionId)) : undefined;
}
+function currentTableStructureDatabaseType(): DatabaseType | undefined {
+ return props.node.connectionId ? tableStructureDatabaseTypeForConnection(connectionStore.getConfig(props.node.connectionId)) : undefined;
+}
+
function rawDatabaseType(): DatabaseType | undefined {
return props.node.connectionId ? connectionStore.getConfig(props.node.connectionId)?.db_type : undefined;
}
@@ -172,6 +176,10 @@ function databaseTypeForNode(node: TreeNode): DatabaseType | undefined {
return node.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : undefined;
}
+function tableStructureDatabaseTypeForNode(node: TreeNode): DatabaseType | undefined {
+ return node.connectionId ? tableStructureDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : undefined;
+}
+
function hasNodeDatabaseContext(node: TreeNode): node is TreeNode & { connectionId: string; database: string } {
return !!node.connectionId && hasTreeNodeDatabaseContext(node);
}
@@ -361,9 +369,10 @@ async function toggle() {
queryStore.updateSql(tab, node.label);
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
const config = connectionStore.getConfig(node.connectionId);
+ const effectiveDbType = effectiveDatabaseTypeForConnection(config);
if (config?.db_type === "sqlserver") {
await connectionStore.loadSqlServerDatabaseObjects(node.connectionId, node.database);
- } else if (usesTreeSchemaMode(config?.db_type) && !connectionUsesDatabaseObjectTreeMode(config)) {
+ } else if (usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) {
await connectionStore.loadSchemas(node.connectionId, node.database);
} else {
await connectionStore.loadTables(node.connectionId, node.database);
@@ -701,6 +710,20 @@ async function openData() {
return queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema);
})();
console.info("[DBX][openData:tab-created]", { traceId, tabId, elapsed: elapsed() });
+
+ // Cancel any previous execution on this tab before starting a new one
+ const existingTab = queryStore.tabs.find((t) => t.id === tabId);
+ if (existingTab?.isExecuting && existingTab.executionId) {
+ await queryStore.cancelTabExecution(tabId);
+ }
+
+ const openDataId = uuid();
+ // Clear previous result so DataGrid doesn't show its internal loading overlay (without stop button)
+ const tab = queryStore.tabs.find((t) => t.id === tabId);
+ if (tab) {
+ tab.result = undefined;
+ tab.results = undefined;
+ }
queryStore.setTableMeta(tabId, {
schema: tableSchema,
tableName: node.label,
@@ -708,11 +731,15 @@ async function openData() {
columns: [],
primaryKeys: [],
});
- queryStore.setExecuting(tabId, true);
+ queryStore.setExecutingWithId(tabId, openDataId);
+
+ // Helper to check if this openData call is still active (not superseded by a newer click)
+ const isActive = () => queryStore.tabs.find((t) => t.id === tabId)?.executionId === openDataId;
try {
console.info("[DBX][openData:ensure-connected:start]", { traceId, elapsed: elapsed() });
await connectionStore.ensureConnected(node.connectionId);
+ if (!isActive()) return;
console.info("[DBX][openData:ensure-connected:done]", { traceId, elapsed: elapsed() });
if (!config) throw new Error("Connection config not found");
@@ -730,6 +757,7 @@ async function openData() {
elapsed: elapsed(),
});
columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label);
+ if (!isActive()) return;
primaryKeys = editablePrimaryKeys(effectiveDbType, columns, node.type === "view" ? "VIEW" : "TABLE");
console.info("[DBX][openData:get-columns:done]", {
traceId,
@@ -748,6 +776,9 @@ async function openData() {
console.warn("[DBX][openData:get-columns:error]", { traceId, elapsed: elapsed(), error });
}
+ // Check if superseded by a newer openData call
+ if (!isActive()) return;
+
const includeRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys);
const fallbackOrderColumns = effectiveDbType === "sqlserver" && !primaryKeys.length ? columns.slice(0, 1).map((column) => column.name) : undefined;
const sql = await buildTableSelectSql({
@@ -773,6 +804,7 @@ async function openData() {
await queryStore.executeTabSql(tabId, sql);
console.info("[DBX][openData:execute:done]", { traceId, tabId, elapsed: elapsed() });
} catch (e: any) {
+ if (!isActive()) return;
console.error("[DBX][openData:error]", { traceId, elapsed: elapsed(), error: e });
queryStore.setErrorResult(tabId, e);
}
@@ -980,7 +1012,7 @@ function dropObjectSqlOptions(): DropObjectSqlOptions | null {
function dropObjectSqlOptionsForNode(node: TreeNode): DropObjectSqlOptions | null {
if (node.type !== "view" && node.type !== "procedure" && node.type !== "function") return null;
return {
- databaseType: databaseTypeForNode(node),
+ databaseType: tableStructureDatabaseTypeForNode(node),
objectType: node.type === "view" ? "VIEW" : node.type === "procedure" ? "PROCEDURE" : "FUNCTION",
schema: node.schema,
name: node.label,
@@ -1421,7 +1453,7 @@ const supportsTruncate = computed(() => {
const canCreateTable = computed(() => {
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
- return (props.node.type === "database" || props.node.type === "schema" || props.node.type === "group-tables") && !!props.node.database && supportsTableStructureEditing(effectiveDatabaseTypeForConnection(config));
+ return (props.node.type === "database" || props.node.type === "schema" || props.node.type === "group-tables") && !!props.node.database && supportsTableStructureEditing(tableStructureDatabaseTypeForConnection(config));
});
const canCreateDatabase = computed(() => {
@@ -1446,12 +1478,12 @@ const canDropDatabase = computed(() => {
const canCreateSchema = computed(() => {
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
- return props.node.type === "database" && usesTreeSchemaMode(config?.db_type) && !connectionUsesDatabaseObjectTreeMode(config);
+ return props.node.type === "database" && usesTreeSchemaMode(effectiveDatabaseTypeForConnection(config)) && !connectionUsesDatabaseObjectTreeMode(config);
});
const canDropSchema = computed(() => {
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
- return props.node.type === "schema" && usesTreeSchemaMode(config?.db_type) && !connectionUsesDatabaseObjectTreeMode(config);
+ return props.node.type === "schema" && usesTreeSchemaMode(effectiveDatabaseTypeForConnection(config)) && !connectionUsesDatabaseObjectTreeMode(config);
});
function tableAdminSqlOptions(): TableAdminSqlOptions {
@@ -2325,7 +2357,7 @@ const canOpenTableImport = computed(() => {
return props.node.type === "table" && !!props.node.database && supportsTableImport(currentDatabaseType());
});
const canOpenStructureEditor = computed(() => {
- return props.node.type === "table" && !!props.node.database && supportsTableStructureEditing(currentDatabaseType());
+ return props.node.type === "table" && !!props.node.database && supportsTableStructureEditing(currentTableStructureDatabaseType());
});
const canOpenFieldLineage = computed(() => {
return props.node.type === "column" && !!props.node.database && !!props.node.tableName && supportsFieldLineage(currentDatabaseType());
diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue
index 0ae0e7f14b..a364a47b21 100644
--- a/apps/desktop/src/components/structure/TableStructureEditor.vue
+++ b/apps/desktop/src/components/structure/TableStructureEditor.vue
@@ -24,7 +24,7 @@ import { queryTimeoutSecsForConnection } from "@/lib/queryTimeout";
import { type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/tableStructureEditorSql";
import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities";
import { getTableStructureCapabilities } from "@/lib/tableStructureCapabilities";
-import { connectionObjectTreeQuerySchema, effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
+import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/jdbcDialect";
import {
buildStructureTargetLabel,
combineDataTypeForDatabase,
@@ -279,7 +279,7 @@ function onIndexColResize(e: MouseEvent, col: number) {
}
const connection = computed(() => (props.connectionId ? store.getConfig(props.connectionId) : undefined));
-const databaseType = computed(() => effectiveDatabaseTypeForConnection(connection.value));
+const databaseType = computed(() => tableStructureDatabaseTypeForConnection(connection.value));
const structureCapabilities = computed(() => getTableStructureCapabilities(databaseType.value));
const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(databaseType.value));
const structureDialect = computed(() => structureCapabilities.value.dialect);
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index d50ec7f024..408e2e60f8 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -167,6 +167,7 @@
driverName: "Driver Name",
driverNamePlaceholder: "Vendor or environment name",
urlParams: "URL Params",
+ gbaseServer: "GBASEDBTSERVER",
sslEnable: "Enable encrypted connection",
caCertPath: "CA Certificate",
caCertPathPlaceholder: "Optional, e.g. ~/.yandex/RootCA.crt",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 41eb7463c9..18c9aaf94c 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -168,6 +168,7 @@
driverName: "驱动名称",
driverNamePlaceholder: "厂商或环境名称",
urlParams: "URL 参数",
+ gbaseServer: "GBASEDBTSERVER",
sslEnable: "启用加密连接",
caCertPath: "CA 证书",
caCertPathPlaceholder: "可选,例如 ~/.yandex/RootCA.crt",
diff --git a/apps/desktop/src/lib/databaseCapabilitySets.ts b/apps/desktop/src/lib/databaseCapabilitySets.ts
index 3f17edf063..65b82042e7 100644
--- a/apps/desktop/src/lib/databaseCapabilitySets.ts
+++ b/apps/desktop/src/lib/databaseCapabilitySets.ts
@@ -27,6 +27,7 @@ export const SCHEMA_AWARE_TYPES = new Set([
"hive",
"databend",
"db2",
+ "informix",
"tdengine",
"xugu",
"iotdb",
@@ -60,6 +61,7 @@ export const TREE_SCHEMA_TYPES = new Set([
"jdbc",
"trino",
"h2",
+ "informix",
"tdengine",
"xugu",
"iotdb",
diff --git a/apps/desktop/src/lib/jdbcDialect.ts b/apps/desktop/src/lib/jdbcDialect.ts
index 0105635a7f..dbd8df6fd5 100644
--- a/apps/desktop/src/lib/jdbcDialect.ts
+++ b/apps/desktop/src/lib/jdbcDialect.ts
@@ -1,6 +1,8 @@
import type { ConnectionConfig, DatabaseType } from "@/types/database";
import { isSchemaAware, usesDatabaseObjectTreeMode, usesTreeSchemaMode } from "@/lib/databaseFeatureSupport";
+type JdbcDialectConnection = Pick & Partial>;
+
const JDBC_DIALECT_MATCHERS: Array<{ type: DatabaseType; patterns: RegExp[] }> = [
{ type: "databend", patterns: [/jdbc:databend:/i, /com\.databend\.jdbc\.DatabendDriver/i, /databend-jdbc/i] },
{ type: "starrocks", patterns: [/starrocks/i] },
@@ -17,22 +19,30 @@ const JDBC_DIALECT_MATCHERS: Array<{ type: DatabaseType; patterns: RegExp[] }> =
{ type: "informix", patterns: [/jdbc:informix/i, /informix/i] },
];
-export function inferJdbcDialect(connection?: Pick): DatabaseType | undefined {
+export function inferJdbcDialect(connection?: JdbcDialectConnection): DatabaseType | undefined {
if (!connection || connection.db_type !== "jdbc") return undefined;
const haystack = [connection.connection_string, connection.jdbc_driver_class, ...(connection.jdbc_driver_paths ?? [])].filter(Boolean).join("\n");
if (!haystack) return undefined;
return JDBC_DIALECT_MATCHERS.find((matcher) => matcher.patterns.some((pattern) => pattern.test(haystack)))?.type;
}
-export function effectiveDatabaseTypeForConnection(connection?: Pick): DatabaseType | undefined {
+export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined {
if (!connection) return undefined;
+ if (connection.db_type === "gbase" && connection.driver_profile === "gbase8a") return "mysql";
+ if (connection.db_type === "gbase" && connection.driver_profile === "gbase8s") return "informix";
if (connection.db_type !== "jdbc") return connection.db_type;
return inferJdbcDialect(connection) ?? "jdbc";
}
-export function connectionUsesDatabaseObjectTreeMode(connection?: Pick): boolean {
+export function tableStructureDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined {
+ if (!connection) return undefined;
+ if (connection.db_type === "gbase" && connection.driver_profile === "gbase8a") return "gbase";
+ return effectiveDatabaseTypeForConnection(connection);
+}
+
+export function connectionUsesDatabaseObjectTreeMode(connection?: JdbcDialectConnection): boolean {
if (!connection) return false;
- if (connection.db_type !== "jdbc") return usesDatabaseObjectTreeMode(connection.db_type);
+ if (connection.db_type !== "jdbc") return usesDatabaseObjectTreeMode(effectiveDatabaseTypeForConnection(connection));
const dialect = inferJdbcDialect(connection);
if (!dialect) return true;
if (dialect === "hive" || dialect === "trino") return false;
@@ -44,16 +54,18 @@ export function connectionUsesSchemaExecutionContext(connection?: Pick | undefined, database: string, schema?: string): string {
+export function connectionObjectTreeQuerySchema(connection: JdbcDialectConnection | undefined, database: string, schema?: string): string {
+ if (connection?.db_type === "gbase" && connection.driver_profile === "gbase8a") return "";
if (connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend") return schema || database;
if (connectionUsesDatabaseObjectTreeMode(connection)) return "";
return schema || database;
}
-export function connectionObjectTreeNodeSchema(connection: Pick | undefined, database: string, schema?: string): string | undefined {
+export function connectionObjectTreeNodeSchema(connection: JdbcDialectConnection | undefined, database: string, schema?: string): string | undefined {
+ if (connection?.db_type === "gbase" && connection.driver_profile === "gbase8a") return undefined;
if (connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend") return schema || database;
if (connectionUsesDatabaseObjectTreeMode(connection)) return undefined;
if (schema) return schema;
- const type = connection?.db_type === "jdbc" ? effectiveDatabaseTypeForConnection(connection) : connection?.db_type;
+ const type = effectiveDatabaseTypeForConnection(connection);
return isSchemaAware(type) ? database : undefined;
}
diff --git a/apps/desktop/src/lib/tableStructureCapabilities.ts b/apps/desktop/src/lib/tableStructureCapabilities.ts
index 3a10220b31..3b36f42fe9 100644
--- a/apps/desktop/src/lib/tableStructureCapabilities.ts
+++ b/apps/desktop/src/lib/tableStructureCapabilities.ts
@@ -1,6 +1,6 @@
import type { DatabaseType } from "@/types/database";
-export type TableStructureDialect = "mysql" | "postgres" | "sqlite" | "duckdb" | "sqlserver" | "oracle" | "h2" | "clickhouse" | "influxdb" | "unsupported";
+export type TableStructureDialect = "mysql" | "postgres" | "sqlite" | "duckdb" | "sqlserver" | "oracle" | "h2" | "clickhouse" | "informix" | "influxdb" | "unsupported";
export interface TableStructureCapabilities {
dialect: TableStructureDialect;
@@ -70,6 +70,14 @@ const mysqlCapabilities = capabilities({
alterPrimaryKey: true,
});
+const gbaseCapabilities = capabilities({
+ dialect: "mysql",
+ createTable: true,
+ addColumn: true,
+ dropColumn: true,
+ renameColumn: true,
+});
+
const postgresCapabilities = capabilities({
dialect: "postgres",
createTable: true,
@@ -193,6 +201,21 @@ const clickhouseCapabilities = capabilities({
comment: true,
});
+const informixCapabilities = capabilities({
+ dialect: "informix",
+ createTable: true,
+ addColumn: true,
+ dropColumn: true,
+ renameColumn: true,
+ alterExistingColumn: true,
+ alterType: true,
+ alterNullability: true,
+ alterDefault: true,
+ createIndex: true,
+ dropIndex: true,
+ rebuildIndex: true,
+});
+
const accessCapabilities = capabilities({
dialect: "h2",
createTable: true,
@@ -221,7 +244,7 @@ const capabilityByType: Partial
goldendb: mysqlCapabilities,
sundb: mysqlCapabilities,
databend: mysqlCapabilities,
- gbase: mysqlCapabilities,
+ gbase: gbaseCapabilities,
postgres: postgresCapabilities,
gaussdb: postgresCapabilities,
kwdb: postgresCapabilities,
@@ -246,6 +269,7 @@ const capabilityByType: Partial
h2: h2Capabilities,
access: accessCapabilities,
clickhouse: clickhouseCapabilities,
+ informix: informixCapabilities,
influxdb: influxdbCapabilities,
};
diff --git a/apps/desktop/src/lib/tableStructureEditorState.ts b/apps/desktop/src/lib/tableStructureEditorState.ts
index 4d566d4557..8ba7f3ec1c 100644
--- a/apps/desktop/src/lib/tableStructureEditorState.ts
+++ b/apps/desktop/src/lib/tableStructureEditorState.ts
@@ -236,6 +236,36 @@ export const DATA_TYPE_OPTIONS: Record = {
"MultiPolygon",
"JSON",
],
+ informix: [
+ "smallint",
+ "integer",
+ "int",
+ "bigint",
+ "int8",
+ "serial",
+ "serial8",
+ "bigserial",
+ "decimal",
+ "numeric",
+ "money",
+ "smallfloat",
+ "float",
+ "real",
+ "char",
+ "varchar",
+ "lvarchar",
+ "nchar",
+ "nvarchar",
+ "text",
+ "clob",
+ "byte",
+ "blob",
+ "boolean",
+ "date",
+ "datetime year to second",
+ "datetime year to fraction",
+ "interval day to second",
+ ],
};
const DATA_TYPE_OPTION_ALIASES: Partial> = {
@@ -243,6 +273,7 @@ const DATA_TYPE_OPTION_ALIASES: Partial> = {
starrocks: "mysql",
goldendb: "mysql",
sundb: "mysql",
+ gbase: "mysql",
gaussdb: "postgres",
kwdb: "postgres",
opengauss: "postgres",
diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts
index c9772cc8f0..d69e520cca 100644
--- a/apps/desktop/src/stores/connectionStore.ts
+++ b/apps/desktop/src/stores/connectionStore.ts
@@ -915,10 +915,11 @@ export const useConnectionStore = defineStore("connection", () => {
);
const visibleNameSet = new Set(visibleNames);
const visibleDatabases = databases.filter((database) => visibleNameSet.has(database.name));
+ const effectiveDbType = effectiveDatabaseTypeForConnection(config);
const children = withSavedSqlRoot(
connectionId,
buildDatabaseTreeNodes(connectionId, visibleDatabases, {
- includeDefaultWhenEmpty: usesTreeSchemaMode(config?.db_type) || shouldIncludeDefaultDatabaseNode(config, visibleDatabases),
+ includeDefaultWhenEmpty: usesTreeSchemaMode(effectiveDbType) || shouldIncludeDefaultDatabaseNode(config, visibleDatabases),
}),
node,
);
@@ -1588,9 +1589,10 @@ export const useConnectionStore = defineStore("connection", () => {
await loadMongoCollections(node.connectionId, node.database);
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
const config = getConfig(node.connectionId);
+ const effectiveDbType = effectiveDatabaseTypeForConnection(config);
if (config?.db_type === "sqlserver") {
await loadSqlServerDatabaseObjects(node.connectionId, node.database, options);
- } else if (usesTreeSchemaMode(config?.db_type) && !connectionUsesDatabaseObjectTreeMode(config)) {
+ } else if (usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) {
await loadSchemas(node.connectionId, node.database, options);
} else {
await loadTables(node.connectionId, node.database, undefined, options);
diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts
index d6be134652..1e8fc47775 100644
--- a/apps/desktop/src/stores/queryStore.ts
+++ b/apps/desktop/src/stores/queryStore.ts
@@ -698,6 +698,15 @@ export const useQueryStore = defineStore("query", () => {
}
}
+ function setExecutingWithId(id: string, executionId: string) {
+ const tab = tabs.value.find((t) => t.id === id);
+ if (!tab) return;
+ tab.isExecuting = true;
+ tab.executionId = executionId;
+ tab.isCancelling = false;
+ tab.queryExecutionStartedAt = Date.now();
+ }
+
function clearExplain(tab: QueryTab) {
tab.explainPlan = undefined;
tab.explainError = undefined;
@@ -952,7 +961,9 @@ export const useQueryStore = defineStore("query", () => {
const elapsed = () => `${Math.round(performance.now() - startedAt)}ms`;
tab.isExecuting = true;
tab.isCancelling = false;
- tab.queryExecutionStartedAt = Date.now();
+ if (!tab.queryExecutionStartedAt) {
+ tab.queryExecutionStartedAt = Date.now();
+ }
tab.executionId = executionId;
tab.lastExecutedSql = sql;
if (!options?.preserveTotalRowCountDuringExecution) {
@@ -1413,7 +1424,12 @@ export const useQueryStore = defineStore("query", () => {
const canceled = await api.cancelQuery(executionId);
if (!canceled) {
const current = tabs.value.find((t) => t.id === id);
- if (current && current.executionId === executionId) current.isCancelling = false;
+ if (current && current.executionId === executionId) {
+ current.isExecuting = false;
+ current.isCancelling = false;
+ current.executionId = undefined;
+ current.queryExecutionStartedAt = undefined;
+ }
}
return canceled;
} catch (e: any) {
@@ -1698,6 +1714,7 @@ export const useQueryStore = defineStore("query", () => {
tableStructureRefreshVersion,
setObjectSource,
setExecuting,
+ setExecutingWithId,
setErrorResult,
setActiveResultIndex,
executeCurrentTab,
diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts
index 7a491111f7..1121036a59 100644
--- a/apps/desktop/src/types/database.ts
+++ b/apps/desktop/src/types/database.ts
@@ -97,6 +97,7 @@ export interface ConnectionConfig {
redis_cluster_nodes?: string;
redis_key_separator?: string;
etcd_endpoints?: string;
+ gbase_server?: string;
one_time?: boolean;
read_only?: boolean;
}
diff --git a/crates/dbx-core/assets/database-drivers.manifest.json b/crates/dbx-core/assets/database-drivers.manifest.json
index 17ef47063b..2361b3b446 100644
--- a/crates/dbx-core/assets/database-drivers.manifest.json
+++ b/crates/dbx-core/assets/database-drivers.manifest.json
@@ -945,10 +945,10 @@
},
{
"dbType": "gbase",
- "label": "GBase",
+ "label": "GBase 8a",
"runtimeMode": "agent",
"mcpMode": "bridge",
- "agentKey": "gbase",
+ "agentKey": "gbase8a",
"singleConnectionPool": false,
"metadataConnectionScoped": false,
"skipTcpProbe": true,
@@ -958,6 +958,11 @@
"profile": "gbase8s",
"label": "GBase 8s",
"agentKey": "gbase8s"
+ },
+ {
+ "profile": "gbase8a",
+ "label": "GBase 8a",
+ "agentKey": "gbase8a"
}
],
"supportLevel": "operate",
@@ -1178,7 +1183,7 @@
"schemaSearch": true,
"diagram": false,
"tableDataEdit": true,
- "tableStructureEdit": false,
+ "tableStructureEdit": true,
"tableImport": false,
"dataTransfer": false,
"sqlFileExecution": true,
diff --git a/crates/dbx-core/src/agent_catalog.rs b/crates/dbx-core/src/agent_catalog.rs
index f0ea1352d9..507c225614 100644
--- a/crates/dbx-core/src/agent_catalog.rs
+++ b/crates/dbx-core/src/agent_catalog.rs
@@ -22,8 +22,10 @@ const ORACLE_PROFILES: &[AgentDriverProfile] = &[
AgentDriverProfile { profile: "oracle-10g", key: "oracle", label: "Oracle", store_visible: false },
];
-const GBASE_PROFILES: &[AgentDriverProfile] =
- &[AgentDriverProfile { profile: "gbase8s", key: "gbase8s", label: "GBase 8s", store_visible: true }];
+const GBASE_PROFILES: &[AgentDriverProfile] = &[
+ AgentDriverProfile { profile: "gbase8s", key: "gbase8s", label: "GBase 8s", store_visible: true },
+ AgentDriverProfile { profile: "gbase8a", key: "gbase8a", label: "GBase 8a", store_visible: true },
+];
const AGENT_CATALOG: &[AgentCatalogEntry] = &[
AgentCatalogEntry {
@@ -119,8 +121,8 @@ const AGENT_CATALOG: &[AgentCatalogEntry] = &[
},
AgentCatalogEntry {
db_type: DatabaseType::Gbase,
- key: "gbase",
- label: "GBase",
+ key: "gbase8a",
+ label: "GBase 8a",
store_visible: true,
profiles: GBASE_PROFILES,
},
diff --git a/crates/dbx-core/src/agent_connection.rs b/crates/dbx-core/src/agent_connection.rs
index d77664a4c0..eb11af88c1 100644
--- a/crates/dbx-core/src/agent_connection.rs
+++ b/crates/dbx-core/src/agent_connection.rs
@@ -48,6 +48,7 @@ pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, da
"client_cert_path": config.client_cert_path,
"client_key_path": config.client_key_path,
"etcd_endpoints": etcd_endpoints,
+ "gbase_server": config.gbase_server,
"jdbc_driver_class": config.jdbc_driver_class.as_deref().unwrap_or(""),
"jdbc_driver_paths": &config.jdbc_driver_paths,
})
@@ -537,6 +538,7 @@ mod tests {
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/crates/dbx-core/src/cloud_sync.rs b/crates/dbx-core/src/cloud_sync.rs
index 78db49d20b..8d6e70cf67 100644
--- a/crates/dbx-core/src/cloud_sync.rs
+++ b/crates/dbx-core/src/cloud_sync.rs
@@ -596,6 +596,7 @@ mod tests {
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs
index 218479f2e2..3ca5ee3d31 100644
--- a/crates/dbx-core/src/connection.rs
+++ b/crates/dbx-core/src/connection.rs
@@ -1338,6 +1338,7 @@ mod tests {
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/crates/dbx-core/src/connection_secrets.rs b/crates/dbx-core/src/connection_secrets.rs
index 12ad11bc54..b005e84ede 100644
--- a/crates/dbx-core/src/connection_secrets.rs
+++ b/crates/dbx-core/src/connection_secrets.rs
@@ -508,6 +508,7 @@ mod tests {
redis_cluster_nodes: String::new(),
redis_key_separator: crate::models::connection::default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/crates/dbx-core/src/db/redis_driver.rs b/crates/dbx-core/src/db/redis_driver.rs
index 9df309ebe7..1329e9fe3b 100644
--- a/crates/dbx-core/src/db/redis_driver.rs
+++ b/crates/dbx-core/src/db/redis_driver.rs
@@ -1990,6 +1990,7 @@ mod tests {
redis_cluster_nodes: String::new(),
redis_key_separator: crate::models::connection::default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs
index 93189a53fb..7063397496 100644
--- a/crates/dbx-core/src/models/connection.rs
+++ b/crates/dbx-core/src/models/connection.rs
@@ -64,6 +64,8 @@ pub struct ConnectionConfig {
pub redis_key_separator: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub etcd_endpoints: String,
+ #[serde(default, skip_serializing_if = "String::is_empty")]
+ pub gbase_server: String,
/// Typed configuration for external tabular sources.
#[serde(default)]
pub external_config: Option,
@@ -356,6 +358,8 @@ struct ConnectionConfigData {
#[serde(default)]
pub etcd_endpoints: String,
#[serde(default)]
+ pub gbase_server: String,
+ #[serde(default)]
pub external_config: Option,
#[serde(default)]
pub jdbc_driver_class: Option,
@@ -404,6 +408,7 @@ impl From for ConnectionConfig {
redis_cluster_nodes: data.redis_cluster_nodes,
redis_key_separator: data.redis_key_separator,
etcd_endpoints: data.etcd_endpoints,
+ gbase_server: data.gbase_server,
external_config: data.external_config,
jdbc_driver_class: data.jdbc_driver_class,
jdbc_driver_paths: data.jdbc_driver_paths,
@@ -1412,6 +1417,7 @@ mod tests {
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs
index f56f707f51..12fd419c62 100644
--- a/crates/dbx-core/src/query.rs
+++ b/crates/dbx-core/src/query.rs
@@ -1983,6 +1983,7 @@ mod tests {
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/crates/dbx-core/src/query_result_sql.rs b/crates/dbx-core/src/query_result_sql.rs
index dc63184e41..d4b7a55671 100644
--- a/crates/dbx-core/src/query_result_sql.rs
+++ b/crates/dbx-core/src/query_result_sql.rs
@@ -195,6 +195,10 @@ pub fn build_paginated_query_sql(options: PaginatedQuerySqlOptions) -> QuerySqlB
return ok(format!("{statement};"));
}
+ if options.database_type == Some(DatabaseType::Informix) {
+ return ok(add_informix_first_limit(&statement, safe_limit, safe_offset));
+ }
+
if options.database_type.is_some_and(uses_fetch_first) {
return ok(add_fetch_first_limit(&statement, safe_limit, safe_offset));
}
@@ -406,10 +410,48 @@ fn add_mysql_limit(statement: &str, limit: usize, offset: usize) -> String {
format!("{statement} LIMIT {limit}{offset_sql};")
}
+fn add_informix_first_limit(statement: &str, limit: usize, offset: usize) -> String {
+ if has_top_level_informix_row_limit(statement) {
+ return format!("{statement};");
+ }
+ let row_limit = if offset > 0 { format!("SKIP {offset} FIRST {limit}") } else { format!("FIRST {limit}") };
+ if statement.len() >= 6 && statement[..6].eq_ignore_ascii_case("SELECT") {
+ let rest = &statement[6..];
+ if let Some((leading, after_modifier)) = strip_sql_server_select_modifier(rest, "DISTINCT") {
+ return format!("SELECT{leading}DISTINCT {row_limit}{after_modifier};");
+ }
+ if let Some((leading, after_modifier)) = strip_sql_server_select_modifier(rest, "UNIQUE") {
+ return format!("SELECT{leading}UNIQUE {row_limit}{after_modifier};");
+ }
+ if let Some((leading, after_modifier)) = strip_sql_server_select_modifier(rest, "ALL") {
+ return format!("SELECT{leading}ALL {row_limit}{after_modifier};");
+ }
+ return format!("SELECT {row_limit}{rest};");
+ }
+ format!("SELECT {row_limit} * FROM ({statement}) dbx_page;")
+}
+
fn has_top_level_limit(sql: &str) -> bool {
top_level_sql_tokens(sql).iter().any(|token| token.text == "LIMIT")
}
+fn has_top_level_informix_row_limit(sql: &str) -> bool {
+ if has_top_level_limit(sql) {
+ return true;
+ }
+ let tokens = top_level_sql_tokens(sql);
+ let Some(select_index) = tokens.iter().position(|token| token.text == "SELECT") else {
+ return false;
+ };
+ let from_index = tokens
+ .iter()
+ .enumerate()
+ .find(|(index, token)| *index > select_index && token.text == "FROM")
+ .map(|(index, _)| index)
+ .unwrap_or(tokens.len());
+ tokens[select_index + 1..from_index].iter().any(|token| token.text == "FIRST" || token.text == "SKIP")
+}
+
fn has_top_level_fetch_first(sql: &str) -> bool {
let tokens = top_level_sql_tokens(sql);
tokens.windows(2).any(|w| w[0].text == "FETCH" && w[1].text == "FIRST")
@@ -831,6 +873,30 @@ mod tests {
assert_eq!(result.sql.unwrap(), "SELECT id FROM users FETCH FIRST 100 ROWS ONLY;");
}
+ #[test]
+ fn uses_skip_first_pagination_for_informix() {
+ let result = build_paginated_query_sql(PaginatedQuerySqlOptions {
+ original_sql: "SELECT id FROM users WHERE active = 1".to_string(),
+ database_type: Some(DatabaseType::Informix),
+ limit: 50,
+ offset: 100,
+ });
+
+ assert_eq!(result.sql.unwrap(), "SELECT SKIP 100 FIRST 50 id FROM users WHERE active = 1;");
+ }
+
+ #[test]
+ fn informix_pagination_keeps_existing_first() {
+ let result = build_paginated_query_sql(PaginatedQuerySqlOptions {
+ original_sql: "SELECT FIRST 20 id FROM users".to_string(),
+ database_type: Some(DatabaseType::Informix),
+ limit: 50,
+ offset: 0,
+ });
+
+ assert_eq!(result.sql.unwrap(), "SELECT FIRST 20 id FROM users;");
+ }
+
#[test]
fn uses_mysql_style_alias_for_pagination() {
let result = build_paginated_query_sql(PaginatedQuerySqlOptions {
diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs
index ca6b99e25d..f9d5497585 100644
--- a/crates/dbx-core/src/schema.rs
+++ b/crates/dbx-core/src/schema.rs
@@ -659,6 +659,7 @@ mod tests {
redis_cluster_nodes: String::new(),
redis_key_separator: crate::models::connection::default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/crates/dbx-core/src/schema/normalization.rs b/crates/dbx-core/src/schema/normalization.rs
index cafb91ff3d..c0a9c50251 100644
--- a/crates/dbx-core/src/schema/normalization.rs
+++ b/crates/dbx-core/src/schema/normalization.rs
@@ -179,6 +179,7 @@ mod tests {
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/crates/dbx-core/src/sql_dialect/table_select.rs b/crates/dbx-core/src/sql_dialect/table_select.rs
index 1c0b30301a..a187f70059 100644
--- a/crates/dbx-core/src/sql_dialect/table_select.rs
+++ b/crates/dbx-core/src/sql_dialect/table_select.rs
@@ -68,6 +68,11 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
return format!("SELECT TOP {limit} {select_columns} FROM {table_alias}{where_clause}{order}");
}
+ if database_type == Some(DatabaseType::Informix) {
+ let row_limit = informix_row_limit_clause(limit, options.offset.unwrap_or(0));
+ return format!("SELECT {row_limit} {select_columns} FROM {table_alias}{where_clause}{order}");
+ }
+
if database_type == Some(DatabaseType::Db2) && options.offset.is_some_and(|offset| offset > 0) {
return build_db2_table_select_page_sql(
&table_alias,
@@ -142,6 +147,10 @@ pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String {
return format!("SELECT TOP {limit} {select_columns} FROM {table}{order_by}");
}
+ if database_type == Some(DatabaseType::Informix) {
+ return format!("SELECT FIRST {limit} {select_columns} FROM {table}{order_by}");
+ }
+
if database_type.is_some_and(uses_fetch_first) {
return format!("SELECT {select_columns} FROM {table}{order_by} FETCH FIRST {limit} ROWS ONLY");
}
@@ -153,6 +162,14 @@ pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String {
format!("SELECT {select_columns} FROM {table}{order_by} LIMIT {limit};")
}
+fn informix_row_limit_clause(limit: usize, offset: usize) -> String {
+ if offset > 0 {
+ format!("SKIP {offset} FIRST {limit}")
+ } else {
+ format!("FIRST {limit}")
+ }
+}
+
pub(super) fn is_oracle_row_id(database_type: Option, name: &str) -> bool {
database_type == Some(DatabaseType::Oracle) && name.eq_ignore_ascii_case(DBX_ROWID_COLUMN)
}
diff --git a/crates/dbx-core/src/sql_dialect/tests.rs b/crates/dbx-core/src/sql_dialect/tests.rs
index 9142a3bd0d..a1f74300da 100644
--- a/crates/dbx-core/src/sql_dialect/tests.rs
+++ b/crates/dbx-core/src/sql_dialect/tests.rs
@@ -278,6 +278,38 @@ fn builds_table_data_where_and_schema_queries() {
);
}
+#[test]
+fn builds_informix_table_data_with_skip_first_pagination() {
+ assert_eq!(
+ build_table_data_select_sql(TableDataSelectSqlOptions {
+ database_type: Some(DatabaseType::Informix),
+ schema: Some("ignored".to_string()),
+ table_name: "users".to_string(),
+ primary_keys: vec!["id".to_string()],
+ columns: vec!["id".to_string(), "name".to_string()],
+ fallback_order_columns: Vec::new(),
+ order_by: None,
+ limit: Some(50),
+ offset: Some(100),
+ where_input: Some("WHERE active = 1".to_string()),
+ include_row_id: false,
+ }),
+ "SELECT SKIP 100 FIRST 50 * FROM users WHERE (active = 1) ORDER BY id ASC"
+ );
+
+ assert_eq!(
+ build_table_select_sql(TableSelectSqlOptions {
+ database_type: Some(DatabaseType::Informix),
+ schema: None,
+ table_name: "systables",
+ columns: &["tabname".to_string()],
+ order_columns: &[],
+ limit: 1,
+ }),
+ "SELECT FIRST 1 tabname FROM systables"
+ );
+}
+
#[test]
fn explicit_table_data_order_overrides_default_key_order() {
assert_eq!(
diff --git a/crates/dbx-core/src/table_structure_sql/column_alter.rs b/crates/dbx-core/src/table_structure_sql/column_alter.rs
index 71476e22a9..6ea05228a9 100644
--- a/crates/dbx-core/src/table_structure_sql/column_alter.rs
+++ b/crates/dbx-core/src/table_structure_sql/column_alter.rs
@@ -1,4 +1,5 @@
use super::column_format::{clickhouse_column_type, column_data_type, column_definition};
+use super::columns::build_drop_column_sql;
use super::comments::build_sqlserver_column_comment_sql;
use super::dialect::{capabilities_for, database_label, StructureDialect};
use super::types::{EditableStructureColumn, SingleColumnAlterSqlOptions, TableStructureSqlResult};
@@ -29,7 +30,7 @@ pub fn build_single_column_alter_sql(options: SingleColumnAlterSqlOptions) -> Ta
warnings.push(format!("Primary key column \"{}\" cannot be dropped from this editor.", original.name));
return TableStructureSqlResult { statements, warnings };
}
- statements.push(format!("ALTER TABLE {table} DROP COLUMN {};", quote_ident(dialect, &original.name)));
+ statements.push(build_drop_column_sql(dialect, &table, &original.name));
return TableStructureSqlResult { statements, warnings };
}
@@ -74,6 +75,7 @@ pub fn build_single_column_alter_sql(options: SingleColumnAlterSqlOptions) -> Ta
StructureDialect::ClickHouse => {
statements.extend(build_clickhouse_existing_column_sql(&table, &options.column, ""))
}
+ StructureDialect::Informix => statements.extend(build_informix_existing_column_sql(&table, &options.column)),
StructureDialect::SqlServer => statements.extend(build_sqlserver_existing_column_sql(
&table,
&options.column,
@@ -191,6 +193,42 @@ pub(super) fn build_postgres_existing_column_sql(table: &str, column: &EditableS
statements
}
+pub(super) fn build_informix_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec {
+ let Some(original) = &column.original else {
+ return Vec::new();
+ };
+ let dialect = StructureDialect::Informix;
+ let mut statements = Vec::new();
+ let mut current_name = original.name.clone();
+ if column.name != original.name {
+ statements.push(format!(
+ "RENAME COLUMN {table}.{} TO {};",
+ quote_ident(dialect, &original.name),
+ quote_ident(dialect, &column.name)
+ ));
+ current_name = column.name.clone();
+ }
+ let type_changed = column.data_type.trim() != original.data_type.trim();
+ let nullable_changed = column.is_nullable != original.is_nullable;
+ let default_changed = normalize_default(Some(&column.default_value)) != original_default(column);
+ if type_changed || nullable_changed || default_changed {
+ let mut parts = vec![quote_ident(dialect, ¤t_name), column_data_type(dialect, column)];
+ if column.is_nullable {
+ parts.push("NULL".to_string());
+ } else {
+ parts.push("NOT NULL".to_string());
+ }
+ let default_value = normalize_default(Some(&column.default_value));
+ if !default_value.is_empty() {
+ parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
+ } else if default_changed {
+ parts.push("DEFAULT NULL".to_string());
+ }
+ statements.push(format!("ALTER TABLE {table} MODIFY ({});", parts.join(" ")));
+ }
+ statements
+}
+
pub(super) fn build_oracle_like_existing_column_sql(
dialect: StructureDialect,
table: &str,
diff --git a/crates/dbx-core/src/table_structure_sql/columns.rs b/crates/dbx-core/src/table_structure_sql/columns.rs
index a4431e88b6..7fbf34b425 100644
--- a/crates/dbx-core/src/table_structure_sql/columns.rs
+++ b/crates/dbx-core/src/table_structure_sql/columns.rs
@@ -1,7 +1,8 @@
use super::column_alter::{
- build_clickhouse_existing_column_sql, build_h2_existing_column_sql, build_mysql_existing_column_sql,
- build_oracle_like_existing_column_sql, build_postgres_existing_column_sql, build_sqlite_existing_column_sql,
- build_sqlserver_existing_column_sql, has_column_extra_change, has_existing_column_attribute_change,
+ build_clickhouse_existing_column_sql, build_h2_existing_column_sql, build_informix_existing_column_sql,
+ build_mysql_existing_column_sql, build_oracle_like_existing_column_sql, build_postgres_existing_column_sql,
+ build_sqlite_existing_column_sql, build_sqlserver_existing_column_sql, has_column_extra_change,
+ has_existing_column_attribute_change,
};
use super::column_format::column_definition;
use super::comments::build_sqlserver_column_comment_sql;
@@ -33,7 +34,7 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu
warnings.push(format!("Primary key column \"{}\" cannot be dropped from this editor.", original.name));
continue;
}
- statements.push(format!("ALTER TABLE {table} DROP COLUMN {};", quote_ident(dialect, &original.name)));
+ statements.push(build_drop_column_sql(dialect, &table, &original.name));
continue;
}
@@ -106,6 +107,7 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu
column,
if has_position_change { &position_clause } else { "" },
)),
+ StructureDialect::Informix => statements.extend(build_informix_existing_column_sql(&table, column)),
StructureDialect::SqlServer => statements.extend(build_sqlserver_existing_column_sql(
&table,
column,
@@ -185,11 +187,13 @@ pub(super) fn build_add_column_sql(
schema: Option<&str>,
table_name: &str,
) -> Vec {
- let add_keyword = if dialect == StructureDialect::SqlServer { "ADD" } else { "ADD COLUMN" };
let definition = column_definition(dialect, column);
let mut statements = if dialect == StructureDialect::Oracle {
vec![format!("ALTER TABLE {table} ADD ({definition});")]
+ } else if dialect == StructureDialect::Informix {
+ vec![format!("ALTER TABLE {table} ADD ({definition});")]
} else {
+ let add_keyword = if dialect == StructureDialect::SqlServer { "ADD" } else { "ADD COLUMN" };
vec![format!("ALTER TABLE {table} {add_keyword} {definition}{position_clause};")]
};
if matches!(dialect, StructureDialect::Postgres | StructureDialect::Oracle) && !clean(&column.comment).is_empty() {
@@ -212,6 +216,13 @@ pub(super) fn build_add_column_sql(
statements
}
+pub(super) fn build_drop_column_sql(dialect: StructureDialect, table: &str, column_name: &str) -> String {
+ if dialect == StructureDialect::Informix {
+ return format!("ALTER TABLE {table} DROP ({});", quote_ident(dialect, column_name));
+ }
+ format!("ALTER TABLE {table} DROP COLUMN {};", quote_ident(dialect, column_name))
+}
+
pub(super) fn column_position_clause(
dialect: StructureDialect,
columns: &[&EditableStructureColumn],
diff --git a/crates/dbx-core/src/table_structure_sql/dialect.rs b/crates/dbx-core/src/table_structure_sql/dialect.rs
index 41df3b1abc..d347b3c5c5 100644
--- a/crates/dbx-core/src/table_structure_sql/dialect.rs
+++ b/crates/dbx-core/src/table_structure_sql/dialect.rs
@@ -11,6 +11,7 @@ pub(super) enum StructureDialect {
Oracle,
H2,
ClickHouse,
+ Informix,
Unsupported,
}
@@ -64,8 +65,7 @@ pub(super) fn capabilities_for(database_type: Option) -> TableStru
| DatabaseType::StarRocks
| DatabaseType::Goldendb
| DatabaseType::Sundb
- | DatabaseType::Databend
- | DatabaseType::Gbase,
+ | DatabaseType::Databend,
) => TableStructureCapabilities {
dialect: StructureDialect::Mysql,
add_column: true,
@@ -82,6 +82,13 @@ pub(super) fn capabilities_for(database_type: Option) -> TableStru
alter_primary_key: true,
..base
},
+ Some(DatabaseType::Gbase) => TableStructureCapabilities {
+ dialect: StructureDialect::Mysql,
+ add_column: true,
+ drop_column: true,
+ rename_column: true,
+ ..base
+ },
Some(
DatabaseType::Postgres
| DatabaseType::Gaussdb
@@ -197,6 +204,17 @@ pub(super) fn capabilities_for(database_type: Option) -> TableStru
comment: true,
..base
},
+ Some(DatabaseType::Informix) => TableStructureCapabilities {
+ dialect: StructureDialect::Informix,
+ add_column: true,
+ drop_column: true,
+ rename_column: true,
+ alter_existing_column: true,
+ create_index: true,
+ drop_index: true,
+ rebuild_index: true,
+ ..base
+ },
_ => base,
}
}
@@ -227,6 +245,7 @@ pub(super) fn dialect_label(dialect: StructureDialect) -> String {
StructureDialect::Oracle => "oracle",
StructureDialect::H2 => "h2",
StructureDialect::ClickHouse => "clickhouse",
+ StructureDialect::Informix => "informix",
StructureDialect::Unsupported => "this database",
}
.to_string()
@@ -243,6 +262,7 @@ pub(super) fn database_type_for_dialect(dialect: StructureDialect) -> Option Some(DatabaseType::Oracle),
StructureDialect::H2 => Some(DatabaseType::H2),
StructureDialect::ClickHouse => Some(DatabaseType::ClickHouse),
+ StructureDialect::Informix => Some(DatabaseType::Informix),
StructureDialect::Unsupported => None,
}
}
diff --git a/crates/dbx-core/src/table_structure_sql/indexes.rs b/crates/dbx-core/src/table_structure_sql/indexes.rs
index 82d7e33fb2..6d46eee979 100644
--- a/crates/dbx-core/src/table_structure_sql/indexes.rs
+++ b/crates/dbx-core/src/table_structure_sql/indexes.rs
@@ -112,7 +112,7 @@ pub(super) fn build_drop_index_sql(
if matches!(dialect, StructureDialect::Mysql | StructureDialect::SqlServer) {
return format!("DROP INDEX {} ON {table};", quote_ident(dialect, index_name));
}
- if matches!(dialect, StructureDialect::Postgres | StructureDialect::Oracle)
+ if matches!(dialect, StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::Informix)
&& schema.is_some_and(|schema| !schema.trim().is_empty())
{
return format!("DROP INDEX {}.{};", quote_ident(dialect, schema.unwrap()), quote_ident(dialect, index_name));
diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs
index 7c6dede93d..41fce3bb6b 100644
--- a/crates/dbx-core/src/table_structure_sql/tests.rs
+++ b/crates/dbx-core/src/table_structure_sql/tests.rs
@@ -117,6 +117,75 @@ fn builds_mysql_column_and_index_changes() {
);
}
+#[test]
+fn builds_informix_column_and_index_changes() {
+ let mut renamed = column("display_name");
+ renamed.data_type = "varchar(120)".to_string();
+ renamed.is_nullable = false;
+ renamed.default_value = "'guest'".to_string();
+ renamed.original = Some(ColumnInfo {
+ name: "name".to_string(),
+ data_type: "varchar(80)".to_string(),
+ is_nullable: true,
+ column_default: None,
+ is_primary_key: false,
+ extra: None,
+ comment: Some(String::new()),
+ });
+ let mut email = column("email");
+ email.is_nullable = false;
+ let mut old_col = column("old_col");
+ old_col.marked_for_drop = true;
+ old_col.original = Some(ColumnInfo {
+ name: "old_col".to_string(),
+ data_type: "varchar(20)".to_string(),
+ is_nullable: true,
+ column_default: None,
+ is_primary_key: false,
+ extra: None,
+ comment: None,
+ });
+ let mut old_index = index("idx_old", &["name"]);
+ old_index.marked_for_drop = true;
+ old_index.original = Some(IndexInfo {
+ name: "idx_old".to_string(),
+ columns: vec!["name".to_string()],
+ is_unique: false,
+ is_primary: false,
+ filter: None,
+ index_type: None,
+ included_columns: None,
+ comment: None,
+ });
+ let mut email_index = index("uniq_users_email", &["email"]);
+ email_index.is_unique = true;
+
+ let result = build_table_structure_change_sql(TableStructureSqlOptions {
+ database_type: Some(DatabaseType::Informix),
+ schema: Some("gbasedbt".to_string()),
+ table_name: "users".to_string(),
+ columns: vec![renamed, email, old_col],
+ indexes: vec![old_index, email_index],
+ foreign_keys: Vec::new(),
+ triggers: Vec::new(),
+ table_comment: None,
+ original_table_comment: None,
+ });
+
+ assert_eq!(result.warnings, Vec::::new());
+ assert_eq!(
+ result.statements,
+ vec![
+ "RENAME COLUMN gbasedbt.users.name TO display_name;",
+ "ALTER TABLE gbasedbt.users MODIFY (display_name varchar(120) NOT NULL DEFAULT 'guest');",
+ "ALTER TABLE gbasedbt.users ADD (email varchar(255) NOT NULL);",
+ "ALTER TABLE gbasedbt.users DROP (old_col);",
+ "DROP INDEX gbasedbt.idx_old;",
+ "CREATE UNIQUE INDEX uniq_users_email ON gbasedbt.users (email);",
+ ]
+ );
+}
+
#[test]
fn mysql_create_index_with_comment() {
let mut col = column("name");
@@ -146,6 +215,69 @@ fn mysql_create_index_with_comment() {
);
}
+#[test]
+fn gbase8a_uses_limited_mysql_ddl() {
+ let mut renamed = column("display_email");
+ renamed.data_type = "varchar(255)".to_string();
+ renamed.original = Some(ColumnInfo {
+ name: "email".to_string(),
+ data_type: "varchar(255)".to_string(),
+ is_nullable: true,
+ column_default: None,
+ is_primary_key: false,
+ extra: None,
+ comment: None,
+ });
+ let new_col = column("nickname");
+ let mut old_col = column("old_col");
+ old_col.marked_for_drop = true;
+ old_col.original = Some(ColumnInfo {
+ name: "old_col".to_string(),
+ data_type: "varchar(20)".to_string(),
+ is_nullable: true,
+ column_default: None,
+ is_primary_key: false,
+ extra: None,
+ comment: None,
+ });
+ let mut index = index("idx_users_email", &["display_email"]);
+ index.original = Some(IndexInfo {
+ name: "idx_users_email".to_string(),
+ columns: vec!["email".to_string()],
+ is_unique: false,
+ is_primary: false,
+ filter: None,
+ index_type: None,
+ included_columns: None,
+ comment: None,
+ });
+
+ let result = build_table_structure_change_sql(TableStructureSqlOptions {
+ database_type: Some(DatabaseType::Gbase),
+ schema: None,
+ table_name: "users".to_string(),
+ columns: vec![renamed, new_col, old_col],
+ indexes: vec![index],
+ foreign_keys: Vec::new(),
+ triggers: Vec::new(),
+ table_comment: None,
+ original_table_comment: None,
+ });
+
+ assert_eq!(
+ result.statements,
+ vec![
+ "ALTER TABLE `users` CHANGE COLUMN `email` `display_email` varchar(255);",
+ "ALTER TABLE `users` ADD COLUMN `nickname` varchar(255);",
+ "ALTER TABLE `users` DROP COLUMN `old_col`;",
+ ]
+ );
+ assert_eq!(
+ result.warnings,
+ vec!["Editing existing indexes is not supported for gbase from this editor.".to_string()]
+ );
+}
+
#[test]
fn mysql_create_unique_index_with_comment_and_btree() {
let mut idx = index("uniq_users_email", &["email"]);
diff --git a/crates/dbx-core/src/table_structure_sql/util.rs b/crates/dbx-core/src/table_structure_sql/util.rs
index 6148518884..fd5cac7ef1 100644
--- a/crates/dbx-core/src/table_structure_sql/util.rs
+++ b/crates/dbx-core/src/table_structure_sql/util.rs
@@ -4,7 +4,11 @@ use super::types::EditableStructureColumn;
pub(super) fn qualified_table(dialect: StructureDialect, schema: Option<&str>, table_name: &str) -> String {
if matches!(
dialect,
- StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::SqlServer | StructureDialect::H2
+ StructureDialect::Postgres
+ | StructureDialect::Oracle
+ | StructureDialect::SqlServer
+ | StructureDialect::H2
+ | StructureDialect::Informix
) && schema.is_some_and(|schema| !schema.trim().is_empty())
{
return format!("{}.{}", quote_ident(dialect, schema.unwrap()), quote_ident(dialect, table_name));
@@ -16,10 +20,20 @@ pub(super) fn quote_ident(dialect: StructureDialect, name: &str) -> String {
match dialect {
StructureDialect::Mysql => format!("`{}`", name.replace('`', "``")),
StructureDialect::SqlServer => format!("[{}]", name.replace(']', "]]")),
+ StructureDialect::Informix if is_simple_informix_identifier(name) => name.to_string(),
_ => format!("\"{}\"", name.replace('"', "\"\"")),
}
}
+fn is_simple_informix_identifier(name: &str) -> bool {
+ let mut chars = name.chars();
+ let Some(first) = chars.next() else {
+ return false;
+ };
+ (first.is_ascii_alphabetic() || first == '_')
+ && chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$')
+}
+
pub(super) fn quote_string(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
@@ -78,6 +92,7 @@ pub(super) fn is_temporal_type_for_default(dialect: StructureDialect, base_type:
StructureDialect::Sqlite => {
matches!(normalized.as_str(), "date" | "datetime" | "timestamp" | "time")
}
+ StructureDialect::Informix => matches!(normalized.as_str(), "date" | "datetime"),
_ => false,
}
}
diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs
index 2ee02553fe..b47afde944 100644
--- a/crates/dbx-core/src/transfer.rs
+++ b/crates/dbx-core/src/transfer.rs
@@ -3330,6 +3330,7 @@ mod tests {
redis_cluster_nodes: String::new(),
redis_key_separator: default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/crates/dbx-core/tests/live_postgres_transfer.rs b/crates/dbx-core/tests/live_postgres_transfer.rs
index 0dfd4208d9..a53a8d2a20 100644
--- a/crates/dbx-core/tests/live_postgres_transfer.rs
+++ b/crates/dbx-core/tests/live_postgres_transfer.rs
@@ -44,6 +44,7 @@ fn postgres_test_config(id: &str, database: &str) -> ConnectionConfig {
redis_cluster_nodes: String::new(),
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
etcd_endpoints: String::new(),
+ gbase_server: String::new(),
external_config: None,
jdbc_driver_class: None,
jdbc_driver_paths: Vec::new(),
diff --git a/packages/app-tests/databaseCapabilities.test.ts b/packages/app-tests/databaseCapabilities.test.ts
index 9b6f71612f..15b0be3f22 100644
--- a/packages/app-tests/databaseCapabilities.test.ts
+++ b/packages/app-tests/databaseCapabilities.test.ts
@@ -223,6 +223,8 @@ test("describes feature support through capability helpers", () => {
assert.equal(supportsTableStructureEditing("opengauss"), true);
assert.equal(supportsTableStructureEditing("redshift"), true);
assert.equal(supportsTableStructureEditing("clickhouse"), true);
+ assert.equal(supportsTableStructureEditing("gbase"), true);
+ assert.equal(supportsTableStructureEditing("informix"), true);
assert.equal(supportsTableStructureEditing("rqlite"), true);
assert.equal(supportsTableStructureEditing("mongodb"), false);
assert.equal(supportsTableStructureEditing("manticoresearch"), false);
diff --git a/packages/app-tests/jdbcDialect.test.ts b/packages/app-tests/jdbcDialect.test.ts
index 0ffc6b4caa..98a77cecf3 100644
--- a/packages/app-tests/jdbcDialect.test.ts
+++ b/packages/app-tests/jdbcDialect.test.ts
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { test } from "vitest";
-import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesSchemaExecutionContext, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, inferJdbcDialect } from "../../apps/desktop/src/lib/jdbcDialect.ts";
+import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesSchemaExecutionContext, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, inferJdbcDialect, tableStructureDatabaseTypeForConnection } from "../../apps/desktop/src/lib/jdbcDialect.ts";
import { supportsTableStructureEditing } from "../../apps/desktop/src/lib/databaseFeatureSupport.ts";
import { qualifiedTableName } from "../../apps/desktop/src/lib/tableSelectSql.ts";
@@ -15,6 +15,10 @@ test("infers JDBC dialect from URL, driver class, and driver jar path", () => {
test("effective database type keeps non-JDBC types and enables compatible JDBC structure editing", () => {
assert.equal(effectiveDatabaseTypeForConnection({ db_type: "postgres" }), "postgres");
assert.equal(effectiveDatabaseTypeForConnection({ db_type: "jdbc" }), "jdbc");
+ assert.equal(effectiveDatabaseTypeForConnection({ db_type: "gbase", driver_profile: "gbase8s" }), "informix");
+ assert.equal(effectiveDatabaseTypeForConnection({ db_type: "gbase", driver_profile: "gbase8a" }), "mysql");
+ assert.equal(tableStructureDatabaseTypeForConnection({ db_type: "gbase", driver_profile: "gbase8a" }), "gbase");
+ assert.equal(tableStructureDatabaseTypeForConnection({ db_type: "gbase", driver_profile: "gbase8s" }), "informix");
assert.equal(
effectiveDatabaseTypeForConnection({
db_type: "jdbc",
@@ -56,6 +60,20 @@ test("JDBC tree shape follows the inferred driver dialect", () => {
);
});
+test("GBase profiles use their compatible object tree shapes", () => {
+ const gbase8a = { db_type: "gbase" as const, driver_profile: "gbase8a" };
+ const gbase8s = { db_type: "gbase" as const, driver_profile: "gbase8s" };
+
+ assert.equal(connectionUsesDatabaseObjectTreeMode(gbase8a), false);
+ assert.equal(connectionObjectTreeQuerySchema(gbase8a, "testdb", undefined), "");
+ assert.equal(connectionObjectTreeNodeSchema(gbase8a, "testdb", undefined), undefined);
+ assert.equal(connectionUsesDatabaseObjectTreeMode(gbase8s), false);
+ assert.equal(connectionObjectTreeQuerySchema(gbase8s, "testdb", "gbasedbt"), "gbasedbt");
+ assert.equal(connectionObjectTreeNodeSchema(gbase8s, "testdb", "gbasedbt"), "gbasedbt");
+ assert.equal(connectionObjectTreeQuerySchema(gbase8s, "testdb", undefined), "testdb");
+ assert.equal(connectionObjectTreeNodeSchema(gbase8s, "testdb", undefined), "testdb");
+});
+
test("Databend JDBC keeps database as schema context for table data", () => {
const databend = { db_type: "jdbc" as const, connection_string: "jdbc:databend://db.example.com:8000/dbx_test" };
diff --git a/packages/app-tests/tableStructureCapabilities.test.ts b/packages/app-tests/tableStructureCapabilities.test.ts
index 275abf7825..ba2ffc7f24 100644
--- a/packages/app-tests/tableStructureCapabilities.test.ts
+++ b/packages/app-tests/tableStructureCapabilities.test.ts
@@ -41,6 +41,21 @@ test("mysql-like databases expose index rebuild and type capabilities", () => {
}
});
+test("gbase 8a exposes limited mysql-compatible structure editing capabilities", () => {
+ const caps = getTableStructureCapabilities("gbase");
+ assert.equal(caps.dialect, "mysql");
+ assert.equal(caps.createTable, true);
+ assert.equal(caps.addColumn, true);
+ assert.equal(caps.dropColumn, true);
+ assert.equal(caps.renameColumn, true);
+ assert.equal(caps.alterExistingColumn, false);
+ assert.equal(caps.createIndex, false);
+ assert.equal(caps.dropIndex, false);
+ assert.equal(caps.rebuildIndex, false);
+ assert.equal(caps.comment, false);
+ assert.equal(canEditTableStructure("gbase"), true);
+});
+
test("redshift reuses postgres column DDL but keeps indexes disabled", () => {
const caps = getTableStructureCapabilities("redshift");
assert.equal(caps.dialect, "postgres");
@@ -93,6 +108,23 @@ test("limited analytic engines can open the editor for supported operations only
assert.equal(canEditTableStructure("clickhouse"), true);
});
+test("informix exposes conservative structure editing capabilities", () => {
+ const caps = getTableStructureCapabilities("informix");
+ assert.equal(caps.dialect, "informix");
+ assert.equal(caps.createTable, true);
+ assert.equal(caps.addColumn, true);
+ assert.equal(caps.dropColumn, true);
+ assert.equal(caps.renameColumn, true);
+ assert.equal(caps.alterExistingColumn, true);
+ assert.equal(caps.comment, false);
+ assert.equal(caps.createIndex, true);
+ assert.equal(caps.dropIndex, true);
+ assert.equal(caps.rebuildIndex, true);
+ assert.equal(caps.indexType, false);
+ assert.equal(caps.alterPrimaryKey, false);
+ assert.equal(canEditTableStructure("informix"), true);
+});
+
test("unsupported non-relational databases do not open the structure editor", () => {
for (const dbType of ["redis", "mongodb", "elasticsearch", "manticoresearch", "neo4j", undefined] as const) {
const caps = getTableStructureCapabilities(dbType);