@@ -29,7 +29,7 @@ import { batchInsertRows, batchUpdateRows, insertRow, updateRow } from '@/lib/ta
2929import { lockUniqueColumns , lockUniqueValues } from '@/lib/table/rows/unique-locks'
3030import { getTableById } from '@/lib/table/service'
3131import { getOrCreateTableSnapshot } from '@/lib/table/snapshot-cache'
32- import type { ColumnDefinition , TableDefinition } from '@/lib/table/types'
32+ import type { ColumnDefinition , RowData , TableDefinition } from '@/lib/table/types'
3333
3434const url = readTestDatabaseUrl ( )
3535if ( process . env . DATABASE_URL !== url ) {
@@ -67,7 +67,7 @@ async function createTable(columns: ColumnDefinition[]): Promise<TableDefinition
6767
6868async function seedRows (
6969 tableId : string ,
70- rows : Array < { id : string ; data : Record < string , string > ; orderKey : string | null } >
70+ rows : Array < { id : string ; data : RowData ; orderKey : string | null } >
7171) {
7272 await db . insert ( userTableRows ) . values ( rows . map ( ( row ) => ( { ...row , tableId, workspaceId } ) ) )
7373}
@@ -177,7 +177,10 @@ describe('table row writes against real PostgreSQL', () => {
177177 }
178178 }
179179
180- async function storedCount ( tableId : string , match : Record < string , string | number > ) {
180+ async function storedCount (
181+ tableId : string ,
182+ match : Record < string , string | number | Record < string , number > >
183+ ) {
181184 const [ { count } ] = await control < { count : number } [ ] > `SELECT count(*)::int AS count
182185 FROM user_table_rows WHERE table_id = ${ tableId } AND data @> ${ control . json ( match ) } `
183186 return count
@@ -326,17 +329,21 @@ describe('table row writes against real PostgreSQL', () => {
326329 } )
327330 await holding
328331
329- const replace = importReplaceRows (
330- table ,
331- [ { id : 'code' , name : 'code' , type : 'string' , unique : true } ] ,
332- { rows : [ { name : 'fresh' , code : 'c-1' } ] , workspaceId } ,
333- 'unique-race'
334- )
335- expect ( await waitForLockWaiters ( table . id , { onOrderLock : 0 , onValueLock : 1 } ) ) . toEqual ( {
336- onOrderLock : 0 ,
337- onValueLock : 1 ,
338- } )
339- release ( )
332+ let replace : Promise < unknown > | undefined
333+ try {
334+ replace = importReplaceRows (
335+ table ,
336+ [ { id : 'code' , name : 'code' , type : 'string' , unique : true } ] ,
337+ { rows : [ { name : 'fresh' , code : 'c-1' } ] , workspaceId } ,
338+ 'unique-race'
339+ )
340+ expect ( await waitForLockWaiters ( table . id , { onOrderLock : 0 , onValueLock : 1 } ) ) . toEqual ( {
341+ onOrderLock : 0 ,
342+ onValueLock : 1 ,
343+ } )
344+ } finally {
345+ release ( )
346+ }
340347
341348 const results = await Promise . allSettled ( [ writer , replace ] )
342349 expect ( results . map ( ( result ) => result . status ) ) . toEqual ( [ 'fulfilled' , 'fulfilled' ] )
@@ -451,6 +458,31 @@ describe('table row writes against real PostgreSQL', () => {
451458 ) . rejects . toThrow ( / m u s t b e u n i q u e / )
452459 expect ( await storedCount ( table . id , { email : 'dup@example.test' } ) ) . toBe ( 0 )
453460 } )
461+
462+ it ( 'rejects a batch update that writes one JSON value to two rows in different key orders' , async ( ) => {
463+ const table = await createTable ( [ { id : 'meta' , name : 'meta' , type : 'json' , unique : true } ] )
464+ await seedRows ( table . id , [
465+ { id : `${ table . id } -a` , data : { meta : { n : 1 } } , orderKey : 'a0' } ,
466+ { id : `${ table . id } -b` , data : { meta : { n : 2 } } , orderKey : 'a1' } ,
467+ ] )
468+
469+ await expect (
470+ batchUpdateRows (
471+ {
472+ tableId : table . id ,
473+ workspaceId,
474+ updates : [
475+ { rowId : `${ table . id } -a` , data : { meta : { x : 1 , y : 2 } } } ,
476+ { rowId : `${ table . id } -b` , data : { meta : { y : 2 , x : 1 } } } ,
477+ ] ,
478+ capabilityGovernedUserId : null ,
479+ } ,
480+ table ,
481+ 'unique-batch-update'
482+ )
483+ ) . rejects . toThrow ( / m u s t b e u n i q u e / )
484+ expect ( await storedCount ( table . id , { meta : { x : 1 , y : 2 } } ) ) . toBe ( 0 )
485+ } )
454486 } )
455487
456488 describe . skipIf ( ! migrated ) ( 'rows_version' , ( ) => {
0 commit comments