diff --git a/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue b/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue
index 84f0f273c4..c5bd02857d 100644
--- a/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue
+++ b/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue
@@ -465,10 +465,6 @@ function copyDeploySqlAll() {
{{ t("diff.copy") }}
-
diff --git a/apps/desktop/src/components/diff/SchemaDiffDialog.vue b/apps/desktop/src/components/diff/SchemaDiffDialog.vue
index c486059f61..74b0c6d76e 100644
--- a/apps/desktop/src/components/diff/SchemaDiffDialog.vue
+++ b/apps/desktop/src/components/diff/SchemaDiffDialog.vue
@@ -51,6 +51,7 @@ import {
type CompatibilityWarning,
type PermissionDiff,
type DependencyGraph,
+ normalizeSchemaDiffDependencyGraph,
} from "@/lib/schema/schemaDiff";
import { compileSchemaDiffTableFilter, filterSchemaDiffTables } from "@/lib/schema/schemaDiffTableFilter";
import { Splitpanes, Pane } from "splitpanes";
@@ -593,7 +594,7 @@ async function handleCompare() {
renameCandidates.value = result.renameCandidates ?? [];
compatibilityWarnings.value = result.compatibilityWarnings ?? [];
permissionDiffs.value = result.permissionDiffs ?? [];
- dependencyGraph.value = result.dependencyGraph ?? null;
+ dependencyGraph.value = normalizeSchemaDiffDependencyGraph(result.dependencyGraph);
// Convert to unified objects
diffObjects.value = convertToSchemaDiffObjects(result.diffs, result.functionDiffs, result.sequenceDiffs, result.ruleDiffs, result.ownerDiffs, result.renameCandidates);
@@ -686,13 +687,19 @@ function formatSchemaSyncPlan(plan: Awaited {
if (deploySqlMode.value === "rollback" && rollbackCompleteness.value === "incomplete") {
return false;
}
- return true;
+ const sql = selectedDeploySql.value.trim();
+ return sql.length > 0 && sql !== "-- No objects selected";
});
const destructiveStatements = computed(() => {
diff --git a/apps/desktop/src/components/diff/__tests__/SchemaDiffDdlPanel.spec.ts b/apps/desktop/src/components/diff/__tests__/SchemaDiffDdlPanel.spec.ts
index f280fb0289..65b0165e29 100644
--- a/apps/desktop/src/components/diff/__tests__/SchemaDiffDdlPanel.spec.ts
+++ b/apps/desktop/src/components/diff/__tests__/SchemaDiffDdlPanel.spec.ts
@@ -18,4 +18,12 @@ describe("SchemaDiffDdlPanel diff highlighting", () => {
expect(panelSource).toContain("rollbackForwardSql?: string;");
expect(panelSource).toContain("props.rollbackForwardSql ?? props.deploySql");
});
+
+ it("does not expose selected-SQL execution from the focused script tab", () => {
+ const focusedScript = panelSource.slice(panelSource.indexOf(""), panelSource.indexOf(""));
+ const selectedScript = panelSource.slice(panelSource.indexOf(""));
+
+ expect(focusedScript).not.toContain("$emit('executeScript')");
+ expect(selectedScript).toContain("$emit('executeScript')");
+ });
});
diff --git a/apps/desktop/src/components/diff/__tests__/SchemaDiffDialog.spec.ts b/apps/desktop/src/components/diff/__tests__/SchemaDiffDialog.spec.ts
index 56128a8b60..95920f4a03 100644
--- a/apps/desktop/src/components/diff/__tests__/SchemaDiffDialog.spec.ts
+++ b/apps/desktop/src/components/diff/__tests__/SchemaDiffDialog.spec.ts
@@ -120,5 +120,7 @@ describe("SchemaDiffDialog fullscreen layout", () => {
expect(dialogSource).toContain("sql: selectedDeploySql.value");
expect(dialogSource).toContain("[selectedDeploySql.value]");
expect(dialogSource).toContain("selectedObjectId.value !== objectId");
+ expect(dialogSource).toContain('selectedDeploySql.value = "";');
+ expect(dialogSource).toContain("const sql = selectedDeploySql.value.trim();");
});
});
diff --git a/apps/desktop/src/lib/__tests__/schema/schemaDiffSelection.spec.ts b/apps/desktop/src/lib/__tests__/schema/schemaDiffSelection.spec.ts
index fb1a0a3215..d03ba4c85c 100644
--- a/apps/desktop/src/lib/__tests__/schema/schemaDiffSelection.spec.ts
+++ b/apps/desktop/src/lib/__tests__/schema/schemaDiffSelection.spec.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
-import { convertToSchemaDiffObjects, selectSchemaDiffInput, selectSchemaDiffInputForObject, setSchemaDiffObjectSelected, type SchemaDiffPreparation } from "../../schema/schemaDiff";
+import { convertToSchemaDiffObjects, normalizeSchemaDiffDependencyGraph, selectSchemaDiffInput, selectSchemaDiffInputForObject, setSchemaDiffObjectSelected, type SchemaDiffPreparation } from "../../schema/schemaDiff";
function createPreparation(): SchemaDiffPreparation {
return {
@@ -74,4 +74,37 @@ describe("schema diff SQL selection projections", () => {
ownerDiffs: [],
});
});
+
+ it("includes the complete predecessor closure in focused table SQL", () => {
+ const result = createPreparation();
+ result.diffs.push({
+ type: "added",
+ objectType: "table",
+ name: "table_c",
+ });
+ result.dependencyGraph = {
+ nodes: [
+ { tableName: "table_a", dependsOn: ["table_b"], dependedBy: [] },
+ { tableName: "table_b", dependsOn: ["table_c"], dependedBy: ["table_a"] },
+ { tableName: "table_c", dependsOn: [], dependedBy: ["table_b"] },
+ ],
+ };
+ const objects = convertToSchemaDiffObjects(result.diffs);
+
+ const focusedInput = selectSchemaDiffInputForObject(result, objects, "table-table_a");
+
+ expect(focusedInput.diffs.map((diff) => diff.name)).toEqual(["table_a", "table_b", "table_c"]);
+ });
+
+ it("normalizes the Rust dependency graph map and snake-case fields", () => {
+ expect(
+ normalizeSchemaDiffDependencyGraph({
+ nodes: {
+ table_a: { table_name: "table_a", depends_on: ["table_b"], depended_by: [] },
+ },
+ }),
+ ).toEqual({
+ nodes: [{ tableName: "table_a", dependsOn: ["table_b"], dependedBy: [] }],
+ });
+ });
});
diff --git a/apps/desktop/src/lib/schema/schemaDiff.ts b/apps/desktop/src/lib/schema/schemaDiff.ts
index df7a72aaf4..766f49cfb5 100644
--- a/apps/desktop/src/lib/schema/schemaDiff.ts
+++ b/apps/desktop/src/lib/schema/schemaDiff.ts
@@ -264,6 +264,42 @@ export interface DependencyGraph {
nodes: DependencyNode[];
}
+interface RawSchemaDiffDependencyNode {
+ tableName?: unknown;
+ table_name?: unknown;
+ dependsOn?: unknown;
+ depends_on?: unknown;
+ dependedBy?: unknown;
+ depended_by?: unknown;
+}
+
+interface RawSchemaDiffDependencyGraph {
+ nodes?: RawSchemaDiffDependencyNode[] | Record;
+}
+
+export function normalizeSchemaDiffDependencyGraph(graph: unknown): DependencyGraph | null {
+ if (!graph || typeof graph !== "object") return null;
+
+ const rawNodes = (graph as RawSchemaDiffDependencyGraph).nodes;
+ const entries = Array.isArray(rawNodes) ? rawNodes : rawNodes && typeof rawNodes === "object" ? Object.values(rawNodes) : [];
+ const nodes = entries.flatMap((node) => {
+ const tableName = typeof node.tableName === "string" ? node.tableName : typeof node.table_name === "string" ? node.table_name : "";
+ if (!tableName) return [];
+
+ const dependsOn = Array.isArray(node.dependsOn) ? node.dependsOn : Array.isArray(node.depends_on) ? node.depends_on : [];
+ const dependedBy = Array.isArray(node.dependedBy) ? node.dependedBy : Array.isArray(node.depended_by) ? node.depended_by : [];
+ return [
+ {
+ tableName,
+ dependsOn: dependsOn.filter((name): name is string => typeof name === "string"),
+ dependedBy: dependedBy.filter((name): name is string => typeof name === "string"),
+ },
+ ];
+ });
+
+ return nodes.length > 0 ? { nodes } : null;
+}
+
export interface MissingRollbackObject {
kind: string;
name: string;
@@ -802,23 +838,47 @@ export function selectSchemaDiffInput(result: SchemaDiffPreparation, objects: Sc
};
}
-function projectSchemaDiffObjectSelection(object: SchemaDiffObject, focusedObjectId: string, selectDescendants: boolean): SchemaDiffObject {
+function projectSchemaDiffObjectSelection(object: SchemaDiffObject, focusedObjectId: string, selectDescendants: boolean, dependencyNames: ReadonlySet): SchemaDiffObject {
const isFocused = object.id === focusedObjectId;
- const selectChildren = selectDescendants || isFocused;
+ const isDependency = !object.parentId && dependencyNames.has(object.name);
+ const selectChildren = selectDescendants || isFocused || isDependency;
return {
...object,
- selected: isFocused || selectDescendants,
- children: object.children?.map((child) => projectSchemaDiffObjectSelection(child, focusedObjectId, selectChildren)),
+ selected: isFocused || selectDescendants || isDependency,
+ children: object.children?.map((child) => projectSchemaDiffObjectSelection(child, focusedObjectId, selectChildren, dependencyNames)),
};
}
+function schemaDiffDependencyClosure(result: SchemaDiffPreparation, objects: SchemaDiffObject[], objectId: string): Set {
+ const focusedObject = findSchemaDiffObject(objects, objectId);
+ if (!focusedObject) return new Set();
+
+ const rootObject = focusedObject.parentId ? findSchemaDiffObject(objects, focusedObject.parentId) : focusedObject;
+ if (!rootObject) return new Set();
+
+ const dependencyNodes = new Map((normalizeSchemaDiffDependencyGraph(result.dependencyGraph)?.nodes ?? []).map((node) => [node.tableName, node]));
+ const dependencies = new Set();
+ const pending = [rootObject.name];
+ while (pending.length > 0) {
+ const name = pending.pop();
+ if (!name) continue;
+ for (const dependency of dependencyNodes.get(name)?.dependsOn ?? []) {
+ if (dependency === rootObject.name || dependencies.has(dependency)) continue;
+ dependencies.add(dependency);
+ pending.push(dependency);
+ }
+ }
+ return dependencies;
+}
+
/** Project one focused tree object onto a fresh selection without mutating checkbox state. */
export function selectSchemaDiffInputForObject(result: SchemaDiffPreparation, objects: SchemaDiffObject[], objectId: string): SelectedSchemaDiffInput {
if (!findSchemaDiffObject(objects, objectId)) {
return { diffs: [], functionDiffs: [], sequenceDiffs: [], ruleDiffs: [], ownerDiffs: [] };
}
- const focusedSelection = objects.map((object) => projectSchemaDiffObjectSelection(object, objectId, false));
+ const dependencyNames = schemaDiffDependencyClosure(result, objects, objectId);
+ const focusedSelection = objects.map((object) => projectSchemaDiffObjectSelection(object, objectId, false, dependencyNames));
return selectSchemaDiffInput(result, focusedSelection);
}