Pool
Every query runs on a pool, which owns the connections and lends one to each unit of work. It is the only part of UQL tied to your database: entities, queries and migrations stay the same whichever pool you build.
One per process
Section titled “One per process”Build the pool once, at module scope, in the uql.config.ts the uql-migrate CLI also reads, and import it everywhere:
import type { Config } from 'uql-orm';import { PgQuerierPool } from 'uql-orm/postgres';import { Post, User } from './shared/models/index.js';
export const pool = new PgQuerierPool({ connectionString: process.env.DATABASE_URL, max: 10, connectionTimeoutMillis: 5_000,});
export default { pool, entities: [User, Post] } satisfies Config;The first argument is the driver’s own config, passed through as is. The second holds UQL’s options: logger, logValues and slowQuery, namingStrategy, a default schema and listeners.
A second pool doubles the connections without adding capacity. The exceptions are dev servers that re-run modules on hot reload, which cache the pool on globalThis (see Next.js), and serverless functions.
Building a pool opens no connection, so a wrong password shows up at the first query. To fail at boot instead, run one there:
await pool.all`SELECT 1`;Which pool
Section titled “Which pool”| Entry point | Class | First argument |
|---|---|---|
uql-orm/ |
Pg |
pg’s Pool |
uql-orm/ |
Neon |
@neondatabase/’s Client |
uql-orm/ |
Crdb |
pg’s Pool |
uql-orm/ |
My |
mysql2’s Pool |
uql-orm/ |
Mariadb |
mariadb’s pool config |
uql-orm/ |
Ms |
mssql’s config |
uql-orm/ |
Bun |
Bun’s SQL. |
uql-orm/ |
Sqlite, Node |
file path, then the driver’s options |
uql-orm/ |
Pglite |
data directory, then PGlite’s options |
uql-orm/ |
Libsql |
@libsql/’s Config, or a client you built |
uql-orm/ |
Turso |
Turso Cloud settings |
uql-orm/ |
Turso |
file path, then the engine’s options |
uql-orm/ |
D1Querier |
the Worker’s D1 binding, or a session from it |
uql-orm/ |
Mongodb |
connection URI, then Mongo |
SQLite, PGlite and embedded Turso keep one connection that every querier shares: the methods are the same, but queries run one at a time and two queriers cannot hold separate transactions. D1, Turso Cloud and Neon hold no connection and talk over fetch(); Turso Cloud gives each querier a server session of its own, and Neon a WebSocket for a transaction or a stream.
How big
Section titled “How big”max (connectionLimit on mysql2 and mariadb, maxPoolSize on MongoDB) caps one process’s connections. Use the smallest of: the server’s limit (max_connections) divided by the processes that connect, the queries the process actually runs at once, and what the database can do in parallel (roughly its cores). An HTTP server handling 40 requests at a time wants tens; a worker taking one job at a time wants two or three.
Always set an acquire timeout (connectionTimeoutMillis on the pg family, connectTimeout on mysql2, acquireTimeout on mariadb). The pg family waits forever without one, so an unreachable or exhausted database hangs every request.
Pool calls inside pool.transaction reuse the transaction’s connection, so even max: 1 cannot deadlock. See Transactions.
When the processes alone can exhaust the server, put a pooler in front instead of shrinking max: PgBouncer or RDS Proxy, Supavisor on Supabase, Hyperdrive on Workers. They run in transaction mode, so keep SET LOCAL, advisory locks and temp tables inside one transaction, and run migrations against the direct endpoint.
Closing it
Section titled “Closing it”pool.end() closes idle connections and waits for borrowed ones. Call it after the server stops accepting requests:
process.on('SIGTERM', async () => { await server.close(); await pool.end();});NestJS does this when app.enableShutdownHooks() is on. Skip it in serverless functions: the platform freezes the process before the close finishes. On D1 and Turso Cloud it does nothing, and a libSQL pool leaves a client you passed in open. Once ended, the pg, mysql2 and SQL Server pools refuse new connections; the single-connection pools reopen on the next query.
When a connection dies while idle
Section titled “When a connection dies while idle”A NAT, load balancer or idle_session_timeout can drop an idle connection. On the pg, CockroachDB, MariaDB and SQL Server pools, UQL logs the drop through the pool’s logger, even with logging off, and discards the connection instead of letting the driver’s 'error' event crash the process. The pg and CockroachDB pools also turn on keepAlive by default.
The query that was running is yours to retry, on connection errors only and never for a non-idempotent write outside a transaction. A querier released with a transaction still open is rolled back and logged; if that rollback fails, the connection is discarded.
More than one pool
Section titled “More than one pool”A read replica or a database per tenant needs its own pool. Write functions against a UniversalQuerier, which takes a pool or a querier, so the caller picks:
import type { UniversalQuerier } from 'uql-orm';
const unpaid = (db: UniversalQuerier, companyId: number) => db.findMany(Invoice, { $where: { companyId, paid: false } });
await unpaid(replica, 42);await primary.transaction((querier) => unpaid(querier, 42));For tenants, build pools on demand in a capped map and end() the ones you evict. The HTTP handler can pick a pool per request. Entities are registered once and every pool serves them.
Reaching the driver
Section titled “Reaching the driver”pool.pool is the driver’s pool on the pg, CockroachDB, mysql2, mariadb and SQL Server pools, for libraries such as attachDatabasePool from @vercel/functions or connect-pg-simple. Bun’s pool has a pg-compatible shim there and its SQL instance at pool.sql; D1’s binding is pool.db. pool.dialect.dialectName names the backend ('postgres', 'cockroachdb', 'mysql', 'mariadb', 'mssql', 'sqlite' or 'mongodb').
In tests
Section titled “In tests”Swap in a pool with no server behind it: PGlite is Postgres in WASM, and new SqliteQuerierPool(':memory:') gives each pool a fresh database.
import { PgliteQuerierPool } from 'uql-orm/pglite';
export const pool = new PgliteQuerierPool();Build one per suite and end() it afterwards, or the runner may not exit. Both hold one connection, so a test that needs its own transaction needs its own pool.