From d5bcd6a19ef062ad4af65b755bfa0b21425672f5 Mon Sep 17 00:00:00 2001 From: anshumancanrock Date: Mon, 14 Sep 2026 02:49:23 +0530 Subject: [PATCH] fix(nip45): return COUNT results for filters with generic tag queries --- .../count-duplicate-event-id-projection.md | 5 ++++ src/repositories/event-repository.ts | 6 +---- .../repositories/event-repository.spec.ts | 27 +++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 .changeset/count-duplicate-event-id-projection.md diff --git a/.changeset/count-duplicate-event-id-projection.md b/.changeset/count-duplicate-event-id-projection.md new file mode 100644 index 00000000..72ba0706 --- /dev/null +++ b/.changeset/count-duplicate-event-id-projection.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +fix: return COUNT results for filters with generic tag queries (`#e`, `#p`, `#g`, `#h`), which projected `event_id` twice and failed with "error: unable to count events" diff --git a/src/repositories/event-repository.ts b/src/repositories/event-repository.ts index aea25f70..822b7fa0 100644 --- a/src/repositories/event-repository.ts +++ b/src/repositories/event-repository.ts @@ -117,16 +117,12 @@ export class EventRepository implements IEventRepository { const queries = filters.map((currentFilter) => { const builder = this.readReplicaDbClient('events').select('events.event_id') - const { isTagQuery } = this.applyFilterConditions(builder, currentFilter) + this.applyFilterConditions(builder, currentFilter) if (typeof currentFilter.limit === 'number') { builder.limit(currentFilter.limit).orderBy('event_created_at', 'DESC').orderBy('event_id', 'asc') } - if (isTagQuery) { - builder.select('events.event_id') - } - builder.whereNull('events.deleted_at').andWhere((bd) => { bd.whereNull('events.expires_at').orWhere('events.expires_at', '>', now) }) diff --git a/test/unit/repositories/event-repository.spec.ts b/test/unit/repositories/event-repository.spec.ts index 5fb1f81e..d36294a7 100644 --- a/test/unit/repositories/event-repository.spec.ts +++ b/test/unit/repositories/event-repository.spec.ts @@ -650,6 +650,33 @@ describe('EventRepository', () => { expect(sql).to.include('"events"."expires_at" is null') expect(sql).to.include('"events"."expires_at" >') }) + + it('projects event_id exactly once for generic tag filters', async () => { + const fromStub = sandbox.stub(rrDbClient, 'from').returns({ + countDistinct: () => ({ + first: async () => ({ count: '1' }), + }), + } as any) + + await repository.countByFilters([{ '#e': ['aaaaaa'] } as any]) + + const sql = fromStub.firstCall.args[0].toString() + expect(sql).to.include('select "events"."event_id" from "events" left join "event_tags"') + expect(sql).to.not.include('"events"."event_id", "events"."event_id"') + }) + + it('projects the same columns in both branches of a mixed union', async () => { + const fromStub = sandbox.stub(rrDbClient, 'from').returns({ + countDistinct: () => ({ + first: async () => ({ count: '1' }), + }), + } as any) + + await repository.countByFilters([{ kinds: [1] }, { '#e': ['aaaaaa'] } as any]) + + const sql = fromStub.firstCall.args[0].toString() + expect(sql.match(/select "events"\."event_id" from "events"/g)).to.have.lengthOf(2) + }) }) describe('.create', () => {