feat(transfer): add tree-based object selection

This commit is contained in:
GIWTO authored and GitHub committed 2026-08-02 16:34:50 +08:00
1 parent fb7b2878fd
commit 473ef910b6
28 files changed
+5395 -205

No files matched your search

+3
View File
@@ -719,6 +719,9 @@ export type {
TransferRequest,
TransferProgress,
TransferMode,
TransferContent,
TransferObjectKind,
TransferObjectSelection,
TransferTableNameCase,
TransferOwnershipPolicy,
TransferOwnershipPreview,
+1 -1
View File
@@ -692,7 +692,7 @@ export async function getTableComment(_connectionId: string, _database: string,
throw new Error("Table comment lookup is not available in the web backend");
}
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: SidebarObjectKind[], filter?: string, limit?: number, offset?: number, catalog?: string): Promise<ObjectInfo[]> {
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: (SidebarObjectKind | "EVENT")[], filter?: string, limit?: number, offset?: number, catalog?: string): Promise<ObjectInfo[]> {
return get(
`/api/schema/objects?${qs({
connection_id: connectionId,
+10 -1
View File
@@ -974,7 +974,7 @@ export async function getTableComment(connectionId: string, database: string, sc
});
}
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: SidebarObjectKind[], filter?: string, limit?: number, offset?: number, catalog?: string): Promise<ObjectInfo[]> {
export async function listObjects(connectionId: string, database: string, schema: string, objectTypes?: (SidebarObjectKind | "EVENT")[], filter?: string, limit?: number, offset?: number, catalog?: string): Promise<ObjectInfo[]> {
return invoke("list_objects", {
connectionId,
database,
@@ -3301,6 +3301,13 @@ export async function listenSqlFileProgress(handler: (progress: SqlFileProgress)
export type TransferMode = "append" | "overwrite" | "upsert";
export type TransferTableNameCase = "preserve" | "lower" | "upper";
export type TransferOwnershipPolicy = "preserve" | "skip" | "reassignMissing";
export type TransferContent = "structureAndData" | "structureOnly" | "dataOnly";
export type TransferObjectKind = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "SEQUENCE" | "EVENT";
export interface TransferObjectSelection {
objectType: TransferObjectKind;
names: string[];
}
export interface TransferRequest {
transferId: string;
@@ -3314,6 +3321,8 @@ export interface TransferRequest {
targetCatalog?: string;
tables: string[];
createTable: boolean;
content: TransferContent;
objects: TransferObjectSelection[];
mode: TransferMode;
targetTableNameCase: TransferTableNameCase;
ownershipPolicy?: TransferOwnershipPolicy;
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { transferObjectFamily, transferObjectKindsForDatabase, isSameTransferFamily, crossFamilyTransferableKinds, TransferObjectFamily } from "@/lib/database/transferObjectKinds";
describe("transferObjectKinds", () => {
it("groups databases into transfer families", () => {
expect(transferObjectFamily("mysql")).toBe(TransferObjectFamily.Mysql);
expect(transferObjectFamily("kingbase")).toBe(TransferObjectFamily.Postgres);
expect(transferObjectFamily("dameng")).toBe(TransferObjectFamily.Oracle);
expect(transferObjectFamily("sqlserver")).toBe(TransferObjectFamily.SqlServer);
expect(transferObjectFamily("sqlite")).toBeUndefined();
});
it("returns per-family object kinds", () => {
expect(transferObjectKindsForDatabase("mysql")).toEqual(["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"]);
expect(transferObjectKindsForDatabase("postgres")).toContain("SEQUENCE");
expect(transferObjectKindsForDatabase("dameng")).toContain("TRIGGER");
expect(transferObjectKindsForDatabase("sqlserver")).toEqual(["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE"]);
expect(transferObjectKindsForDatabase("sqlite")).toEqual([]);
});
it("detects same-family transfers", () => {
expect(isSameTransferFamily("mysql", "mysql")).toBe(true);
expect(isSameTransferFamily("mysql", "postgres")).toBe(false);
expect(isSameTransferFamily("oracle", "dameng")).toBe(true);
expect(isSameTransferFamily("postgres", "sqlite")).toBe(false);
expect(isSameTransferFamily("sqlserver", "sqlserver")).toBe(true);
expect(isSameTransferFamily("sqlserver", "mysql")).toBe(false);
});
it("limits cross-family transferable kinds to sequences only", () => {
// mysql participates: VIEW is disabled (query body not translated) and
// mysql has no sequences on either side
expect(crossFamilyTransferableKinds("mysql", "dameng")).toEqual([]);
expect(crossFamilyTransferableKinds("dameng", "mysql")).toEqual([]);
expect(crossFamilyTransferableKinds("mysql", "sqlserver")).toEqual([]);
// sqlserver <-> dameng: sequences only (plain DDL)
expect(crossFamilyTransferableKinds("sqlserver", "dameng")).toEqual(["SEQUENCE"]);
expect(crossFamilyTransferableKinds("dameng", "sqlserver")).toEqual(["SEQUENCE"]);
// same family: all source kinds
const same = crossFamilyTransferableKinds("mysql", "mysql");
expect(same).toContain("TRIGGER");
expect(same).toContain("EVENT");
// unsupported db
expect(crossFamilyTransferableKinds("sqlite", "mysql")).toEqual([]);
});
});
@@ -0,0 +1,86 @@
import type { DatabaseType } from "@/types/database";
export enum TransferObjectFamily {
Mysql = "mysql",
Postgres = "postgres",
Oracle = "oracle",
SqlServer = "sqlserver",
}
export type TransferObjectKind = "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | "PROCEDURE" | "FUNCTION" | "TRIGGER" | "SEQUENCE" | "EVENT";
const MYSQL_KINDS: TransferObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"];
const POSTGRES_KINDS: TransferObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE"];
const ORACLE_KINDS: TransferObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE"];
const SQLSERVER_KINDS: TransferObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE"];
const FAMILY_BY_DB = new Map<DatabaseType, TransferObjectFamily>([
["mysql", TransferObjectFamily.Mysql],
["postgres", TransferObjectFamily.Postgres],
["kingbase", TransferObjectFamily.Postgres],
["gaussdb", TransferObjectFamily.Postgres],
["kwdb", TransferObjectFamily.Postgres],
["opengauss", TransferObjectFamily.Postgres],
["oracle", TransferObjectFamily.Oracle],
["dameng", TransferObjectFamily.Oracle],
["oceanbase-oracle", TransferObjectFamily.Oracle],
["sqlserver", TransferObjectFamily.SqlServer],
]);
export function transferObjectFamily(dbType?: DatabaseType): TransferObjectFamily | undefined {
return dbType ? FAMILY_BY_DB.get(dbType) : undefined;
}
export function isSameTransferFamily(a?: DatabaseType, b?: DatabaseType): boolean {
const fa = transferObjectFamily(a);
const fb = transferObjectFamily(b);
return !!fa && fa === fb;
}
export function transferObjectKindsForDatabase(dbType?: DatabaseType): TransferObjectKind[] {
switch (transferObjectFamily(dbType)) {
case TransferObjectFamily.Mysql:
return [...MYSQL_KINDS];
case TransferObjectFamily.Postgres:
return [...POSTGRES_KINDS];
case TransferObjectFamily.Oracle:
return [...ORACLE_KINDS];
case TransferObjectFamily.SqlServer:
return [...SQLSERVER_KINDS];
default:
return [];
}
}
/**
* Kinds selectable for a transfer between the two databases. Within one
* family every kind of the source is allowed; across families only
* sequences are allowed (plain DDL without a query body), and only when
* both sides support the type. Views are excluded: the backend rewrites
* only the DDL wrapper, quoting and schema qualifiers, not the view
* query body, so cross-family view bodies could run unchanged on an
* incompatible target.
*/
export function crossFamilyTransferableKinds(a?: DatabaseType, b?: DatabaseType): TransferObjectKind[] {
if (isSameTransferFamily(a, b)) {
return transferObjectKindsForDatabase(a);
}
const aFam = transferObjectFamily(a);
const bFam = transferObjectFamily(b);
// Cross-family DDL conversion is only implemented and validated for
// MySQL / SQL Server / Oracle families. Postgres-family sources or
// targets (postgres/kingbase/gaussdb/kwdb/opengauss) are not supported
// by the cross-family executor, so nothing is selectable there.
const supported = new Set([TransferObjectFamily.Mysql, TransferObjectFamily.SqlServer, TransferObjectFamily.Oracle]);
if (!aFam || !bFam || !supported.has(aFam) || !supported.has(bFam)) return [];
const aKinds = transferObjectKindsForDatabase(a);
const bKinds = transferObjectKindsForDatabase(b);
// Cross-family VIEW transfer is disabled: the backend only rewrites the
// DDL wrapper/quoting/schema qualifiers and cannot translate the view
// query body (IFNULL, TOP, GETDATE, … would run unchanged on an
// incompatible target). Sequences are plain DDL and remain transferable
// when both sides support them.
const allowed: TransferObjectKind[] = [];
if (aKinds.includes("SEQUENCE") && bKinds.includes("SEQUENCE")) allowed.push("SEQUENCE");
return allowed;
}