fix(schema-diff): keep deploy script preview in sync with selection

This commit is contained in:
jearhe authored and GitHub committed 2026-08-30 00:47:45 +08:00
1 parent 1302c0a27b
commit 1637948023
6 files changed
+225 -61

No files matched your search

@@ -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(() => {
@@ -33,6 +33,7 @@ import {
flattenSchemaDiffObjects,
schemaDiffSelectionTargets,
selectSchemaDiffInput,
selectSchemaDiffInputForObject,
selectedSchemaDiffObjects,
setSchemaDiffObjectSelected,
setSchemaDiffObjectSelectedWithDependencies,
@@ -108,8 +109,11 @@ const loading = ref(false);
const diffObjects = ref<SchemaDiffObject[]>([]);
const diffGroups = ref<OperationGroup[]>([]);
const selectedObjectId = ref<string | null>(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<SchemaDiffPreparation | null>(null);
const targetDbVersion = ref<string | null>(null);
@@ -124,7 +128,8 @@ const renameCandidates = ref<RenameCandidate[]>([]);
const compatibilityWarnings = ref<CompatibilityWarning[]>([]);
const permissionDiffs = ref<PermissionDiff[]>([]);
const dependencyGraph = ref<DependencyGraph | null>(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<ReturnType<typeof api.generateSchemaSyncPlan>>, input: ReturnType<typeof selectSchemaDiffInput>, 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(() => {
<SchemaDiffDdlPanel
:selected-object="selectedObject"
:focused-object="selectedTreeObject"
:deploy-sql="deploySql"
:deploy-sql-all="deploySqlAll"
:deploy-sql="focusedDeploySql"
:deploy-sql-all="selectedDeploySql"
:rollback-forward-sql="selectedForwardDeploySql"
:compatibility-warnings="selectedCompatibilityWarnings"
:rollback-sql="rollbackSql"
:deploy-sql-mode="deploySqlMode"
@@ -1055,7 +1101,7 @@ const targetConnectionInfo = computed(() => {
<!-- Deploy Review Step -->
<template v-else-if="step === 'deploy-review'">
<SchemaDiffDeployStep
v-model:deploy-sql="deploySql"
v-model:deploy-sql="selectedDeploySql"
:selected-objects="diffObjects"
:target-connection-id="targetConnectionId"
:target-database="targetDatabase"
@@ -13,4 +13,9 @@ describe("SchemaDiffDdlPanel diff highlighting", () => {
it("never applies the focused-row outline to empty padding lines", () => {
expect(panelSource).toContain('if (line.isPadding || focusedLineNumber === null) return "";');
});
it("keeps rollback comparison based on the selected forward SQL", () => {
expect(panelSource).toContain("rollbackForwardSql?: string;");
expect(panelSource).toContain("props.rollbackForwardSql ?? props.deploySql");
});
});
@@ -71,4 +71,19 @@ describe("SchemaDiffDialog fullscreen layout", () => {
expect(targetMatch).toBeGreaterThan(comparisonScope);
expect(targetMatch).toBeLessThan(options);
});
it("keeps focused and selected deployment SQL projections separate", () => {
expect(dialogSource).toContain('const focusedDeploySql = ref("");');
expect(dialogSource).toContain('const selectedDeploySql = ref("");');
expect(dialogSource).toContain("const input = selectSchemaDiffInputForObject(result, diffObjects.value, objectId);");
expect(dialogSource).toContain(':deploy-sql="focusedDeploySql"');
expect(dialogSource).toContain(':deploy-sql-all="selectedDeploySql"');
expect(dialogSource).not.toContain("deploySqlAll.value = result.syncSql");
});
it("executes the selected SQL rather than the focused preview", () => {
expect(dialogSource).toContain("sql: selectedDeploySql.value");
expect(dialogSource).toContain("[selectedDeploySql.value]");
expect(dialogSource).toContain("selectedObjectId.value !== objectId");
});
});