@@ -17,7 +17,8 @@ import { describe, expect, it, onTestFinished, vi } from 'vitest'
1717import { powerSyncCollectionOptions } from '../src'
1818import { PowerSyncTransactor } from '../src/PowerSyncTransactor'
1919import { TEST_DATABASE_IMPLEMENTATION } from './test-db-implementation'
20- import type { AbstractPowerSyncDatabase } from '@powersync/node'
20+ import type { AbstractPowerSyncDatabase , LockContext } from '@powersync/node'
21+ import type { PendingMutation } from '@tanstack/db'
2122
2223const APP_SCHEMA = new Schema ( {
2324 users : new Table ( {
@@ -275,6 +276,70 @@ describePowerSync(`PowerSync Integration`, () => {
275276 ) . true
276277 } )
277278
279+ it ( `should complete transactions that delete one key and insert another (same-millisecond tie)` , async ( ) => {
280+ const db = await createDatabase ( )
281+
282+ const options = powerSyncCollectionOptions ( {
283+ database : db ,
284+ table : APP_SCHEMA . props . documents ,
285+ } )
286+ const collection = createCollection ( options )
287+ onTestFinished ( ( ) => collection . cleanup ( ) )
288+ await collection . stateWhenReady ( )
289+
290+ // Seed the row which will be deleted in the transaction
291+ await collection . insert ( { id : `a` , name : `row a` } ) . isPersisted . promise
292+
293+ const { trackedTableName } = options . utils . getMeta ( )
294+
295+ class SameMillisecondTransactor extends PowerSyncTransactor {
296+ protected override async handleDelete (
297+ mutation : PendingMutation < any > ,
298+ context : LockContext ,
299+ waitForCompletion ?: boolean ,
300+ ) {
301+ const result = await super . handleDelete (
302+ mutation ,
303+ context ,
304+ waitForCompletion ,
305+ )
306+ // Simulate the same-millisecond tie: ensure the delete's diff row
307+ // wins the `ORDER BY timestamp DESC` readback of the insert below.
308+ await context . execute (
309+ `UPDATE ${ trackedTableName } SET timestamp = '9999-12-31T23:59:59.999Z'` ,
310+ )
311+ return result
312+ }
313+ }
314+
315+ const transactor = new SameMillisecondTransactor ( { database : db } )
316+
317+ const tx = createTransaction ( {
318+ autoCommit : false ,
319+ mutationFn : async ( { transaction } ) => {
320+ await transactor . applyTransaction ( transaction )
321+ } ,
322+ } )
323+
324+ tx . mutate ( ( ) => {
325+ collection . delete ( `a` )
326+ collection . insert ( { id : `b` , name : `row b` } )
327+ } )
328+
329+ const outcome = await Promise . race ( [
330+ tx . commit ( ) . then ( ( ) => `persisted` as const ) ,
331+ new Promise < `timed out`> ( ( resolve ) =>
332+ setTimeout ( ( ) => resolve ( `timed out` ) , 2_000 ) ,
333+ ) ,
334+ ] )
335+ expect ( outcome ) . toBe ( `persisted` )
336+
337+ const documents = await db . getAll < { id : string } > (
338+ `SELECT id FROM documents` ,
339+ )
340+ expect ( documents . map ( ( doc ) => doc . id ) ) . toEqual ( [ `b` ] )
341+ } )
342+
278343 it ( `should handle transactions with multiple collections` , async ( ) => {
279344 const db = await createDatabase ( )
280345 await createTestData ( db )
0 commit comments