diff --git a/apps/desktop/src/components/diff/SchemaDiffDialog.vue b/apps/desktop/src/components/diff/SchemaDiffDialog.vue index 3b367b3e58..c486059f61 100644 --- a/apps/desktop/src/components/diff/SchemaDiffDialog.vue +++ b/apps/desktop/src/components/diff/SchemaDiffDialog.vue @@ -18,11 +18,12 @@ import SchemaDiffOptionsPanel from "@/components/diff/SchemaDiffOptionsPanel.vue import { getSchemaDiffOptionsForDbType } from "@/lib/schema/schemaDiffOptions"; import { buildDeployTxResult } from "@/lib/schema/deployTxResult"; -import { createConcurrencyLimiter, mapWithConcurrency, schemaDiffMetadataConcurrency, schemaDiffMetadataLoadPlan } from "@/lib/schema/schemaDiffMetadataLoad"; +import { loadSchemaDetails } from "@/lib/schema/schemaDiffMetadataLoad"; +import { getSchemaDiffNextProgressStep, isSchemaDiffPostgresLike, shouldLoadSchemaDiffExtraObjects, type SchemaDiffProgressPhase } from "@/lib/schema/schemaDiffProgress"; import { createSchemaDiffTableListLoader, type SchemaDiffTableIdentity } from "@/lib/schema/schemaDiffTableList"; import { normalizeSchemaDiffCompareOptions } from "@/types/schemaDiff"; import type { SchemaDiffCompareOptions, SchemaDiffConfig, FieldMappingEntry } from "@/types/schemaDiff"; -import type { DatabaseType, ObjectSourceKind, TableInfo } from "@/types/database"; +import type { DatabaseType, ObjectSourceKind } from "@/types/database"; import { convertToSchemaDiffObjects, detectDestructiveSchemaDiffStatements, @@ -46,13 +47,12 @@ import { type SchemaDiffPreparation, type MissingRollbackObject, type RollbackCompleteness, - type TableSchemaDetail, type RenameCandidate, type CompatibilityWarning, type PermissionDiff, type DependencyGraph, } from "@/lib/schema/schemaDiff"; -import { compileSchemaDiffTableFilter, filterSchemaDiffTables, isSchemaDiffView } from "@/lib/schema/schemaDiffTableFilter"; +import { compileSchemaDiffTableFilter, filterSchemaDiffTables } from "@/lib/schema/schemaDiffTableFilter"; import { Splitpanes, Pane } from "splitpanes"; import "splitpanes/dist/splitpanes.css"; @@ -105,7 +105,17 @@ const optionTree = computed(() => { }); // Compare state +interface SchemaDiffProgress { + phase: SchemaDiffProgressPhase; + current?: number; + total?: number; + objectName?: string; +} + const loading = ref(false); +const schemaDiffProgress = ref(null); +const schemaDiffHasExtraObjectPhase = ref(false); +let comparisonRequestId = 0; const diffObjects = ref([]); const diffGroups = ref([]); const selectedObjectId = ref(null); @@ -278,9 +288,47 @@ const canDeploy = computed(() => { return selectedSchemaDiffObjects(diffObjects.value).length > 0; }); +const schemaDiffProgressCount = computed(() => { + const progress = schemaDiffProgress.value; + return progress && progress.total !== undefined && progress.total > 0 && progress.current !== undefined ? { current: progress.current, total: progress.total } : null; +}); + +const schemaDiffProgressPercent = computed(() => { + const count = schemaDiffProgressCount.value; + return count ? Math.min(100, Math.round((count.current / count.total) * 100)) : null; +}); + +const schemaDiffProgressLabel = computed(() => { + switch (schemaDiffProgress.value?.phase) { + case "loading-table-lists": + return t("diff.progress.loadingObjects"); + case "loading-source-details": + return t("diff.progress.loadingSourceDetails"); + case "loading-target-details": + return t("diff.progress.loadingTargetDetails"); + case "loading-extra-objects": + return t("diff.progress.loadingExtraObjects"); + case "comparing": + return t("diff.progress.comparing"); + case "generating": + return t("diff.progress.generating"); + default: + return ""; + } +}); + +const schemaDiffNextProgressLabel = computed(() => { + const nextStep = getSchemaDiffNextProgressStep(schemaDiffProgress.value?.phase, schemaDiffHasExtraObjectPhase.value); + return nextStep ? t("diff.progress.next", { step: t(`diff.progress.${nextStep}`) }) : ""; +}); + function resetComparisonResultState() { + comparisonRequestId++; selectedDeploySqlGeneration++; focusedDeploySqlGeneration++; + loading.value = false; + schemaDiffProgress.value = null; + schemaDiffHasExtraObjectPhase.value = false; step.value = "config"; diffObjects.value = []; diffGroups.value = []; @@ -320,6 +368,11 @@ watch( sourceSchema.value = props.prefillSchema; } } + } else { + comparisonRequestId++; + loading.value = false; + schemaDiffProgress.value = null; + schemaDiffHasExtraObjectPhase.value = false; } }, { immediate: true }, @@ -385,51 +438,14 @@ function handleFieldMappingsUpdate(mappings: FieldMappingEntry[]) { } } -/** Map a JDBC table_type to an ObjectSourceKind for getTableDdl routing. - * Views and materialized views need the object_type parameter so the - * backend can call DBMS_METADATA.GET_DDL with the correct type. */ -function isViewOrMaterializedView(tableType: string): ObjectSourceKind | undefined { - switch (tableType.toUpperCase().replace(/\s+/g, "_")) { - case "VIEW": - return "VIEW"; - case "MATERIALIZED_VIEW": - return "MATERIALIZED_VIEW"; - default: - return undefined; - } -} - -interface SchemaDetailLoadContext { - connectionId: string; - database: string; - schema: string; - dbType: string; - options: SchemaDiffCompareOptions; -} - -async function loadSchemaDetails(tables: TableInfo[], context: SchemaDetailLoadContext): Promise { - const concurrency = schemaDiffMetadataConcurrency(context.dbType, tables.length); - const runMetadataQuery = createConcurrencyLimiter(concurrency); - - return mapWithConcurrency(tables, concurrency, async (table) => { - const objectType = isViewOrMaterializedView(table.table_type); - const loadPlan = schemaDiffMetadataLoadPlan(isSchemaDiffView(table), context.options); - const ddlPromise = loadPlan.ddl ? runMetadataQuery(() => api.getTableDdl(context.connectionId, context.database, context.schema, table.name, objectType)) : Promise.resolve(""); - const [columns, indexes, foreignKeys, triggers, ddl] = await Promise.all([ - loadPlan.columns ? runMetadataQuery(() => api.getColumns(context.connectionId, context.database, context.schema, table.name)) : Promise.resolve([]), - loadPlan.indexes ? runMetadataQuery(() => api.listIndexes(context.connectionId, context.database, context.schema, table.name)) : Promise.resolve([]), - loadPlan.foreignKeys ? runMetadataQuery(() => api.listForeignKeys(context.connectionId, context.database, context.schema, table.name)) : Promise.resolve([]), - loadPlan.triggers ? runMetadataQuery(() => api.listTriggers(context.connectionId, context.database, context.schema, table.name)) : Promise.resolve([]), - ddlPromise, - ]); - - return { name: table.name, columns, indexes, foreignKeys, triggers, ddl }; - }); -} - async function handleCompare() { + const requestId = ++comparisonRequestId; + const isCurrentRequest = () => requestId === comparisonRequestId; + loading.value = true; step.value = "compare"; + schemaDiffProgress.value = { phase: "loading-table-lists" }; + schemaDiffHasExtraObjectPhase.value = false; // Reset Phase 4 state to prevent stale data from previous compares rollbackSql.value = ""; @@ -446,6 +462,9 @@ async function handleCompare() { const dbType = targetConfig?.db_type || "mysql"; const sourceDbType = sourceConfig?.db_type || dbType; const opts = normalizeSchemaDiffCompareOptions(activeConfig.value?.options, dbType); + const isPostgresLike = isSchemaDiffPostgresLike(dbType); + const hasExtraObjectPhase = shouldLoadSchemaDiffExtraObjects(dbType, opts); + schemaDiffHasExtraObjectPhase.value = hasExtraObjectPhase; const tableFilter = compileSchemaDiffTableFilter(opts); const sourceTableIdentity: SchemaDiffTableIdentity = { connectionId: sourceConnectionId.value, database: sourceDatabase.value, schema: sourceSchema.value }; @@ -453,28 +472,45 @@ async function handleCompare() { // The dialog can remain open while either database changes, so Compare must not reuse // the table list cached while configuring the comparison. const [srcTables, tgtTables] = await Promise.all([schemaDiffTableListLoader.load(sourceTableIdentity, { refresh: true }), schemaDiffTableListLoader.load(targetTableIdentity, { refresh: true })]); + if (!isCurrentRequest()) return; // Explicit (visual) table selection is applied here, BEFORE any per-table // metadata details are loaded, so metadata requests only happen for the // final table set. `undefined`/empty means no restriction (legacy path). const { sourceTables, targetTables } = filterSchemaDiffTables(srcTables, tgtTables, tableFilter, opts, opts.selectedTables); - const sourceDetails = await loadSchemaDetails(sourceTables, { - connectionId: sourceConnectionId.value, - database: sourceDatabase.value, - schema: sourceSchema.value, - dbType: sourceDbType, - options: opts, - }); + schemaDiffProgress.value = { phase: "loading-source-details", current: 0, total: sourceTables.length }; + const sourceDetails = await loadSchemaDetails( + sourceTables, + { + connectionId: sourceConnectionId.value, + database: sourceDatabase.value, + schema: sourceSchema.value, + dbType: sourceDbType, + options: opts, + onProgress: (progress) => { + if (isCurrentRequest()) schemaDiffProgress.value = { phase: "loading-source-details", ...progress }; + }, + }, + api, + ); + if (!isCurrentRequest()) return; - const targetDetails = await loadSchemaDetails(targetTables, { - connectionId: targetConnectionId.value, - database: targetDatabase.value, - schema: targetSchema.value, - dbType, - options: opts, - }); - - const isPostgresLike = dbType === "postgres" || dbType === "opengauss"; + schemaDiffProgress.value = { phase: "loading-target-details", current: 0, total: targetTables.length }; + const targetDetails = await loadSchemaDetails( + targetTables, + { + connectionId: targetConnectionId.value, + database: targetDatabase.value, + schema: targetSchema.value, + dbType, + options: opts, + onProgress: (progress) => { + if (isCurrentRequest()) schemaDiffProgress.value = { phase: "loading-target-details", ...progress }; + }, + }, + api, + ); + if (!isCurrentRequest()) return; // Fetch new object types for PostgreSQL-like databases const promises: Promise[] = []; @@ -495,7 +531,9 @@ async function handleCompare() { promises.push(api.listOwners(targetConnectionId.value, targetDatabase.value, targetSchema.value)); } + if (hasExtraObjectPhase) schemaDiffProgress.value = { phase: "loading-extra-objects" }; const results = await Promise.all(promises); + if (!isCurrentRequest()) return; let idx = 0; const srcFunctions = opts?.functions && isPostgresLike ? results[idx++] : []; const tgtFunctions = opts?.functions && isPostgresLike ? results[idx++] : []; @@ -506,6 +544,7 @@ async function handleCompare() { const srcOwners = opts?.owners && isPostgresLike ? results[idx++] : []; const tgtOwners = opts?.owners && isPostgresLike ? results[idx++] : []; + schemaDiffProgress.value = { phase: "comparing" }; const result = await api.prepareSchemaDiff({ sourceTables, targetTables, @@ -545,6 +584,7 @@ async function handleCompare() { customParams: m.customParams, })) || [], }); + if (!isCurrentRequest()) return; // Extract new result fields rollbackSql.value = result.rollbackSyncSql ?? ""; @@ -565,15 +605,21 @@ async function handleCompare() { // current selection projection below rather than from the initial full SQL. lastDiffResult.value = result; deploySqlMode.value = "forward"; + schemaDiffProgress.value = { phase: "generating" }; await regenerateSelectedDeploySql(); await regenerateFocusedDeploySql(); + if (!isCurrentRequest()) return; + schemaDiffProgress.value = null; step.value = "result"; } catch (e: any) { + if (!isCurrentRequest()) return; + schemaDiffProgress.value = null; + schemaDiffHasExtraObjectPhase.value = false; toast(e?.message || String(e), 5000); step.value = "config"; } finally { - loading.value = false; + if (isCurrentRequest()) loading.value = false; } } @@ -1021,8 +1067,31 @@ const targetConnectionInfo = computed(() => {
- - {{ t("diff.comparing") }} +
+
+ + {{ schemaDiffProgressLabel }} +
+
+ {{ t("diff.progress.count", schemaDiffProgressCount) }} + {{ schemaDiffProgressPercent }}% +
+
+
+
+
+
+
+
{{ schemaDiffProgress.objectName }}
+
{{ schemaDiffNextProgressLabel }}
+
@@ -1278,6 +1347,23 @@ const targetConnectionInfo = computed(() => {