Skip to content

Releases: Acro/simple-builder

v3.0.0 — sql`` tagged template, a real SQL lexer, identifier safety

Choose a tag to compare

@Acro Acro released this 24 Jul 20:52
3dead7f

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 a username column.
  • 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 an exports map; no dual-package hazard. Complete types (the old index.d.ts only typed scalar arrays).
  • publint --strict clean; attw green 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 at node_modules/simple-builder/llms.txt for AI agents.

Breaking changes vs 2.4.2

Every one is a fix, and each is unit-tested:

  1. Lowercase in ? now expands (was left as a literal placeholder, mis-binding values).
  2. The caller's partials array is no longer mutated.
  3. An empty object passed to VALUES ? / SET ? / WHERE ? throws instead of emitting () VALUES ().
  4. Non-identifier object keys throw.
  5. Fewer values than placeholders throws instead of binding undefined.
  6. OFFSET ? is no longer misread as SET ?.
  7. ? 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