Universal queries
Write a schema and queries once, then run them on SQLite, PostgreSQL or MySQL. The database is picked at runtime, not build time.
Why
Some software can't know its database in advance. A self-hostable app might run on SQLite locally and Postgres in production. A library shipping embedded storage wants users to bring their own engine. A test suite wants to run the same logic on every supported driver. Universal queries make all of these a configuration detail instead of an architecture problem.
Setup
- Define the schema with universal columns:
import {table} from 'rado'
import {boolean, id, json, text} from 'rado/universal'
const User = table('user', {
id: id(),
name: text().notNull(),
active: boolean().default(true),
settings: json<{theme: string}>()
})
Each universal column emits the correct SQL type per dialect. See the type table.
- Connect whichever driver the environment dictates:
import {connect as sqlite} from 'rado/driver/better-sqlite3'
import {connect as postgres} from 'rado/driver/pg'
const db = process.env.DATABASE_URL
? postgres(new Pool({connectionString: process.env.DATABASE_URL}))
: sqlite(new Database('local.db'))
- Query as usual:
const names = await db.select(User.name).from(User).where(eq(User.active, true))
The same query object emits dialect-correct SQL for whatever database executes it. Identifier quoting, parameter style, JSON access syntax and boolean representation are all handled.
Typing universal code
A function that should accept any database can use the generic Database
type:
import type {Database} from 'rado'
async function countUsers(db: Database) {
return db.$count(User)
}
Sync- or dialect-specific code can narrow:
SyncDatabase<'sqlite'>, AsyncDatabase<'postgres'> and friends.
Dialect-specific corners
When one database needs special treatment, sql.universal localizes the
difference to a single expression:
import {sql} from 'rado'
const epochNow = sql.universal({
sqlite: sql`unixepoch()`,
postgres: sql`extract(epoch from now())::int`,
mysql: sql`unix_timestamp()`
})
And since migrations run through the same dialect machinery,
db.migrate(User) keeps any of the three databases in sync with the same
schema definition.
What to watch out for
- Stick to features all your target databases share (the types help: e.g.
returningwon't be offered on a MySQL-typed database) - Test against every engine you claim to support. Rado's own test suite runs shared integration tests over all drivers
- Column behaviors at the edges differ (numeric precision, date handling); the universal column types pick safe mappings, but verify your edge cases