fix(schema-diff): guard deploy script execution

This commit is contained in:
skyler authored and GitHub committed 2026-08-30 10:35:08 +08:00
1 parent 469bd88c37
commit f664918e95
6 files changed
+125 -18

No files matched your search

@@ -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: [] }],
});
});
});
+65 -5
View File
@@ -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<string, RawSchemaDiffDependencyNode>;
}
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<string>): 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<string> {
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<string>();
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);
}