@@ -180,6 +180,104 @@ describeIfDb("createDrizzleAccessPolicyStore", () => {
180180 }
181181 } ) ;
182182
183+ test ( "upsertPolicy is atomic: two concurrent patches to different fields on an existing row both land, neither reverts the other" , async ( ) => {
184+ const setupSql = postgres ( scratchUrl , { max : 1 } ) ;
185+ try {
186+ const store = createDrizzleAccessPolicyStore ( drizzle ( setupSql ) ) ;
187+ await store . upsertPolicy ( "tnt_race_existing" , {
188+ selfSignup : "off" ,
189+ allowedDomains : [ ] ,
190+ tenancyCreation : "owners" ,
191+ } ) ;
192+ } finally {
193+ await setupSql . end ( ) ;
194+ }
195+
196+ // A plain `Promise.all` of two real calls does not reliably force
197+ // the worst-case interleaving on a fast local connection: one
198+ // call's whole read-modify-write often finishes before the other's
199+ // read even starts, so the two never actually overlap. Instead, a
200+ // third connection takes the row's lock first and holds it open
201+ // while both real `upsertPolicy` calls start and queue up behind
202+ // it — releasing it then guarantees both calls' reads had to
203+ // happen without seeing the other's write yet, exactly the
204+ // interleaving that silently reverted one admin's change.
205+ const holderSql = postgres ( scratchUrl , { max : 1 } ) ;
206+ const sqlA = postgres ( scratchUrl , { max : 1 } ) ;
207+ const sqlB = postgres ( scratchUrl , { max : 1 } ) ;
208+ try {
209+ const storeA = createDrizzleAccessPolicyStore ( drizzle ( sqlA ) ) ;
210+ const storeB = createDrizzleAccessPolicyStore ( drizzle ( sqlB ) ) ;
211+
212+ let holderReady : ( ) => void = ( ) => undefined ;
213+ const holderHasLock = new Promise < void > ( ( resolve ) => {
214+ holderReady = resolve ;
215+ } ) ;
216+ let releaseHolder : ( ) => void = ( ) => undefined ;
217+ const releaseSignal = new Promise < void > ( ( resolve ) => {
218+ releaseHolder = resolve ;
219+ } ) ;
220+ const holderTx = holderSql . begin ( async ( tx ) => {
221+ await tx `select * from access_policy.policy where tenant_id = 'tnt_race_existing' for update` ;
222+ holderReady ( ) ;
223+ await releaseSignal ;
224+ } ) ;
225+
226+ await holderHasLock ;
227+
228+ const racers = Promise . all ( [
229+ storeA . upsertPolicy ( "tnt_race_existing" , { selfSignup : "open" } ) ,
230+ storeB . upsertPolicy ( "tnt_race_existing" , {
231+ allowedDomains : [ "acme.example" ] ,
232+ } ) ,
233+ ] ) ;
234+ // Give both calls time to actually issue their row-locking read
235+ // and start queuing behind the holder before it releases.
236+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
237+
238+ releaseHolder ( ) ;
239+ await holderTx ;
240+ await racers ;
241+
242+ const policy = await storeA . getPolicy ( "tnt_race_existing" ) ;
243+ expect ( policy . selfSignup ) . toBe ( "open" ) ;
244+ expect ( policy . allowedDomains ) . toEqual ( [ "acme.example" ] ) ;
245+ expect ( policy . tenancyCreation ) . toBe ( "owners" ) ;
246+ } finally {
247+ await holderSql . end ( ) ;
248+ await sqlA . end ( ) ;
249+ await sqlB . end ( ) ;
250+ }
251+ } ) ;
252+
253+ test ( "upsertPolicy is atomic: two concurrent first-writes for a brand-new tenant both land, neither reverts the other" , async ( ) => {
254+ const sql = postgres ( scratchUrl , { max : 5 } ) ;
255+ try {
256+ const store = createDrizzleAccessPolicyStore ( drizzle ( sql ) ) ;
257+
258+ // No row exists yet for this tenant, so both calls race the
259+ // create path too — the ensure-row-then-lock step inside
260+ // `upsertPolicy` has to serialize this case as well, not only
261+ // the existing-row case above.
262+ await Promise . all ( [
263+ store . upsertPolicy ( "tnt_race_new" , { selfSignup : "open" } ) ,
264+ store . upsertPolicy ( "tnt_race_new" , {
265+ allowedDomains : [ "acme.example" ] ,
266+ } ) ,
267+ ] ) ;
268+
269+ const policy = await store . getPolicy ( "tnt_race_new" ) ;
270+ expect ( policy . selfSignup ) . toBe ( "open" ) ;
271+ expect ( policy . allowedDomains ) . toEqual ( [ "acme.example" ] ) ;
272+
273+ const rows =
274+ await sql `select count(*)::int as count from access_policy.policy where tenant_id = 'tnt_race_new'` ;
275+ expect ( rows [ 0 ] ?. [ "count" ] ) . toBe ( 1 ) ;
276+ } finally {
277+ await sql . end ( ) ;
278+ }
279+ } ) ;
280+
183281 test ( "pending invites: a domain match is found for any email on that domain" , async ( ) => {
184282 const sql = postgres ( scratchUrl , { max : 1 } ) ;
185283 try {
0 commit comments