From 0b8af7f139c43dd54ffef1f6f7723a373488fa3f Mon Sep 17 00:00:00 2001 From: metallicmonkey365 Date: Tue, 20 Oct 2026 11:43:29 -0400 Subject: [PATCH 1/4] Default generate to PGlite --- src/generate.ts | 69 +++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/generate.ts b/src/generate.ts index f337094..ff3c91c 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -12,7 +12,7 @@ export const generate = async ({ schemas, defaultDatabase, dbDir, - pglite = false, + pglite = true, migrationsDir, }: Pick & { pglite?: boolean @@ -28,14 +28,6 @@ export const generate = async ({ const db = new PGlite() - await migrate({ - client: db as any, - migrationsDir, - defaultDatabase, - cwd: process.cwd(), - schemas, - }) - const server = net.createServer(async (socket) => { const connection = await fromNodeSocket(socket, { serverVersion: "16.3 (PGlite)", @@ -60,32 +52,47 @@ export const generate = async ({ }) }) - await new Promise((resolve) => server.listen(0, resolve)) - const port = (server.address() as any).port - const connectionString = `postgres://postgres:postgres@127.0.0.1:${port}/postgres` + try { + await migrate({ + client: db as any, + migrationsDir, + defaultDatabase, + cwd: process.cwd(), + schemas, + }) - const prevDbUrl = process.env.DATABASE_URL - process.env.DATABASE_URL = connectionString + await new Promise((resolve) => server.listen(0, resolve)) + const port = (server.address() as any).port + const connectionString = `postgres://postgres:postgres@127.0.0.1:${port}/postgres` - await zg.generate({ - db: { - connectionString, - }, - schemas: Object.fromEntries( - schemas.map((s) => [s, { include: "*", exclude: [] }]), - ), - outDir: dbDir, - }) + const prevDbUrl = process.env.DATABASE_URL + process.env.DATABASE_URL = connectionString - await dumpTree({ - targetDir: path.join(dbDir, "structure"), - defaultDatabase: "postgres", - schemas, - }) + try { + await zg.generate({ + db: { + connectionString, + }, + schemas: Object.fromEntries( + schemas.map((s) => [s, { include: "*", exclude: [] }]), + ), + outDir: dbDir, + }) - server.close() - if (prevDbUrl === undefined) delete process.env.DATABASE_URL - else process.env.DATABASE_URL = prevDbUrl + await dumpTree({ + targetDir: path.join(dbDir, "structure"), + defaultDatabase: "postgres", + schemas, + }) + } finally { + if (prevDbUrl === undefined) delete process.env.DATABASE_URL + else process.env.DATABASE_URL = prevDbUrl + } + } finally { + ;(server as any).closeAllConnections?.() + await new Promise((resolve) => server.close(() => resolve())) + await db.close() + } return } From c0c4e6682ae234a92c1e8a3cb051665eceaed219 Mon Sep 17 00:00:00 2001 From: metallicmonkey365 Date: Tue, 20 Oct 2026 11:49:02 -0400 Subject: [PATCH 2/4] Default generate to PGlite --- src/cli.ts | 9 +++++++-- src/init.ts | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 9a9bdec..b376c58 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -35,10 +35,15 @@ import { getProjectContext } from "./get-project-context" "generate", "generate types and sql documentation from database", (yargs) => { - yargs.option("pglite", { type: "boolean", default: false }) + yargs.option("pglite", { + type: "boolean", + default: true, + describe: + "generate from migrations using an in-memory PGlite instance, so no running Postgres is required; use --no-pglite to generate from an external Postgres via DATABASE_URL", + }) }, async (argv) => { - generate({ ...(await getProjectContext()), pglite: !!argv.pglite }) + await generate({ ...(await getProjectContext()), pglite: !!argv.pglite }) }, ) .parse() diff --git a/src/init.ts b/src/init.ts index b84f9cd..9917e0d 100644 --- a/src/init.ts +++ b/src/init.ts @@ -16,7 +16,8 @@ export const initPgstrap = async (ctx: Pick) => { pkg.scripts["db:migrate"] = "pgstrap migrate" pkg.scripts["db:reset"] = "pgstrap reset" - pkg.scripts["db:generate"] = "pgstrap generate" + pkg.scripts["db:generate"] = "pgstrap generate --pglite" + pkg.scripts["db:generate:postgres"] = "pgstrap generate --no-pglite" pkg.scripts["db:create-migration"] = "pgstrap create-migration" if (!pkg.devDependencies) pkg.devDependencies = {} From e200bb8803d66e2ce4a3cbb0b1e15718ccb799de Mon Sep 17 00:00:00 2001 From: metallicmonkey365 Date: Tue, 20 Oct 2026 11:49:30 -0400 Subject: [PATCH 3/4] Default generate to PGlite --- tests/generate.default-pglite.test.ts | 139 ++++++++++++++++++++++++++ tests/init.test.ts | 5 +- 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 tests/generate.default-pglite.test.ts diff --git a/tests/generate.default-pglite.test.ts b/tests/generate.default-pglite.test.ts new file mode 100644 index 0000000..e30f2ea --- /dev/null +++ b/tests/generate.default-pglite.test.ts @@ -0,0 +1,139 @@ +import { afterEach, test, expect } from "bun:test" +import fs from "fs" +import os from "os" +import path from "path" +import { generate } from "../src/generate" + +const migrationFile = ` +exports.up = async (pgm) => { + pgm.createTable('foo', { id: 'id' }) +} +exports.down = async (pgm) => { + pgm.dropTable('foo') +} +` + +// Nothing listens on port 1, so any attempt to use this database fails fast. +// Setting DATABASE_URL to it proves which code path generation took: the +// pglite path never touches it, the external postgres path fails against it. +const UNREACHABLE_DB_URL = + "postgres://postgres:postgres@127.0.0.1:1/unreachable" + +const originalDbUrl = process.env.DATABASE_URL + +afterEach(() => { + if (originalDbUrl === undefined) delete process.env.DATABASE_URL + else process.env.DATABASE_URL = originalDbUrl +}) + +const setupProject = () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "pgstrap-generate-")) + const migrationsDir = path.join(tmp, "migrations") + fs.mkdirSync(migrationsDir, { recursive: true }) + fs.writeFileSync( + path.join(migrationsDir, "001_create_table.js"), + migrationFile, + ) + return { tmp, migrationsDir } +} + +test("generate uses pglite by default, without a running postgres", async () => { + const { tmp, migrationsDir } = setupProject() + process.env.DATABASE_URL = UNREACHABLE_DB_URL + + // No `pglite` option passed: this is the default `pgstrap generate` path + // used by the scaffolded `db:generate` script. + await generate({ + schemas: ["public"], + defaultDatabase: "postgres", + dbDir: path.join(tmp, "db"), + migrationsDir, + }) + + const zapatosFile = path.join(tmp, "db", "zapatos", "schema.d.ts") + const structureFile = path.join( + tmp, + "db", + "structure", + "public", + "tables", + "foo", + "table.sql", + ) + + expect(fs.existsSync(zapatosFile)).toBe(true) + expect(fs.existsSync(structureFile)).toBe(true) + // The caller's DATABASE_URL must be restored afterwards. + expect(process.env.DATABASE_URL).toBe(UNREACHABLE_DB_URL) + + fs.rmSync(tmp, { recursive: true, force: true }) +}, 30_000) + +test("generate with pglite: false uses the external postgres from DATABASE_URL", async () => { + const { tmp, migrationsDir } = setupProject() + const dbDir = path.join(tmp, "db") + + // The opt-out must really take the external path: with an unreachable + // database it fails instead of silently falling back to pglite. This + // runs in a subprocess because zapatos' connection pool surfaces + // connection failures as unhandled rejections, which would kill the + // test runner process itself (pre-existing behavior of the external + // path) — the subprocess's exit code is the observable contract. + const scriptPath = path.join(tmp, "run-generate.ts") + fs.writeFileSync( + scriptPath, + `import { generate } from ${JSON.stringify( + path.join(import.meta.dir, "..", "src", "generate"), + )} +await generate({ + schemas: ["public"], + defaultDatabase: "postgres", + dbDir: process.env.PGSTRAP_TEST_DB_DIR!, + migrationsDir: process.env.PGSTRAP_TEST_MIGRATIONS_DIR!, + pglite: false, +}) +`, + ) + + const proc = Bun.spawnSync(["bun", "run", scriptPath], { + cwd: path.join(import.meta.dir, ".."), + env: { + ...process.env, + DATABASE_URL: UNREACHABLE_DB_URL, + PGSTRAP_TEST_DB_DIR: dbDir, + PGSTRAP_TEST_MIGRATIONS_DIR: migrationsDir, + }, + }) + + expect(proc.exitCode).not.toBe(0) + expect(proc.stdout.toString() + proc.stderr.toString()).toContain( + "ECONNREFUSED", + ) + expect(fs.existsSync(path.join(dbDir, "zapatos", "schema.d.ts"))).toBe(false) + + fs.rmSync(tmp, { recursive: true, force: true }) +}, 30_000) + +test("generate restores DATABASE_URL when generation fails", async () => { + const { tmp, migrationsDir } = setupProject() + process.env.DATABASE_URL = UNREACHABLE_DB_URL + + // A dbDir inside a regular file cannot be written to, so generation fails + // after the temporary pglite connection string has been installed in + // DATABASE_URL. The original value must still be restored. + const blocker = path.join(tmp, "blocker") + fs.writeFileSync(blocker, "not a directory") + + await expect( + generate({ + schemas: ["public"], + defaultDatabase: "postgres", + dbDir: path.join(blocker, "db"), + migrationsDir, + }), + ).rejects.toThrow() + + expect(process.env.DATABASE_URL).toBe(UNREACHABLE_DB_URL) + + fs.rmSync(tmp, { recursive: true, force: true }) +}, 30_000) diff --git a/tests/init.test.ts b/tests/init.test.ts index cd4ec4b..e416546 100644 --- a/tests/init.test.ts +++ b/tests/init.test.ts @@ -25,6 +25,9 @@ test("initPgstrap writes scripts to package.json", async () => { ) expect(pkg.scripts["db:migrate"]).toBe("pgstrap migrate") expect(pkg.scripts["db:reset"]).toBe("pgstrap reset") - expect(pkg.scripts["db:generate"]).toBe("pgstrap generate") + expect(pkg.scripts["db:generate"]).toBe("pgstrap generate --pglite") + expect(pkg.scripts["db:generate:postgres"]).toBe( + "pgstrap generate --no-pglite", + ) expect(pkg.scripts["db:create-migration"]).toBe("pgstrap create-migration") }) From cc3fc355db32e042109aeac51a74134a0f727bb1 Mon Sep 17 00:00:00 2001 From: metallicmonkey365 Date: Tue, 20 Oct 2026 11:50:07 -0400 Subject: [PATCH 4/4] Default generate to PGlite --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20e3127..3ae757e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ npm install pgstrap --save-dev - `npm run db:migrate` - Run pending migrations - `npm run db:reset` - Drop and recreate the database, then run all migrations -- `npm run db:generate` - Generate types and structure dumps. Use `pgstrap generate --pglite` to run migrations against an in-memory PGlite instance. +- `npm run db:generate` - Generate types and structure dumps. Generation runs migrations against an in-memory PGlite instance by default, so no running Postgres is required. To generate from an external Postgres instead (via `DATABASE_URL`), use `pgstrap generate --no-pglite`, also scaffolded by `pgstrap init` as `npm run db:generate:postgres`. - `npm run db:create-migration` - Create a new migration file ### Configuration