import type { DatabaseType, QueryResult } from "@/types/database.ts"; 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; export const DATABASE_EXPORT_INSERT_BATCH_SIZE = 100; export interface ExportedTableSql { displayName: string; databaseType?: DatabaseType; schema?: string; tableName?: string; qualifiedTableName?: string; ddl?: string; columns: string[]; columnTypes?: Array; columnExtras?: Array; spatialColumns?: QueryResult["spatial_columns"]; spatialValues?: QueryResult["spatial_values"]; rows: QueryResult["rows"]; truncated?: boolean; } export interface BuildDatabaseSqlExportOptions { databaseName: string; exportedAt?: Date | string; tables: ExportedTableSql[]; rowLimitPerTable?: number; insertBatchSize?: number; connectionId?: string; database?: string; schema?: string; } export interface BuildExportInsertStatementsOptions { databaseType?: DatabaseType; schema?: string; tableName?: string; qualifiedTableName?: string; columns: string[]; columnTypes?: Array; columnExtras?: Array; spatialColumns?: QueryResult["spatial_columns"]; spatialValues?: QueryResult["spatial_values"]; rows: QueryResult["rows"]; /** 生成 INSERT 时需要排除的列名(例如导出时不带主键),忽略大小写匹配。 */ excludeColumns?: string[]; batchSize?: number; } export interface BuildExportPageSqlOptions { databaseType?: DatabaseType; driverProfile?: string; identifierQuote?: string; schema?: string; tableName: string; limit?: number; offset?: number; } export interface AllDatabaseExportPlanInput { databases: string[]; schemaAware: boolean; schemasByDatabase?: Record; /** 数据库类型,用于判断是否为单数据库架构(如达梦、Oracle)。 * 对于这类数据库,选中的"数据库"实际上就是 schema 本身, * 不应再对每个"数据库"展开所有 schema 做笛卡尔积。 */ dbType?: DatabaseType; } export interface AllDatabaseExportPlanItem { database: string; schema: string; fileStem: string; 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; enabled: boolean; } export function buildInsertStatements(options: BuildExportInsertStatementsOptions): Promise { return api.buildExportInsertStatements(options); } export async function buildExportPageSql(options: BuildExportPageSqlOptions): Promise { return buildTableSelectSql({ databaseType: options.databaseType, driverProfile: options.driverProfile, identifierQuote: options.identifierQuote, schema: options.schema, tableName: options.tableName, limit: options.limit ?? DATABASE_EXPORT_PAGE_SIZE, offset: options.offset, }); } export function generateDatabaseExportId(): string { return uuid(); } export function shouldUseDatabaseBackupSnapshot(databaseType: DatabaseType | undefined, includeData: boolean, desktopRuntime: boolean): boolean { return desktopRuntime && includeData && (databaseType === "mysql" || databaseType === "postgres"); } export async function runDatabaseExportUntilTerminal(request: api.DatabaseExportRequest, onProgress: (progress: api.ExportProgress) => void): Promise { return new Promise((resolve, reject) => { api .exportDatabaseSql(request, (progress) => { onProgress(progress); if (progress.status === "Done" || progress.status === "Cancelled") { resolve(progress); } else if (progress.status === "Error") { reject(new Error(progress.error || "Export failed")); } }) .catch(reject); }); } export async function runWithDatabaseBackupSnapshot(options: DatabaseBackupSnapshotOptions, operation: (snapshotSessionId: string | undefined) => Promise, shouldPropagateCleanupError: (result: T) => boolean = () => true): Promise { if (!options.enabled) return operation(undefined); const snapshot = await api.beginDatabaseBackupSnapshot(options.connectionId, options.database); let result: T | undefined; let operationCompleted = false; try { result = await operation(snapshot.sessionId); operationCompleted = true; return result; } finally { await api.rollbackManualTransaction(snapshot.sessionId).catch((error) => { if (operationCompleted && shouldPropagateCleanupError(result as T)) throw error; }); } } export function buildAllDatabaseExportPlan(options: AllDatabaseExportPlanInput): AllDatabaseExportPlanItem[] { // 对于 schema-aware 的单库类型(达梦、Oracle、OceanBase-Oracle 等),选中的"数据库" // 实际上就是 schema 本身,不应再展开所有 schema 做笛卡尔积,直接将选中项作为 schema 导出。 // firebird/questdb/access 等单库但非 schema-aware 类型必须走 flatMap 保留真实 database, // 否则 database:"" 会覆盖后端 db_config.database 破坏连接。 const singleDatabase = options.dbType ? SINGLE_DATABASE_TYPES.has(options.dbType) : false; if (singleDatabase && options.schemaAware) { return options.databases.map((schema) => ({ database: "", schema, fileStem: schema, displayName: schema, })); } // PostgreSQL supports multiple schemas in one database. Keep the database // as the export unit so the backend can produce one restore script for all // schemas instead of one file per schema. if (options.dbType === "postgres" && options.schemaAware) { return options.databases.map((database) => ({ database, schema: "", fileStem: database, displayName: database, })); } return options.databases.flatMap((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; return exportSchemas.map((schema) => ({ database, schema, fileStem: includeSchemaInFileName ? `${database}.${schema}` : database, displayName: includeSchemaInFileName ? `${database}.${schema}` : database, })); }); } export function buildDatabaseSqlExport(options: BuildDatabaseSqlExportOptions): Promise { return api.buildDatabaseSqlExport({ ...options, exportedAt: options.exportedAt instanceof Date ? options.exportedAt.toISOString() : options.exportedAt, }); }