pg-mem in-memory Postgres with Kysely for fast DB tests: working versions (tested 2026-10-15)
Run Kysely queries against pg-mem through its node-postgres adapter: serial ids, RETURNING, a frozen now(), unique/check/foreign-key constraints, atomic multi-row inserts, joins with aggregates, and snapshot restore (pg-mem does not roll back multi-query transactions).
passed6 passed · 0 failed2026-10-15 20:09 UTC- Status
- passed
- Last passed
- 2026-10-15 20:09 UTC
- Tests
- 6 / 6
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions4
kysely@0.29.6@types/node@26.6.5tsx@4.23.15pg-mem@3.0.14
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38799498972
The code
Exactly the files that passed. Your agent can pull them with get_recipe("pg-mem-kysely-postgres-tests") (install), or see them on GitHub.
package.json
{
"name": "pg-mem-kysely-postgres-tests",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"kysely": "0.29.6"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15",
"pg-mem": "3.0.14"
}
}
src/db.ts
import { newDb, DataType } from "pg-mem";
import { Kysely, PostgresDialect, sql, type Generated } from "kysely";
export interface DB {
courses: { id: Generated<number>; slug: string; title: string; hours: number; created_at: Generated<Date> };
enrollments: { id: Generated<number>; course_id: number; email: string };
}
/** In-memory Postgres (pg-mem) wired to Kysely through pg-mem's node-postgres adapter. */
export function makeDb() {
const mem = newDb();
// Freeze now(): built-ins live in pg_catalog, so override there (public.registerFunction is shadowed).
mem.getSchema("pg_catalog").registerFunction({ name: "now", returns: DataType.timestamptz, implementation: () => new Date("2026-10-15T20:00:00Z"), impure: true }, true);
mem.public.none(`
create table courses (id serial primary key, slug text not null unique, title text not null, hours int not null check (hours < 120), created_at timestamptz not null default now());
create table enrollments (id serial primary key, course_id int not null references courses(id), email text not null, unique (course_id, email));
`);
const { Pool } = mem.adapters.createPg();
const db = new Kysely<DB>({ dialect: new PostgresDialect({ pool: new Pool() }) });
return { db, mem };
}
export async function addCourse(db: Kysely<DB>, slug: string, title: string, hours: number) {
return db.insertInto("courses").values({ slug, title, hours }).returning(["id", "slug", "created_at"]).executeTakeFirstOrThrow();
}
/**
* One multi-row INSERT is atomic on real Postgres and on pg-mem. Note: pg-mem does NOT undo
* earlier statements when a multi-query transaction (BEGIN ... ROLLBACK over separate queries)
* fails, so keep must-be-atomic writes in a single statement or use mem.backup() in tests.
*/
export async function enroll(db: Kysely<DB>, slug: string, emails: string[]) {
const c = await db.selectFrom("courses").select("id").where("slug", "=", slug).executeTakeFirstOrThrow();
const rows = await db.insertInto("enrollments").values(emails.map((email) => ({ course_id: c.id, email }))).returning("id").execute();
return rows.length;
}
export async function enrollOneByOneInTransaction(db: Kysely<DB>, slug: string, emails: string[]) {
return db.transaction().execute(async (trx) => {
const c = await trx.selectFrom("courses").select("id").where("slug", "=", slug).executeTakeFirstOrThrow();
for (const email of emails) await trx.insertInto("enrollments").values({ course_id: c.id, email }).execute();
return emails.length;
});
}
export async function roster(db: Kysely<DB>) {
return db
.selectFrom("courses as c")
.leftJoin("enrollments as e", "e.course_id", "c.id")
.select(["c.slug", sql<number>`count(e.id)::int`.as("students")])
.groupBy("c.slug")
.orderBy("c.slug")
.execute();
}
test/db.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { makeDb, addCourse, enroll, enrollOneByOneInTransaction, roster } from "../src/db.ts";
test("insert with returning, serial ids and default now()", async () => {
const { db } = makeDb();
const a = await addCourse(db, "cits", "CITS Orientation", 8);
const b = await addCourse(db, "rigging", "Rigging", 16);
assert.equal(a.id, 1);
assert.equal(b.id, 2);
assert.equal(new Date(a.created_at).toISOString(), "2026-10-15T20:00:00.000Z");
await db.destroy();
});
test("constraints: unique and check", async () => {
const { db } = makeDb();
await addCourse(db, "cits", "CITS", 8);
await assert.rejects(addCourse(db, "cits", "dup", 8), /duplicate key|unique/i);
await assert.rejects(addCourse(db, "long", "Too long", 200), /check/i);
await db.destroy();
});
test("multi-row insert is atomic: a duplicate rejects the whole batch", async () => {
const { db } = makeDb();
await addCourse(db, "cits", "CITS", 8);
await assert.rejects(enroll(db, "cits", ["[email protected]", "[email protected]", "[email protected]"]), /duplicate key|unique/i);
assert.deepEqual(await roster(db), [{ slug: "cits", students: 0 }]);
assert.equal(await enroll(db, "cits", ["[email protected]", "[email protected]"]), 2);
assert.deepEqual(await roster(db), [{ slug: "cits", students: 2 }]);
await db.destroy();
});
test("pg-mem gotcha: Kysely transaction errors propagate but earlier statements are not rolled back", async () => {
const { db, mem } = makeDb();
await addCourse(db, "cits", "CITS", 8);
const snap = mem.backup();
await assert.rejects(enrollOneByOneInTransaction(db, "cits", ["[email protected]", "[email protected]"]), /duplicate key|unique/i);
// On real Postgres this would be 0. pg-mem keeps the first row, so restore a snapshot instead.
assert.deepEqual(await roster(db), [{ slug: "cits", students: 1 }]);
snap.restore();
assert.deepEqual(await roster(db), [{ slug: "cits", students: 0 }]);
await db.destroy();
});
test("foreign keys enforced and left join aggregates", async () => {
const { db } = makeDb();
await addCourse(db, "cits", "CITS", 8);
await addCourse(db, "forklift", "Forklift", 4);
await enroll(db, "forklift", ["[email protected]"]);
await assert.rejects(db.insertInto("enrollments").values({ course_id: 99, email: "[email protected]" }).execute(), /foreign key/i);
assert.deepEqual(await roster(db), [{ slug: "cits", students: 0 }, { slug: "forklift", students: 1 }]);
await db.destroy();
});
test("backup and restore snapshots for fast test isolation", async () => {
const { db, mem } = makeDb();
await addCourse(db, "cits", "CITS", 8);
const snap = mem.backup();
await addCourse(db, "extra", "Extra", 2);
snap.restore();
assert.deepEqual((await roster(db)).map((r) => r.slug), ["cits"]);
await db.destroy();
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.