From e76eb545d7d2bbd763e696c947da9e0cd34e1ecd Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 28 Sep 2026 16:23:58 +0300 Subject: [PATCH] perf: group catalog source reads by identity --- .../catalog-discovery.repository.ts | 16 ++++++- test/tbank-catalog-local.e2e-spec.ts | 44 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts b/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts index 43547bf..23cbc0e 100644 --- a/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts +++ b/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts @@ -123,11 +123,23 @@ export class CatalogDiscoveryRepository { } readSources(inputs: CatalogSource[]) { + // Share identity predicates across UIDs instead of preparing one OR per row. + const groups = new Map(); + for (const input of inputs) { + const key = JSON.stringify([ + input.kind, + sourceProvider(input), + input.kind === 'detail' ? input.instrumentType : (input.rangeKey ?? ''), + ]); + const group = groups.get(key); + if (group) group.push(input); + else groups.set(key, [input]); + } return this.prisma.tbankInstrumentSource.findMany({ where: { provider, - OR: inputs.map((input) => ({ - instrumentUid: input.uid, + OR: [...groups.values()].map(([input, ...rest]) => ({ + instrumentUid: { in: [input, ...rest].map((entry) => entry.uid) }, kind: input.kind, source: sourceProvider(input), rangeKey: diff --git a/test/tbank-catalog-local.e2e-spec.ts b/test/tbank-catalog-local.e2e-spec.ts index 611b42a..2b532c0 100644 --- a/test/tbank-catalog-local.e2e-spec.ts +++ b/test/tbank-catalog-local.e2e-spec.ts @@ -178,6 +178,50 @@ describe('local stock/bond search (SQLite HTTP)', () => { }); afterAll(async () => app?.close()); + it('reads only requested source identities when batching UIDs, types and periods', async () => { + const repository = app.get(CatalogDiscoveryRepository); + const fetchedAt = new Date(); + for (const uid of ['a', 'b']) { + await prisma.tbankInstrument.create({ + data: { provider: 'tbank', uid, instrumentType: 'share', metadata: {}, fetchedAt }, + }); + } + const expected = [ + { instrumentUid: 'a', source: 'tbank', kind: 'detail', rangeKey: 'share' }, + { instrumentUid: 'b', source: 'tbank', kind: 'detail', rangeKey: 'share' }, + { instrumentUid: 'a', source: 'tbank', kind: 'dividends', rangeKey: '2026' }, + { instrumentUid: 'b', source: 'tbank', kind: 'dividends', rangeKey: '2027' }, + { instrumentUid: 'b', source: 'moex', kind: 'bond-events', rangeKey: '' }, + { instrumentUid: 'a', source: 'ncr', kind: 'rating', rangeKey: '' }, + ]; + const distractors = [ + { instrumentUid: 'a', source: 'tbank', kind: 'detail', rangeKey: 'bond' }, + { instrumentUid: 'b', source: 'tbank', kind: 'dividends', rangeKey: '2026' }, + { instrumentUid: 'a', source: 'tbank', kind: 'dividends', rangeKey: '2027' }, + { instrumentUid: 'a', source: 'moex', kind: 'bond-events', rangeKey: '' }, + { instrumentUid: 'b', source: 'tbank', kind: 'bond-events', rangeKey: '' }, + { instrumentUid: 'a', source: 'tbank', kind: 'rating', rangeKey: '' }, + ]; + await prisma.tbankInstrumentSource.createMany({ + data: [...expected, ...distractors].map((identity) => ({ + ...identity, provider: 'tbank', fetchedAt, payload: identity, + })), + }); + const rows = await repository.readSources([ + { uid: 'a', instrumentType: 'share', kind: 'detail' }, + { uid: 'b', instrumentType: 'share', kind: 'detail' }, + { uid: 'a', instrumentType: 'share', kind: 'detail' }, + { uid: 'a', instrumentType: 'share', kind: 'dividends', rangeKey: '2026' }, + { uid: 'b', instrumentType: 'share', kind: 'dividends', rangeKey: '2027' }, + { uid: 'b', instrumentType: 'bond', kind: 'bond-events' }, + { uid: 'a', instrumentType: 'bond', kind: 'rating' }, + { uid: 'missing', instrumentType: 'share', kind: 'detail' }, + ]); + expect(rows.map((row) => row.payload)).toEqual(expect.arrayContaining(expected)); + expect(rows).toHaveLength(expected.length); + expect(await repository.readSources([])).toEqual([]); + }); + it('returns missing without waiting for discovery, then partial while enrichment is blocked; observe never launches work', async () => { const found = deferred<(typeof share)[]>(); const detail = deferred();