Releases: Acro/simple-builder
Release list
v3.0.0 — sql`` tagged template, a real SQL lexer, identifier safety
A ground-up modernization for 2026, staying true to the original purpose: your SQL stays visible. Still one file, still zero dependencies.
The ? partials API is byte-identical to 2.4.2 for all documented usage — a differential fuzzer proves it against the vendored original on every CI run (20k scenarios per run). Existing require('simple-builder').pg callers are unaffected.
New: the sql tagged template (recommended)
const { pg, sql } = require('simple-builder')
pg(sql`SELECT * FROM users WHERE id = ${id}`)
// { text: 'SELECT * FROM users WHERE id = $1', values: [id] }Every ${interpolation} is always a bound parameter — you cannot forget to parameterise, so accidental injection is structurally impossible. Nothing is scanned for ?, so jsonb operators need no escaping. Fragments nest and compose:
const active = onlyActive ? sql`AND active = ${true}` : sql.empty
pg(sql`SELECT * FROM users WHERE org = ${org} ${active}`)
pg(sql`SELECT * FROM ${sql.id('public','users')} WHERE id IN ${[1,2,3]}`)
// { text: 'SELECT * FROM "public"."users" WHERE id IN ($1,$2,$3)', values: [1,2,3] }Helpers: sql.id / sql.value / sql.join / sql.raw (unsafe by construction, documented) / sql.empty.
A real lexer replaces the ? scanner
Naive ?-counting corrupts real SQL:
pg(["SELECT * FROM t WHERE tags ?| ARRAY['x'] AND id = ?", 5])
// 2.4.2 → "... WHERE tags $1| ARRAY['x'] AND id = ?" ← operator destroyed, value misbound
// 3.0.0 → "... WHERE tags ?| ARRAY['x'] AND id = $1" ✓The lexer skips string literals, quoted identifiers, line/block comments (nested), and dollar-quoted bodies ($$…$$, $tag$…$tag$); recognises ?| / ?& / ?? as operators; and honours \? as an escaped literal ?. Dialect differences (pg E'…' escapes, MySQL's whitespace-after--- rule, # comments, ANSI_QUOTES / NO_BACKSLASH_ESCAPES via withMode()) are verified against real Postgres 16 and MySQL 8 in CI.
Identifiers are safe by default
Values were always parameterised. Identifiers are the vector — no driver can bind one, and object keys become column names.
- Object keys are allow-listed to plain identifiers (
col,tbl.col); anything else throws. Legitimate keys pass through byte-for-byte. - Allow-listing, not auto-quoting, on purpose — quoted identifiers are case-sensitive while Postgres folds unquoted names to lower case, so auto-quoting
{ userName: … }would stop matching ausernamecolumn. sql.id(...parts)covers the dynamic case: quotes per dialect, doubles embedded quotes, adds its own quotes so callers can't create a mismatch.
Packaging
- TypeScript source → compiled CJS (
dist/) + ESM wrapper (esm/) via anexportsmap; no dual-package hazard. Complete types (the oldindex.d.tsonly typed scalar arrays). publint --strictclean;attwgreen across node10 / node16-CJS / node16-ESM / bundler. Both gate CI and publish.- Published with npm provenance attestations.
- Ships
llms.txt— the complete API, lexing rules, security model and recipes in one file, landing atnode_modules/simple-builder/llms.txtfor AI agents.
Breaking changes vs 2.4.2
Every one is a fix, and each is unit-tested:
- Lowercase
in ?now expands (was left as a literal placeholder, mis-binding values). - The caller's partials array is no longer mutated.
- An empty object passed to
VALUES ?/SET ?/WHERE ?throws instead of emitting() VALUES (). - Non-identifier object keys throw.
- Fewer values than placeholders throws instead of binding
undefined. OFFSET ?is no longer misread asSET ?.?inside literals / identifiers / comments / dollar-quotes is no longer a placeholder;?|/?&survive.
Major version is for these plus the packaging and module-format change; the documented ? API itself is unchanged.
Verification
64 unit tests, exact type-level tests, 26 executable documentation examples, CJS+ESM boundary tests against the packed tarball, a latest-TypeScript typings gate under node16 resolution, integration tests against real Postgres 16 + MySQL 8, and two fuzzers (20k differential scenarios vs the 2.4.2 oracle + 20k property scenarios). Node 16–24 matrix.
Full changelog: #5