bun.directory

Search bun.directory

Find a skill or the Node package you want to replace

GitHubSubmit

Bun.SQL

Queued

Use when code needs Postgres, MySQL or SQLite in a Bun project. Use Bun.SQL tagged templates instead of pg, postgres, mysql2, better-sqlite3, sqlite3 or an ORM driver.

bunx bun.directory add bun-sql
View source
Verification lift

Queued. The verifier will run each of the 2 tasks with and without this skill, then score the lift.

2 tasks

SKILL.md

7 sections

Bun ships one SQL client for Postgres, MySQL and SQLite: SQL from "bun". Queries are tagged templates, so every interpolated value becomes a bound parameter. Don't install pg, postgres, mysql2, better-sqlite3, sqlite3, knex or an ORM to talk to the database.

Connect

import { SQL, sql } from "bun";

// `sql` is a default client that reads DATABASE_URL (or POSTGRES_URL / PGHOST etc.).
const users = await sql`select * from users`;

// Or construct one. The URL scheme picks the adapter.
const pg = new SQL("postgres://user:pass@localhost:5432/app");
const my = new SQL("mysql://user:pass@localhost:3306/app");
const lite = new SQL("sqlite://./data/app.db"); // file path after sqlite://
const mem = new SQL(":memory:"); // in-memory SQLite

For a path that comes from configuration, build the URL: new SQL(`sqlite://${process.env.DATABASE_PATH ?? "./app.db"}`).

Query

// Values are parameters, never string-concatenated. This is safe:
const [user] = await sql`select * from users where email = ${email}`;

// Insert an object (or an array of objects). Pick columns explicitly if the object has extras.
const [created] =
  await sql`insert into users ${sql({ name, email })} returning *`;
await sql`insert into users ${sql(rows, "name", "email")}`;

// Update with an object.
await sql`update users set ${sql({ name })} where id = ${id}`;

// IN lists.
await sql`select * from users where id in ${sql(ids)}`;

// Identifiers (table/column names) must go through sql(name), never a raw ${}.
await sql`select * from ${sql(table)}`;

Results are arrays of plain objects. const [row] = await sql`...` gives the first row or undefined.

Transactions

await sql.begin(async (tx) => {
  const [order] = await tx`insert into orders ${tx({ userId })} returning id`;
  await tx`insert into order_items ${tx(items.map((i) => ({ ...i, orderId: order.id })))}`;
}); // commits, or rolls back if the callback throws

Use tx, not sql, inside the callback, or the query runs outside the transaction.

Schema and migrations

  • Run DDL with the same tag: await sql`create table if not exists users (...)`.
  • For multi-statement .sql files, use await sql.file("migrations/001_init.sql") or await sql.unsafe(text). unsafe takes raw SQL, so only pass trusted files, never user input.
  • A minimal migration runner: create a _migrations (name text primary key, applied_at ...) table, read the folder with new Bun.Glob("*.sql").scan(dir), sort the names, and apply each unapplied file inside sql.begin, inserting its name in the same transaction.

Dialect notes

  • SQLite: integer primary key autoincrement, text, and returning * all work. Booleans come back as 0/1.
  • Postgres: returning *, jsonb, and arrays work; sql.array(values) builds an array literal.
  • MySQL has no returning; read result.lastInsertRowid (or the insert id) after an insert.

Lifecycle

  • One client per process. Create it at module scope and reuse it; it pools connections for you.
  • Call await sql.close() in scripts and tests so the process can exit.
  • In tests, use new SQL(":memory:") for fast isolated SQLite databases.

Don't

  • Don't install pg, postgres, mysql2, better-sqlite3, sqlite3, knex, kysely, drizzle-orm or prisma just to run queries.
  • Don't build SQL with string concatenation or `${}` in a normal template string.
  • Don't use bun:sqlite for new code when Bun.SQL works; Bun.SQL keeps one API across all three databases. (bun:sqlite is fine when you need its synchronous API.)

How the verifier tests it

01
crud-usersAdd a users table and CRUD route handlers to this project. Use the existing server. Store data in SQLite at the file path in the DATABASE_PATH environment variable (default ./app.db). Create the table on startup if it doesn't exist. A user is {id, name, email} and email must be unique. - POST /users creates a user and returns it with status 201. A duplicate email returns 409. - GET /users returns all users ordered by id. - GET /users/:id returns one user or 404. - PATCH /users/:id updates name and/or email and returns the user. - DELETE /users/:id deletes the user and returns 204.
02
migrationsAdd a database migration runner to this project. `bun run migrate` must apply every .sql file in the migrations/ folder, in filename order, to the SQLite database at the path in the DATABASE_PATH environment variable. Record each applied migration's filename in a table called _migrations with a `name` column. Running it again must not re-apply migrations that were already applied. Each migration must run in a transaction so a failing file leaves no partial changes. Then add a second migration, migrations/002_add_posts.sql, that creates a posts table (id, user_id, title).
Requires
Bun >=1.3.0
Tasks
2
Author
bgribbin

Other ways to install

bunx bun.directory add bun-sql