From 163794802300e334b19f3bba73273de867a0cf64 Mon Sep 17 00:00:00 2001 From: jearhe <0verme7753@gmail.com> Date: Sun, 30 Aug 2026 00:47:45 +0800 Subject: [PATCH] fix(schema-diff): keep deploy script preview in sync with selection --- .../components/diff/SchemaDiffDdlPanel.vue | 3 +- .../src/components/diff/SchemaDiffDialog.vue | 166 +++++++++++------- .../diff/__tests__/SchemaDiffDdlPanel.spec.ts | 5 + .../diff/__tests__/SchemaDiffDialog.spec.ts | 15 ++ .../schema/schemaDiffSelection.spec.ts | 77 ++++++++ apps/desktop/src/lib/schema/schemaDiff.ts | 20 +++ 6 files changed, 225 insertions(+), 61 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/schema/schemaDiffSelection.spec.ts diff --git a/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue b/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue index 16645b7cd8..84f0f273c4 100644 --- a/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue +++ b/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue @@ -39,6 +39,7 @@ const props = defineProps<{ focusedObject?: SchemaDiffObject | null; deploySql: string; deploySqlAll: string; + rollbackForwardSql?: string; compatibilityWarnings?: CompatibilityWarning[]; rollbackSql?: string; deploySqlMode?: "forward" | "rollback"; @@ -71,7 +72,7 @@ const rollbackConnectorKey = ref(0); const rollbackHunks = computed(() => { if (!props.rollbackSql) return []; - return buildHunks(props.deploySql, props.rollbackSql); + return buildHunks(props.rollbackForwardSql ?? props.deploySql, props.rollbackSql); }); const hunks = computed(() => { diff --git a/apps/desktop/src/components/diff/SchemaDiffDialog.vue b/apps/desktop/src/components/diff/SchemaDiffDialog.vue index eb2b14554f..3b367b3e58 100644 --- a/apps/desktop/src/components/diff/SchemaDiffDialog.vue +++ b/apps/desktop/src/components/diff/SchemaDiffDialog.vue @@ -33,6 +33,7 @@ import { flattenSchemaDiffObjects, schemaDiffSelectionTargets, selectSchemaDiffInput, + selectSchemaDiffInputForObject, selectedSchemaDiffObjects, setSchemaDiffObjectSelected, setSchemaDiffObjectSelectedWithDependencies, @@ -108,8 +109,11 @@ const loading = ref(false); const diffObjects = ref([]); const diffGroups = ref([]); const selectedObjectId = ref(null); -const deploySql = ref(""); -const deploySqlAll = ref(""); +const focusedDeploySql = ref(""); +const focusedForwardDeploySql = ref(""); +const focusedRollbackSql = ref(""); +const selectedDeploySql = ref(""); +const selectedForwardDeploySql = ref(""); const executing = ref(false); const lastDiffResult = ref(null); const targetDbVersion = ref(null); @@ -124,7 +128,8 @@ const renameCandidates = ref([]); const compatibilityWarnings = ref([]); const permissionDiffs = ref([]); const dependencyGraph = ref(null); -let deploySqlGeneration = 0; +let selectedDeploySqlGeneration = 0; +let focusedDeploySqlGeneration = 0; // Rename candidates panel const showRenamePanel = ref(true); @@ -274,13 +279,17 @@ const canDeploy = computed(() => { }); function resetComparisonResultState() { - deploySqlGeneration++; + selectedDeploySqlGeneration++; + focusedDeploySqlGeneration++; step.value = "config"; diffObjects.value = []; diffGroups.value = []; selectedObjectId.value = null; - deploySql.value = ""; - deploySqlAll.value = ""; + focusedDeploySql.value = ""; + focusedForwardDeploySql.value = ""; + focusedRollbackSql.value = ""; + selectedDeploySql.value = ""; + selectedForwardDeploySql.value = ""; lastDiffResult.value = null; rollbackSql.value = ""; rollbackCompleteness.value = "complete"; @@ -552,17 +561,12 @@ async function handleCompare() { // Group by operation type and object kind diffGroups.value = groupDiffObjects(diffObjects.value); - // Save full result and apply column rename detection to SQL + // Save the structured result; both script previews are generated from the + // current selection projection below rather than from the initial full SQL. lastDiffResult.value = result; - deploySqlAll.value = result.syncSql; - if (opts?.detectRenames && opts.renameThreshold) { - deploySqlAll.value = injectColumnRenameSql(deploySqlAll.value, result.diffs, opts.renameThreshold); - if (rollbackSql.value) { - rollbackSql.value = injectColumnRenameSql(rollbackSql.value, result.diffs, opts.renameThreshold, true); - } - } deploySqlMode.value = "forward"; - await regenerateDeploySql(); + await regenerateSelectedDeploySql(); + await regenerateFocusedDeploySql(); step.value = "result"; } catch (e: any) { @@ -585,7 +589,7 @@ function handleToggleGroupSelection(operationType: DiffOperationType, selected: } } rebuildDiffGroups(); - void regenerateDeploySql(); + void regenerateSelectedDeploySql(); } function handleToggleObjectSelection(object: SchemaDiffObject, selected: boolean) { @@ -595,7 +599,7 @@ function handleToggleObjectSelection(object: SchemaDiffObject, selected: boolean } if (!changed) return; rebuildDiffGroups(); - void regenerateDeploySql(); + void regenerateSelectedDeploySql(); } function updateObjectSelection(objectId: string, selected: boolean): boolean { @@ -615,43 +619,81 @@ function rebuildDiffGroups() { })); } -async function regenerateDeploySql() { - const result = lastDiffResult.value; - if (!result) { - deploySql.value = "-- No objects selected"; - rollbackSql.value = ""; - return; - } - - const generation = ++deploySqlGeneration; - const options = normalizeSchemaDiffCompareOptions(activeConfig.value?.options, getDbType()); - const input = selectSchemaDiffInput(result, diffObjects.value); - let plan; - try { - plan = await api.generateSchemaSyncPlan(input, { - databaseType: getDbType(), - targetSchema: schemaDiffDeployTargetSchema(getDbType(), targetDatabase.value, targetSchema.value), - cascadeDelete: options.cascadeDelete, - sourceDialect: options.sourceDialect ? normalizeDialectKind(options.sourceDialect) : sourceDbType.value ? databaseTypeToDialectKind(sourceDbType.value) : undefined, - fieldMappings: options.fieldMappings, - enableRollback: options.enableRollback, - }); - } catch (error: any) { - if (generation === deploySqlGeneration) toast(error?.message || String(error), 5000); - return; - } - if (generation !== deploySqlGeneration) return; +function buildSchemaSyncPlanOptions(options: SchemaDiffCompareOptions) { + return { + databaseType: getDbType(), + targetSchema: schemaDiffDeployTargetSchema(getDbType(), targetDatabase.value, targetSchema.value), + cascadeDelete: options.cascadeDelete, + sourceDialect: options.sourceDialect ? normalizeDialectKind(options.sourceDialect) : sourceDbType.value ? databaseTypeToDialectKind(sourceDbType.value) : undefined, + fieldMappings: options.fieldMappings, + enableRollback: options.enableRollback, + }; +} +function formatSchemaSyncPlan(plan: Awaited>, input: ReturnType, options: SchemaDiffCompareOptions) { let forwardSql = plan.syncSql || "-- No objects selected"; let nextRollbackSql = plan.rollbackSyncSql ?? ""; - rollbackCompleteness.value = plan.rollbackCompleteness ?? "complete"; - missingRollbackObjects.value = plan.missingRollbackObjects ?? []; if (options.detectRenames && options.renameThreshold) { forwardSql = injectColumnRenameSql(forwardSql, input.diffs, options.renameThreshold); if (nextRollbackSql) nextRollbackSql = injectColumnRenameSql(nextRollbackSql, input.diffs, options.renameThreshold, true); } - rollbackSql.value = nextRollbackSql; - deploySql.value = deploySqlMode.value === "rollback" && nextRollbackSql ? nextRollbackSql : forwardSql; + return { forwardSql, rollbackSql: nextRollbackSql }; +} + +async function regenerateSelectedDeploySql() { + const result = lastDiffResult.value; + const generation = ++selectedDeploySqlGeneration; + if (!result) { + selectedForwardDeploySql.value = "-- No objects selected"; + selectedDeploySql.value = "-- No objects selected"; + rollbackSql.value = ""; + return; + } + + const options = normalizeSchemaDiffCompareOptions(activeConfig.value?.options, getDbType()); + const input = selectSchemaDiffInput(result, diffObjects.value); + let plan; + try { + plan = await api.generateSchemaSyncPlan(input, buildSchemaSyncPlanOptions(options)); + } catch (error: any) { + if (generation === selectedDeploySqlGeneration) toast(error?.message || String(error), 5000); + return; + } + if (generation !== selectedDeploySqlGeneration) return; + + const formatted = formatSchemaSyncPlan(plan, input, options); + rollbackCompleteness.value = plan.rollbackCompleteness ?? "complete"; + missingRollbackObjects.value = plan.missingRollbackObjects ?? []; + selectedForwardDeploySql.value = formatted.forwardSql; + rollbackSql.value = formatted.rollbackSql; + selectedDeploySql.value = deploySqlMode.value === "rollback" && formatted.rollbackSql ? formatted.rollbackSql : formatted.forwardSql; +} + +async function regenerateFocusedDeploySql(objectId: string | null = selectedObjectId.value) { + const result = lastDiffResult.value; + const generation = ++focusedDeploySqlGeneration; + if (!result || !objectId || !findSchemaDiffObject(diffObjects.value, objectId)) { + focusedForwardDeploySql.value = "-- No objects selected"; + focusedRollbackSql.value = ""; + focusedDeploySql.value = "-- No objects selected"; + return; + } + + const options = normalizeSchemaDiffCompareOptions(activeConfig.value?.options, getDbType()); + const input = selectSchemaDiffInputForObject(result, diffObjects.value, objectId); + let plan; + try { + plan = await api.generateSchemaSyncPlan(input, buildSchemaSyncPlanOptions(options)); + } catch (error: any) { + if (generation === focusedDeploySqlGeneration && selectedObjectId.value === objectId) toast(error?.message || String(error), 5000); + return; + } + if (generation !== focusedDeploySqlGeneration || selectedObjectId.value !== objectId) return; + + const formatted = formatSchemaSyncPlan(plan, input, options); + focusedForwardDeploySql.value = formatted.forwardSql; + focusedRollbackSql.value = formatted.rollbackSql; + focusedDeploySql.value = deploySqlMode.value === "rollback" && formatted.rollbackSql ? formatted.rollbackSql : formatted.forwardSql; } function switchDeploySqlMode(mode: "forward" | "rollback") { @@ -660,10 +702,10 @@ function switchDeploySqlMode(mode: "forward" | "rollback") { return; } deploySqlMode.value = mode; - if (mode === "rollback" && rollbackSql.value) { - deploySql.value = rollbackSql.value; - } else { - void regenerateDeploySql(); + selectedDeploySql.value = mode === "rollback" && rollbackSql.value ? rollbackSql.value : selectedForwardDeploySql.value; + focusedDeploySql.value = mode === "rollback" && focusedRollbackSql.value ? focusedRollbackSql.value : focusedForwardDeploySql.value; + if (mode === "rollback" && !rollbackSql.value) { + void regenerateSelectedDeploySql(); } } @@ -676,7 +718,7 @@ const canExecuteDeploy = computed(() => { const destructiveStatements = computed(() => { const databaseType = store.getConfig(targetConnectionId.value)?.db_type; - return detectDestructiveSchemaDiffStatements(deploySql.value, databaseType); + return detectDestructiveSchemaDiffStatements(selectedDeploySql.value, databaseType); }); function applyRename(rc: RenameCandidate) { @@ -703,7 +745,8 @@ function applyRename(rc: RenameCandidate) { } if (found) { rebuildDiffGroups(); - void regenerateDeploySql(); + void regenerateSelectedDeploySql(); + void regenerateFocusedDeploySql(); toast(t("diff.renameApplied"), 2000); } } @@ -720,11 +763,12 @@ function ignoreRename(index: number) { } renameCandidates.value.splice(index, 1); rebuildDiffGroups(); - void regenerateDeploySql(); + void regenerateSelectedDeploySql(); + void regenerateFocusedDeploySql(); } async function handleExecuteScript() { - if (!deploySql.value.trim() || deploySql.value.trim() === "-- No objects selected") { + if (!selectedDeploySql.value.trim() || selectedDeploySql.value.trim() === "-- No objects selected") { toast(t("diff.noObjectsSelected"), 3000); return; } @@ -743,10 +787,10 @@ async function executeDeploySql() { const failed = await executeWithProductionSqlGuard({ connection: targetConnection, database: targetDatabase.value, - sql: deploySql.value, + sql: selectedDeploySql.value, source: t("production.sourceSchemaDiff"), execute: async () => { - const txLog = await api.executeScriptWith2pc(targetConnectionId.value, targetDatabase.value, [deploySql.value], targetSchema.value, destructiveStatements.value.length > 0); + const txLog = await api.executeScriptWith2pc(targetConnectionId.value, targetDatabase.value, [selectedDeploySql.value], targetSchema.value, destructiveStatements.value.length > 0); return txLog; }, }); @@ -769,6 +813,7 @@ function showDeployTxResult(txLog: any) { } async function handleSelectObject(reviewObject: SchemaDiffObject) { selectedObjectId.value = reviewObject.id; + void regenerateFocusedDeploySql(reviewObject.id); const obj = reviewObject.parentId ? (findSchemaDiffObject(diffObjects.value, reviewObject.parentId) ?? reviewObject) : reviewObject; // Dynamically fetch DDL for objects that don't have pre-generated DDL @@ -1035,8 +1080,9 @@ const targetConnectionInfo = computed(() => { {