DB drivers

Point crust's sql builtin at Postgres, MySQL, SQLite, or anything you can wire up via init.ts.

The sql builtin reads $DATABASE_URL and instantiates a Bun SQL client at first call:

const SQL = (Bun as { SQL?: new (url: string) => unknown }).SQL;
if (SQL) return new SQL(url);
return Bun.sql; // fallback for builds that ship Bun.sql.unsafe instead

That makes driver choice = URL scheme. Set the right $DATABASE_URL once per session (or in ~/.config/crust/init.ts), and every sql "..." stage uses it.

For databases Bun.SQL doesn’t speak natively — SQLite, ClickHouse, DuckDB — register a custom crust.fn in init.ts. You keep the same shell-line ergonomics; only the connector underneath changes.

At a glance

EngineURL schemeHow crust connectsNotes
Postgrespostgres:// / postgresql://new Bun.SQL(url)First-class. Recommended for most workflows.
MySQL / MariaDBmysql://new Bun.SQL(url)Requires a recent Bun build with MySQL support compiled in.
SQLite (file)n/a (use bun:sqlite)crust.fn("sql", ...) shim in init.tsBun.SQL doesn’t speak SQLite. Wire it up yourself in 10 lines.
Anything elseany URLcrust.fn("sql", ...) in init.tsDuckDB, ClickHouse, Snowflake — bring the npm client of your choice.

Postgres (default)

The path of least resistance. Bun ships a native Postgres client.

export DATABASE_URL='postgres://app:app@localhost:5432/app'

# Quick health check — assert fails the line (exit 1) if it's not ok
sql "select 1 as ok" | assert (r => r.ok === 1)

# Stream rows downstream
sql "select id, email from users limit 5" | (r => r.email)

# Parameterized
sql "select * from users where tier = $1" "pro" | (r => r.id)

Connection pooling

Bun.SQL pools connections by default — crust caches the client across sql calls within a session. To force a reconnect (after credentials rotate, or to swap databases mid-session), set $DATABASE_URL to a new value and restart the shell, or call the internal reset hook from init.ts:

import { _resetSqlClient } from "crust/builtinFns/sql";

crust.fn("sql-reset", () => {
  _resetSqlClient();
  return "ok";
});
// Then in the shell:  sql-reset

SSL / TLS

Append ?sslmode=require (or ?sslmode=verify-full) to your DATABASE_URL. The Bun client honors Postgres SSL params:

export DATABASE_URL='postgres://app:secret@db.example.com:5432/app?sslmode=require'

For client certificates use ?sslrootcert=...&sslcert=...&sslkey=... — paths are absolute or relative to $PWD.

MySQL / MariaDB

Same builtin, different URL scheme. Requires a Bun build with MySQL support — check bun --revision and bunx bun-info if sql "select 1" errors out with “no Bun.SQL”.

export DATABASE_URL='mysql://app:app@localhost:3306/app'

sql "select id, email from users limit 5" | (r => r.email)

# MySQL uses ? placeholders, not $1
sql "select * from users where tier = ?" "pro" | (r => r.id)

Placeholders differ by engine. Postgres uses $1, $2, … and MySQL uses ?. When you write SQL by hand with the sql "..." builtin, match the engine’s style. The Bun.sql template tag handles this for you automatically.

SQLite (via bun:sqlite)

Bun.SQL doesn’t speak SQLite — but bun:sqlite does, and you can shim it under the same shell-line surface in init.ts. The shim below replaces the built-in sql for the rest of the session.

// ~/.config/crust/init.ts
import { Database } from "bun:sqlite";

const db = new Database(process.env.SQLITE_PATH ?? "./app.sqlite");

crust.fn("sql", (...args: unknown[]) => {
  // Source mode:    sql "select ..." [params]      → args[0] is the query string
  // Transform mode: ... | sql "select ..." [params] → args[0] is the upstream item
  const first = args[0];
  const isSource = typeof first === "string";
  const query = isSource ? first : String(args[1] ?? "");
  const params = isSource ? args.slice(1) : args.slice(2);
  if (!query) throw new Error("sql: missing query");
  return db.query(query).all(...params);
});

Then from the shell:

export SQLITE_PATH=./app.sqlite

sql "select count(*) as n from users" | (r => r.n)

# SQLite uses ? placeholders
sql "select * from users where tier = ?" "pro" | (r => r.id)

The shim above keeps sql row-streaming behavior consistent — function-as-source flattens arrays into per-row items downstream, so sql "select ..." | (r => r.email) works the same as on Postgres.

Multiple databases in one session

You’ll often want a primary plus a read replica, or prod-read plus a local sandbox. Register named functions in init.ts; they all coexist.

// ~/.config/crust/init.ts
const primary = new Bun.SQL(process.env.PRIMARY_URL!);
const replica = new Bun.SQL(process.env.REPLICA_URL!);

const make = (client: Bun.SQL) => async (...args: unknown[]) => {
  const first = args[0];
  const isSource = typeof first === "string";
  const query = isSource ? first : String(args[1] ?? "");
  const params = isSource ? args.slice(1) : args.slice(2);
  return await client.unsafe(query, params);
};

crust.fn("psql", make(primary));
crust.fn("rsql", make(replica));

Now the prompt has two namespaced stages:

# Write to the primary
psql "insert into events (kind, payload) values ($1, $2)" "click" '{"x":1}'

# Read from the replica
rsql "select id, kind from events order by id desc limit 10" | (e => e.id)

Other engines via npm clients

Anything with an npm-shipped client fits the same crust.fn mold. Register the wrapper in init.ts; use the same client directly from a .ts script.

# After init.ts has registered dql / chql:
dql "select count(*) c from read_parquet('events/*.parquet')" | (r => r.c)
chql "select user_id, count() c from clicks group by user_id" | (r => r.user_id)

Troubleshooting

ErrorLikely cause
sql: no connection (set $DATABASE_URL)$DATABASE_URL is empty. Set it inline, in your shell rc, or in init.ts via process.env.DATABASE_URL = ....
sql: this Bun build has no Bun.SQL or Bun.sql.unsafe supportYour Bun is too old (or compiled without the relevant driver). Upgrade with bun upgrade and retry.
sql: client missing .unsafe — parameterised query unsupportedA custom crust.fn("sql", …) shim is in place that doesn’t implement parameterized queries. Add params support or fall back to template-tag form.
Hangs on first queryNetwork reachability or pg_hba / firewall. Try pg_isready -d "$DATABASE_URL" (Postgres) before retrying.