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.
bun add uql-ormConnect
Section titled “Connect”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.
Connection semantics differ by engine
Section titled “Connection semantics differ by engine”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, {});SQLite: two ways in
Section titled “SQLite: two ways in”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:' })Streaming buffers here
Section titled “Streaming buffers here”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.