Skip to content

Bun Native SQL

Under Bun the driver dependency disappears: no pg, no mysql2, no better-sqlite3 native build. uql-orm/bunSql drives Bun’s own SQL clients through one pool, with the entities, queries and migrations you already have.

Terminal window
bun add uql-orm

BunSqlQuerierPool takes Bun’s SQL.Options verbatim and infers the dialect from it, so the connection string is the only thing that changes between engines:

import { setQuerierPool } from 'uql-orm';
import { BunSqlQuerierPool } from 'uql-orm/bunSql';
export const pool = new BunSqlQuerierPool({ url: 'postgres://localhost:5432/app' });
setQuerierPool(pool);
URL or option Dialect UQL emits Bun adapter
postgres://, postgresql:// PostgreSQL postgres
mysql:// MySQL or MariaDB, per the server mysql
CockroachDB connection URL CockroachDB postgres
sqlite://, file:, :memory:, filename SQLite sqlite

The dialect UQL emits and the adapter Bun dials are two separate decisions. Bun’s sql client falls back to PostgreSQL, silently, whenever it does not recognize an adapter, so a misread URL produces Postgres syntax against a MySQL server; BunSqlQuerierPool normalizes the options per dialect instead. CockroachDB is the clearest case: Bun connects with its postgres adapter while UQL keeps the dialect id cockroachdb and goes on emitting CockroachDB SQL.

BunSqlPostgresDialect is a distinct dialect from PgDialect, not an alias for it. Bun binds parameters differently from node-postgres, so the JSON casts differ (( $N::text )::jsonb where Bun needs it, array-literal encoding for ANY/ALL); without that a $set or $push on a jsonb column would produce the wrong value or throw. You never select these: the pool picks them from the options.

For PostgreSQL and MySQL, Bun exposes .reserve() to take a dedicated connection from its pool, so UQL reserves on acquire and releases when the querier is released. Bun’s SQLite client has no reservation and throws if you call it, so the pool hands back a singleton handle instead. Either way withQuerier and transaction behave the same, and a querier releases itself when an await using binding goes out of scope:

await using querier = await pool.getQuerier();
const users = await querier.findMany(User, {});

uql-orm/bunSql reaches SQLite through Bun’s sql client; Sqlite3QuerierPool reaches it through bun:sqlite automatically under Bun. Both install nothing. Prefer Sqlite3QuerierPool for a SQLite-only app, since it is the same code path as better-sqlite3 elsewhere; prefer bunSql when one codebase points at several engines.

const pool = new BunSqlQuerierPool({ url: 'sqlite://:memory:' });
// or: new BunSqlQuerierPool({ filename: ':memory:' })

findManyStream works, but Bun’s client exposes no cursor to UQL, so the fallback path runs: the full result set is fetched, then yielded row by row, with the memory profile of findMany. Where streaming a large table matters, use pg with pg-query-stream, node:sqlite, or Turso’s embedded engine, which stream natively. See Streaming.

Bun’s own result objects are mapped to the standard shape, so affectedRows and lastInsertRowid arrive as the changes, ids and firstId every other driver reports.