2929 * as one transaction, while an author-declared federated lookup still
3030 * makes the plan cross-datasource.
3131 *
32+ * [#21918] The same four, for every OTHER anchor the registry injects into a
33+ * federated object and the object does not provision, read from the
34+ * registry's provenance rather than from a column name: `owning_business_unit_id`
35+ * on a business-unit delete, and `owner_id` / `created_by` / `updated_by` on a
36+ * user delete. Their blocks are at the foot of this file, and the predicate's
37+ * own pin is `federated-object.test.ts`.
38+ *
3239 * The seed rows are written straight into the stub's store, so no write path
33- * other than the delete under test runs. The door pin is
34- * `packages/qa/dogfood/test/organization-delete-federated-fixture.dogfood.test.ts`.
40+ * other than the delete under test runs. The door pins are
41+ * `packages/qa/dogfood/test/organization-delete-federated-fixture.dogfood.test.ts`
42+ * and `packages/qa/dogfood/test/business-unit-and-user-delete-federated-fixture.dogfood.test.ts`.
3543 */
3644
3745import { describe , it , expect } from 'vitest' ;
@@ -44,12 +52,26 @@ const PACKAGE_ID = 'test-21910';
4452
4553type Row = Record < string , unknown > ;
4654
55+ /** Every field name a `where` filters on, at any depth, as `object.field`. */
56+ function filteredColumns ( object : string , where : unknown , out : string [ ] = [ ] ) : string [ ] {
57+ if ( Array . isArray ( where ) ) {
58+ for ( const w of where ) filteredColumns ( object , w , out ) ;
59+ } else if ( where && typeof where === 'object' ) {
60+ for ( const [ k , v ] of Object . entries ( where ) ) {
61+ if ( k . startsWith ( '$' ) ) filteredColumns ( object , v , out ) ;
62+ else out . push ( `${ object } .${ k } ` ) ;
63+ }
64+ }
65+ return out ;
66+ }
67+
4768/**
48- * A stub driver that records every read into a shared log and can be told to
49- * refuse reads of one object with an exact error object. Its `find` applies
50- * the caller's `limit` after the filter, by presence.
69+ * A stub driver that records every read into a shared log, with the columns
70+ * each read filters on, and can be told to refuse reads of one object with an
71+ * exact error object. Its `find` applies the caller's `limit` after the
72+ * filter, by presence.
5173 */
52- function makeDriver ( name : string , log : { reads : string [ ] ; begun : number } ) {
74+ function makeDriver ( name : string , log : { reads : string [ ] ; probes : string [ ] ; begun : number } ) {
5375 const tables : Record < string , Row [ ] > = { } ;
5476 const failReads = new Map < string , unknown > ( ) ;
5577 const rowsOf = ( o : string ) : Row [ ] => ( tables [ o ] ??= [ ] ) ;
@@ -69,6 +91,7 @@ function makeDriver(name: string, log: { reads: string[]; begun: number }) {
6991 registerExternalObject ( ) { } ,
7092 async find ( o : string , ast : any ) {
7193 log . reads . push ( o ) ;
94+ log . probes . push ( ...filteredColumns ( o , ast ?. where ) ) ;
7295 const failure = failReads . get ( o ) ;
7396 if ( failure !== undefined ) throw failure ;
7497 const hit = ( tables [ o ] ?? [ ] ) . filter ( ( r ) => matches ( r , ast ?. where ) ) ;
@@ -176,7 +199,7 @@ function unknownColumnRefusal(object: string, column: string) {
176199}
177200
178201async function makeEngine ( objects : any [ ] ) {
179- const log = { reads : [ ] as string [ ] , begun : 0 } ;
202+ const log = { reads : [ ] as string [ ] , probes : [ ] as string [ ] , begun : 0 } ;
180203 const warnings : string [ ] = [ ] ;
181204 const logger = {
182205 debug ( ) { } , info ( ) { } , error ( ) { } ,
@@ -268,3 +291,164 @@ describe('[#21910] the cascade atomicity plan agrees with the scan about who tak
268291 expect ( warnings . filter ( ( w ) => w . includes ( NOT_ATOMIC ) ) ) . toHaveLength ( 1 ) ;
269292 } ) ;
270293} ) ;
294+
295+ // ---------------------------------------------------------------------------
296+ // [#21918] Every other injected anchor of a federated object, read from the
297+ // registry's provenance: `owning_business_unit_id` (ADR-0117 D1) on a business
298+ // unit delete, and the owner and audit lookups `owner_id` / `created_by` /
299+ // `updated_by` on a user delete. #21910's predicate named `organization_id`
300+ // alone, so on the showcase with its federated fixture a business-unit delete
301+ // answered 400 (`INVALID_FILTER` on `showcase_ext_customer.owning_business_unit_id`)
302+ // and a user delete answered 500 (on `created_by`). The door pins are
303+ // `packages/qa/dogfood/test/business-unit-and-user-delete-federated-fixture.dogfood.test.ts`.
304+ // ---------------------------------------------------------------------------
305+
306+ /** The business-unit object a federated object's injected `owning_business_unit_id` names. */
307+ const BUSINESS_UNIT = {
308+ name : 'sys_business_unit' ,
309+ label : 'Business Unit' ,
310+ fields : { name : { name : 'name' , label : 'Name' , type : 'text' as const } } ,
311+ } ;
312+
313+ /** The user object a federated object's injected `owner_id` / `created_by` / `updated_by` name. */
314+ const USER = {
315+ name : 'sys_user' ,
316+ label : 'User' ,
317+ fields : { name : { name : 'name' , label : 'Name' , type : 'text' as const } } ,
318+ } ;
319+
320+ /**
321+ * Federated, with two lookups the AUTHOR declared on the anchors' targets: an
322+ * `owner_id` of its own (it maps a real remote column) and `unit_ref`.
323+ */
324+ const FEDERATED_AUTHOR_ANCHORS = {
325+ name : 'ext_assignment' ,
326+ label : 'External Assignment' ,
327+ datasource : REMOTE ,
328+ external : { remoteName : 'assignments' } ,
329+ fields : {
330+ title : { name : 'title' , label : 'Title' , type : 'text' as const } ,
331+ owner_id : { name : 'owner_id' , label : 'Remote Owner' , type : 'lookup' as const , reference : 'sys_user' } ,
332+ unit_ref : { name : 'unit_ref' , label : 'Unit' , type : 'lookup' as const , reference : 'sys_business_unit' } ,
333+ } ,
334+ } ;
335+
336+ const UNIT_ID = 'bu_21918' ;
337+ const USER_ID = 'usr_21918' ;
338+
339+ async function makeAnchorEngine ( objects : any [ ] ) {
340+ const made = await makeEngine ( [ ORGANIZATION , BUSINESS_UNIT , USER , ...objects ] ) ;
341+ made . local . seed ( 'sys_business_unit' , { id : UNIT_ID , name : 'Doomed Unit' } ) ;
342+ made . local . seed ( 'sys_user' , { id : USER_ID , name : 'Doomed User' } ) ;
343+ return made ;
344+ }
345+
346+ /** The columns a delete's scan probed on one object, deduplicated and sorted. */
347+ const probedOn = ( log : { probes : string [ ] } , object : string ) : string [ ] =>
348+ [ ...new Set ( log . probes . filter ( ( p ) => p . startsWith ( `${ object } .` ) ) ) ] . sort ( ) ;
349+
350+ describe ( '[#21918] the cascade scan skips every injected anchor a federated object does not provision' , ( ) => {
351+ it ( 'a business-unit delete never probes a federated object on its injected owning_business_unit_id, and lands' , async ( ) => {
352+ const { engine, local, remote, log } = await makeAnchorEngine ( [ LOCAL , FEDERATED ] ) ;
353+ // PREMISE: the registered anchor is the registry's own, and unprovisioned.
354+ expect ( resolveInjectedColumnProvenance ( engine . getSchema ( 'ext_customer' ) , 'owning_business_unit_id' ) )
355+ . toBe ( 'injected-unprovisioned' ) ;
356+ remote . failReads . set ( 'ext_customer' , unknownColumnRefusal ( 'ext_customer' , 'owning_business_unit_id' ) ) ;
357+
358+ log . reads . length = 0 ;
359+ log . probes . length = 0 ;
360+ await engine . delete ( 'sys_business_unit' , { where : { id : UNIT_ID } } as any ) ;
361+
362+ expect ( local . has ( 'sys_business_unit' , UNIT_ID ) ) . toBe ( false ) ;
363+ expect ( log . reads ) . not . toContain ( 'ext_customer' ) ;
364+ // CONTROL: the scan ran, and probed the LOCAL object's injected anchor of the same name.
365+ expect ( probedOn ( log , 'acct' ) ) . toContain ( 'acct.owning_business_unit_id' ) ;
366+ } ) ;
367+
368+ it ( 'a user delete never probes a federated object on its injected owner_id, created_by or updated_by, and lands' , async ( ) => {
369+ const { engine, local, remote, log } = await makeAnchorEngine ( [ LOCAL , FEDERATED ] ) ;
370+ const schema = engine . getSchema ( 'ext_customer' ) ;
371+ for ( const column of [ 'owner_id' , 'created_by' , 'updated_by' ] ) {
372+ expect ( resolveInjectedColumnProvenance ( schema , column ) , column ) . toBe ( 'injected-unprovisioned' ) ;
373+ }
374+ remote . failReads . set ( 'ext_customer' , unknownColumnRefusal ( 'ext_customer' , 'created_by' ) ) ;
375+
376+ log . reads . length = 0 ;
377+ log . probes . length = 0 ;
378+ await engine . delete ( 'sys_user' , { where : { id : USER_ID } } as any ) ;
379+
380+ expect ( local . has ( 'sys_user' , USER_ID ) ) . toBe ( false ) ;
381+ expect ( log . reads ) . not . toContain ( 'ext_customer' ) ;
382+ // CONTROL: the LOCAL object's injected owner and audit lookups ARE probed.
383+ expect ( probedOn ( log , 'acct' ) ) . toEqual (
384+ expect . arrayContaining ( [ 'acct.created_by' , 'acct.owner_id' , 'acct.updated_by' ] ) ,
385+ ) ;
386+ } ) ;
387+
388+ it ( 'still probes lookups the AUTHOR declared on a federated object, and a business-unit probe failure propagates (#8895)' , async ( ) => {
389+ const { engine, local, remote, log } = await makeAnchorEngine ( [ FEDERATED_AUTHOR_ANCHORS ] ) ;
390+ const injected = unknownColumnRefusal ( 'ext_assignment' , 'unit_ref' ) ;
391+ remote . failReads . set ( 'ext_assignment' , injected ) ;
392+
393+ log . probes . length = 0 ;
394+ const err : any = await engine . delete ( 'sys_business_unit' , { where : { id : UNIT_ID } } as any ) . catch ( ( e ) => e ) ;
395+
396+ expect ( err ) . toBe ( injected ) ;
397+ expect ( err . code ) . toBe ( 'INVALID_FILTER' ) ;
398+ expect ( err . status ) . toBe ( 400 ) ;
399+ // The probe that ran is the author's column, never the injected anchor beside it.
400+ expect ( probedOn ( log , 'ext_assignment' ) ) . toEqual ( [ 'ext_assignment.unit_ref' ] ) ;
401+ expect ( local . has ( 'sys_business_unit' , UNIT_ID ) ) . toBe ( true ) ;
402+ } ) ;
403+
404+ it ( 'still probes an owner_id the AUTHOR declared on a federated object, and a user probe failure propagates (#8895)' , async ( ) => {
405+ const { engine, local, remote, log } = await makeAnchorEngine ( [ FEDERATED_AUTHOR_ANCHORS ] ) ;
406+ expect ( resolveInjectedColumnProvenance ( engine . getSchema ( 'ext_assignment' ) , 'owner_id' ) ) . toBe ( 'author' ) ;
407+ const injected = unknownColumnRefusal ( 'ext_assignment' , 'owner_id' ) ;
408+ remote . failReads . set ( 'ext_assignment' , injected ) ;
409+
410+ log . probes . length = 0 ;
411+ const err : any = await engine . delete ( 'sys_user' , { where : { id : USER_ID } } as any ) . catch ( ( e ) => e ) ;
412+
413+ expect ( err ) . toBe ( injected ) ;
414+ expect ( err . code ) . toBe ( 'INVALID_FILTER' ) ;
415+ expect ( err . status ) . toBe ( 400 ) ;
416+ expect ( probedOn ( log , 'ext_assignment' ) ) . toEqual ( [ 'ext_assignment.owner_id' ] ) ;
417+ expect ( local . has ( 'sys_user' , USER_ID ) ) . toBe ( true ) ;
418+ } ) ;
419+ } ) ;
420+
421+ describe ( '[#21918] the cascade atomicity plan agrees with the scan for every injected anchor' , ( ) => {
422+ it ( 'runs a business-unit delete as one transaction when injected anchors were its only cross-datasource references' , async ( ) => {
423+ const { engine, local, log, warnings } = await makeAnchorEngine ( [ LOCAL , FEDERATED ] ) ;
424+
425+ await engine . delete ( 'sys_business_unit' , { where : { id : UNIT_ID } } as any ) ;
426+
427+ expect ( local . has ( 'sys_business_unit' , UNIT_ID ) ) . toBe ( false ) ;
428+ expect ( log . begun ) . toBe ( 1 ) ;
429+ expect ( warnings . filter ( ( w ) => w . includes ( NOT_ATOMIC ) ) ) . toEqual ( [ ] ) ;
430+ } ) ;
431+
432+ it ( 'runs a user delete as one transaction when injected anchors were its only cross-datasource references' , async ( ) => {
433+ const { engine, local, log, warnings } = await makeAnchorEngine ( [ LOCAL , FEDERATED ] ) ;
434+
435+ await engine . delete ( 'sys_user' , { where : { id : USER_ID } } as any ) ;
436+
437+ expect ( local . has ( 'sys_user' , USER_ID ) ) . toBe ( false ) ;
438+ expect ( log . begun ) . toBe ( 1 ) ;
439+ expect ( warnings . filter ( ( w ) => w . includes ( NOT_ATOMIC ) ) ) . toEqual ( [ ] ) ;
440+ } ) ;
441+
442+ it ( 'CONTROL: lookups the author declared on a federated object still make both plans cross-datasource' , async ( ) => {
443+ for ( const [ object , id ] of [ [ 'sys_business_unit' , UNIT_ID ] , [ 'sys_user' , USER_ID ] ] as const ) {
444+ const { engine, local, log, warnings } = await makeAnchorEngine ( [ LOCAL , FEDERATED_AUTHOR_ANCHORS ] ) ;
445+
446+ await engine . delete ( object , { where : { id } } as any ) ;
447+
448+ expect ( local . has ( object , id ) , object ) . toBe ( false ) ;
449+ expect ( log . reads , object ) . toContain ( 'ext_assignment' ) ;
450+ expect ( log . begun , object ) . toBe ( 0 ) ;
451+ expect ( warnings . filter ( ( w ) => w . includes ( NOT_ATOMIC ) ) , object ) . toHaveLength ( 1 ) ;
452+ }
453+ } ) ;
454+ } ) ;
0 commit comments