Skip to content
NewFrom Mongoose to SQL: migrating MongoDB to PostgreSQL7 min read

PostgreSQL

PostgreSQL is UQL’s most complete backend, with native arrays, JSON operators, full-text and pgvector semantic search, and RETURNING for generated ids.

Terminal window
npm install uql-orm pg
import { PgQuerierPool } from 'uql-orm/postgres';
export const pool = new PgQuerierPool({
connectionString: process.env.DATABASE_URL,
max: 10,
});

The pool takes node-postgres’ PoolConfig, with keepAlive: true by default. See Pool for sizing and shutdown, and PGlite for the same dialect in-process, for tests.

Streaming also needs npm install pg-query-stream, which UQL loads on first use.

All four isolation levels work. serializable and repeatable read can fail with 40001; retry the whole transaction, never a statement inside it.

await pool.transaction(run, { isolationLevel: 'serializable' });

Behind PgBouncer or another transaction-mode pooler, session state does not carry between statements, so keep SET LOCAL, advisory locks and temp tables inside one transaction callback.

RLS backs up UQL’s security filters and also covers raw SQL. Set the tenant inside the transaction:

await pool.transaction(async (querier) => {
await querier.run`SELECT set_config('app.tenant_id', ${tenantId}, true)`;
return querier.findMany(Invoice, { $limit: 50 });
});

Hyperdrive keeps a warm Postgres pool at the edge for Workers, and pg runs on it unchanged. It needs nodejs_compat and a compatibility date of 2024-09-23 or later:

wrangler.jsonc
{
"compatibility_date": "2026-08-01",
"compatibility_flags": ["nodejs_compat"],
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<id>" }],
}

The binding exists only inside a request, so build the pool there, with a small max since Hyperdrive does the real pooling:

src/index.ts
import { PgQuerierPool } from 'uql-orm/postgres';
import { Item } from './models';
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const pool = new PgQuerierPool({
connectionString: env.HYPERDRIVE.connectionString,
max: 5,
});
try {
return Response.json(await pool.findMany(Item, { $limit: 20 }));
} finally {
ctx.waitUntil(pool.end());
}
},
};

Run migrations from CI against the database’s own hostname, not through the binding.

Bun SQL drops pg under Bun. See also CockroachDB, Supabase, Neon and Serverless.