Rado GitHub

Coming from Drizzle

Rado deliberately aligns its query building API with Drizzle ORM. There is no reason for people to learn two slightly different ways to spell the same SQL. If you know Drizzle, you mostly know rado already. This page collects everything comparison-related in one place: what's identical, what's different, what's missing, and how to port a project.

For the full origin story, read Taking the Drizzle challenge.

Summary

Rado might be for you if you like Drizzle but:

  • aren't using its ORM/relations features
  • want simpler, faster TypeScript types
  • want to choose the database at runtime, not build time
  • want a lightweight dependency (~8.5 kB gzipped with Postgres utilities, versus ~20.9 kB for the Drizzle equivalent)
  • want migrations that run anywhere, including the browser

Mind the trade-offs: rado has a much smaller ecosystem and no drizzle-kit.

Changing imports

Most code ports by swapping import sources:

DrizzleRado
drizzle-ormrado
drizzle-orm/pg-corerado/postgres
drizzle-orm/sqlite-corerado/sqlite
drizzle-orm/mysql-corerado/mysql
drizzle-orm/better-sqlite3rado/driver/better-sqlite3
drizzle-orm/node-postgresrado/driver/pg
drizzle-orm/mysql2rado/driver/mysql2
import {sql, eq, and, or} from 'rado' // was 'drizzle-orm'
import {pgTable, integer, text} from 'rado/postgres' // was 'drizzle-orm/pg-core'

Connecting differs slightly: rado's driver modules export connect rather than drizzle:

// Drizzle
const db = drizzle(new Database('app.db'))
// Rado
const db = connect(new Database('app.db'))

What's the same

Tables, columns, operators and query chaining will all feel familiar:

// This is valid in both libraries
const users = await db
  .select({id: User.id, name: User.name})
  .from(User)
  .leftJoin(Post, eq(Post.authorId, User.id))
  .where(and(gt(User.id, 10), isNull(User.email)))
  .orderBy(desc(User.id))
  .limit(10)

So are insert().values().onConflictDoUpdate(), update().set(), delete(), returning(), $with/with CTEs, union and friends, sql templates with sql.placeholder, .prepare(), transactions with tx.rollback(), $dynamic(), $count, and column modifier chains like .notNull().default().

Rado also ships Drizzle-named compatibility helpers so common type-level code ports unchanged:

import type {InferInsertModel, InferSelectModel, SQL, SQLWrapper} from 'rado'
import {getTableColumns, TransactionRollbackError} from 'rado'

What's different

Queries are immutable

In rado every method returns a new query, so partial queries are reusable:

const base = db.select(count()).from(User)
const over1 = base.where(gt(User.id, 1)) // base is untouched
const over2 = base.where(gt(User.id, 2)) // so this works as expected

In Drizzle, chained methods mutate the query in place (hence its $dynamic() workaround being mandatory for reuse). Rado keeps $dynamic() purely as a typing convenience for reassignment loops.

include instead of the relational query API

Drizzle has a second API (db.query.users.findMany({with: ...})) backed by a separately declared relations file. Rado expresses the same single-query nested fetch inside the regular builder:

// Rado
const users = await db
  .select({
    id: User.id,
    name: User.name,
    posts: include(
      db
        .select({id: Post.id, title: Post.title})
        .from(Post)
        .where(eq(Post.userId, User.id))
    )
  })
  .from(User)

// Drizzle
const usersRelations = relations(User, ({many}) => ({posts: many(Post)}))
const users = await db.query.users.findMany({
  columns: {id: true, name: true},
  with: {posts: {columns: {id: true, title: true}}}
})

One API to learn; selection and filtering work the same at every nesting level. See Include.

Selecting a bare field

Rado lets a selection be a single expression, yielding an array of values:

const names = await db.select(User.name).from(User) // Array<string>

Drizzle requires an object selection.

Universal queries

Rado queries can be written once and executed against SQLite, PostgreSQL or MySQL chosen at runtime, via rado/universal column types. Drizzle requires committing to a dialect at build time. See Universal queries.

Migrations

Drizzle ships drizzle-kit, a separate (closed-source, heavyweight) package that generates .sql migration files on disk. Rado builds in a single declarative method:

await db.migrate(User, Post)

It diffs schema against database and applies changes on the spot, anywhere, including browsers and edge runtimes. Less ceremony, less control: for carefully reviewed production migrations you may still prefer a dedicated tool. See Migrations.

Simpler types

Rado's public types take at most a few generic parameters and use a fraction of the conditional types (about an order of magnitude fewer than Drizzle's codebase). In practice: faster type-checking, readable tooltips, and Database/Select/Table types you can actually write in annotations without filling in a row of anys.

What's missing

  • The relational query API (db.query.*.findMany): by design; use Include
  • drizzle-kit style SQL-file migrations: use db.migrate or an external migration tool
  • Ecosystem: Drizzle has integrations, studio tooling and a large community; rado is a focused query builder
  • Some driver coverage: rado supports the drivers listed here; Drizzle supports more