|
1 | 1 | import { describe, expect, test } from "bun:test"; |
2 | 2 | import { resolveReferentialActions } from "../../src/referential-actions.js"; |
| 3 | +import { buildExpectedSchema } from "../../src/expected-schema.js"; |
| 4 | +import { diff } from "../../src/diff/index.js"; |
| 5 | +import { emit } from "../../src/emit/index.js"; |
3 | 6 | import { MetaDataLoader, InMemorySource } from "@metaobjectsdev/metadata"; |
4 | 7 |
|
5 | 8 | async function loadDoc(doc: unknown) { |
@@ -65,3 +68,88 @@ describe("resolveReferentialActions", () => { |
65 | 68 | expect(resolveReferentialActions(week, ref)).toEqual({ onDelete: undefined, onUpdate: undefined }); |
66 | 69 | }); |
67 | 70 | }); |
| 71 | + |
| 72 | +// --------------------------------------------------------------------------- |
| 73 | +// Round-trip: buildExpectedSchema → diff (against empty) → emit |
| 74 | +// |
| 75 | +// Asserts that the relationship-derived defaults (and explicit overrides) |
| 76 | +// flow all the way through to the emitted DDL — for both Postgres |
| 77 | +// (ALTER TABLE … ADD CONSTRAINT) and SQLite (inline FOREIGN KEY clause). |
| 78 | +// --------------------------------------------------------------------------- |
| 79 | + |
| 80 | +const EMPTY_SCHEMA = { tables: [], views: [] }; |
| 81 | + |
| 82 | +function makeDoc(rel: Record<string, unknown>) { |
| 83 | + return { |
| 84 | + "metadata.root": { |
| 85 | + package: "acme", |
| 86 | + children: [ |
| 87 | + { "object.entity": { name: "Program", children: [ |
| 88 | + { "field.long": { name: "id" } }, |
| 89 | + { "identity.primary": { "@fields": "id" } }, |
| 90 | + ] } }, |
| 91 | + { "object.entity": { name: "Week", children: [ |
| 92 | + { "field.long": { name: "id" } }, |
| 93 | + { "field.long": { name: "programId" } }, |
| 94 | + rel, |
| 95 | + { "identity.reference": { name: "ref_program", "@fields": ["programId"], "@references": "Program" } }, |
| 96 | + { "identity.primary": { "@fields": "id" } }, |
| 97 | + ] } }, |
| 98 | + ], |
| 99 | + }, |
| 100 | + }; |
| 101 | +} |
| 102 | + |
| 103 | +async function buildSnapshotForRel(rel: Record<string, unknown>) { |
| 104 | + const { root, errors } = await new MetaDataLoader().load([ |
| 105 | + new InMemorySource(JSON.stringify(makeDoc(rel))), |
| 106 | + ]); |
| 107 | + expect(errors).toHaveLength(0); |
| 108 | + return buildExpectedSchema(root); |
| 109 | +} |
| 110 | + |
| 111 | +describe("end-to-end FK actions in emitted DDL", () => { |
| 112 | + test("composition default → ON DELETE CASCADE / ON UPDATE CASCADE (Postgres ADD CONSTRAINT)", async () => { |
| 113 | + const snapshot = await buildSnapshotForRel({ |
| 114 | + "relationship.composition": { name: "program", "@objectRef": "Program", "@cardinality": "one" }, |
| 115 | + }); |
| 116 | + const { changes } = await diff(snapshot, EMPTY_SCHEMA); |
| 117 | + const { up } = emit(changes, { dialect: "postgres" }); |
| 118 | + expect(up).toContain('ADD CONSTRAINT "weeks_program_id_fk"'); |
| 119 | + expect(up).toContain('REFERENCES "programs" ("id") ON DELETE CASCADE ON UPDATE CASCADE'); |
| 120 | + }); |
| 121 | + |
| 122 | + test("composition default → inline FOREIGN KEY … ON DELETE CASCADE ON UPDATE CASCADE (SQLite CREATE TABLE)", async () => { |
| 123 | + const snapshot = await buildSnapshotForRel({ |
| 124 | + "relationship.composition": { name: "program", "@objectRef": "Program", "@cardinality": "one" }, |
| 125 | + }); |
| 126 | + const { changes } = await diff(snapshot, EMPTY_SCHEMA); |
| 127 | + const { up } = emit(changes, { dialect: "sqlite" }); |
| 128 | + expect(up).toContain('FOREIGN KEY ("program_id") REFERENCES "programs" ("id") ON DELETE CASCADE ON UPDATE CASCADE'); |
| 129 | + }); |
| 130 | + |
| 131 | + test("explicit @onDelete: set-null overrides composition default; @onUpdate stays cascade (Postgres)", async () => { |
| 132 | + const snapshot = await buildSnapshotForRel({ |
| 133 | + "relationship.composition": { |
| 134 | + name: "program", "@objectRef": "Program", "@cardinality": "one", |
| 135 | + "@onDelete": "set-null", "@onUpdate": "cascade", |
| 136 | + }, |
| 137 | + }); |
| 138 | + const { changes } = await diff(snapshot, EMPTY_SCHEMA); |
| 139 | + const { up } = emit(changes, { dialect: "postgres" }); |
| 140 | + expect(up).toContain('REFERENCES "programs" ("id") ON DELETE SET NULL ON UPDATE CASCADE'); |
| 141 | + expect(up).not.toContain("ON DELETE CASCADE"); |
| 142 | + }); |
| 143 | + |
| 144 | + test("explicit @onDelete: set-null overrides composition default (SQLite inline)", async () => { |
| 145 | + const snapshot = await buildSnapshotForRel({ |
| 146 | + "relationship.composition": { |
| 147 | + name: "program", "@objectRef": "Program", "@cardinality": "one", |
| 148 | + "@onDelete": "set-null", "@onUpdate": "cascade", |
| 149 | + }, |
| 150 | + }); |
| 151 | + const { changes } = await diff(snapshot, EMPTY_SCHEMA); |
| 152 | + const { up } = emit(changes, { dialect: "sqlite" }); |
| 153 | + expect(up).toContain('FOREIGN KEY ("program_id") REFERENCES "programs" ("id") ON DELETE SET NULL ON UPDATE CASCADE'); |
| 154 | + }); |
| 155 | +}); |
0 commit comments