@@ -33,6 +33,11 @@ describe.runIf(Boolean(databaseUrl))('persistDocumentAcls in PostgreSQL', () =>
3333 let sql : Sql
3434 const schemaName = `acl_write_${ generateId ( ) . replaceAll ( '-' , '' ) } `
3535
36+ const fannedOut = async ( ) =>
37+ (
38+ await sql < { document_id : string } [ ] > `SELECT document_id FROM fan_out ORDER BY document_id`
39+ ) . map ( ( row ) => row . document_id )
40+
3641 const projected = ( ) =>
3742 sql < { id : string ; acl : string [ ] | null } [ ] > `
3843 SELECT id, acl FROM embedding_search
@@ -68,6 +73,15 @@ describe.runIf(Boolean(databaseUrl))('persistDocumentAcls in PostgreSQL', () =>
6873 )`
6974 }
7075 await installProjectionSourceAcl ( sql )
76+ /**
77+ * Counts the documents whose write fired the projection fan-out: the trigger fires on any
78+ * assignment of `acl`, changed or not, so this is the cost an unchanged write must not pay.
79+ */
80+ await sql `CREATE TABLE fan_out (document_id text NOT NULL)`
81+ await sql . unsafe ( `CREATE FUNCTION count_fan_out() RETURNS trigger LANGUAGE plpgsql AS $$
82+ BEGIN INSERT INTO fan_out VALUES (NEW.id); RETURN NEW; END; $$` )
83+ await sql `CREATE TRIGGER count_fan_out AFTER UPDATE OF connector_id, acl ON document
84+ FOR EACH ROW EXECUTE FUNCTION count_fan_out()`
7185 await sql `ALTER TABLE embedding_search DISABLE TRIGGER embedding_search_source_acl_set`
7286 await sql `ALTER TABLE embedding_keyword_tin DISABLE TRIGGER embedding_keyword_tin_source_acl_set`
7387 } , 60_000 )
@@ -79,7 +93,7 @@ describe.runIf(Boolean(databaseUrl))('persistDocumentAcls in PostgreSQL', () =>
7993 } )
8094
8195 beforeEach ( async ( ) => {
82- await sql `TRUNCATE embedding_search, embedding_keyword_tin, document`
96+ await sql `TRUNCATE embedding_search, embedding_keyword_tin, document, fan_out `
8397 await sql `INSERT INTO document (id, external_id, connector_id, acl, acl_verified_at) VALUES
8498 ('doc-same', 'file-same', 'admin', ARRAY[${ ALICE } ], now() - interval '1 day'),
8599 ('doc-moved', 'file-moved', 'admin', ARRAY[${ ALICE } ], now() - interval '1 day')`
@@ -93,7 +107,7 @@ describe.runIf(Boolean(databaseUrl))('persistDocumentAcls in PostgreSQL', () =>
93107 }
94108 } )
95109
96- it ( 'refreshes the evidence of an unchanged ACL without rewriting any chunk projection row ' , async ( ) => {
110+ it ( 'refreshes the evidence of an unchanged ACL without firing the projection fan-out ' , async ( ) => {
97111 await expect ( persist ( new Map ( [ [ 'file-same' , [ ALICE ] ] ] ) ) ) . resolves . toEqual ( {
98112 updated : 1 ,
99113 rejected : 0 ,
@@ -103,6 +117,7 @@ describe.runIf(Boolean(databaseUrl))('persistDocumentAcls in PostgreSQL', () =>
103117 SELECT acl, acl_verified_at > now() AT TIME ZONE 'UTC' - interval '1 minute' AS fresh
104118 FROM document WHERE id = 'doc-same'`
105119 expect ( stored ) . toEqual ( { acl : [ ALICE ] , fresh : true } )
120+ expect ( await fannedOut ( ) ) . toEqual ( [ ] )
106121 expect ( ( await projected ( ) ) . filter ( ( row ) => row . id . includes ( '-same-' ) ) ) . toEqual ( [
107122 { id : 'kw-same-filled' , acl : [ ALICE ] } ,
108123 { id : 'kw-same-unfilled' , acl : null } ,
@@ -111,7 +126,8 @@ describe.runIf(Boolean(databaseUrl))('persistDocumentAcls in PostgreSQL', () =>
111126 ] )
112127 } )
113128
114- it ( 'propagates a changed ACL to every chunk projection row, filled or not' , async ( ) => {
129+ /** A chunk the backfill has not filled keeps a NULL ACL; the backfill copies the current one. */
130+ it ( 'propagates a changed ACL to every filled chunk projection row' , async ( ) => {
115131 await expect ( persist ( new Map ( [ [ 'file-moved' , [ BOB ] ] ] ) ) ) . resolves . toEqual ( {
116132 updated : 1 ,
117133 rejected : 0 ,
@@ -121,9 +137,9 @@ describe.runIf(Boolean(databaseUrl))('persistDocumentAcls in PostgreSQL', () =>
121137 expect ( stored . acl ) . toEqual ( [ BOB ] )
122138 expect ( ( await projected ( ) ) . filter ( ( row ) => row . id . includes ( '-moved-' ) ) ) . toEqual ( [
123139 { id : 'kw-moved-filled' , acl : [ BOB ] } ,
124- { id : 'kw-moved-unfilled' , acl : [ BOB ] } ,
140+ { id : 'kw-moved-unfilled' , acl : null } ,
125141 { id : 'vec-moved-filled' , acl : [ BOB ] } ,
126- { id : 'vec-moved-unfilled' , acl : [ BOB ] } ,
142+ { id : 'vec-moved-unfilled' , acl : null } ,
127143 ] )
128144 } )
129145
@@ -156,9 +172,10 @@ describe.runIf(Boolean(databaseUrl))('persistDocumentAcls in PostgreSQL', () =>
156172 { id : 'doc-moved' , acl : [ ALICE , BOB ] } ,
157173 { id : 'doc-same' , acl : [ ALICE ] } ,
158174 ] )
175+ expect ( await fannedOut ( ) ) . toEqual ( [ 'doc-moved' ] )
159176 expect (
160- ( await projected ( ) ) . filter ( ( row ) => row . id . endsWith ( '-unfilled ' ) ) . map ( ( row ) => row . acl )
161- ) . toEqual ( [ [ ALICE , BOB ] , null , [ ALICE , BOB ] , null ] )
177+ ( await projected ( ) ) . filter ( ( row ) => row . id . endsWith ( '-filled ' ) ) . map ( ( row ) => row . acl )
178+ ) . toEqual ( [ [ ALICE , BOB ] , [ ALICE ] , [ ALICE , BOB ] , [ ALICE ] ] )
162179 } )
163180 it ( 'writes a changed ACL group larger than one change batch completely' , async ( ) => {
164181 const ids = Array . from (
0 commit comments