diff --git a/apps/desktop/src/components/transfer/DataTransferDialog.vue b/apps/desktop/src/components/transfer/DataTransferDialog.vue index 18e89b688f..953153d6b0 100644 --- a/apps/desktop/src/components/transfer/DataTransferDialog.vue +++ b/apps/desktop/src/components/transfer/DataTransferDialog.vue @@ -21,7 +21,7 @@ import TransferTaskTree from "@/components/transfer/TransferTaskTree.vue"; import type { DatabaseType } from "@/types/database"; import type { TransferTask, TransferTaskConfig } from "@/types/database"; import { isSchemaAware, supportsTransfer } from "@/lib/database/databaseCapabilities"; -import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; +import { transferDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import { isDorisFamilyCatalogCapable } from "@/lib/database/databaseFeatureSupport"; import { decodeTransferDatabaseOption, encodeTransferDatabaseOptions, isSameTransferDatabase, isTransferDatabaseSelected, normalizeTransferCatalog } from "@/lib/database/dataTransferSelection"; import { formatDatabaseLabel } from "@/lib/database/defaultDatabase"; @@ -60,7 +60,7 @@ const transferDialogStyle = { const store = useConnectionStore(); -const sqlConnections = computed(() => store.connections.filter((c) => supportsTransfer(effectiveDatabaseTypeForConnection(c)))); +const sqlConnections = computed(() => store.connections.filter((c) => supportsTransfer(transferDatabaseTypeForConnection(c)))); // Source state const sourceConnectionId = ref(""); @@ -115,7 +115,7 @@ const treeDisabledGroups = computed(() => { } const sourceConfig = store.getConfig(sourceConnectionId.value); const targetConfig = store.getConfig(targetConnectionId.value); - const allowed = crossFamilyTransferableKinds(effectiveDatabaseTypeForConnection(sourceConfig), effectiveDatabaseTypeForConnection(targetConfig)); + const allowed = crossFamilyTransferableKinds(transferDatabaseTypeForConnection(sourceConfig), transferDatabaseTypeForConnection(targetConfig)); return presentKinds.filter((k) => k !== "TABLE" && !allowed.includes(k)); }); @@ -133,9 +133,9 @@ const showCrossFamilyViewHint = computed(() => { const targetConfig = store.getConfig(targetConnectionId.value); if (transferContent.value === "dataOnly") return false; if (!sourceConfig || !targetConfig) return false; - const allowed = crossFamilyTransferableKinds(effectiveDatabaseTypeForConnection(sourceConfig), effectiveDatabaseTypeForConnection(targetConfig)); + const allowed = crossFamilyTransferableKinds(transferDatabaseTypeForConnection(sourceConfig), transferDatabaseTypeForConnection(targetConfig)); if (!allowed.includes("VIEW")) return false; - return !isSameTransferFamily(effectiveDatabaseTypeForConnection(sourceConfig), effectiveDatabaseTypeForConnection(targetConfig)) && (selectedObjects.value.VIEW?.size ?? 0) > 0; + return !isSameTransferFamily(transferDatabaseTypeForConnection(sourceConfig), transferDatabaseTypeForConnection(targetConfig)) && (selectedObjects.value.VIEW?.size ?? 0) > 0; }); const pendingSourceSchemaPrefill = ref(""); const pendingSelectedTablesPrefill = ref(null); @@ -409,7 +409,7 @@ async function loadObjects(isCancelled: () => boolean = () => false) { const config = store.getConfig(connectionId); const needsSchema = isSchemaAware(config?.db_type); const schema = needsSchema && schemaValue ? schemaValue : database; - const kinds = transferObjectKindsForDatabase(effectiveDatabaseTypeForConnection(config)); + const kinds = transferObjectKindsForDatabase(transferDatabaseTypeForConnection(config)); const groups: Partial> = {}; for (const kind of kinds) { try { diff --git a/apps/desktop/src/components/transfer/__tests__/DataTransferDialog.spec.ts b/apps/desktop/src/components/transfer/__tests__/DataTransferDialog.spec.ts index eba89698d6..ed4ef4dd56 100644 --- a/apps/desktop/src/components/transfer/__tests__/DataTransferDialog.spec.ts +++ b/apps/desktop/src/components/transfer/__tests__/DataTransferDialog.spec.ts @@ -67,7 +67,7 @@ describe("DataTransferDialog transfer prefill", () => { }); it("routes table-only database kinds through the table-list request", () => { - expect(dialogSource).toContain("const kinds = transferObjectKindsForDatabase(effectiveDatabaseTypeForConnection(config))"); + expect(dialogSource).toContain("const kinds = transferObjectKindsForDatabase(transferDatabaseTypeForConnection(config))"); expect(dialogSource).toContain("for (const kind of kinds)"); expect(dialogSource).toContain('if (kind === "TABLE")'); expect(dialogSource).toContain("await api.listTables(connectionId, database, schema"); @@ -75,13 +75,18 @@ describe("DataTransferDialog transfer prefill", () => { it("keeps MongoDB collection loading ahead of the generic object-kind path", () => { const mongoCollectionBranch = dialogSource.indexOf("if (isMongoConnection(connectionId))"); - const genericObjectKinds = dialogSource.indexOf("const kinds = transferObjectKindsForDatabase(effectiveDatabaseTypeForConnection(config))"); + const genericObjectKinds = dialogSource.indexOf("const kinds = transferObjectKindsForDatabase(transferDatabaseTypeForConnection(config))"); expect(mongoCollectionBranch).toBeGreaterThan(-1); expect(dialogSource).toContain("await api.mongoListCollections(connectionId, database)"); expect(mongoCollectionBranch).toBeLessThan(genericObjectKinds); }); + it("admits connections via the transfer resolver so doris-family mysql connections stay selectable", () => { + expect(dialogSource).toContain("supportsTransfer(transferDatabaseTypeForConnection(c))"); + expect(dialogSource).not.toContain("supportsTransfer(effectiveDatabaseTypeForConnection(c))"); + }); + it("disables non-table groups for data-only and cross-family transfers", () => { expect(dialogSource).toContain("treeDisabledGroups"); expect(dialogSource).toContain('transferContent.value === "dataOnly"'); diff --git a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts index 4cefce84d0..d1148c6b04 100644 --- a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts @@ -25,7 +25,10 @@ import { setGaussdbIdentifierQuoteStyle, setGaussdbTargetServerType, supportsGaussdbIdentifierQuoteStyle, + transferDatabaseTypeForConnection, } from "@/lib/database/jdbcDialect"; +import { supportsTransfer } from "@/lib/database/databaseFeatureSupport"; +import { transferObjectKindsForDatabase } from "@/lib/database/transferObjectKinds"; describe("jdbc dialect inference", () => { it("detects InterSystems IRIS and Caché JDBC connections", () => { @@ -223,6 +226,34 @@ describe("jdbc dialect inference", () => { expect(effectiveDatabaseTypeForConnection(gbase8sProfileWithLegacyGbaseUrl)).toBe("jdbc"); }); + it("keeps doris-family mysql connections on the mysql transfer path", () => { + // Doris/SelectDB/StarRocks connections are saved as db_type=mysql with a + // doris-family driver_profile. The SQL dialect stays doris/starrocks + // (3-part catalog names), but transfer admission and object kinds must ride + // the raw mysql db_type: the standalone doris/starrocks manifest entries are + // not transfer-capable, so the effective type would hide these connections + // from the transfer dialog and drop their non-table object kinds. + for (const driver_profile of ["starrocks", "doris", "selectdb"]) { + const connection = { db_type: "mysql" as const, driver_profile }; + const transferType = transferDatabaseTypeForConnection(connection); + + expect(transferType).toBe("mysql"); + expect(supportsTransfer(transferType)).toBe(true); + expect(transferObjectKindsForDatabase(transferType)).toContain("VIEW"); + } + }); + + it("resolves transfer types for non-doris-family connections through the effective type", () => { + expect(transferDatabaseTypeForConnection(undefined)).toBeUndefined(); + expect(transferDatabaseTypeForConnection({ db_type: "mysql" })).toBe("mysql"); + expect(transferDatabaseTypeForConnection({ db_type: "postgres" })).toBe("postgres"); + expect(transferDatabaseTypeForConnection({ db_type: "gbase" })).toBe("mysql"); + expect(transferDatabaseTypeForConnection({ db_type: "jdbc", connection_string: "jdbc:gbase://localhost:5258/dbx_test" })).toBe("gbase"); + // A generic-JDBC Doris URL keeps its raw db_type (never admitted), matching + // the pre-effective-type behavior for unknown jdbc connections. + expect(transferDatabaseTypeForConnection({ db_type: "jdbc", connection_string: "jdbc:doris://localhost:9030/dbx_test" })).toBe("jdbc"); + }); + it("uses Hive tree and execution semantics for Inceptor JDBC metadata", () => { const connection = { db_type: "jdbc" as const, diff --git a/apps/desktop/src/lib/database/jdbcDialect.ts b/apps/desktop/src/lib/database/jdbcDialect.ts index 0673100ba3..535670a156 100644 --- a/apps/desktop/src/lib/database/jdbcDialect.ts +++ b/apps/desktop/src/lib/database/jdbcDialect.ts @@ -82,6 +82,22 @@ export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConne return inferJdbcDialect(connection) ?? "jdbc"; } +/** + * Database type the data-transfer pipeline treats a connection as. Doris-family + * engines (Doris/SelectDB/StarRocks) are saved as `db_type=mysql` with a + * doris/starrocks `driver_profile`, and the transfer backend routes them through + * the MySQL object family (catalog routing is driver_profile-aware); the + * standalone `doris`/`starrocks` manifest entries are not transfer-capable. + * Resolve those connections back to their raw db_type so they stay selectable in + * the transfer dialog and keep the MySQL object kinds — the effective type alone + * would drop them from the connection list and disable every non-table kind. + */ +export function transferDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined { + const effective = effectiveDatabaseTypeForConnection(connection); + if (effective === "doris" || effective === "starrocks") return connection?.db_type; + return effective; +} + export function connectionUsesConnectionRootSchemaMode(connection?: JdbcDialectConnection): boolean { const type = effectiveDatabaseTypeForConnection(connection); return !!type && CONNECTION_ROOT_SCHEMA_TYPES.has(type);