diff --git a/tests/global-setup.ts b/tests/global-setup.ts index c4d428e..d779515 100644 --- a/tests/global-setup.ts +++ b/tests/global-setup.ts @@ -1,57 +1,10 @@ import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql"; -import { drizzle } from "drizzle-orm/node-postgres"; -import { migrate } from "drizzle-orm/node-postgres/migrator"; -import { Pool } from "pg"; -import path from "node:path"; let container: StartedPostgreSqlContainer; -// Every test file clones this database instead of replaying the migration -// chain itself. Shared with tests/integration/setup.ts via TEST_TEMPLATE_DB. -const TEMPLATE_DB = "ledgr_test_template"; - export async function setup() { - container = await new PostgreSqlContainer("postgres:17-alpine") - .withCommand([ - "postgres", - // One throwaway database per test file, all on this single server. The - // default max_connections (100) sits below what the fork pool can demand - // — 14 workers holding a pool each exhausts it, and Postgres starts - // terminating connections mid-run (57P01). - "-c", - "max_connections=300", - // Durability buys nothing for a server destroyed at teardown. - "-c", - "fsync=off", - "-c", - "synchronous_commit=off", - "-c", - "full_page_writes=off", - ]) - .start(); - - const connectionString = container.getConnectionUri(); - process.env.DATABASE_URL = connectionString; - process.env.TEST_TEMPLATE_DB = TEMPLATE_DB; - - // Migrate once, here, into a template database. Cloning that template per - // test file replaces one full migration run per file with a file copy. - const admin = new Pool({ connectionString, max: 1 }); - await admin.query(`CREATE DATABASE "${TEMPLATE_DB}"`); - await admin.end(); - - const templateUrl = new URL(connectionString); - templateUrl.pathname = `/${TEMPLATE_DB}`; - const pool = new Pool({ connectionString: templateUrl.toString(), max: 1 }); - try { - await migrate(drizzle({ client: pool }), { - migrationsFolder: path.join(process.cwd(), "src/db/migrations"), - }); - } finally { - // The template must have no open connections, or CREATE DATABASE ... - // TEMPLATE fails with 55006 for every test file that follows. - await pool.end(); - } + container = await new PostgreSqlContainer("postgres:17-alpine").start(); + process.env.DATABASE_URL = container.getConnectionUri(); } export async function teardown() { diff --git a/tests/integration/setup.ts b/tests/integration/setup.ts index 10f1c7f..8b3483b 100644 --- a/tests/integration/setup.ts +++ b/tests/integration/setup.ts @@ -5,35 +5,11 @@ import { randomUUID } from "crypto"; import * as schema from "../../src/db/schema"; import path from "node:path"; -// Each worker holds a pool for the life of its test file. Left at pg's default -// of 10 these overrun the server's connection limit once vitest scales forks to -// the core count; the suite needs only a couple of concurrent queries per file. -const POOL_MAX = 4; - -// CREATE DATABASE ... TEMPLATE briefly conflicts when several workers clone the -// same template at once (55006). It clears on its own, so retry rather than -// serialize every worker behind a lock. -const CLONE_RETRIES = 10; - -async function cloneTemplate(admin: Pool, dbName: string, template: string) { - for (let attempt = 0; ; attempt++) { - try { - await admin.query(`CREATE DATABASE "${dbName}" TEMPLATE "${template}"`); - return; - } catch (error) { - const code = (error as { code?: string }).code; - if (code !== "55006" || attempt >= CLONE_RETRIES) throw error; - await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1))); - } - } -} - export async function createTestDb() { const connectionString = process.env.DATABASE_URL || "postgresql://ledgr:ledgr@localhost:5432/ledgr_test"; const dbName = `test_${randomUUID().replace(/-/g, "")}`; - const template = process.env.TEST_TEMPLATE_DB; // Isolate each test file in its own *database* (not a schema). Migrations // reference tables as `"public".""`, which only resolves when the @@ -41,27 +17,18 @@ export async function createTestDb() { // on those qualified references. A throwaway database per file gives every test // its own public schema, keeps the concurrent suite isolated, and is robust to // future migrations regardless of how they qualify identifiers. - const admin = new Pool({ connectionString, max: 1 }); - if (template) { - await cloneTemplate(admin, dbName, template); - } else { - await admin.query(`CREATE DATABASE "${dbName}"`); - } + const admin = new Pool({ connectionString }); + await admin.query(`CREATE DATABASE "${dbName}"`); await admin.end(); const url = new URL(connectionString); url.pathname = `/${dbName}`; - const pool = new Pool({ connectionString: url.toString(), max: POOL_MAX }); + const pool = new Pool({ connectionString: url.toString() }); const db = drizzle({ client: pool, schema }); - - // The template arrives already migrated. Without one (a direct DATABASE_URL, - // no global setup) fall back to replaying the chain. - if (!template) { - await migrate(db, { - migrationsFolder: path.join(process.cwd(), "src/db/migrations"), - }); - } + await migrate(db, { + migrationsFolder: path.join(process.cwd(), "src/db/migrations"), + }); return { db, @@ -69,7 +36,7 @@ export async function createTestDb() { await pool.end(); // DROP DATABASE cannot run while connections are open; FORCE terminates any // stragglers (Postgres 13+). - const admin2 = new Pool({ connectionString, max: 1 }); + const admin2 = new Pool({ connectionString }); await admin2.query(`DROP DATABASE IF EXISTS "${dbName}" WITH (FORCE)`); await admin2.end(); },