Skip to content

Commit 3c78e61

Browse files
fix: add tie-breaker to upsertMany (#553)
* fix: add tie-breaker to upsertMany * fix: resolve merge artifacts and add changeset --------- Co-authored-by: Ricardo Cabral <me@ricardocabral.io>
1 parent dec061d commit 3c78e61

3 files changed

Lines changed: 45 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
Fix replaceable batch upserts to apply NIP-01 tie-breaker semantics when timestamps are equal by comparing event IDs.

src/repositories/event-repository.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,9 @@ export class EventRepository implements IEventRepository {
308308
'event_tags',
309309
'expires_at',
310310
])
311-
.whereRaw('"events"."event_created_at" < "excluded"."event_created_at"')
311+
.whereRaw(
312+
'("events"."event_created_at" < "excluded"."event_created_at" or ("events"."event_created_at" = "excluded"."event_created_at" and "events"."event_id" > "excluded"."event_id"))',
313+
)
312314
.then(prop('rowCount') as () => number, () => 0)
313315
}
314316

test/unit/repositories/event-repository.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,4 +629,41 @@ describe('EventRepository', () => {
629629
)
630630
})
631631
})
632+
633+
describe('upsertMany', () => {
634+
it('returns 0 when no events are provided', async () => {
635+
const result = await repository.upsertMany([])
636+
637+
expect(result).to.equal(0)
638+
})
639+
640+
it('applies NIP-01 tie-breaker in batch conflict condition', async () => {
641+
const thenStub = sandbox.stub().callsFake((onfulfilled) => Promise.resolve(onfulfilled({ rowCount: 1 })))
642+
const whereRawStub = sandbox.stub().returns({ then: thenStub })
643+
const mergeStub = sandbox.stub().returns({ whereRaw: whereRawStub })
644+
const onConflictStub = sandbox.stub().returns({ merge: mergeStub })
645+
const insertStub = sandbox.stub().returns({ onConflict: onConflictStub })
646+
const masterDbClientStub = sandbox.stub().returns({ insert: insertStub }) as unknown as DatabaseClient
647+
648+
;(masterDbClientStub as any).raw = sandbox.stub().returns('conflict-target')
649+
650+
repository = new EventRepository(masterDbClientStub, rrDbClient)
651+
652+
const event: Event = {
653+
id: 'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a',
654+
pubkey: '55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503',
655+
created_at: 1564498626,
656+
kind: 0,
657+
tags: [],
658+
content: '{"name":"ottman@minds.io"}',
659+
sig: 'd1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9',
660+
[ContextMetadataKey]: { remoteAddress: { address: '::1' } as any },
661+
}
662+
663+
const result = await repository.upsertMany([event])
664+
665+
expect(whereRawStub).to.have.been.calledOnceWithExactly('("events"."event_created_at" < "excluded"."event_created_at" or ("events"."event_created_at" = "excluded"."event_created_at" and "events"."event_id" > "excluded"."event_id"))')
666+
expect(result).to.equal(1)
667+
})
668+
})
632669
})

0 commit comments

Comments
 (0)