diff --git a/Makefile b/Makefile index 550c43af7..87a44e546 100644 --- a/Makefile +++ b/Makefile @@ -209,6 +209,8 @@ web-gate-integration: ## Run Layer 2 web-only integration tests (skipped until o web-gate-postgres: build-web ## Run fail-loud Postgres-backed web integration tests (requires VARLENS_PG_URL) @if [ -z "$$VARLENS_PG_URL" ]; then echo "VARLENS_PG_URL is required for web-gate-postgres. This is intentionally opt-in and never part of default desktop CI."; exit 2; fi npx vitest run --project web-gate tests/web-gate/integration + VARLENS_RUN_POSTGRES_E2E=1 npx vitest run --project main \ + tests/main/storage/postgres-cases-query-repository.e2e.test.ts web-gate-parity: web-data-verify ## Run Layer 3 parity scenarios (opt-in; boots Electron, switches native ABI) @echo "=== web-gate-parity (opt-in; switches native module to Electron ABI) ===" diff --git a/src/main/storage/postgres/PostgresCasesQueryRepository.ts b/src/main/storage/postgres/PostgresCasesQueryRepository.ts index faf97feb6..5f7e7b1f3 100644 --- a/src/main/storage/postgres/PostgresCasesQueryRepository.ts +++ b/src/main/storage/postgres/PostgresCasesQueryRepository.ts @@ -74,7 +74,16 @@ export class PostgresCasesQueryRepository { LEFT JOIN ${schemaName}."case_cohort_links" ccl ON ccl.case_id = c.id LEFT JOIN ${schemaName}."cohort_groups" cg ON cg.id = ccl.cohort_id ${whereSql} - GROUP BY c.id, cm.affected_status, cm.sex + GROUP BY + c.id, + c.name, + c.file_path, + c.file_size, + c.variant_count, + c.created_at, + c.genome_build, + cm.affected_status, + cm.sex ORDER BY ${orderColumn} ${orderDirection} LIMIT $${values.length + 1} OFFSET $${values.length + 2} diff --git a/tests/main/storage/postgres-cases-query-repository.e2e.test.ts b/tests/main/storage/postgres-cases-query-repository.e2e.test.ts new file mode 100644 index 000000000..f1c2443ae --- /dev/null +++ b/tests/main/storage/postgres-cases-query-repository.e2e.test.ts @@ -0,0 +1,82 @@ +import { randomBytes } from 'node:crypto' + +import { Client, Pool } from 'pg' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { PostgresCasesQueryRepository } from '../../../src/main/storage/postgres/PostgresCasesQueryRepository' +import { POSTGRES_MIGRATIONS } from '../../../src/main/storage/postgres/migrations/definitions' +import { PostgresMigrationRunner } from '../../../src/main/storage/postgres/migrations/PostgresMigrationRunner' + +const RUN = process.env.VARLENS_RUN_POSTGRES_E2E === '1' +const PG_URL = + process.env.VARLENS_PG_URL ?? + 'postgres://varlens:varlens_dev_password@127.0.0.1:55432/varlens_dev' + +describe.skipIf(!RUN)('PostgresCasesQueryRepository — migrated cases view', () => { + const schema = `varlens_test_cases_query_${Date.now()}_${randomBytes(4).toString('hex')}` + let pool: Pool + let probe: Client + + beforeAll(async () => { + pool = new Pool({ connectionString: PG_URL, max: 2 }) + probe = new Client({ connectionString: PG_URL }) + await probe.connect() + await new PostgresMigrationRunner(pool, schema, POSTGRES_MIGRATIONS).migrate() + }, 60_000) + + afterAll(async () => { + if (probe) await probe.end() + if (pool) await pool.end() + + const cleaner = new Client({ connectionString: PG_URL }) + await cleaner.connect() + await cleaner.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`) + await cleaner.end() + }, 60_000) + + it('queries a ready case with metadata and cohorts through the view', async () => { + const now = Date.now() + const caseResult = await probe.query<{ id: number }>( + `INSERT INTO "${schema}"."cases" + (name, file_path, file_size, variant_count, created_at, genome_build) + VALUES ('view-backed-case', '/tmp/view-backed-case.json', 42, 1, $1, 'GRCh38') + RETURNING id`, + [now] + ) + const caseId = Number(caseResult.rows[0].id) + await probe.query( + `INSERT INTO "${schema}"."case_metadata" (case_id, affected_status, sex) + VALUES ($1, 'affected', 'female')`, + [caseId] + ) + const cohortResult = await probe.query<{ id: number }>( + `INSERT INTO "${schema}"."cohort_groups" (name, created_at) + VALUES ('view-backed-cohort', $1) + RETURNING id`, + [now] + ) + await probe.query( + `INSERT INTO "${schema}"."case_cohort_links" (case_id, cohort_id) + VALUES ($1, $2)`, + [caseId, cohortResult.rows[0].id] + ) + + const result = await new PostgresCasesQueryRepository(pool, schema).queryCases({ + limit: 25, + offset: 0 + }) + + expect(result).toEqual({ + data: [ + expect.objectContaining({ + id: caseId, + name: 'view-backed-case', + affected_status: 'affected', + sex: 'female', + cohort_names: ['view-backed-cohort'] + }) + ], + total_count: 1 + }) + }, 60_000) +}) diff --git a/tests/main/storage/postgres-cases-query-repository.test.ts b/tests/main/storage/postgres-cases-query-repository.test.ts index 6c9371951..e61409e37 100644 --- a/tests/main/storage/postgres-cases-query-repository.test.ts +++ b/tests/main/storage/postgres-cases-query-repository.test.ts @@ -66,6 +66,19 @@ describe('PostgresCasesQueryRepository', () => { expect.stringContaining('COUNT(*)::int AS total_count'), ['%new%'] ) + const rowsSql = String(pool.query.mock.calls[0]?.[0]) + const groupBySql = rowsSql.slice(rowsSql.indexOf('GROUP BY'), rowsSql.indexOf('ORDER BY')) + for (const projectedCaseColumn of [ + 'c.id', + 'c.name', + 'c.file_path', + 'c.file_size', + 'c.variant_count', + 'c.created_at', + 'c.genome_build' + ]) { + expect(groupBySql).toContain(projectedCaseColumn) + } }) it('runs the no-filter count through the named (prepared) path', async () => {