Modèle balisé `sql` (sûr contre l'injection)

sql est un modèle balisé (tagged template) emprunté à Bun.SQL : il produit une paire { text, params } sûre par construction. Chaque valeur interpolée {value} devient un paramètre lié — elle n'est jamais concaténée dans le SQL. Les identifiants passent par sql.id() ; la composition par fragments imbriqués, sql.join et sql.if. Tout est du JS pur (identique sur V8 et QuickJS) ; seul { text, params } traverse vers Rust.

Astuce
L'injection est impossible par construction : une valeur interpolée finit toujours dans params, jamais dans text. Regardez "1; DROP TABLE users;--" ci-dessous — la charge utile reste un seul paramètre lié, inerte. Et grâce au seam de dialecte (SQLite ? par défaut), la même API fonctionnera sur Postgres ($1..$N) le jour où bext aura un client PG, sans changer une ligne de vos requêtes.

Compilé en direct

Chaque exemple appelle .compile() pendant le rendu de cette page — aucune base de données n'est touchée. La colonne de gauche est la source ; celle de droite, le { text, params } réellement produit.

Scalaires → paramètres liés

SourceTypeScript
const q = sql`SELECT * FROM users WHERE id = ${42} AND name = ${"o'brien"}`;
q.compile();
Compilé — { text, params }JSON
{"text":"SELECT * FROM users WHERE id = ? AND name = ?","params":[42,"o'brien"]}

Tentative d'injection → paramètre inerte

SourceTypeScript
const evil = "1; DROP TABLE users;--";
const q = sql`SELECT * FROM t WHERE id = ${evil}`;
q.compile(); // the injection stays a single bound param
Compilé — { text, params }JSON
{"text":"SELECT * FROM t WHERE id = ?","params":["1; DROP TABLE users;--"]}

Composition via sql.join

SourceTypeScript
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
Compilé — { text, params }JSON
{"text":"SELECT * FROM t WHERE active = ? AND age > ?","params":[true,18]}

Identifiant + tableau IN (…)

SourceTypeScript
const q = sql`SELECT * FROM ${sql.id("user table")} WHERE id IN (${[1, 2, 3]})`;
q.compile(); // identifier quoted; array expands to ?, ?, ?
Compilé — { text, params }JSON
{"text":"SELECT * FROM \"user table\" WHERE id IN (?, ?, ?)","params":[1,2,3]}
src/app/examples/sql-tagged/page.tsxTSX
// 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: [...] }