From 5d65be03a301453080e3d5e1dcf7a4d6ffef7e75 Mon Sep 17 00:00:00 2001 From: Elias <45232878+mapan0424@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:25:16 +0800 Subject: [PATCH] fix(xugu): exclude synthetic scopes from schema export --- .../export/DatabaseExportDialog.vue | 6 +-- .../export/__tests__/databaseExport.spec.ts | 37 +++++++++++++++++++ apps/desktop/src/lib/export/databaseExport.ts | 16 +++++++- 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/lib/export/__tests__/databaseExport.spec.ts diff --git a/apps/desktop/src/components/export/DatabaseExportDialog.vue b/apps/desktop/src/components/export/DatabaseExportDialog.vue index 0c2e84eab..03fe06268 100644 --- a/apps/desktop/src/components/export/DatabaseExportDialog.vue +++ b/apps/desktop/src/components/export/DatabaseExportDialog.vue @@ -12,7 +12,7 @@ import * as api from "@/lib/backend/api"; import type { ExportProgress } from "@/lib/backend/api"; import { isSchemaAware, isSingleDatabase } from "@/lib/database/databaseFeatureSupport"; import { databaseOptionsForConnection, fetchNamespaceOptionsForConnection } from "@/composables/useDatabaseOptions"; -import { buildAllDatabaseExportPlan, generateDatabaseExportId, runDatabaseExportUntilTerminal, runWithDatabaseBackupSnapshot, shouldUseDatabaseBackupSnapshot, type AllDatabaseExportPlanItem } from "@/lib/export/databaseExport"; +import { buildAllDatabaseExportPlan, filterExportableSchemas, generateDatabaseExportId, runDatabaseExportUntilTerminal, runWithDatabaseBackupSnapshot, shouldUseDatabaseBackupSnapshot, type AllDatabaseExportPlanItem } from "@/lib/export/databaseExport"; import { buildSelectedTablesPayload, isDatabaseExportTableSelectionValid } from "@/lib/export/databaseExportSelection"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; import { useToast } from "@/composables/useToast"; @@ -215,7 +215,7 @@ async function loadSchemas(preferredSchema = "") { return; } - const schemaList = await api.listSchemas(connectionId.value, database.value); + const schemaList = filterExportableSchemas(await api.listSchemas(connectionId.value, database.value), config?.db_type); const selected = preferredSchema && schemaList.includes(preferredSchema) ? preferredSchema : schemaList.includes("public") ? "public" : (schemaList[0] ?? ""); schemas.value = schemaList; schema.value = selected; @@ -289,7 +289,7 @@ async function buildExportPlanForDatabases(dbs: string[]): Promise = {}; if (schemaAware) { for (const db of dbs) { - schemasByDatabase[db] = await api.listSchemas(connectionId.value, db); + schemasByDatabase[db] = filterExportableSchemas(await api.listSchemas(connectionId.value, db), dbType); } } return buildAllDatabaseExportPlan({ databases: dbs, schemaAware, schemasByDatabase, dbType }); diff --git a/apps/desktop/src/lib/export/__tests__/databaseExport.spec.ts b/apps/desktop/src/lib/export/__tests__/databaseExport.spec.ts new file mode 100644 index 000000000..80b7cd639 --- /dev/null +++ b/apps/desktop/src/lib/export/__tests__/databaseExport.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { buildAllDatabaseExportPlan, filterExportableSchemas } from "@/lib/export/databaseExport"; +import { XUGU_PUBLIC_SYNONYM_SCOPE, XUGU_SCHEDULER_JOB_SCOPE } from "@/lib/sidebar/xuguPublicSynonyms"; + +describe("database export schema selection", () => { + it("hides Xugu synthetic tree scopes while preserving real schemas", () => { + const schemas = ["APP_TEST", "GUEST", XUGU_PUBLIC_SYNONYM_SCOPE, XUGU_SCHEDULER_JOB_SCOPE]; + + expect(filterExportableSchemas(schemas, "xugu")).toEqual(["APP_TEST", "GUEST"]); + }); + + it("does not apply Xugu filtering to other database types", () => { + const schemas = ["APP_TEST", XUGU_PUBLIC_SYNONYM_SCOPE, XUGU_SCHEDULER_JOB_SCOPE]; + + expect(filterExportableSchemas(schemas, "postgres")).toEqual(schemas); + }); + + it("excludes synthetic scopes from an all-database Xugu export plan", () => { + expect( + buildAllDatabaseExportPlan({ + databases: ["SHOP_DEMO"], + schemaAware: true, + dbType: "xugu", + schemasByDatabase: { + SHOP_DEMO: ["APP_TEST", XUGU_PUBLIC_SYNONYM_SCOPE, XUGU_SCHEDULER_JOB_SCOPE], + }, + }), + ).toEqual([ + { + database: "SHOP_DEMO", + schema: "APP_TEST", + fileStem: "SHOP_DEMO", + displayName: "SHOP_DEMO", + }, + ]); + }); +}); diff --git a/apps/desktop/src/lib/export/databaseExport.ts b/apps/desktop/src/lib/export/databaseExport.ts index 7be694707..2b6477d73 100644 --- a/apps/desktop/src/lib/export/databaseExport.ts +++ b/apps/desktop/src/lib/export/databaseExport.ts @@ -3,6 +3,7 @@ import * as api from "@/lib/backend/api.ts"; import { buildTableSelectSql } from "@/lib/table/tableSelectSql.ts"; import { uuid } from "@/lib/common/utils.ts"; import { SINGLE_DATABASE_TYPES } from "@/lib/database/databaseCapabilitySets"; +import { isXuguSyntheticScope } from "@/lib/sidebar/xuguPublicSynonyms"; export const DATABASE_EXPORT_ROW_LIMIT = 10_000; export const DATABASE_EXPORT_PAGE_SIZE = 500; @@ -76,6 +77,19 @@ export interface AllDatabaseExportPlanItem { displayName: string; } +/** + * Return schemas that can be exported as ordinary schema-owned objects. + * + * Xugu exposes database-global namespaces (public synonyms and scheduler jobs) + * through reserved synthetic schema names so the sidebar can reuse its normal + * tree loading path. They are not real schemas and must not be offered by the + * schema export selector. + */ +export function filterExportableSchemas(schemas: readonly string[], databaseType?: DatabaseType): string[] { + if (databaseType !== "xugu") return [...schemas]; + return schemas.filter((schema) => !isXuguSyntheticScope(schema)); +} + export interface DatabaseBackupSnapshotOptions { connectionId: string; database: string; @@ -153,7 +167,7 @@ export function buildAllDatabaseExportPlan(options: AllDatabaseExportPlanInput): })); } return options.databases.flatMap((database) => { - const schemas = options.schemaAware ? (options.schemasByDatabase?.[database] ?? []).filter((schema) => schema.trim()) : [database]; + const schemas = options.schemaAware ? filterExportableSchemas(options.schemasByDatabase?.[database] ?? [], options.dbType).filter((schema) => schema.trim()) : [database]; const exportSchemas = schemas.length > 0 ? schemas : [database]; const includeSchemaInFileName = options.schemaAware && exportSchemas.length > 1;