@@ -142,3 +142,70 @@ describe("end-to-end FK actions in emitted DDL", () => {
142142 expect ( up ) . toContain ( 'FOREIGN KEY ("program_id") REFERENCES "programs" ("id") ON DELETE SET NULL ON UPDATE CASCADE' ) ;
143143 } ) ;
144144} ) ;
145+
146+ // ---------------------------------------------------------------------------
147+ // Referential actions declared DIRECTLY on the identity.reference (the FK).
148+ //
149+ // Regression: an @onDelete / @onUpdate authored on the identity.reference was
150+ // previously ignored — only a correlated relationship node supplied actions —
151+ // so FKs declared with @onDelete : cascade emitted a bare FK (no ON DELETE),
152+ // silently dropping cascade intent.
153+ // ---------------------------------------------------------------------------
154+
155+ function weekDocRef ( refAttrs : Record < string , unknown > , rel ?: Record < string , unknown > ) {
156+ return { "metadata.root" : { package : "acme" , children : [
157+ { "object.entity" : { name : "Program" , children : [
158+ { "field.long" : { name : "id" } } ,
159+ { "identity.primary" : { "name" : "id" , "@fields" : "id" } } ,
160+ ] } } ,
161+ { "object.entity" : { name : "Week" , children : [
162+ { "field.long" : { name : "id" } } ,
163+ { "field.long" : { name : "programId" } } ,
164+ ...( rel ? [ rel ] : [ ] ) ,
165+ { "identity.reference" : { name : "ref_program" , "@fields" : [ "programId" ] , "@references" : "Program" , ...refAttrs } } ,
166+ { "identity.primary" : { "name" : "id" , "@fields" : "id" } } ,
167+ ] } } ,
168+ ] } } ;
169+ }
170+
171+ async function loadWeekRef ( refAttrs : Record < string , unknown > , rel ?: Record < string , unknown > ) {
172+ const { root, errors } = await loadDoc ( weekDocRef ( refAttrs , rel ) ) ;
173+ expect ( errors ) . toHaveLength ( 0 ) ;
174+ const week = root . objects ( ) . find ( ( o ) => o . name === "Week" ) ! ;
175+ const ref = week . referenceIdentities ( ) [ 0 ] ! ;
176+ return { root, week, ref } ;
177+ }
178+
179+ describe ( "reference-level @onDelete / @onUpdate (declared on identity.reference)" , ( ) => {
180+ test ( "reference @onDelete wins with no relationship; onUpdate stays absent" , async ( ) => {
181+ const { week, ref } = await loadWeekRef ( { "@onDelete" : "cascade" } ) ;
182+ expect ( resolveReferentialActions ( week , ref ) ) . toEqual ( { onDelete : "cascade" , onUpdate : undefined } ) ;
183+ } ) ;
184+
185+ test ( "reference @onUpdate is honored independently" , async ( ) => {
186+ const { week, ref } = await loadWeekRef ( { "@onDelete" : "restrict" , "@onUpdate" : "cascade" } ) ;
187+ expect ( resolveReferentialActions ( week , ref ) ) . toEqual ( { onDelete : "restrict" , onUpdate : "cascade" } ) ;
188+ } ) ;
189+
190+ test ( "'setnull' is accepted as an alias for the canonical 'set-null'" , async ( ) => {
191+ const { week, ref } = await loadWeekRef ( { "@onDelete" : "setnull" } ) ;
192+ expect ( resolveReferentialActions ( week , ref ) ) . toEqual ( { onDelete : "set-null" , onUpdate : undefined } ) ;
193+ } ) ;
194+
195+ test ( "reference @onDelete overrides a correlated relationship's action" , async ( ) => {
196+ const { week, ref } = await loadWeekRef (
197+ { "@onDelete" : "restrict" } ,
198+ { "relationship.composition" : { name : "program" , "@objectRef" : "Program" , "@cardinality" : "one" } } ,
199+ ) ;
200+ expect ( resolveReferentialActions ( week , ref ) ) . toEqual ( { onDelete : "restrict" , onUpdate : "cascade" } ) ;
201+ } ) ;
202+
203+ test ( "end-to-end: reference @onDelete: cascade → ON DELETE CASCADE (Postgres), no relationship node" , async ( ) => {
204+ const { root } = await loadWeekRef ( { "@onDelete" : "cascade" } ) ;
205+ const snapshot = buildExpectedSchema ( root ) ;
206+ const { changes } = await diff ( snapshot , EMPTY_SCHEMA ) ;
207+ const { up } = emit ( changes , { dialect : "postgres" } ) ;
208+ expect ( up ) . toContain ( 'ADD CONSTRAINT "weeks_program_id_fk"' ) ;
209+ expect ( up ) . toContain ( 'REFERENCES "programs" ("id") ON DELETE CASCADE' ) ;
210+ } ) ;
211+ } ) ;
0 commit comments