`sql` tagged template (injection-safe)
sql is a tagged template stolen from Bun.SQL: it produces an injection-safe { text, params } pair by construction. Every interpolated {value} becomes a bound param — it is never concatenated into the SQL text. Identifiers go through sql.id(); composition through nested fragments, sql.join, and sql.if. It's all pure JS (identical on V8 and QuickJS); only { text, params } crosses into Rust.
Tip
Injection is impossible by construction: an interpolated value always lands in
params, never in text. Look at "1; DROP TABLE users;--" below — the payload stays a single, inert bound param. And thanks to the dialect seam (SQLite ? by default), the very same API will work over Postgres ($1..$N) once bext has a PG client, without changing a single one of your queries.Compiled live
Each example calls .compile() while this page renders — no database is touched. The left column is the source; the right is the { text, params } it actually produces.
Scalars → bound params
const q = sql`SELECT * FROM users WHERE id = ${42} AND name = ${"o'brien"}`;
q.compile();{"text":"SELECT * FROM users WHERE id = ? AND name = ?","params":[42,"o'brien"]}Injection attempt → inert param
const evil = "1; DROP TABLE users;--";
const q = sql`SELECT * FROM t WHERE id = ${evil}`;
q.compile(); // the injection stays a single bound param{"text":"SELECT * FROM t WHERE id = ?","params":["1; DROP TABLE users;--"]}Composition via sql.join
const filters = [sql`active = ${true}`, sql`age > ${18}`];
const q = sql`SELECT * FROM t WHERE ${sql.join(filters, " AND ")}`;
q.compile(); // nested fragments share one param counter{"text":"SELECT * FROM t WHERE active = ? AND age > ?","params":[true,18]}Identifier + IN (…) array
const q = sql`SELECT * FROM ${sql.id("user table")} WHERE id IN (${[1, 2, 3]})`;
q.compile(); // identifier quoted; array expands to ?, ?, ?{"text":"SELECT * FROM \"user table\" WHERE id IN (?, ?, ?)","params":[1,2,3]}// src/app/.../page.tsx — injection-safe `sql` tagged template (Bun.SQL steal).
// One ergonomic tagged template → an injection-safe { text, params } pair.
import { sql } from "@bext-stack/framework";
// Interpolated values become BOUND PARAMS — never concatenated into SQL:
const q = sql`SELECT * FROM users WHERE id = ${id} AND name = ${name}`;
q.compile();
// → { text: "SELECT * FROM users WHERE id = ? AND name = ?", params: [id, name] }
// Identifiers (table/column names) — quoted, not parameterised:
sql`SELECT * FROM ${sql.id("my table")}`;
// Arrays expand to ?, ?, ? for IN (...):
sql`SELECT * FROM t WHERE id IN (${[1, 2, 3]})`;
// Composition — nested fragments, sql.if, sql.join share one param counter:
const filters = [sql`active = ${true}`, sql`age > ${minAge}`];
sql`SELECT * FROM t WHERE ${sql.join(filters, " AND ")}`;
sql`SELECT * FROM t ${sql.if(name, sql`WHERE name = ${name}`)}`;
// Same query, run for real over the in-process SQLite bridge:
import { database } from "@bext-stack/framework";
const db = database(".bext/data/app.db");
const rows = db.all(sql`SELECT * FROM users WHERE id = ${id}`);
// Dialect seam — SQLite (?) by default, Postgres ($1..$N) when bext gets a PG client:
import { POSTGRES_DIALECT } from "@bext-stack/framework";
q.compile(POSTGRES_DIALECT); // → { text: "... id = $1 ...", params: [...] }