Changelog
0.39.0 (2026-07-20)
Added
- PowDB nested projections:
withruns as one statement. PowDB 0.18 adds nested projections (shaped results) to PowQL: a projection field can be a whole correlated child query returning a per-parent JSON array. Turbine now compiles eligiblewithclauses straight into the parent statement, sodb.users.findMany({ with: { posts: { orderBy: { views: 'desc' }, limit: 3 } } })runs as ONE PowQL statement, with per-parent ordering, limits, and offsets applied natively by the engine, childless parents kept ([]for hasMany,nullfor to-one), and arbitrary nesting depth sharing one alias counter, the same single-query shape Turbine'sjson_aggstrategy gives Postgres. On an engine >= 0.18 this replaces the batched N+1 loaders as the default relation path; an explicitrelationLoadStrategy: 'batched'opts back out, and'join'also prefers nesting (it is the strictly better server-side path: no fan-out, survives parent paging, keeps childless parents). Ineligible shapes silently fall back to the loaders with identical output: many-to-many (the junction-order stitch has no nested equivalent), bigint-typed child columns (JSON cannot carry them losslessly), a to-one relation with paging, parentdistinct, and projection-key collisions. PII-tagged child columns stay excluded at the query level;select/omit/includePiiare honored; child JSON values are re-coerced per column type (datetime micros come back asDate).explain()shows the engine's nested plan. Everything is capability-gated on the probed engine version: older engines keep the loaders byte-for-byte, and the feature lights up automatically once the 0.18 driver packages are on npm. - PowDB typed wire error classes (engine >= 0.17). PowDB 0.17 error frames
carry a stable one-byte error class, and
wrapPowdbErrornow classifies by it before the message-substring families, so a server-sanitized message ("query execution error") still maps to the right typed error: timeout →TimeoutError(E002), memory/size limit →ValidationError(E003), read-only refusal →ReadOnlyError(E018,reason: 'snapshot'), auth failure / rate limiting →ConnectionError(E004), constraint violation →UniqueConstraintError(E008), cooperative cancellation →ConnectionError(E004, final). The specific message families keep precedence where they extract richer detail (constraint and column names, RBAC-vs-snapshot read-only reasons); classless errors from older servers keep the exact pre-0.17 behavior.
Changed
- PowDB driver dev/test baseline bumped to
@zvndev/powdb-client/@zvndev/powdb-embedded^0.17.0(the peer range is unchanged and already admits 0.18). The PowQL lexer escape set was re-verified byte-identical through the 0.18 engine line, soPOWQL_LEXER_TESTED_CEILINGis now'0.18'.
0.38.1 (2026-07-19)
Fixed
- createMany respects declared column types in its UNNEST casts. The
bulk-insert cast picker fell back to a name-based heuristic (
*_idimpliesbigint[],*_atimpliestimestamptz[]) whenever the metadata carried no precomputed array type, so acreateManyagainst a text or uuid foreign key (for exampleauthor_id text) failed withinvalid input syntax for type bigint. The column's declared type inpgTypesnow always wins; the heuristic survives only for columns entirely absent from the metadata. Build-only regression tests pin both behaviors. - PowDB embedded version detection under tsx on Node 20. The optional-peer
helper resolved the addon's version with
require('<pkg>/package.json'), which tsx's CommonJS hook on Node 20 fails to load even though resolution succeeds, so every version-gated capability threw E017 ("could not report a version") on that lane. The helper now resolves the path and reads the file directly, which is loader-independent. - Error-code enforcement script recognizes typed subclasses. The CI check
now knows
ReadOnlyError(E018) andDestructivePushRefusal(theValidationErrorsubclass thrown byschemaPush), which it previously flagged as untracked, keeping the error-codes gate red.
These three were the standing CI failures on main; the branch is fully green again, and the hardened tag-publish gate introduced in 0.38.0 (which correctly refused to publish over the red integration lane) now passes end to end.
0.38.0 (2026-07-19)
Added
- Studio data-tab power pass. The Data tab grows the tools a real inspection session needs. Per-column filters (equals, not, contains, comparisons, IS NULL / IS NOT NULL) stack with the text search, compile to fully parameterized SQL on the server (validated column and operator whitelists, capped at 10), and are refused outright on redacted PII columns, including null checks, so a hidden value cannot be probed. Rows are selectable (checkbox column with select-all) with a selection bar for Copy JSON / Copy CSV / Delete selected / Clear; an Export modal copies or downloads the current page or selection as JSON or CSV; double-clicking any cell copies its raw value; and the page size is adjustable from 25 to 500 rows (persisted).
- Studio bulk writes, still PK-addressed. In
--writemode,/api/row/insertand/api/row/deleteaccept arowsarray (capped at 500): each entry passes the same per-row validation as a single write (full-primary-key addressing, column checks against introspected metadata), compiles to its own single-row statement, and the batch runs in one all-or-nothing transaction; a delete whose primary key matches nothing rolls the whole batch back. This powers multi-select delete and the new Paste rows flow (bulk insert from pasted TSV/CSV with a header row, or a JSON array, with live parse preview and per-row errors). Predicate-based mutations and bulk update remain deliberately unsupported. - Studio query-tab visibility. After a run, View SQL / Copy SQL expose the single statement the builder compiled to. Builder validation messages now render (a disabled Run button explains which clause is incomplete instead of graying out silently), and the where builder gains IS NULL / IS NOT NULL operators. Loading a saved query now restores NOT combinators and null-check clauses correctly, the save dialog locks the target table to the builder's table, and deleting a saved query asks for confirmation.
- Studio keyboard shortcuts, for real. The shortcuts the command palette
advertised now work: Cmd+Enter runs, Cmd+S saves (instead of the browser
save dialog), G then Q / D / S switches tabs, R refreshes data, Shift+R
reloads the schema. Tab buttons keep
aria-selectedin sync. - Typed row editor. Enum and boolean columns get dropdowns, JSON columns validate before submit with the parse error shown inline, and timestamp fields carry ISO 8601 placeholders.
Fixed
- Nested-write errors no longer embed user-supplied values. The
connect/update miss messages in the nested-write engine interpolated the
full
where/connectobject (including values such as email addresses) into the exception text even in safe error mode, contradicting the PII-safe errors contract. They now follow the same safe/verbose convention asNotFoundError: key names only in safe mode, full detail under verbose. - Demo mode now honors its "nothing is saved" promise for saved queries.
Saving a query in
turbine studio --demowrote.turbine/studio-queries.jsoninto the working directory, and demo sessions displayed the project's real saved queries. Demo saved queries now live in memory only, die with the process, and the real file is never read or written. - Prototype-safe field validation. Field names that collide with
Object.prototypemembers (constructor,toString,__proto__, ...) inwhere/orderBy/select/ nested-writedatapreviously bypassed the unknown-column check via inherited lookups and crashed with aTypeError. All user-keyed metadata lookups are nowObject.hasOwn-guarded and throw the normal typedValidationError(E003). - Studio boot failures are visible. A schema-load error now renders an error box with a Retry button on every tab instead of leaving the default Query tab on an eternal "Loading schema...". Live demo-mode toggles no longer reset the composed builder query, current table, filters, or selection; builder results re-run after a toggle so redaction on screen always reflects the server state.
- Studio data-grid honesty. Column headers render verbatim
(
created_at, notCREATED_AT), redacted PII columns show a sorting-disabled tooltip instead of a lying sort arrow, rate-limit (429) responses surface a retry hint, and nullable-typed columns (number | null) are classified correctly by the value editors.
Changed
- Release gate hardened. The tag-triggered publish workflow now requires a
live Postgres integration run (seeded fixture) and the packed-tarball smoke
job before
npm publish, matching the PR gate instead of trusting unit tests alone. - Site. The hero version badge derives its tagline from the changelog at
build time (it can no longer go stale), a full changelog page ships at
/changelog, the landing page gains cards for
turbine doctor, multi-engine support,explain(), and the MCP server, and the comparison table is dated with Prisma'srelationJoinsmarked as Preview.
0.37.0 (2026-07-18)
Added
- Studio demo mode.
npx turbine studio --demoboots Studio with no database and noDATABASE_URL: a seeded in-memory sample dataset (users with PII-tagged emails and phones, posts, comments, orgs, relations wired) served by Turbine's own SQLite engine over the Node built-innode:sqlite:memory:(Node 22.5+). A demo banner carries two live toggles, PII (hidden/shown) and Writes (off/on), so the three Studio modes can be experienced in one session: read-only and redacted on boot, flip to see real-looking PII reveal warnings, flip again to insert/edit/delete rows. Writes genuinely apply to the in-memory store (edits stick, refresh shows them) but nothing is ever saved anywhere: the store dies with the process and every launch starts pristine. The full security model applies (token auth, Origin checks on mutating routes, rate limiting, nonce CSP), the mode switcher route exists only in demo mode, and the Postgres path is byte-identical when the flag is off.
0.36.1 (2026-07-18)
PowDB 0.16 support. The 0.16 driver contract is byte-identical to 0.15 (the release is an engine-internal index-correctness fix plus documentation), so this is a verification-and-pinning release, not a feature round.
Changed
- PowDB 0.16 verified and pinned. The full live matrix (networked server +
embedded addon) runs green on 0.16; dev dependencies track
^0.16.0(the optional peer range>=0.7.1 <1.0.0already admits it). The PowQL literal-escaper's tested lexer ceiling is bumped to0.16after verifying the 0.16 lexer is untouched. - NUL-byte regression coverage. PowDB 0.16 fixed wrong rows from
non-unique string indexes on values with embedded NUL bytes (a new on-disk
index format, rebuilt automatically on first writable open). A live
integration test now locks the fix in through the ORM surface: indexed
equality, prefix lookups, and index-driven updates around
"A"vs"A\0"neighbors. The test fails on the 0.15 addon and passes on 0.16. - Read-only snapshot note. The engines page documents the 0.16 index upgrade nuance for snapshot fleets: a read-only open rebuilds the affected indexes in memory on every open until a writable open persists the new format, so run snapshots through one writable open (or take them from a 0.16 primary).
0.36.0 (2026-07-18)
The safety release: first-class PII field tagging with opt-in return semantics,
an opt-in writable Studio (read-only stays the default), an honest and hardened
migration story (destructive gate on push, declared indexes in DDL and diff,
a sanctioned backfill recipe), and the where-clause cache paths unified onto a
single canonical walk with a sampled production cross-check. Reviewed by five
independent passes (product, strategy, security, code quality, UX) before
release; every confirmed finding was fixed or explicitly documented below.
Added
- PII fields. Tag a column
pii: trueindefineSchema(or.pii()on the fluent builder) and Turbine excludes it from every default projection: top-level rows, relation subqueries (with), the batched loader, positional JSON encoding, PowDB loaders and native joins, and the row a write returns. It comes back only when explicitly named inselect, or via the newincludePii: trueread option, which restores every PII column at the top level and at every nestedwithlevel of that query. Filtering, ordering, and grouping by a PII column stay allowed (naming the column is itself the opt-in). Untagged schemas emit byte-identical SQL. The SQL cache key carries the flag, so a cached no-PII statement can never serve an opt-in call. - PII is enforced at the SQL level on writes. A write against a table with
PII columns (
create,createMany,update,delete,upsert, nested writes) returns an explicit non-PII projection instead ofRETURNING *:RETURNING "col", ...on Postgres and SQLite, a projected follow-upSELECTon MySQL, and per-columnOUTPUT INSERTED./OUTPUT DELETED.on SQL Server. PII values are persisted normally; they simply never cross the wire back unrequested. A PII-tagged primary key stays in the projection so the returned row remains addressable. Tables with no PII columns keepRETURNING *byte-for-byte. PowDB is the one exception (itsreturningkeyword takes no column list per the driver spec), so the returned row is stripped client-side there; its upsert reselect already projects non-PII columns. - Writable Studio (opt-in).
turbine studio --writeenables single-row insert/update/delete from the Data tab. Every write is addressed by the row's full primary key (the predicate is rebuilt from the PK alone, so a widenedwherecannot reach the database), compiled by the same validated builders as the library, runs in its own transaction with the same parameterized statement timeout and pinnedsearch_path, and requires a matchingOriginheader. Without the flag the write endpoints do not exist (requests 404) and every transaction remainsBEGIN READ ONLY. The UI shows a persistent WRITE MODE banner and a delete confirmation. - Studio PII redaction. PII-tagged columns render as a redaction
placeholder in every tab, applied server-side before serialization (table
rows, builder rows, nested relation rows, and the echoed post-write row).
Redacted columns are also excluded from the Data-tab substring search and
from
orderBy, so a redacted value cannot be probed or inferred through sort position.--show-piireveals values for a launch, with a loud terminal warning and a persistent PII SHOWN banner in the browser. - Studio row editor: explicit set-NULL. Nullable non-PK columns get a
per-field NULL toggle (insert and edit) that sends an explicit
nullparameter end-to-end; a blank field still means "unchanged" (edit) or "use the default" (insert). A null-toggled PII field sendsnull, never the redaction placeholder. - Destructive gate on
push.turbine pushnow scans the statements it is about to apply with the same destructive-SQL scanner asmigrate upand refuses to run them without the two-step typed confirmation (the literal phrasedestroy my data, thenyes) or an explicit--allow-destructive. The diff is computed once and the confirmed statements are exactly the ones applied (no re-diff between confirmation and apply), and the refusal is a typedDestructivePushRefusal(exported, extendsValidationError, carries the offending statements) rather than a message-text convention. - Declared indexes in SQL DDL.
defineSchematable-levelindexes: [{ columns, unique?, name? }]now emitCREATE [UNIQUE] INDEXfromschemaToSQL/push, andschemaDiffadds declared indexes missing from the live database (reverse:DROP INDEX). Matching is by name with a definition check: a name that matches an existing index whose uniqueness, column list, or partial-WHEREdiffers produces a warning, never a drop. A declared index that resolves to the same name as an automatic FK index supersedes it, so declaring a UNIQUE index on an FK column works. Undeclared database indexes are surfaced as warnings and never dropped. - Backfill migration recipe.
turbine migrate create <name> --recipe backfillscaffolds the sanctioned two-phase pattern for changing a populated column's type: nullable add, batched keyedUPDATEloop,SET NOT NULL(with theCHECK ... NOT VALID+VALIDATEnote for huge tables), and an atomic rename swap, fully commented and reversible. - Check constraints round-trip. Table-level
checksnow survivegenerate: the metadata emitter writes them intometadata.ts, andschemaDiffdiffs named checks (add missing, warn on expression drift). TURBINE_CACHE_CHECK_SAMPLE. Opt-in sampled production re-verification of the SQL template cache: set it to a rate in (0, 1] and that fraction of cache hits rebuild the statement and compare byte-for-byte, logging once per fingerprint and throwing on mismatch. The dev-mode always-on cross-check is unchanged.
Changed
- The where-clause walk is unified, at every level.
fingerprintWhere,buildWhereClause, andcollectWhereParams(the three-way hand-synced functions behind two previously shipped cache bugs) now consume one canonical enumeration (walkWhereinsrc/query/where-compile.ts) with a single column-aware scalar classifier. The relation sub-where walkers (the relation-filterEXISTSbody and the relationwith-clausewhere) are consumers of the same walk through one shared scoped trio, so no hand-mirrored where walker remains anywhere in the query builder. The dev-mode cross-check and the new sampled production cross-check stand as tripwires on top. - The query builder is physically decomposed. The 7,000-line
query/builder.tsis now a 2,300-line execution facade over four cohesive modules (query/where.ts,query/relations.ts,query/writes.ts,query/aggregates.ts). Pure refactor: the publicQueryInterfaceAPI is unchanged and the emitted SQL is byte-identical (the full suite's exact SQL assertions pass with zero expectation edits). - Studio CSP hardened. The inline UI script is authorized by a
per-request nonce (
script-src 'self' 'nonce-...');unsafe-inlineis gone fromscript-src. Mutating routes reject absent as well as mismatchedOriginheaders. redactUrlredacts every credential. Multi-URL strings (primary plus replicas) have all userinfo passwords redacted, plus case-insensitivepassword=query parameters.- PowQL literal escaper version ceiling. The embedded
literal-materialization fallback (pre-0.14 addons) now refuses to run
against an engine line newer than the escaping rules were verified on
(
POWQL_LEXER_TESTED_CEILING), with a typed upgrade-pointing error, instead of assuming a future lexer tokenizes escapes identically. - Bundle-size claims are measured and gated.
.size-limit.jsbudgets are re-baselined to measured brotli sizes (main 52.36 kB, edge 39.78 kB) andnpm run sizenow runs inprepublishOnly, so stale size marketing cannot ship again. - Docs honesty pass. New "Migrations in Practice" page documenting exactly
what
migrate create --autocannot do (blindUSINGcasts, rename detection,SET NOT NULLbackfill) and the sanctioned recipes; theschemaDiffexample now matches the real signature; the engines page states up front that the CLI drives PostgreSQL only;TURBINE_E018added to the README error table; every "read-only Studio" claim reconciled with the opt-in write mode.
Fixed
turbine pushcould apply destructive statements without confirmation (it bypassed the gatemigrate upalready had).- Declared-index emission could produce duplicate index names against the automatic FK indexes (apply-time failure) or silently skip a declared UNIQUE index whose name matched an existing plain index.
--recipewith a missing name now errors instead of silently creating a plain migration.push --helpnow documents--allow-destructive.schemaDiffundeclared-index warnings now fire whenever a table defines anindexesarray, including after the last declaration is removed (they were previously silenced exactly when the operator most needed them).- A relation filter with a
nullvalue ({ some: null }) is now treated identically by the SQL build and the cached-parameter collection paths (latent asymmetry, unreachable through the public types). - The SQL-cache segment for global filters now fingerprints another table's filter with that table's own column/relation context. Previously, a function global filter whose shape varied inside a nested relation filter could collapse two different SQL texts onto one cache entry (an exotic configuration; the dev-mode cross-check would have caught it).
0.35.0 (2026-07-16)
PowDB 0.14/0.15 adoption: the embedded transport joins the native typed wire,
relation loading can compile to native server-side joins, read-only snapshot
serving is a first-class deployment mode with a typed routing error, and every
engine gains explain(). Adversarially reviewed pre-release (19 findings
confirmed and fixed, including three high-severity ones and two pre-existing
loader bugs the new parity testing exposed).
Added
- Native PowQL joins for relation loading (PowDB >= 0.13). Passing
relationLoadStrategy: 'join'(per query, or client-wide via the newTurbinePowdbOptions.relationLoadStrategy) compiles eligible top-level relations - hasMany, hasOne, belongsTo, and many-to-many through the junction - to hash-accelerated server-side joins instead of keyed batch lookups: no key lists, no 1,000-key chunking. Eligibility requires no parentlimit/offsetand a unique (or primary-key) correlation column; ineligible relations silently use the batched loaders, so results are identical either way. PowDB's default remains the batched loaders. - Embedded native typed transport (PowDB addon >= 0.14). Embedded queries
now run through the engine's parameterized
queryWithParamsAPI: real positional$Nbinding (token-level, injection-inert) and the same lossless typed result cells as the networked wire, decoded by one shared path. A JSONnull, a missing field, and the string"null"are now distinguishable on the embedded transport too. Older addons keep the literal-materialization path unchanged. Embeddeddisconnect()now performs a real checkpoint-flushclose()on addon >= 0.14. - Read-only snapshot serving (PowDB >= 0.14). Open an embedded snapshot
with
turbinePowDB({ embedded: dir, readonly: true }, schema), or point at apowdb-server --readonly. A newReadOnlyError(TURBINE_E018) is the routing signal:reason: 'snapshot'means nothing can write there (route writes to the primary),reason: 'rbac'means this connection's role may not write. A client-levelreadonly: trueoption fails writes fast locally, before the wire, on both transports. explain()on every table accessor.db.posts.explain(args)compiles the exact statementfindMany(args)would run and returns the engine's plan as text lines:EXPLAINon PostgreSQL/CockroachDB/YugabyteDB and MySQL,EXPLAIN QUERY PLANon SQLite, nativeexplainon PowDB (lowered executed plans since PowDB 0.14, selectivity estimates since 0.15). SQL Server throws a typed E017. Plan text is diagnostic output, not a stable API, and middleware does not run for it.- Driver-spec error taxonomy. PowDB error mapping now covers the full
quasi-stable family list from the upstream driver spec: query timeouts keep
the engine's message, client-disconnect cancellations map to
ConnectionError, bounded-join rejections map toValidationErrorwith the engine's fix hint, anddatabase is closedmaps toConnectionError.
Fixed
- Two pre-existing PowDB relation-loader bugs, exposed by the new
join-vs-loader parity testing: a relation correlated on a datetime column
silently stitched to empty (Date object identity was used as a map key), and
a relation
selectthat omitted the foreign key returned[](the correlation column is now fetched internally and stripped from the output). $transactionon PowDB now carries the pool'sreadonlyflag and capability set into the transaction scope; previously the read-only fail-fast guard and version gates did not apply inside transactions.- An embedded PowDB transaction queued behind the single-writer gate when
disconnect()ran no longer executes against the closed handle; it fails with a typedConnectionError.
Changed
- Dev/test matrix pinned to PowDB 0.15 (client, embedded addon, server). 0.15 itself required no driver-surface changes: its per-index statistics and cardinality-aware conjunction planning benefit Turbine-generated queries, including the new native joins, with no code change.
- The upstream PowDB driver spec (
docs/integrations/powql-for-drivers.mdin the PowDB repository) is now the contract the driver is built against.
0.34.0 (2026-07-15)
PowDB engine parity with PowDB 0.12/0.13: the JSON document API, the lossless native wire, code-first doc-field indexes, catalog introspection, and typed connection errors. Adversarially reviewed pre-release (16 findings confirmed and fixed, including a CJS build break and a retry race).
Added
- JSON documents on PowDB (engine >= 0.12). The
jsoncolumn type is first-class: objects and arrays bind as typed document parameters, and Turbine's existing JSON API compiles to PowQL path expressions with every path segment and value bound as a typed parameter:JsonFilterwhere-filters (equals/not/gt/gte/lt/lte/hasKey), top-level and inside relation filters. A digit-only segment addresses an array index (SQL-engine parity).equals: nullmatches a JSONnullor a missing key on PowDB (documented divergence).containsand pathlessequalsthrow a per-operator E017 (PowQL has no containment operator).- JSON-path
orderBy(with numeric casting) andgroupByJSON-path group keys and aggregate targets, with the same alias, ordering, and error semantics as the SQL engines (including_countbeing selected by default and orderable without being requested). Every JSON feature is capability-gated: pre-0.12 engines get a typed E017 with an upgrade hint instead of an engine parse error.
- Native typed wire (engine >= 0.13, networked). The networked transport
uses PowDB's lossless
queryNativeRawAPI when the client and server support it: a JSONnull, a missing field, and the string"null"are distinguishable end-to-end, and every result is coerced according to the wire that actually served it (heterogeneous injected pools included). - Doc-field expression indexes (engine >= 0.13).
defineSchemaacceptsindexes: [{ docField, path, unique? }](and plain column indexes);powqlSchemaDDLemits the parenthesizedalter T add index (.col->"seg")DDL. Numeric path segments are validated as non-negative integers at schema-build time. Declared code-first indexes never arm the missing-index advisor (SQL DDL generators do not create them). - Catalog introspection (engine >= 0.10).
introspectPowdbDatabase(exported fromturbine-orm/powdb) reads a live PowDB catalog viaschemaanddescribestatements intoSchemaMetadata. Relations are always empty (PowDB has no declared foreign keys) and the primary key is inferred heuristically, sodefineSchemaremains the recommended path. - Typed connection errors + opt-in stale-read retry. Protocol-level
failures (including the "received unexpected frame" shape produced by a
stale idle socket) map to
ConnectionError(E004) with.causepreserved, andauth_failedmaps to a typed error. TheretryStaleReadsoption replays a first-statement read once on that exact signature: never a write, never inside a transaction (the action is threaded per call, so concurrent operations cannot confuse the retry decision). - Unique doc-field index violations map to
UniqueConstraintError(E008).
Changed
- JSON-path
orderBydefaults to NULLS LAST in both directions on PostgreSQL and SQLite (previously PostgreSQL'sDESCdefault put null/missing-path rows first). This matches pick-row relation ordering (0.33) and engines whose path ordering is nulls-last in both directions, so the same query orders identically across every driver. Passnulls: 'first' | 'last'to override. - PowDB
protocol_error-class failures now surface asConnectionError(E004) instead ofValidationError(E003). Update error handling that matched on E003 for connection-shaped failures. - Engine note documented:
_sumover a group with no value at the JSON path returnsnullon SQL engines and0on PowDB.
0.33.0 (2026-07-15)
Added
- Opt-in LATERAL plan for pick-row relation ordering (PostgreSQL). Pick-row
ordering entries accept
plan: 'lateral'to compile asLEFT JOIN LATERAL (SELECT ... ORDER BY ... LIMIT 1) ON trueinstead of the default correlated scalar subquery. Results are identical (verified row-for-row on live PostgreSQL, including NULLS placement, pagination, the batched relation-load strategy, and streaming); the join plan can be substantially faster on large parent sets where the ordering subquery dominates. The default plan's SQL is byte-for-byte unchanged.planis validated strictly: unknown values throw E003, and dialects without lateral join support (SQLite, MySQL, SQL Server, PowDB) throw a typed E017 via the newsupportsLateralJoindialect capability flag rather than emitting broken SQL. Lateral applies to top-level ordering entries; nested pick ordering keeps the subquery plan.
0.32.2 (2026-07-15)
Fixed
groupBycan order by every column the result actually contains.orderByongroupBypreviously validated keys against the table's physical columns only, so ordering by a JSON-path group-key alias threw E003 and ordering by an aggregate threw E005, even though HAVING already accepted both.orderBynow supports plain by-columns, JSON-path group-key aliases (explicit or default),_count, and_sum/_avg/_min/_maxblocks including JSON aggregate targets keyed by their alias, with{ sort, nulls }specs, on all four SQL dialects. The ORDER BY re-emits the same expression as the SELECT list (already-bound JSON path parameters are reused, no extra binds). Ordering by an aggregate that was not requested, or by an unknown key, throws aValidationErrorlisting the valid keys for that call. PowDB refuses aggregate order keys with a typed E017 instead of emitting invalid PowQL.
0.32.1 (2026-07-14)
A first-run and hardening release: the new-project funnel now works out of the box on current tooling defaults, the SQL cache polices its own invariants in dev, and a latent MySQL pagination-cache bug is fixed. Fully adversarially reviewed (11 findings confirmed and fixed pre-release).
Fixed
- CommonJS projects work with the CLI.
npm init -yon npm 11 writes"type": "commonjs"; in such projects the config and schema loaders received a CJS-interop double-wrappeddefaultexport, read every field asundefined, and every command failed with a misleading "No database URL provided". Both loaders now unwrap the interop shape, and config load errors are surfaced with the file name and cause instead of being silently swallowed.turbine initprints a note about the project module type. - MySQL pagination cache correctness. On dialects that inline LIMIT/OFFSET
literals into SQL (MySQL), the values are now part of the SQL-cache
fingerprint (top-level and per-relation
withlimits). Previously a cache hit could silently reuse a different limit or offset value. Parameterized dialects (PostgreSQL, SQLite, SQL Server) are unaffected. distinctcache fingerprint now uses the user-supplied column order, matching the emittedDISTINCT ONclause.- Published tarball ships no install scripts.
prepareis stripped at pack time and restored afterward, so consumers using install-script auditing get no warnings. Local dev hook installation is unchanged. - Docs accuracy sweep in
docs/USING-TURBINE-ORM.md, the README, and the site: Studio described as it exists today (ORM-native builder, port 4983), the realdb.pipeline([...])API (atomic by default,{ transactional: false }to opt out), error table extended through E017, CLI command list matchesturbine --help(includingmcpandmigrate deploy), CJS wording made precise, and the quickstart requires Node 20 to matchengines.
Added
- CLI auto-loads
.env. Everyturbinecommand loads a local.envat startup (viaprocess.loadEnvFile, Node 20.12+). Variables already in the environment always win, a warning is printed when an.env-sourcedDATABASE_URLoverrides a differingurlinturbine.config.ts, and unreadable.envfiles degrade to a warning instead of crashing. turbine()with no arguments now falls back to theDATABASE_URLenvironment variable when no pool, connection string, or explicit connection fields are provided, matching what the docs and generated JSDoc always said.- Dev-mode SQL-cache cross-check. When
NODE_ENVis notproduction, every SQL-cache hit rebuilds the statement fresh and verifies the cached SQL and parameters match exactly, throwing aValidationError(E003) on any lockstep mismatch. Zero overhead in production; disable withTURBINE_DISABLE_CACHE_CHECK=1. This class of invariant violation shipped silent wrong-results bugs twice before; it now fails loudly at development time. - Broader driver-error mapping.
wrapPgErrornow maps57014(server-sidestatement_timeoutcancellation) toTimeoutError(E002), and connection-class failures (SQLSTATE08xxx,53300,57P01-57P03, plus driver-levelECONNREFUSED/ECONNRESET/ETIMEDOUT/ENOTFOUND/EPIPE) toConnectionError(E004), all preserving the original error as.cause. - Quickstart smoke gate in CI. A new job installs the packed tarball into a
scratch CommonJS project and runs the documented quickstart literally (init,
push, generate, first query via
turbine()with an.env-sourced URL) against a real PostgreSQL service.
Changed
- Benchmarks re-measured on 0.32.0 against Prisma 7.6 (adapter-pg,
relationJoins) and Drizzle 0.45 on local PostgreSQL 17.9 over a Unix socket, which isolates per-query overhead instead of hiding it behind network latency. All ten scenarios published, including the ones Turbine does not win. README, the site benchmarks page, andbenchmarks/RESULTS.mdall carry the new numbers and methodology.
0.32.0 (2026-07-13)
The last two raw-SQL escape hatches for version-driven data models, designed first and adversarially reviewed (14 findings confirmed and fixed pre-release, including two cache/SQL-validity bugs in the new code itself).
Added
- Pick-row relation ordering. Order parents by a column or JSON path taken
from ONE row of a to-many relation, chosen by an inner ordering and optional
filter:
orderBy: { versions: { pick: { orderBy: { createdAt: 'desc' }, where: {...} }, by: { field: 'data', path: ['title'] }, direction: 'asc' } }. Compiles to a correlated scalar subquery in ORDER BY (no FROM-clause restructuring), cache-safe, works with both relation-load strategies. Parents with zero related rows sort last by default. hasMany only; not combinable withdistinct(clear error); plain-columnbyworks on SQLite/MySQL/SQL Server, JSONbyfollows the JSON-ordering dialect rules. - JSON-path group keys and aggregate targets in
groupBy.by: [{ field: 'data', path: ['category'], alias? }]and_sum: { price: { field: 'data', path: ['price'] } }(also_avg,_min,_max;_sum/_avgalways cast numeric).havingworks on the aliases; result-key collisions are detected on the EMITTED column names and throw upfront. distinctOnrow source forgroupBy(Postgres only): aggregate over the newest row per key,distinctOn: { columns: ['instanceId'], orderBy: { createdAt: 'desc' } }wraps the row source inSELECT DISTINCT ON.
Fixed
distinctcombined with any relation-basedorderBy(_count, to-one column, and the new pick shape) crashed at runtime with invalid SQL ("missing FROM-clause entry"). Now rejected with a clearValidationErrorupfront. The_count/to-one crash predates this release.- SQL-cache collision on multi-column to-one relation
orderBy. The cache fingerprint sorted entries while the compiler preserved their order, so{ name: 'asc', email: 'desc' }and the swapped literal shared one cache entry and a warm cache silently served the wrong ORDER BY precedence. The fingerprint now captures insertion order. Predates this release. - JSON-path parameters now encode per dialect. SQLite/MySQL/SQL Server
JSON extraction takes a
'$.a.b'JSONPath string, not a Postgrestext[]— JSON filters, JSON ordering, and the new JSON groupBy now actually execute on those engines (verified live onnode:sqlite). Predates this release for JSON filters/ordering.
0.31.0 (2026-07-13)
Three production-blocking bug fixes plus two new query capabilities that eliminate common raw-SQL escape hatches in version-driven / multi-tenant data models.
Fixed
- Owned-pool
disconnect()leaked every driver connection on the networked PowDB path —turbinePowDB({host, port})never patcheddisconnect(), so the driver pool'sclose()never ran and a one-shot script hung until the server's 300s idle timeout. Owned pools now close ondisconnect(), andPowdbPool.end()additionally destroys still-checked-out clients (the driver'sclose()only reaps idle ones). Queries afterdisconnect()throw a typedConnectionErroron both transports. - Nested-relation
orderByrejected camelCase columns —with: { fields: { orderBy: { sortOrder: 'asc' } } }threw E003 because the relation-subquery path skipped the target table's columnMap. Nested orderBy now accepts exactly what top-level orderBy accepts, unified across the join strategy, batched loader, m2m, and the MSSQL override; belongsTo/hasOne subqueries with anorderBynow order before theirLIMIT 1. - Cold-client false E017 on a same-tick transaction burst — the
re-entrancy marker was planted with
AsyncLocalStorage.enterWith()in the caller's context, so sibling$transactioncalls launched in one tick could see each other's markers (runtime-dependent). The marker now lives only inside the transaction callback's async subtree via a new optionalwrapTransactionCallbackdriver seam — the failure is impossible by construction. Implicit nested-write transactions plant the same marker. One contract change: a second raw manualbeginfrom the same context now queues FIFO (bounded bytransactionQueueTimeoutMs) instead of throwing E017. - Connection release now honors the destroy contract (adversarial-review
finding):
release(err)with a truthy error destroys the connection instead of re-idling it, and any release with an un-endedbeginfires a bounded best-effortrollbackfirst — so a$transactiontimeout can no longer return a connection with an open server-side transaction to the pool (which blocked the next transaction on PowDB's global write lock).
Added
- Column-to-column
wherecomparison —{ equals: { col: 'otherField' } }(alsonot/gt/gte/lt/lte) compiles to"a" = "b"with no bound param; cache-fingerprint-safe, works in relation filters andwith.where. On json/jsonb columns anequalsobject stays a JSON value. - JSON-path
orderByon same-table json/jsonb columns —orderBy: { data: { path: ['weight'], direction: 'asc', type: 'numeric' } }, top-level and in nestedwithorderBy. Cross-relation JSON ordering and grouped JSON aggregates are deferred to a designed 0.32.
Docs
docs/internal/NEXT-INTEGRATIONS.mdno longer claims PowDB was "declined" — the driver shipped in 0.22 and is load-bearing. (Per-call/per-tablewarnOnUnlimited, also requested, already shipped in 0.30.0.)
0.30.0 (2026-07-13)
Fixes and features for JSONB-heavy, Prisma-migrated workloads, plus alignment with PowDB 0.10. Adversarially reviewed before ship; 15 review findings (including one critical) fixed pre-release.
Fixed
- JSON/array filters inside relation filters were silently dropped. A
JsonFilter({ path, equals }) undersome/every/none— or in awith.where— compiled to a broken jsonb equality and matched nothing, with no error. Both paths now route through the real JSON/array clause builders, with the SQL-cache fingerprint and cache-hit param-collect mirrors kept in lockstep. - Postgres enum columns failed
createManywith "column is of type X but expression is of type text". The bulkUNNEST($n::text[])form defeated Postgres's enum inference. Enum columns (recognized via introspected metadata) now get an explicit::"EnumName"cast on every write bind — create, createMany (::"EnumName"[]), update, updateMany, upsert. Gated to the Postgres dialect; cross-schema type-name collisions are excluded via the newly recorded type schema. - FK/relation name collisions produced unsound generated types and
unreachable relations. A camelCase FK like
currentVersionIdderived a belongsTo that shadowed the scalar column (TS2430 under--strict, relation-where misroutes at runtime). Relation naming now disambiguates per-FK-column (currentVersion), with legacy names preserved wherever they were collision-free — regenerating a working schema does not rename its relations.turbine mcp, the SQLite/MySQL/MSSQL introspectors, and the newschemaDefToMetadataall share one naming implementation, and the generate-typecheck CI gate now compiles the colliding fixture under strict tsc. turbine-orm/powdbwas unusable from CommonJS with ESM-only@zvndev/powdb-client≥ 0.9 (ERR_PACKAGE_PATH_NOT_EXPORTED): the CJS build lowered the lazyimport()torequire(). Optional-peer loads (powdb, mysql2, mssql) now route through a.ctshelper whose NodeNext-built copy keeps a realimport(). Peer ranges widened to>=0.7.1 <1.0.0.- A failed BEGIN no longer emits a best-effort ROLLBACK (all engines).
Previously a
$transactionwhose BEGIN threw (e.g. PowDB queue timeout) still sent ROLLBACK, which on single-handle engines (embedded PowDB) landed inside the other open transaction — silent partial commits. The PowDB pools additionally refuse to forward commit/rollback from a scope that never acquired the transaction gate. (found by adversarial review)
Added
-
JsonFilter range operators
gt/gte/lt/lte(withpath): numeric values compare via a::numericcast, strings as extracted text.db.products.findMany({ where: { data: { path: ['rating'], gte: 4 } } }). -
schemaDefToMetadata(def)— pureSchemaDef→SchemaMetadataconverter, so code-first schemas drive non-SQL engines without a live database:turbinePowDB(pool, schemaDefToMetadata(mySchema)). -
PowDB concurrent transactions queue FIFO instead of throwing E017 under the single-writer lock. Re-entrant and nested transactions still throw E017 immediately (queueing them would deadlock; detection via a chained AsyncLocalStorage marker that survives cross-pool nesting). New
transactionQueueTimeoutMsoption (default 30000;0/Infinitywaits forever) →TimeoutErrorE002 on elapse. -
turbine generate --no-timestamp— omits theGenerated at:header so regenerated output is byte-identical. -
warnOnUnlimitedper call and per table —findMany({ warnOnUnlimited: false }), orwarnOnUnlimited: { userProfiles: false }in config (accessor or snake_case keys). -
PowDB 0.10 alignment: reserved PowQL words (incl. the new
schema/describekeywords) are backtick-quoted automatically in bare-identifier positions of generated PowQL; the server's new "transaction gate timeout" maps toTimeoutErrorE002.
0.29.0 (2026-07-12)
Feature: batch $transaction([...]) pipelines on drivers that support it.
Changed
- Batch
$transactionis one write burst on pipelining drivers. The array form previously awaited each deferred query sequentially — N statements cost N round trips even though the batch is a single atomic unit. When the checked-outPgCompatPoolClientadvertises the new additive capability flagsupportsPipelining: true,transactionBatchnow dispatches every statement back-to-back inside BEGIN/COMMIT and collects replies in order (Promise.allSettled— all in-flight replies drain before a ROLLBACK, and the lowest-index failure is thrown, wrapped as before). The PowDB pool's checked-out clients advertise the flag;node-postgrespaths are byte-identical to 0.28.x (flag absent → sequential path unchanged), and the pipelined path is disabled for dialects withresultStrategy: 'reselect'.
Added
PgCompatPoolClient.supportsPipelining?: boolean— optional, additive; any PgCompat driver whose connection preserves FIFO reply order over a single socket can opt in to get the batched-transaction fast path.
0.28.3 (2026-07-11)
Patch: turbine generate output typechecks again.
Fixed
- Generated client failed
tscwith TS2415 ("incorrectly extends") since 0.26. The baseTurbineClient.$transactiongained the batch-array overload ($transaction([...queries])), but the generator's interface-merge for the typed client still emitted only the callback signature — and a merged member must be compatible with the base member on its own. The generatedTurbineClientinterface now redeclares both overloads (typed callback + batch array). Found dogfooding the BataDB demo app on 0.28.2. - New regression gate:
generate-typecheck.test.tscompiles freshly generated output withtsc --noEmitagainst the repo's own source types (path-mapped), so template ↔ client-type drift can never ship again — string-pin tests alone stayed green through this break.
0.28.2 (2026-07-10)
Post-smoke-audit patch for 0.28.1 — consumer typecheck + docs honesty.
Fixed
@types/pgrestored todependencies. Moving it todevDependenciesin 0.28.1 broke stricttscfor consumers whose projects do not setskipLibCheck(published.d.tsfiles importpgtypes). Runtime was always fine; TypeScript-first installs were not. Release gate now requires a pack-install + strict consumer typecheck before publish.- README PowDB capability blurb rewritten to match the real engine (returning writes, auto or UUID PKs, client-side relation loads incl. m2m, nested writes for hasMany/hasOne/belongsTo).
- Bundle-size claim clarified as brotli import graph excluding
pg, not dual-build install size. - STABILITY.md stable CLI list includes
doctor,mcp,migrate deploy. - Committed
site/lib/version.tsregenerated so the git tree matches the package version.
0.28.1 (2026-07-10)
Gold-standard OSS hygiene pass — trust surface for consumers and CI, not a feature drop.
Improved
- Error messages include stable codes. Every
TurbineErrormessage is prefixed with its code tag (e.g.[TURBINE_E008] …) so logs are greppable without structured field access. Branch onerr.code/instanceof— do not parse the message text. - Coverage floors ratcheted to lines/statements 80%, functions 82%, branches 82% (measured actuals ~82–84%).
- Engine CI jobs are hard gates (MySQL, SQL Server, CockroachDB, PowDB) — no longer
continue-on-error. - Pack-smoke verifies
sqlite,powdb, andadapterssubpath exports in addition to main/serverless/mysql/mssql. - Seeded generative SQL-safety fuzz (
sql-safety-fuzz.test.ts) overquoteIdent, equality/LIKE, and numeric filters. - Query module split:
query/filters.ts(filter-shape guards + fingerprints) andquery/deferred.ts(DeferredQuery/ options types) extracted frombuilder.ts. @types/pgmoved todevDependencies— runtimedependenciesis justpg.engines.nodeis>=20(matches the CI matrix; Node 18 is EOL).- SECURITY.md / STABILITY.md supported-version tables updated for 0.28.x.
- CODE_OF_CONDUCT.md added; linked from README + CONTRIBUTING.
- Internal sprint/strategy scaffolding moved under
docs/internal/.
0.28.0 (2026-07-09)
Parity sprint — the largest feature release since multi-engine support. A batch of gaps closed at once: query ergonomics (NULLS ordering, relation _count, ordering by a relation), schema completeness (referential actions, code-first enums/arrays/vector/checks), global filters for soft-delete and multi-tenancy, read replicas, a read-only MCP server for AI agents, seed-as-code, a non-interactive migrate deploy, Zod generation, and views + generated columns. Everything is additive — the Postgres default and the existing findMany/with/where API are unchanged, and npm i turbine-orm still installs only pg.
Added — query ergonomics
NULLS FIRST/NULLS LASTordering.orderByvalues accept a spec object{ sort: 'asc' | 'desc', nulls?: 'first' | 'last' }alongside the plain'asc'/'desc'direction:orderBy: { lastLoginAt: { sort: 'desc', nulls: 'last' } }→ORDER BY "last_login_at" DESC NULLS LAST. Applies everywhere anorderByis compiled — top-levelfindMany/stream,groupBy, and the inner subquery of awithrelation. Plain directions are byte-identical to before. Behavioral note:NULLS FIRST/LASTis a PostgreSQL and SQLite feature — on MySQL and SQL Server, explicit nulls placement throwsUnsupportedFeatureError(E017) rather than emitting broken SQL.- Relation
_countinwith.with: { _count: true }counts every to-many relation of the table;with: { _count: { posts: true } }counts only the named ones. Each becomes a correlatedCOUNT(*)scalar subquery (hasMany + manyToMany via the junction), assembled into a typed_count: { [relation]: number }object per row. Coexists with real relation subqueries. Errors:RelationError(E005) on an unknown relation,ValidationError(E003) on a to-one relation. The batched load strategy computes it with one grouped follow-up per counted relation — output deep-equal to the join strategy. - Ordering by a relation.
orderBy: { posts: { _count: 'desc' } }orders by a to-many relation's count (correlatedCOUNT(*));orderBy: { author: { name: 'asc' } }orders by a to-one relation's target column (correlated scalar subquery,{ sort, nulls }supported). Relation ordering adds no bound params. To-many relations allow only_count; to-one allow real target columns. Unknown relation → E005, invalid key/column → E003.
Added — schema completeness
- Referential actions on foreign keys.
referencesnow accepts{ target, onDelete?, onUpdate? }in addition to the'table.column'string, and the fluent builder gains.references(target, { onDelete, onUpdate }). Actions:'cascade','restrict','set null','set default','no action'; omitted clauses default toNO ACTION. DDL emitsON DELETE …/ON UPDATE …; introspection reads them back frompg_constraint, andschemaDiffdetects action changes (non-destructiveDROP CONSTRAINT+ADD CONSTRAINT). The plain string form is unchanged. - Code-first enums.
defineSchema(tables, { enums: { post_status: ['draft', 'published', 'archived'] } })declares enum types; columns opt in with{ type: 'enum', enumName: 'post_status' }. DDL emitsCREATE TYPE … AS ENUM (…)before the tables that use it (labels are single-quote escaped), and codegen maps enum columns to string-literal unions. - Array columns.
{ type: 'text', array: true }emitsTEXT[](works withvarcharlengths →VARCHAR(8)[]), maps toT[]in generated types, and is queryable with the existinghas/hasEvery/hasSomeoperators end-to-end. - pgvector columns.
{ type: 'vector', dimensions: 1536 }emitsvector(1536)and, by default, prependsCREATE EXTENSION IF NOT EXISTS vector;(passextensions: 'manual'to emit a comment instead); maps tonumber[]. Gated on the Postgres dialect — a dialect without vector support throwsUnsupportedFeatureError(E017). - Check constraints. Column-level
check: 'price >= 0'emits an inlineCHECK (expr); table-levelchecks: [{ name?, expression }]emitCONSTRAINT "name" CHECK (expr)(or a bareCHECKwhen unnamed). Introspection reads them back. A violation throwsCheckConstraintError(E011) at write time. - New package-root exports:
ReferentialAction,ReferenceDef,CheckDef,CheckMetadata, andDefineSchemaOptions.
Added — client
- Global filters (soft-delete / multi-tenancy).
TurbineConfig.globalFiltersmaps a table accessor to aWhereClause— or a() => WhereClauseevaluated per query build — that is AND-merged into the compiledWHEREof every read and mutation, and every relation subquery targeting that table (join and batched strategies, relation filterssome/every/none,_count, and relationorderBy). Function filters enable per-request tenancy via closure.create/createManyare never filtered. A per-queryskipGlobalFilters: true | string[]opts out. Behavioral note: the empty-whereguard onupdate/deletestill checks the user-suppliedwhere, so a global filter never turns an unguarded mass mutation into an allowed one. Filter shape participates in the SQL cache; values are re-collected per build. - Read replicas.
TurbineConfig.replicas(connection strings orPgCompatPools) load-balances read-only operations outside a transaction (findMany,findFirst,findUnique,*OrThrow,count,aggregate,groupBy,findManyStream) round-robin across replicas; ALL writes,$transactionbodies,pipeline,raw/sql,$listen/$notify, and observability flushes stay on the primary.client.$primary()returns a cached view that pins every operation (reads included) to the primary — for reading your own writes without replication lag. String replicas are owned pools (closed ondisconnect()); external pools follow the caller-owns-lifecycle contract. Zero replicas = today's single-pool path, unchanged. - Batch
$transaction(DeferredQuery[])overload. Pass an array ofbuild*deferred queries and they run atomically on one connection (BEGIN… each query …COMMIT), returning a positionally-typed results tuple. Any error rolls the whole batch back and rethrows; an empty array resolves to[]. The callback form is unchanged —$transactionaccepts either.
Added — CLI & tooling
turbine mcp— zero-dependency, read-only MCP server. Speaks JSON-RPC 2.0 over stdio (protocol2025-06-18, server nameturbine-orm) for AI agents like Claude Code and Cursor. Six read-only tools —schema_overview,table_detail,migrate_status,doctor_report,explain_query(schema-validatedfindMany-style builder args only — no free-form SQL),sample_rows(≤ 50 rows, table validated against the introspected schema). Every database access runs insideBEGIN READ ONLY; there is no write surface and no raw-SQL execution path. Malformed frames return a JSON-RPC error without crashing.--include/--excludescope the exposed tables.- Studio / Observe refuse non-loopback binds by default.
--hostother than loopback (127.0.0.1/localhost/::1) exits 1 unless you pass--allow-remote(loud warning when you do). Matches the “local single-user tool” security model instead of warn-and-proceed. turbine migrate deploy— non-interactive production apply. Never prompts (works with no TTY): applies all pending migrations inside the same advisory-lock + per-migration-transaction machinery asmigrate up, reportsN applied, and supports--dry-runto list pending without applying. Refuses to run (exit 1, clear message) on a checksum mismatch or a missing migration file, so a drifted history fails the deploy instead of diverging. It never auto-generates, seeds, or pushes.- Seed-as-code.
turbine seedresolves the seed from the configseedfield (orseedFilealias) or the first default candidate —seed.ts,seed.js, thenseed.sql..tsruns throughnpx tsx(clear error iftsxis missing),.jsis imported (a default-export function is called),.sqlruns as SQL. NewdefineSeed(fn)export wires up a client fromDATABASE_URL, runs your function, and disconnects. turbine generate --zod. Emits azod.tsfile alongside the generated types with aXSchema/XCreateSchema/XUpdateSchemaper table, derived from column metadata (scalars,z.coerce.date()for dates,z.enum([...])for enums,.array(),z.array(z.number())for vectors,.nullable(); Create/Update optionality mirrors the generated input types). The generated file imports the user-sidezoddep; the Turbine runtime never does.turbine generate --include-views. Introspects views and materialized views as read-only entities (isView: true): codegen emits an entity type and read accessors; a no-PK view's accessor omits thefindUniquefamily (Omit<QueryInterface<T>, 'findUnique' | 'findUniqueOrThrow'>). Behavioral note: every write builder throwsValidationError(E003) "cannot write to a view".- STORED generated columns. Introspection detects
GENERATED ALWAYS AS (…) STOREDcolumns; codegen keeps them in the entity type but omits them from*Create/*Updateinputs. Behavioral note:create/update/upsertrejectdatacontaining a generated column withValidationError(E003) before hitting Postgres.
Docs
- New pages: Global Filters, Read Replicas, MCP Server, Seeding, Zod Schemas, and Views & Generated Columns. Extended the API Reference (NULLS ordering, relation
_count, ordering by a relation), Schema & Migrations (referential actions, enums, arrays, vector, checks), Transactions (batch$transaction), Relations, and the CLI page (migrate deploy,mcp,--zod,--include-views, seed-as-code). - Quickstart honesty: site
/quickstartdocumentstsx,"type": "module",schemavsschemaFile, and the empty-DB path (defineSchema→push→generate) alongside the existing-tables path. - Comparison copy corrected: the Drizzle Studio row previously read "paid tier" — local Drizzle Studio is free (only the hosted Drizzle Gateway is paid). Softened across the README, landing page, and Drizzle migration guide.
Chore
- Removed broken root
npm run examplesscript (missingexamples/examples.ts); addedexamples/README.mdindex.npm run dogfoodunchanged. - Widened optional peer
mssqlto^10 || ^11 || ^12(matches testedmssql@12).
0.27.1 (2026-07-08)
Identical to 0.27.0 with an internal lint cleanup in the destructive-statement scanner; 0.27.1 is the canonical release (0.27.0's npm artifact predates the tagged source).
0.27.0 (2026-07-08)
Destructive migrations now require explicit, triple confirmation. A migration file containing data-destroying SQL should never run just because it exists — and "one flag and your table is gone" is too easy.
Added
- Data-loss gate on
migrate upandmigrate down. Before applying, Turbine scans every pending migration (UP direction) / every DOWN section being rolled back for destructive statements:DROP TABLE,DROP SCHEMA,DROP COLUMN,TRUNCATE,DELETE FROM,UPDATEwithout aWHERE, andALTER COLUMN … TYPE(potentially lossy cast). Comments, string literals, and dollar-quoted bodies are stripped first, so-- DROP TABLE xor a seeded string containing SQL never false-positives;DROP INDEX/DROP CONSTRAINT/DROP TRIGGERare deliberately not flagged (recreatable, no row data). When something is found:- Interactive CLI: prints an itemized report (statement kind, target object, what it destroys), then requires typing the literal phrase
destroy my data, then a finalyes. Anything else aborts with nothing applied. - Non-interactive (CI/pipes): always refuses; proceeding requires the explicit
--allow-destructiveflag, which also prints a loud warning. - Programmatic API (
migrateUp/migrateDown): refuses by default with an itemizedMigrationError; opt in withallowDestructive: true. - The refusal is checked BEFORE anything runs — a refused batch applies zero migrations.
- Interactive CLI: prints an itemized report (statement kind, target object, what it destroys), then requires typing the literal phrase
- (
turbine pushandmigrate create --autowere already non-destructive: the schema differ has never emitted forwardDROP TABLE/DROP COLUMNstatements — dropped columns are detected but excluded from executable statements.)
0.26.0 (2026-07-08)
Dogfood release from migrating a large production Prisma app onto Turbine — a batch of correctness fixes the migration surfaced, plus the tooling that makes Turbine's correlated relation loading robust on schemas that grew up under batched-loader ORMs: a missing-FK-index doctor, an opt-in batched loading strategy, and an opt-in lean JSON wire encoding.
Fixed
timestamp(without time zone) columns are now parsed as UTC (correctness, behavior change). Both the pg driver default (OID 1114) and nestedjson_aggstrings parsed offset-less timestamps in server-local time — the same row produced a different instant per deployment region, and every timestamp shifted by the machine's UTC offset. Turbine now pins offset-less timestamps to UTC (parseDbDate), matching Prisma/Rails/Django semantics. The OID 1114 parser is registered only for Turbine-owned pools (external/serverless pools are never touched). Opt out withutcTimestamps: falseif you relied on local-time parsing.hasOnerelation subqueries correlated in the wrong direction. ThehasOnepath reused thebelongsTocorrelation (target.pk = parent.fk) instead oftarget.fk = parent.pk, producing wrong rows (or type-mismatch errors when the parent column wasn't a key). OnlybelongsToreverses the correlation now. (Also fixed in the SQL ServerFOR JSON PATHpath.)- Wide tables no longer hit Postgres's 100-argument limit in relation subqueries.
json_build_objecttakes 2 arguments per column, so relations targeting tables with >50 columns failed withcannot pass more than 100 arguments to a function. The Postgres dialect now chunks into(jsonb_build_object(…) || jsonb_build_object(…))::jsonconcatenation. distinct+orderByno longer conflicts with Postgres's DISTINCT ON rule.SELECT DISTINCT ON (cols) … ORDER BY other_colis rejected by Postgres unless the DISTINCT columns lead the ORDER BY. Turbine now orders by the distinct columns in an inner query and applies the user'sorderByin an outer wrapper. (distinct+ vectororderBythrows a clear validation error instead of emitting broken SQL.)- Top-level
selectcombined withwithno longer drops the relation from the result type. When a generated entity interface declares optional relation props, the with-key was also akeyof Tand the select-narrowingPicksilently removed it — forcing users to dropselectand over-fetch. The with-clause keys are now unioned back into the result type (andomitcan no longer strip a relation thewithclause populates). Compile-time regression tests included.
Added
npx turbine doctor— missing-FK-index advisor. Turbine loadswithrelations as correlated subqueries: the child table is probed once per parent row, so an unindexed FK column costs a full table scan per parent — pathological on large tables, and invisible on the ORMs people migrate from (batchedIN (ids)loading pays a missing index only once, so those schemas routinely lack FK indexes).doctorintrospects the database, derives every column set relations will probe (hasMany/hasOne child FKs, belongsTo reference keys, many-to-many junction keys), reports the unindexed ones sorted by table row count with the exactCREATE INDEXstatement, and--fixwrites a ready-to-apply migration. Measured on a production-shaped dataset: one missing FK index turned a 659-parent query into 659 sequential scans of a 357K-row table (17.8s); with the index the same correlated query ran in 62ms — faster than the batched equivalent (92ms).- Dev-mode missing-index warning. In non-production, the first query that builds a relation subquery over an unindexed FK logs a one-time warning naming the relation, the table/columns, and the exact index DDL (only when the schema metadata actually carries index info, so
defineSchema-only users see no false positives). relationLoadStrategy: 'join' | 'batched'— opt-in batched relation loading, as a client-level default (TurbineConfig) or per-query onfindMany/findFirst/findUnique.'batched'runs the base query withoutjson_aggsubqueries, then one flat follow-up per relation (WHERE fk = ANY($1), chunked at 1,000 keys) and stitches client-side — results are deep-equal to the join strategy (verified by integration tests running both). Useful when FK indexes are missing or result sets are huge (flat rows transfer leaner than nested JSON). Sibling relations and key chunks load concurrently (keys travel as oneANY($1)array parameter, chunked at 32K). Honors per-relationwhere/select/omit/orderBy/nestedwith; per-relationlimitis applied client-side per parent. Transaction-safe (follow-ups run on the same connection). Measured on a production-shaped worst case (8,814 parents × 6 relation trees, unindexed FKs, WAN link): join strategy 6.5s,batched~1.9s — roughly 2× faster than a leading batched-loader ORM's equivalent query (~3.7s) on the same data.- Implicit
ison bare to-one relation filters (Prisma-compatible).where: { vendor: { name: { contains: 'x' } } }now works without the explicitiswrapper;is: null/isNot: nullcompile to NOT EXISTS / EXISTS.WhereClausetypes to-many relation props assome/every/nonefilters and to-one props as bare-or-is/isNot. - Relation filters inside
with.whereand at any nesting depth. A relation'swith … wherecan now filter by the relation's own relations (with: { items: { where: { stage: { is: { active: true } } } } }), and relation filters recurse throughsome/none/every/is/isNotwithOR/AND/NOTsupport at every level. jsonEncoding: 'positional'— opt-in lean wire encoding forwithrelations (Postgres-only, default'object'unchanged). Relation subqueries emitjson_agg(json_build_array(…))instead ofjson_build_object('key', …), so key names stop being repeated in every nested object of every row; positions are mapped back to keys client-side and parsed output is byte-identical to the object encoding (integration tests assert deep-equality under both). Measured on a 14-column hasMany relation: 39% fewer wire bytes and ~13% faster end-to-endfindMany— the win grows with column count and result size. Composes with everything (select/omit, ordered/limited relations, hasOne/belongsTo, m2m, nested trees);relationLoadStrategy: 'batched'simply bypasses it (no JSON aggregation there).benchmarks/json-encoding-bench.tsreproduces the numbers.
0.25.0 (2026-07-06)
Added
turbineHttpnow accepts your generated client type for fully typed accessors.turbineHttp(pool, SCHEMA)returned the baseTurbineClient, so the generated typed accessors (db.users,db.posts, …) were invisible to TypeScript on the serverless/edge path — you had to cast at the call site. It now takes a backward-compatible type parameter:turbineHttp<TurbineClient>(pool, SCHEMA)(passing your generated client type) gives the exact same typed accessors as the TCP-pathturbine()factory, no cast. The runtime object is unchanged — the base constructor already materializes those accessors per schema table — and existing untyped calls keep working (default = base client). Found dogfooding the BataDB edge path. (#30)
0.24.0 (2026-07-06)
Dogfood fixes from building a fresh Next.js app on 0.23.2 (#28). Five papercuts that made the first-run experience worse than it should be — a silent empty generate, two rejected column types, doc drift, undocumented prereqs, and an inaccurate serial type. All fixed; the only behavior change is the serial mapping (below), which is safe for existing databases.
Changed
serialnow emitsSERIAL(int4), notBIGSERIAL(int8) — behavior change for NEW pushes. Aserialprimary key was typednumberbut, being int8, read back over the wire as a string for large values — the generated type lied.serialnow maps toSERIAL(int4), whose values fit in a JSnumberand are returned as numbers, so the type is accurate end-to-end. A newbigserialcolumn type covers 64-bit auto-increment keys (int8; large values still read back as string, now documented). Existing databases are unaffected:turbine push/migrate --autonever auto-narrow a live column's integer width, so aserialcolumn created asBIGSERIALbefore 0.24.0 is left exactly as-is. Only brand-newCREATE TABLEs getSERIAL. (#28)
Added
bigserial,timestamptz, andjsonbare now first-classdefineSchemacolumn types. The docs' type table listedtimestamptzandjsonb, butdefineSchemarejected them; they now work.timestamptzis an explicit spelling of the timezone-aware timestamp Turbine already emitted fortimestamp;jsonbis an explicit spelling of whatjsonalready emitted. Both aliases (timestamp,json) still work unchanged. (#28)TurbineConfigis now exported as an alias ofTurbineCliConfigfromturbine-orm/cli, matching what the docs import. (#28)turbine generate --allow-emptyescape hatch for the two new guards below.
Fixed
turbine generateno longer silently emits an empty client. Theschemaconfig field is the Postgres schema name (defaultpublic), but the docs told users to put their schema file path there — sogenerateintrospectedWHERE table_schema = './turbine/schema.ts', matched zero tables, and wrote an empty typed client with no error.generatenow errors (exit 1) whenschemalooks like a file path (with a hint pointing atschemaFile) and when introspection matches 0 tables. Pass--allow-emptyto override. (#28)- Clearer CLI error for the CommonJS case. When a project's
package.jsonlacks"type": "module", loading a.tsconfig/schema fails with Node's rawERR_REQUIRE_ESM; the CLI now appends a hint to add"type": "module". (#28)
Docs
USING-TURBINE-ORM.md§0 corrected: the config example usedschema: './turbine/schema.ts'(should beschema: 'public'+schemaFile:) andmigrations:(should bemigrationsDir:). Added a CLI prerequisites section documenting that the CLI needstsxinstalled and"type": "module"inpackage.json, with the exact error messages — neither is set bycreate-next-app. README quickstart updated to match. (#28)
0.23.2 (2026-07-03)
Fixed
- Published files no longer reference missing source maps.
sourceMap/declarationMapemitted//# sourceMappingURL=…comments in every publisheddist/*.js/*.d.ts, but thefilesallowlist excludes the.mapfiles themselves — Node ignores the dangling reference, but Next.js/turbopack's stricter loader logged a failed-to-map warning on every stack trace through Turbine. Maps are now emitted only for local builds and never referenced from published output. (#25, #26)
Changed
pgdependency bumped 8.20.0 → 8.22.0 (upstream fixes; no API change). (#13)- CI: actions/checkout 5 → 7, actions/setup-node 5 → 6; dev-dependency refresh. Docs site upgraded to Next 16 + Tailwind 4 with a real site-build CI gate, and the site's displayed version is now derived from the root
package.jsonat build time (can't drift from the published package). (#4, #12, #23, #27) - New docs page: Turbine + BataDB guide (
turbineorm.dev/batadb) — typed Turbine over BataDB's edge HTTP driver or direct TCP, with the dual-transport pattern. (#24)
0.23.1 (2026-07-03)
Coordinated release with PowDB 0.8.0 — the PowDB engine's optional peers now accept the newly published @zvndev/powdb-client@0.8.0 / @zvndev/powdb-embedded@0.8.0, and the full test suite runs green against those exact published artifacts (1113 passing / 0 failing).
Changed
- Widened the PowDB optional-peer range to
^0.7.1 || ^0.8.0for both@zvndev/powdb-clientand@zvndev/powdb-embedded, ahead of the coordinated PowDB0.8.0release. The engine surface Turbine uses (query/write/tx/returning) is unchanged in0.8.0— Turbine does not callapplyRetainedUnits— so the widening is safe and additive. Both remain optional peers (peerDependenciesMetaunchanged); a defaultnpm i turbine-ormstill installs onlypg.
Fixed
- CJS build now compiles under TypeScript 6.0.
tsconfig.cjs.jsonsetsignoreDeprecations: "6.0"for itsmodule: CommonJS/moduleResolution: node10pairing, which TS 6.0 otherwise rejects as a hard error (TS5107). ThetypescriptdevDependency floor moves to^6.0.3so the option is recognized. Emitteddist/output is byte-identical to the previous toolchain (verified by a fulldistdiff); no runtime or API change.
0.23.0 (2026-06-29)
PowDB Phase B — server-generated PKs, many-to-many, nested writes, composite-key upsert — plus a correctness fix for relation filters. All PowDB-only; the four SQL engines are untouched and npm i turbine-orm still installs only pg.
Fixed
- Relation filters (
some/none/every) no longer return stale results on PowDB (correctness, affects 0.22.0). PowDB's executor caches anin (<subquery>)result by plan shape, ignoring the literal, so a second relation filter of the same shape with a different value returned the first query's rows (reproduced against the raw embedded addon, no Turbine). Turbine no longer emits an IN-subquery for relation filters: it resolves the inner predicate to a literal key list (resolveRelationFilters) and filters within (<list>), which is always correct. This covers hasMany / hasOne / belongsTo (the 0.22.0 shapes) and the new manyToMany filters, at every nesting level, onfindMany/findUnique/update/delete/count/aggregate/groupBy. Trades one extra round-trip per relation filter for correctness. (Reported upstream to PowDB; the SQL engines were never affected — they use realEXISTS/json_agg.)
Added
- Server-generated / auto-increment primary keys. New
ColumnMetadata.isGeneratedflag distinguishes a DB-assigned PK (serial /IDENTITY/ PowDBauto) from a client-side default. On PowDB,powqlSchemaDDLnow emits theautomodifier (unique auto id: int) for a generated int PK andcreate/createManylet the engine assign the id (read back viareturning) instead of synthesizing a client UUID. Introspection sets the flag fromnextval(/is_identity; the code generator emits it. No change to the SQL engines — the flag is additive and they already omit undefined PKs and rely onRETURNING. - many-to-many nested reads —
with: { tags: true }on a junction relation now loads through the junction (batched, chunked at 1,000 keys), with correct empty-array semantics and nestedwhere/with. - many-to-many relation filters —
where: { tags: { some/none/every } }through a junction table. - Nested writes on PowDB — relation ops in
create/updatedata (create,connect,connectOrCreate,disconnect,set,delete,update,upsertfor hasMany/hasOne/belongsTo) now run through the shared nested-write engine as one flat top-level transaction (PowDB is single-writer / no savepoints, so the whole tree commits or rolls back together). Same coverage as the SQL engines. - Composite-key upsert on PowDB — PowQL's native
upsert … on .coltakes a single conflict column, so a composite PK now falls back to an atomic reselect-or-write transaction. - Live
powdb-integrationcoverage for every item above (6 new tests against the real embedded addon), plus build-only DDL tests for theautomodifier and the composite-PK fix.
Changed
powqlSchemaDDLno longer marks each column of a composite PK individuallyunique. PowDB has no composite-unique constraint (itsuniqueis single-column), and per-columnuniquewrongly forbade, e.g., a member having two tags. Composite-PK columns are nowrequiredonly; a single-column PK still getsunique.
Still unsupported on PowDB (throws UnsupportedFeatureError / E017)
- Composite-key relation filters and composite-key m2m (PowQL has no tuple-
in(a,b) in (…)), nested writes insidecreateMany/upsert(usecreate/update), and the unchanged Postgres-only set: pgvector, LISTEN/NOTIFY, RLSsessionContext, cursor streaming.
0.22.0 (2026-06-28)
New engine: PowDB. Turbine now runs on PowDB, a single-node embedded database with its own query language (PowQL — not SQL), behind the same findMany / with / where / create API. PowDB is the only engine that runs both in-process (embedded) and over a network client against the same data. Postgres remains the default and primary target; PowDB is an additive optional-peer subpath export — npm i turbine-orm still installs only pg.
Added
turbine-orm/powdb—await turbinePowDB(target, schema, options?). Because PowQL shares no surface with SQL, this is not aDialect: a parallelPowqlInterface(PowQL generator with the same public method surface asQueryInterface) is wired in via the newqueryInterfaceFactoryseam, leaving the four SQL engines byte-identical.- Two transports. Embedded (in-process) via the native addon
@zvndev/powdb-embedded, and networked via@zvndev/powdb-clientagainst apowdb-server. Both are optional peer dependencies (^0.7.1), loaded by dynamicimport()— neither is pulled by a default install. - Embedded durability control.
turbinePowDB({ embedded, syncMode: 'full' | 'normal' | 'off', memoryLimit })exposes PowDB 0.7.1'ssetSyncMode/openWithMemoryLimit. WithsyncMode: 'normal', embedded writes drop ~440× (fsync off the commit path) and beat SQLite oncreate(0.009 vs 0.016 ms p50),update(0.008 vs 0.012),createMany(0.278 vs 1.197), and nestedwith— while keeping a real storage engine, indexes, and WAL.syncMode/memoryLimitare feature-detected; using them on a pre-0.7.1 addon raises a clearConnectionError. - Honest capability surface. PowDB writes use the
reselectstrategy with client-assigned UUID PKs (PowDB generates no IDs), N+1 relation loaders (nojson_agg— keys chunked at 1,000), single-writer transactions (no nesting), and code-defined schemas viadefineSchema(no wire introspection). Not-yet-built capabilities throwUnsupportedFeatureError(E017): many-to-many relation filters/nested reads, composite-key relations/reads/upsert, nested writes, cursor pagination /findManyStream, JSON/array/full-text/pgvector filters and vector ordering — plus the Postgres-only trio (pgvector, LISTEN/NOTIFY, RLSsessionContext).
- Two transports. Embedded (in-process) via the native addon
/engines#powdbdocs — a full PowDB section (both transports,syncMode, the embedded-beats-SQLite benchmark, the E017 list, and the platform-binary caveat), plus PowDB coverage in the README "Database engines" section andbenchmarks/CROSS-ENGINE-RESULTS.md.src/test/powdb.integration.test.ts— 10 tests exercising the real embedded addon (gated viaskipGateso the unit lane stays green without it) and wired to a new in-processpowdb-integrationCI job (live addon on Linux, no container).
Fixed
- Empty-
whereguard now gates on the compiled PowQL filter, mirroring the SQL path —{ OR: [] }/{ AND: [] }/{ NOT: {} }/{ OR: [{ field: undefined }] }can no longer bypass the mass-mutation guard onupdateMany/deleteMany. - Single-writer transaction model hardened. A re-entrant or concurrent
db.$transactionon the networked transport used to hang forever on PowDB's global write lock; a pool-levelactiveTransactionguard (both transports) now throwsE017immediately. Nestedtx.$transactionlikewise throws (no savepoints). - Embedded driver errors are now typed. Embedded addon failures (every one tagged
GenericFailure) are mapped by message shape to the right Turbine error (E010not-null,E003type/parse,E008unique), and addon load/shape failures raise a typedConnectionError.
Notes
- Platform binaries (embedded).
@zvndev/powdb-embedded0.7.1 ships prebuilt binaries for darwin-arm64 and linux-glibc (x64/arm64); other platforms (musl/Alpine, Windows, Intel macOS) build from source at install. The networked transport has no such constraint. musl/Intel-mac prebuilts are tracked upstream for PowDB 0.7.2.
0.21.0 (2026-06-26)
Multi-dialect: SQLite, MySQL, and SQL Server engines. Turbine is still Postgres-first, but the query/result core is now engine-agnostic behind a dialect/driver seam, and three new engines ship as additive subpath exports. Drivers are optional peer dependencies — npm i turbine-orm still installs only pg.
Added
turbine-orm/sqlite—turbineSqlite(path | ':memory:', schema). Zero new dependency via Node's built-innode:sqlite(Node ≥ 22.5;better-sqlite3is a documented fallback for older Node). Single-query nestedwithviajson_group_array/json_object(with ajson()subresult wrap so nested trees aren't double-encoded),RETURNINGwrites (SQLite ≥ 3.35),PRAGMA-based introspection, and savepoint-nested transactions. Runs in-process — its full integration suite executes against:memory:in the normal unit lane.turbine-orm/mysql—turbineMysql(config | pool, schema). Optional peermysql2; MySQL 8.0+ (enforced on connect). NestedwithviaJSON_ARRAYAGG/JSON_OBJECT. MySQL has noRETURNING, so writes use the newreselectstrategy (execute, then re-fetch the row);INSERT … ON DUPLICATE KEY UPDATEupserts;information_schemaintrospection;GET_LOCKmigration locking.createManyreturns[](rows are inserted; re-query if you need them).turbine-orm/mssql—turbineMssql(config | pool, schema). Optional peermssql. Nestedwithvia a dedicatedFOR JSON PATHgenerator; writes return rows viaOUTPUT/MERGE;OFFSET … FETCHpaging;INFORMATION_SCHEMA+sys.*introspection;sp_getapplocklocking./enginesdocs page with a per-engine capability matrix, plus a "Database engines" section in the README and CLAUDE.md.- E017
UnsupportedFeatureError— thrown when a Postgres-only feature (pgvector, LISTEN/NOTIFY, RLSsessionContext) is invoked on an engine whose capability flag reports it unsupported, rather than failing with a confusing driver error.
Changed
- The
Dialectcontract became a real multi-engine seam —resultStrategy(returning/reselect/output), aTurbineDriverdriver abstraction (everyBEGIN/COMMIT/SAVEPOINT/isolation/set_configliteral now routes through the dialect),DialectIntrospector, and additive SQL hooks (wrapJsonSubresult,aggSupportsInlineOrderBy,castAggregate,buildInClause,buildRelationSubquery,buildLimitOffset,buildUpdate/buildDeleteStatement). PostgreSQL output is byte-identical and unchanged — engines that don't define a hook fall back to the exact prior SQL. - Engine drivers (
mysql2,mssql) are devDependencies + optional peerDependencies; the root runtime dependency set remains exactlypg. A CIpack-smokecheck verifies each engine subpath imports with its driver absent, and newmysql:8/ SQL Server 2022 CI service-container jobs run the real integration suites.
Fixed
- MySQL optimistic-lock conflicts now throw
OptimisticLockError. On thereselectpath the version-checked UPDATE was followed by a re-fetch onwhereonly (no version predicate), so a conflict silently returned the stale row. The conflict is now detected from the UPDATE's affected-row count — identical behavior to theRETURNING/OUTPUTengines.
Also in this release (product-review sprint)
- Repositioning + onboarding fixes: README and landing now lead with the "safety bundle" (read-only Studio, PII-safe errors, one dependency, checksummed migrations). Fixed copy-paste-breaking docs: serverless
SCHEMAcasing (the generator now also emits a lowercaseschemaalias), the non-existentdb.$queryRaw(→db.sql/db.raw),timeoutMs→timeout, and thewithRetry()"built-in" claim. - Security: closed a LOW stored-XSS gap in the Observe dashboard (
row.model/row.actionnow escaped); hardened the Studio/Observe token check to SHA-256 +crypto.timingSafeEqual. - Docs + tests: new nested-writes, optimistic-locking, and framework-recipes pages; Studio security-perimeter tests (401/403/429/READ-ONLY).
0.19.2 (2026-06-10)
Patch release from a full product review + gold-standard audit — closes a HIGH silent-wrong-rows hole in the SQL cache, makes two documented-but-broken behaviors actually work, and brings the docs/site in line with what the library does.
Fixed
where-key order can no longer cross-bind parameters on a warm SQL cache (HIGH). The cache fingerprint sortedwherekeys, but the SQL-build and cache-hit param-collect paths iterated object-insertion order — so two queries with the same fields in different key order shared one cached SQL string but pushed parameters in different positions.findMany({ where: { tenantId, id } })followed byfindMany({ where: { id, tenantId } })could execute the cached SQL with the values swapped, silently returning the wrong row (a cross-tenant-leak class when the permuted fields are same-typed). Key enumeration is now canonicalized (sorted) across every fingerprint/build/collect triple — top-levelwhere, relation-filter sub-wheres (some/every/none), alias wheres, nestedwithrelation order, cursor, and thefindUniquesimple (plain-equality) path. 14 regression tests warm the cache then assert each column binds its own value. Array members (OR/AND/NOT) remain positional.{ equals: value }works as plain equality on any column.where: { email: { equals: 'a@b.com' } }— documented in the README and the most common operator a migrating Prisma user reaches for — previously threwValidationErrorbecauseequalswas treated only as a JSONB filter key. It now compiles to"col" = $n(and{ equals: null }toIS NULL), parameterized, on the build path, the cache-hit path, and relation filters, while JSON/JSONB columns keep their existing containment behavior.- Nested-write input types are now real. The exported
NestedCreateOp/NestedUpdateOp/ConnectOrCreateOptypes were referenced by zero code —create({ data })was typed asPartial<T>so thecreate/connect/connectOrCreate/update/upsert/disconnect/deleterelation ops the runtime already supported were invisible to TypeScript.CreateArgs/UpdateArgsare now generic over the target's relations and surface the full nested-write op palette (matchingnested-write.tsexactly), recursively, via the sameRelationDescriptorbrand that powerswithinference. Untyped clients collapse to the oldPartial<T>, so nothing breaks; generated clients get typed nested writes with no generator changes.
Changed
- Middleware docs no longer teach a silently-broken soft-delete pattern. The README, the
$useJSDoc, and the site/queries+/observabilitypages showed middleware mutatingparams.args.whereto injectdeletedAt: null— which does nothing, because SQL is generated before middleware runs. Replaced with working patterns (query timing, result transformation) plus an explicit "params.argsis a read-only snapshot" warning, and the soft-delete recipe is now an explicitwherefilter / scoped helper. - New docs:
/studioand/observabilitypages. Studio (the flagship read-only-Studio differentiator) and the$observe()/$on('query')/npx turbine observeobservability surface were undocumented on the site; both now have full pages, in the sidebar and sitemap, with the security model spelled out. /queriesdocumentscursor,take, anddistinct; benchmarks page carries a measurement-vintage caveat; homepage repositioned around Postgres-native depth (pgvector, RLS, LISTEN/NOTIFY, full-text) with a side-by-side pgvector comparison, since single-query nested relations are now table stakes across ORMs. Bundle-size claims corrected to the measured ~31 kB (main) / ~22 kB (edge) brotli.- CLI help completed.
turbine observeand themigrate create --auto/--allow-drift/--stepflags are now in--help; column alignment fixed.
Errors
- Site
/errorspage now documents E015OptimisticLockErrorand E016ExclusionConstraintError(they shipped in 0.15.0 / 0.18.0 but were never added to the page), and thecheck-error-codesCI gate — which had silently failed since 0.15.0 because its known-classes list was missing both — passes again.
Tooling / CI
- DB-gated integration tests now report as skipped (via a
skipGate()helper) instead of silently passing 0 tests whenDATABASE_URLis unset. test:unitis now a glob instead of a hand-maintained file list (new test files can no longer be silently dropped from the release gate);test:watchwatches the full suite.npm auditis a hard gate on production deps (--audit-level=high); the YugabyteDB integration job moved from per-PR to a nightly cron (pinned image) to stop burning ~6 min of red CI per push; newpack-smokejob installs the built tarball and verifies ESM/CJS/CLI/serverless;release.ymlgained a post-publish smoke test and skips publishing a version that already exists on npm.- Added
SECURITY.mdsupported-versions refresh,.github/dependabot.yml, dead-code removal (isReadOnlyStatement, orphaned Studio CSS), and a CLAUDE.md architecture/LOC sync. Backfilled git tags for v0.11.0–v0.14.0 and v0.16.0 (published to npm without tags).
0.19.1 (2026-06-09)
Patch release fixing everything found by the v0.19.0 post-release audit — most importantly a broken new-user CLI happy path and two remaining silent-wrong-rows holes in the query builder.
Fixed
- CLI can load its own scaffolded
turbine.config.tsagain (HIGH).turbine initscaffolds TypeScript files, but every subsequent command failed on current Node: the loader calledmodule.register('tsx/esm', …), which tsx rejects with "tsx must be loaded with --import instead of --loader" on every Node version that hasmodule.register()— and the bare catch misreported it as "tsx is not installed", so following the suggested fix didn't help. The loader now uses tsx's supported programmatic API (tsx/esm/apiregister(), with amodule.registerfallback only for pre-4.0 tsx), and when registration genuinely fails the CLI reports the real underlying error (newfailedstatus) instead of misdiagnosing.turbine initalso notes the tsx requirement in Next Steps when tsx isn't installed. Verified end-to-end against the built CLI in a clean project. - The v0.19.0 unknown-operator guard no longer has a cache-hit bypass (HIGH). The guard ran only in
buildWhereClause(the cache-miss path); once an equality query warmed the SQL cache for a field set, a misspelled operator (e.g.startWith) flowed throughcollectWhereParamsunguarded and executedcol = $1with the operator object as the value — silently returning wrong rows, the exact bug 0.19.0 headlined as fixed. The guard now runs on both the build and param-collect paths (sharedassertBindableEqualityValue), and unmatched plain objects fingerprint distinctly from equality (key:obj(...)vskey:eq) so they can never share a cache entry. The same guard now also covers relation-filter sub-wheres (some/every/none). Regression tests warm the cache with equality before asserting the throw. - Nested relation
wherenow supports the full scalar filter surface (HIGH).with: { posts: { where: … } }was equality-only on the server: operator objects were bound as literal values (silent zero/wrong rows) andORproducedUnknown column "OR"— while the Studio Query tab offered the full operator palette at every nesting level. Relationwhere(hasMany, belongsTo/hasOne, and manyToMany) now supports operator objects (gt/gte/lt/lte/not/in/notIn/contains/startsWith/endsWith+mode: 'insensitive'),null(IS NULL), andOR/AND/NOTcombinators — fully parameterized against the relation alias, mirrored on the cache-hit param-collect path, with shape-awarewith-fingerprinting (an equality where and an operator where can no longer share a cached SQL string). Misspelled operators in nested wheres throw the sameValidationErroras top-level ones. - The operator guard no longer rejects class-instance equality values.
where: { data: Buffer.from(…) }on abyteacolumn threwUnknown operators "0", "1"…(and Decimal-style wrappers on numeric columns likewise). Only plain object literals — the actual typo shape — throw now; Buffers, Dates, arrays, and other class instances bind as values, consistently on both cold and cached paths. limit: 0/orderBy: {}on a to-many relation no longer corrupt the query.limit: 0took the wrapped-subquery path but skipped the LIMIT clause (truthiness vs!== undefinedmismatch), silently dropping nested relations;orderBy: {}rendered a danglingORDER BY.limit: 0now renders a realLIMIT $n(empty array result) andorderBywith no defined entries is treated as absent — on both hasMany and manyToMany, build and collect paths.- Studio's Query tab now respects
--schema./api/builderran unqualified SQL resolved via the connection'ssearch_path, so with--schema acmethe Data tab readacme.userswhile the Query tab silently readpublic.users. The builder transaction now pinsset_config('search_path', $1, true)to the configured schema. npx turbine --versionprints the version again. Via thenode_modules/.bin/turbinesymlink the package-root walk started in the consumer's tree and never found turbine-orm's package.json; the path is nowrealpathSync-resolved first.turbine init's example schema is now valid. The scaffold usedtype: 'timestamptz', whichdefineSchemarejects (the valid name istimestamp, which maps to TIMESTAMPTZ).new TurbineClient(config)without schema metadata fails fast with an actionableValidationErrorpointing atturbine generate, instead of an opaqueTypeError: Cannot read properties of undefined (reading 'tables').
Changed
- README/docs caught up with the v0.19.0 Studio. The npm README still documented the removed raw-SQL tab, SQL saved queries, the dead SELECT/WITH-only parser, and the pre-0.17.0
SET LOCALtimeout form. README, site docs (/cli,/compatibility,/transactions), CLAUDE.md, and thestudio.tsheader now describe the ORM-native reality — the security headline is "no SQL input surface at all". Site version strings (homepage hero, docs sidebar) now come from a singlesite/lib/version.tsinstead of being hardcoded per page. - Legacy raw-SQL saved queries are dropped with a console notice when Studio loads
.turbine/studio-queries.json, instead of silently (the file isn't rewritten until a new query is saved, so the entries remain recoverable). npm run buildcleansdist/first. The 0.19.0 tarball shipped ~286 kB of stale compiled artifacts from the long-deletedsrc/query.ts.- Missing public type exports added:
WhereClause,WhereOperator,WhereValue,HavingClause, andMiddlewareFnare now importable from the package root (previouslyimport type { WhereClause } from 'turbine-orm'failed with TS2305 and there was no deep-import escape hatch).
Tests
- New suite
where-guard-cache-and-relation-where(28 tests: cache-warm bypass regressions, Buffer/class-instance equality, nested-relation operator/combinator SQL + param alignment + cache-hit parity,limit: 0/orderBy: {}edge cases).relation-limit-paramand the new suite added totest:unit(the former had been missing from the list — part of why the cache bypass shipped green). Full suite: 1251 passing, 1 skipped, 0 failures (incl. live-DB integration).
0.19.0 (2026-06-09)
Studio goes ORM-native, plus two correctness fixes to the query builder. Studio no longer has a raw-SQL surface — every query is composed visually in Turbine ORM and previewed as the exact findMany call you'd write. Two query-builder bugs are fixed, one of which silently returned wrong data.
Changed
- Studio is now ORM-native (
src/cli/studio.ts,src/cli/studio-ui.html). The raw-SQL tab, its/api/queryendpoint, and SQL-kind saved queries are gone. The default (and only) authoring surface is the visual Query composer — afindManybuilder that drills into relations (with) recursively to any depth, picking fields (select/omit), filters (where), andorderBy/limitat every level, with a live TypeScript preview to copy into your code. Tabs are now Query / Data / Schema.orderBy/limitcontrols are hidden for to-one relations (they always resolve to a single row). Saved queries are builder-only; legacy raw-SQL entries are dropped on load. Polish: system font stack (no external font fetch / CSP violation),204favicon,reltuplesrow estimates clamped to ≥ 0.
Fixed
- Unknown
whereoperators now throw instead of silently returning wrong rows. A misspelled operator (e.g.startWithforstartsWith, or any unrecognized key) previously fell through to plain equality —col = $1with the operator object as the value — quietly returning zero/wrong rows with no error. The WHERE builder now throwsValidationErrorfor any plain object on a non-JSON column that matches no known filter shape, naming the offending key(s) and listing supported operators. JSON/JSONB column equality is unaffected.orderByon an unknown field andselect/omitpassed as an array (instead of the{ field: true }object) now produce the same[turbine]+ "Known fields" error format. limiton a to-one relation no longer crashes the query. Awithclause on abelongsTo/hasOnerelation that carried alimit(e.g.with: { author: { limit: 10 } }) pushed the limit value as a parameter but rendered a literalLIMIT 1, leaving an orphaned, untyped$Nthat Postgres rejected withcould not determine data type of parameter $1(and shifting every later placeholder). To-one relations now ignorelimitentirely on both the SQL-build and param-collect paths;hasMany/manyToManyare unchanged.
Tests
- New suites:
relation-limit-param(parameter/placeholder alignment, including the to-one-limit regression) and expandedoperator-validation(misspelled/unknown operators, empty filter objects, orderBy + select/omit shape). Full suite: 1223 passing, 1 skipped, 0 failures (incl. live-DB integration).
0.18.0 (2026-06-08)
Feature release: aggregate filtering, a typed raw-SQL escape hatch, many-to-many + self-relations, pgvector similarity search, RLS session context, and LISTEN/NOTIFY realtime. This is also the first npm release to carry the 0.17.0 release-readiness fixes below (0.17.0 was tagged in the changelog but never published).
Added
groupByHAVING — filter aggregate groups with ahavingclause:groupBy({ by: ['userId'], _count: true, having: { _count: { gt: 1 } } }), or filter on a column aggregate viahaving: { viewCount: { _sum: { gte: 100 } } }. Supports_count/_sum/_avg/_min/_maxwith operatorsgt/gte/lt/lte/in/notIn(a bare number is shorthand for equality). HAVING params continue the WHERE param numbering; every value is bound as$N. Unknown columns or operators throwValidationErrorbefore any SQL is built.- Typed raw SQL —
db.sql<T>(src/typed-sql.ts) — a typed escape hatch alongsidedb.raw:db.sql<{ id: number; name: string }>`SELECT id, name FROM users WHERE id = ${id}`returnsT[]when awaited,.one()returnsT | null, and.scalar<V>()returns the first column of the first row ornull. Every${value}is bound as a$Nparameter — injection payloads become data, never SQL. - Many-to-many through junction tables (
buildManyToManySubqueryinsrc/query/builder.ts) —generateauto-detects pure junction tables (exactly two single-column FKs forming a two-column PK, no payload columns) and adds amanyToManyrelation to both endpoints, loadable viafindMany({ with: { tags: true } })with nestedwhere/orderBy/limit. Composite-key junctions supported. Junctions carrying payload columns stay ordinaryhasMany(by design). For non-pure or hand-built junctions, declare them in a code-first schema viamanyToMany: [{ name, target, through, sourceKey, targetKey, references? }](applyManyToManyRelations/ManyToManyDefinsrc/schema-builder.ts). - Self-relations — a self-referencing FK (e.g.
categories.parent_id → categories.id) introspects to abelongsTo+ ahasManyon the same table, queryable as nestedparent/childrentrees at arbitrary depth. EachbuildRelationSubquery()call allocates a fresh alias, so parent and child references never collide. A lone self-FK auto-names thebelongsTofor the singular table and thehasManyfor the table. - pgvector similarity search (
VectorFilter/VectorOrderByinsrc/query/types.ts) — KNN ranking viaorderBy: { embedding: { distance: { to: number[], metric: 'cosine', direction?: 'asc' | 'desc' } } }, and distance WHERE filtering viawhere: { embedding: { distance: { to, metric: 'l2', lt: 0.3 } } }(lt/lte/gt/gte). Metricsl2/cosine/ipmap to<->/<=>/<#>. The query vector is bound as$n::vector. Non-number elements, NaN/Infinity, unknown metrics, and distance ops on non-vector columns all throwValidationError. Requires the pgvector extension + avectorcolumn. - RLS session context (
$transactionsessionContext+$withSessioninsrc/client.ts) —db.$transaction(fn, { sessionContext: { 'app.current_tenant': id } })applies each entry asSELECT set_config(name, value, true)afterBEGIN, so Postgres RLS policies usingcurrent_setting()filter rows per transaction (the GUC auto-resets on commit). Values may be string/number/boolean.db.$withSession(ctx, fn)is the single-purpose shorthand. Invalid setting names throwValidationErrorand roll the transaction back before any query runs. - LISTEN/NOTIFY realtime (
src/realtime.ts,$listen/$notifyinsrc/client.ts) —const sub = await db.$listen('channel', (payload) => { ... })subscribes on a dedicated connection (requires a persistent pool; not available over serverless HTTP drivers),await db.$notify('channel', 'msg')publishes in one round-trip (works everywhere), andawait sub.unsubscribe()issuesUNLISTEN. Channel names are validated as plain identifiers; the payload is bound as a parameter and delivered to the handler as a string.disconnect()force-releases open subscriptions.
Tests
- Full suite: 1208 passing, 1 skipped (pgvector live assertions, extension-gated — skipped, not failed, when the
vectorextension is unavailable), 0 failures. - New suites:
group-by-having,typed-sql,many-to-many,self-relation,pgvector,rls-session,realtime. Each pairs build-only SQL/parameterization assertions (no DB) withDATABASE_URL-gated integration coverage against bootstrapped, isolated tables.
Docs
- README: added typed-SQL (
db.sql<T>), groupBy HAVING, RLS session-context, and LISTEN/NOTIFY subsections under Usage Examples; a new "Vector search (pgvector)" section; many-to-many and self-relation examples in the relations content; and Many-to-many / Vector search / LISTEN/NOTIFY rows in the Comparison table.
0.17.0 (2026-06-06)
Release-readiness pass: two correctness fixes, the PII-safe error guarantee made real, honest footprint numbers, and a size gate that measures something.
Fixed
- Studio is no longer broken on plain PostgreSQL (CRITICAL). Every Studio data/query request issued
SET LOCAL statement_timeout = $1, which Postgres rejects (SETdoes not accept bind parameters) — so each query 500'd withsyntax error at or near "$1". The CockroachDB/YugabyteDB adapters had the same flaw. All now useSELECT set_config('statement_timeout', $1, true), the parameterizable transaction-local form. The unit test mocked the pool and never sent the SQL to a real server, so the bug shipped green; a new integration test (studio-timeout-integration) runs the real SQL against a live connection. UniqueConstraintErrorand other constraint errors no longer leak row values (CRITICAL). The error.messageunconditionally appended Postgres's rawdetailstring (e.g.Key (email)=(alice@x.com) already exists.), contradicting the documented "PII-safe, never the actual user data, safe to log" guarantee. The rawdetailis now only appended inverbosemode; in the defaultsafemode the message carries column/constraint/key names only. Structured fields (.columns,.column,.constraint) and.causestill expose full detail for programmatic use. Applies to E008/E009/E010/E011/E016.- belongsTo nested writes with a NOT NULL foreign key now work (HIGH).
posts.create({ data: { …, user: { connect/create/connectOrCreate } } })previously inserted the parent row before setting the FK (via a follow-up UPDATE), failing the NOT NULLuser_idconstraint on the initial INSERT. belongsTo relations are now resolved before the parent INSERT and their FK folded into it. The hasMany direction is unchanged. New integration coverage exercises all three belongsTo ops.
Changed
- Honest footprint claims. The README/package description previously headlined "~110 KB" and contrasted it with "Prisma's 1.6 MB WASM" — but that compared a minified bundle against an unpacked install (Turbine's own unpacked install is ~1.7 MB). Claims are now led by the true, durable differentiator — one dependency, no WASM engine — with correctly-labeled bundle figures (~27 KB brotli main entry, ~19 KB edge).
size-limitnow measures the real bundled import graph (@size-limit/esbuild) instead of the 2.6 KB barrel file, so the gate guards an actual number. Limits: 35 kB main, 25 kB edge.exportsmap now liststypesfirst in each conditional export so TypeScript resolves declarations correctly under all module-resolution modes.
Added
@types/pgis now a runtime dependency — Turbine's public.d.tsre-exportspgtypes, so strict consumers (skipLibCheck: false) no longer hitTS7016.
Tests
- Full suite: 1127 passing against a live database (0 failures). New regression tests: belongsTo nested-write integration (3), studio statement-timeout integration (3), PII-safe constraint-error messages (4).
test:coveragenow runs the full unit set (it had drifted to a stale ~20-file subset that leftnested-write.ts/observe.tsnear-uncovered, failing the gate at 69.6% functions). Coverage now passes: lines 78%, functions 83%, branches 86%.
Docs
SECURITY.mdsupported-versions table updated (was stuck at 0.5.x/0.6.x). README error-code list extended to E016. README full-text-search note corrected (thesearchfilter shipped in 0.15).CONTRIBUTING.mdarchitecture tree updated to thesrc/query/submodule split.CLAUDE.mdcoverage thresholds and seed-dataset sizes corrected. Benchmark results dated and version-caveated (measured on 0.7.1; core read path unchanged).
0.16.0 (2026-05-18)
Feature release: observability, nested write update/upsert, is/isNot relation filters, cursor pagination tests, Neon guide.
Added
- Event emitter —
db.$on('query', fn)anddb.$off('query', fn)fire after every query with SQL text, params, duration, model, action, and row count. Param redaction in safe mode. Listener errors never crash queries. - Observability module —
db.$observe({ connectionString })buffers per-minute aggregated metrics (count, avg, p50, p95, p99, errors) and flushes to a dedicated_turbine_metricstable in a separate database. Non-blocking (fire-and-forget), 1-connection pool, configurable retention. Auto-starts fromTURBINE_OBSERVE_URLenv var. turbine observeCLI — local read-only dashboard for viewing query metrics. Same security model as Studio (loopback binding, 192-bit token, HttpOnly cookies, CSP, X-Frame-Options: DENY). Dark-theme SVG charts, top models table, error rates, time range selector.- Nested write
update—{ posts: { update: { where: { id: 1 }, data: { title: 'new' } } } }inupdate()context. Array form supported. BelongsTo derives where from parent FK automatically. - Nested write
upsert—{ posts: { upsert: { where: { id: 1 }, create: {...}, update: {...} } } }. Checks existence, creates with FK injection or updates. is/isNotrelation filters — for to-one relations (belongsTo/hasOne):where: { author: { is: { name: 'Alice' } } }. Generates EXISTS/NOT EXISTS subqueries.- Neon guide —
/neonpage on turbineorm.dev: "Turbine + Neon in 60 Seconds" covering install, generate, Node.js + serverless connections, migrations, and why Turbine on Neon.
Fixed
- All Biome lint violations resolved —
npm run lintnow exits clean.
Tests
- 812 unit tests (up from 711), all passing.
- Added test suites:
event-emitter(10),observe(12),nested-write-update-upsert(15),is-isNot-filter(6),cursor-pagination(7),client-branches(17).
0.15.0 (2026-05-17)
Feature release: select/omit type narrowing, optimistic locking, full-text search, retry utility, security hardening.
Added
- Select/omit compile-time type narrowing —
findMany,findUnique,findFirstacceptselectandomitargs that narrow the return type at compile time viaQueryResult<T, R, W, S, O>. Preserveswithrelation additions while narrowing base entity fields. - Optimistic locking —
update({ optimisticLock: { field: 'version', expected: 3 } })auto-increments the version field and throwsOptimisticLockError(E015) on concurrent modification. - Full-text search —
TextSearchFiltertype withsearch,config, andlanguageoptions. Generatesto_tsvector @@ plainto_tsquerySQL with injection protection. - Retry utility —
withRetry(fn, opts)anddb.$retry(fn, opts)with exponential backoff + jitter. Only retries errors markedisRetryable(deadlocks, serialization failures). ExclusionConstraintError(E016) — maps pg error code 23P01 viawrapPgError().- SQL safety property tests — 22 injection payloads verified against WHERE, UPDATE SET, and CREATE SQL generation.
- Migrate from Drizzle documentation page at
/migrate-from-drizzle.
Security
- Studio: Added
Content-Security-Policyheader, rate limiting (100 req/60s per token), ESCAPE clause on ILIKE queries, 10KB query length limit. - Adapters:
statementTimeout()now returns parameterized{ sql, params }instead of interpolated strings.
Changed
- Release workflow supports
workflow_dispatchwith dry-run mode and auto-tag creation. - Git tags synced through v0.10.0.
Tests
- 711 unit tests (up from 686), all passing.
- Added test suites:
optimistic-lock,retry,text-search,sql-safety-property.
0.14.0 (2026-05-10)
Dialect-owned type metadata for future database packages.
Added
- Added optional
Dialect.typeToTypeScript()and a PostgreSQL-backed implementation so PostgreSQL introspection can route generated TypeScript types through the dialect contract without source-breaking query-only dialect implementers. - Added optional
Dialect.arrayType()and wired PostgreSQL introspection/query fallback bulk-insert casts through the dialect contract. - Added dialect-neutral
dialectType,arrayType, anddialectTypesschema metadata aliases while preserving the existingpgType,pgArrayType, andpgTypesfields for compatibility.
Tests
- Added dialect contract coverage for type mapping and metadata serialization.
0.13.3 (2026-05-10)
Final integration fixture-size assertion patch.
Fixed
- Updated the remaining stream-ordering integration assertion to respect the documented 8-user seed fixture while still verifying ordering and limit behavior.
Tests
- Targets the single remaining failure from GitHub CI run
25619796677(findManyStreamordering).
0.13.2 (2026-05-10)
Integration-suite stabilization for the v0.13 patch recovery.
Fixed
- Prevented unknown
withrelations from sharing the no-relation SQL cache key before relation validation runs. - Updated legacy integration expectations for safe not-found messages and seeded fixture sizes.
Tests
- GitHub CI run
25619387838passed build, typecheck, lint, coverage, unit, error-code, and security jobs; this patch targets the remaining integration-only failures.
0.13.1 (2026-05-10)
CI hardening patch for the v0.13 dialect-hook release.
Fixed
- Generated the Studio UI fixture before typecheck and test scripts so clean CI checkouts can resolve
studio-ui.generated.js. - Changed an internal Postgres bulk-insert dialect guard to throw
ValidationError, preserving Turbine error-code enforcement. - Corrected integration cursor pagination expectations for the seeded 8-user fixture.
- Fixed the query cache fingerprint for
isEmpty: truevsisEmpty: falsearray filters and now emitscardinality(...)checks that exclude empty arrays from non-empty queries. - Updated the CI package-size gate to parse npm output robustly and match the current published tarball size budget.
Tests
- CI failure triage covered typecheck, build, unit, coverage, error-code, and integration logs from GitHub Actions run
25619209599.
0.13.0 (2026-05-10)
DDL and migration dialect hooks for future MySQL/SQLite packages.
Added
- Extended the
Dialectcontract with schema DDL builders for column types, column definitions, table creation, primary keys, and indexes. - Added migration tracking SQL builders so dialect packages can own
_turbine_migrationsDDL and applied-migration record queries. schemaToSQL()/schemaToSQLString()now accept an optional dialect for build-time DDL generation while preserving PostgreSQL as the default.- Added MySQL-style build-only regression coverage for backtick DDL,
BIGINT AUTO_INCREMENT,DATETIME,JSON, FK indexes, and MySQL-shaped migration tracking SQL.
Changed
- The root
turbine-ormpackage remains PostgreSQL-only at runtime. This release removes another dialect-package blocker; it does not ship MySQL/SQLite drivers, introspection, or migration execution. - Exported schema DDL dialect input types and
SchemaSqlOptionsfor future dialect packages.
Tests
- 681 unit tests, 0 failures.
- Lint, typecheck, and build clean.
0.12.0 (2026-05-10)
DML dialect hooks for future MySQL/SQLite packages.
Added
- Extended the
Dialectcontract with DML SQL builders forINSERT, bulk insert, upsert, andRETURNINGclauses. - PostgreSQL's default dialect now owns the existing
RETURNING *,UNNEST(...), andON CONFLICTgeneration instead of hardcoding those primitives inQueryInterface. - Bulk insert dialect builders now return both SQL and params so non-Postgres dialects can use row-major
VALUESparams while PostgreSQL keeps column-arrayUNNESTparams. - Added MySQL-style build-only regression coverage for DML output: backtick identifiers,
?placeholders,VALUES (?, ?), (?, ?),ON DUPLICATE KEY UPDATE, and no Postgres-onlyRETURNING/UNNEST/ON CONFLICT.
Changed
- No public Postgres import or runtime behavior changes. This is still foundation work for dialect packages, not MySQL/SQLite GA support.
- Exported DML dialect input/result types for future dialect packages.
Tests
- 679 unit tests, 0 failures.
- Lint, typecheck, and build clean.
0.11.0 (2026-05-10)
Dialect interface foundation for MySQL/SQLite expansion.
Added
- Dialect contract (
src/dialect.ts) with PostgreSQL implementation and public exports for future@turbine-orm/mysql/@turbine-orm/sqlitepackages. - Query builder dialect seam: identifiers, placeholders, nested relation JSON aggregation, case-insensitive LIKE, JSON contains/path operations, and relation correlation now route through the active dialect while preserving PostgreSQL output by default.
- Internal
TurbineConfig.dialectoption so dialect packages can inject their SQL primitive implementation without forking the public client shape. - Dialect regression tests proving a MySQL-style dialect can emit backtick identifiers,
?placeholders,JSON_OBJECT,JSON_ARRAYAGG,JSON_CONTAINS, and non-ILIKEinsensitive search.
Changed
- PostgreSQL remains the default and
turbine-ormimports are unchanged. This release is a compatibility-preserving foundation step, not a MySQL/SQLite GA release. - Test scripts now include the dialect contract suite.
Tests
- 678 unit tests, 0 failures.
- Lint, typecheck, and build clean.
0.10.0 (2026-05-09)
Database adapters, composite FK support, marketing rewrite, full hardening pass.
Added
- Database adapter system (
src/adapters/): pluggableDatabaseAdapterinterface for PG-compatible databases. Shipscockroachdb(table-based locking, introspection overrides, transaction_timeout syntax),yugabytedb(distributed table locks), and no-opalloydb/timescaleadapters. New./adapterssubpath export. - Composite foreign key support:
introspect.tsnow groups FK rows by constraint name.RelationDef.foreignKey/referenceKeyacceptstring | string[]. NewbuildCorrelation()utility generates AND-joined equality clauses. Code generation emits array literals for composite FKs. - Compatibility docs page (
/compatibility): CockroachDB, YugabyteDB, AlloyDB, Timescale connection guides and feature matrices. - Multi-DB architecture plan (
docs/MULTI_DB_PLAN.md): design doc for future MySQL/SQLite/SQL Server as separate@turbine-orm/*packages. - Dynamic OG + Twitter images: Next.js
ImageResponseroutes for social sharing previews. - JSON-LD structured data on landing page (SoftwareApplication schema).
- Per-page canonical URLs and Twitter card metadata across all docs.
upsertandgroupBydocumentation in API reference.- 56 new tests for database adapters (CockroachDB, YugabyteDB, pg-compat).
- 64 new tests for coverage gaps:
client-coverage.test.ts(middleware, timeouts, external pools, SAVEPOINTs) andschema-diff.test.ts(all schemaDiff patterns). - 10 new tests for relation filter field validation.
- 14 new tests for composite FK correlation and introspection.
Fixed
- Validation gap in
buildSubWhereForRelation(medium-severity security finding): now validates column existence against target table metadata, throwsValidationErrorwith field name and available columns. - Multi-column FK introspection bug: composite FKs no longer split into separate single-column relations.
- Dead
/roadmaplink in Prisma migration page removed. - Pipeline API inconsistency across docs unified to
db.pipeline(...). - Sidebar accessibility: Escape key closes mobile menu, aria-expanded + aria-labels added.
- Removed 7 unused imports caught during lint cleanup.
Changed
- Landing page messaging rewritten: leads with "110 KB. One dep." and actual differentiators (Studio, PII-safe errors, migrations). Feature section titled "What Prisma and Drizzle don't ship."
- README top section reframed around real moat; json_agg moved to "How it works" supporting section.
- Comparison tables expanded (9 rows, install size + Studio first).
migrate.tsandstudio.tsnow accept optionalDatabaseAdapterfor pluggable locking and timeout strategies. Fully backwards-compatible.- Hero badge changed from
v0.9topre-1.0. - Cleaned 6 vestigial empty route directories from
site/app/.
Tests
- 674 unit tests, 0 failures (up from 530 in v0.9.2).
- Lint: 0 warnings, 0 errors (72 warnings suppressed with targeted
biome-ignorecomments including justification). - TypeScript: strict check clean.
0.9.2 (2026-04-14)
Docs + positioning patch. Sharpens the landing-page/README pitch around the
real differentiators (one runtime dep, Studio, code-first + DB-first in one
CLI, first-class edge runtimes) and demotes json_agg as a headline feature
since Drizzle and Prisma 7 both use it. Ships four new site doc pages, a
contributor seed script, and two new internal design docs (dialect roadmap +
full DX reference).
Docs
- New site pages:
/relations(deep-dive onwith, nested options, relation filters, payload-size warnings with concrete numbers),/transactions($transactioncallback form, isolation, timeouts, nested SAVEPOINTs, retry loops forDeadlockError/SerializationFailureError,pipeline()semantics),/serverless(Neon / Vercel Postgres / Cloudflare Hyperdrive / Supabase walkthroughs,PgCompatPoolcontract, edge memory budget table),/migrate-from-prisma(promoted fromdocs/with an 8-step checklist and schema translation example). - Landing hero + README rewritten to lead with "one runtime dependency,"
built-in Studio, code-first + DB-first in the same CLI, and edge runtime
support.
json_aggmoved to the last feature rather than the first. - Sidebar + sitemap updated to include the four new pages.
Internal docs
docs/NEXT-INTEGRATIONS.md— post-v1.0 dialect roadmap. Tier 1: CockroachDB (~1 engineer-week, PG-wire already compatible) and MySQL (4–6 weeks asturbine-orm/mysqlsubpath). Tier 2: SQLite (3 weeks; also wins internal CI speed). Tier 3 skip: SQL Server (engineering cost too high, audience already captured by TypeORM), MongoDB (philosophical mismatch). Tier 4 declined: PowDB (custom binary wire protocol, no SQL, nojson_agg, noinformation_schema— revisit when PG-wire compat layer ships).docs/USING-TURBINE-ORM.md— 19-section full DX reference covering schema / client / reads / where / with / writes / transactions / pipeline / streaming / raw SQL / errors / CLI / migrations / Studio / testing / deployment / intentional non-features. Each section includes port notes for building a similar TypeScript client against another database.
Contributor DX
scripts/seed-test-db.sh+scripts/docker-compose.yml— one-command Postgres seed for the integration-test database (throwaway Docker Compose on port 54329, benchmark seeder on first run).CONTRIBUTING.mdupdated with the new path..c8rc.json— scope comment added (whycli/,generate.ts,introspect.ts,serverless.ts,index.tsare excluded) and thresholds raised: lines 57→65, functions 64→70, statements 57→65, branches 80→82.
No runtime changes. 530/530 unit tests pass.
0.9.1 (2026-04-10)
Docs + tests patch. Restores accurate messaging around deep with-clause
type inference (shipped since 0.7.1) and locks in the end-to-end inference
path with compile-time assertions. No runtime changes — WithResult,
RelationDescriptor, and the generator's branded *Relations output were
already in place and correct; this release just stops claiming otherwise in
the README/landing page and adds a regression guard.
Tests
- End-to-end compile-time assertions for deep
withinference through real call sites (src/test/with-inference.test.ts). The existing tests only verifiedWithResultin isolation; the new sections 8 and 9 exercisefindMany/findUnique/findFirst/findUniqueOrThrowat 1/2/3 nested levels via both explicit type arguments and plain call-site literal inference (users.findMany({ with: { posts: { with: { comments: ... } } } })). If inference regresses at the user-facing signature,tsx --testnow exits non-zero because the test file fails to typecheck. 530/530 unit tests pass.
Docs
- README + site/app/page.mdx: removed the "deep
withtype inference lands in v1.0" caveat that slipped into 0.9.0 and replaced it with an accurate description of the shipped feature. Deep inference has been working end-to-end since 0.7.1 via the recursiveWithResultmapped type and the generator'sRelationDescriptor-branded*Relationsinterfaces — the 0.9.0 README was wrong, not the runtime. - CLAUDE.md "Type System" section corrected: removed the stale "Current
limitation:
withclause return types do not reflect included relations at the type level" paragraph (a pre-0.7.1 artifact) and replaced it with an accurate architecture note coveringTypedWithClause,WithResult,RelationDescriptor, andApplyCardinality.
0.9.0 (2026-04-09)
Studio Premium. Turbine now ships a premium, read-only Studio web UI — the
only Postgres ORM with a Studio your DBA will approve. Loopback-bound by
default, random per-process auth token, every query runs inside
BEGIN READ ONLY + SET LOCAL statement_timeout = '30s', and a strict
SELECT/WITH parser blocks statement stacking. No mutations, no writes, no way
around the transaction guard — the posture is unchanged from 0.8.0 and a
product review (8.1/10) found zero CRITICAL or HIGH vulnerabilities.
Added
- Premium Studio UI with Data / Schema / SQL / Builder tabs — a
single-file embedded HTML/CSS/JS bundle served by the CLI
turbine studiocommand, matching the turbineorm.dev dark theme. - Cmd+K command palette for fast navigation across tables, tabs, and saved queries.
- Saved queries — named SQL snippets persisted to
.turbine/studio-queries.jsonand surfaced in the SQL tab and command palette. - Visual query composer (Builder tab) with live TypeScript preview —
pick a table, compose
where/orderBy/with/limitvisually, and watch the matchingdb.table.findMany(...)code render in real time. - Full-text search across table rows — the Data tab now supports
substring search across every text column via a
searchquery parameter. - Sortable tables, JSON modal, toasts, keyboard shortcuts — every data table is column-sortable; JSON/JSONB cells open in a full-screen modal; toast notifications confirm saves/errors; keyboard shortcuts for tab switching, row navigation, and query execution.
- Four new backend endpoints:
/api/builder(preview SQL for a visual composer payload),/api/saved-queries(GET/POST/DELETE), and asearchquery parameter on/api/tables/:name. - CLI flag smoke test (
src/test/cli-flags.test.ts) — locks in everyturbine studioflag (--port,--host,--no-open) against the argument parser.
Changed
- Migration advisory lock ID is now derived from the database name via
FNV-1a — fixes cluster-wide contention when two databases on the same
Postgres cluster both run
turbine migrate upconcurrently. The previous implementation used a static lock ID, so a migration in database A would block a migration in database B. Lock ID is now a stable per-database 32-bit FNV-1a hash ofcurrent_database()(top bit cleared for positiveint4, matching Postgres advisory-lock semantics). - README Studio section rewritten to headline read-only as a design feature, not a limitation. The old "Turbine Studio is planned but not yet available" bullet under Limitations has been removed.
Fixed
- Two
anyleaks insrc/schema-builder.tspublic signatures — the column definition builder now exposes precise generic types end-to-end. - Search endpoint parameter-index bug in
/api/tables/:name— the search clause was using a stale$Nindex when combined with pagination params. - Biome template-literal lint errors in
src/query.tssurfaced by the new biome rules shipped in 2.4.10.
Security
- Studio posture unchanged from v0.8.0. Loopback default (
127.0.0.1, loud warning on non-loopback binds), 24-byte random hex token generated per process,SameSite=StrictHttpOnlyauth cookies, every query wrapped inBEGIN READ ONLY+SET LOCAL statement_timeout = '30s', SELECT/WITH-only parser that strips comments and rejects non-trailing semicolons (blocks statement stacking), and security headers (X-Content-Type-Options,X-Frame-Options: DENY,Referrer-Policy: no-referrer). Product review scored the surface 8.1/10 and found zero CRITICAL or HIGH vulnerabilities.
0.8.0 (2026-04-09)
Added
- Real Postgres extended-query pipeline protocol.
pipeline()now uses the wire-level pipeline protocol (parse/bind/describe/execute/sync in a single TCP flush) on connections that support it, viapipeline-submittable.ts. Falls back to sequential execution for HTTP drivers, mocks, and other non-TCP connections. Verified 2.58× speedup over sequential on Neon. - SQL template caching with shape-keyed fingerprinting. Queries with the
same WHERE/WITH/ORDER BY structure (same keys and operators, different values)
reuse cached SQL text. FNV-1a 64-bit hashing generates deterministic prepared
statement names. LRU cache at 1,000 entries. Cache hit/miss stats via
queryInterface.cacheStats(). - Prepared statement support. When
preparedStatements: true(default for owned pools), queries use pg's{ name, text, values }object form. Postgres caches the execution plan after the first call. Disable per-client or viaTURBINE_DISABLE_PREPARED=1env var. Automatically disabled for external pools (serverless drivers). - Streaming speculative first fetch.
findManyStreamnow issues aLIMIT batchSize+1first query. If the result fits in one batch, rows are yielded directly withoutDECLARE CURSORoverhead. Only large result sets escalate to server-side cursors. - Default
batchSizeincreased from 100 to 1000. Reduces FETCH round-trips from 500 to 50 for a 50K-row drain, closing the streaming performance gap. parseNestedRowshort-circuit. Empty hasMany ('[]'), null belongsTo ('null',null), and pre-parsed arrays skipJSON.parseentirely.PipelineError(TURBINE_E014) with per-query result status array (.results),.failedIndex, and.failedTagfor diagnosing partial pipeline failures in non-transactional mode.PipelineOptionstype withtransactional(default true) andtimeoutfields. Non-transactional mode uses per-query Sync for error isolation.pipelineSupported(pool)public probe — check at runtime whether a pool supports the real pipeline protocol.TurbineConfigflags:preparedStatements(boolean),sqlCache(boolean).- New benchmark scenarios: pipeline (5-query dashboard batch) and hot findUnique (500× same shape, rotating IDs).
- 88 new unit tests (pipeline-submittable: 12, sql-cache: 54, stream-and-parse: 19, pipeline integration: 3). 486 tests total.
Changed
- Benchmark results updated. With SQL caching, prepared statements, and
streaming optimizations, Turbine now wins or ties 6/8 scenarios on Neon.
L2 nested reads: 1.59× faster than Drizzle. Streaming 50K rows: at parity
with Prisma (~3.2 s), 1.49× faster than Drizzle. See
benchmarks/RESULTS.md.
Docs
- Benchmarks reconciled against a real pooled database. The
README benchmark table and the "Turbine is fastest in every
scenario" framing dated from a local Postgres run; a full three-way
head-to-head against Prisma 7.6 (with
relationJoins) and Drizzle 0.45 on Neon (US-East, pooled, PostgreSQL 17.8) shows all three ORMs land within ~5 ms of each other on every read scenario because network latency dominates. Turbine'sfindManyStreamis actually ~1.5× slower than keyset pagination for drain-all workloads because ofBEGIN/DECLARE/CLOSE/COMMIToverhead. README, strategic plan, and thestreaming-csvexample have all been rewritten to pitch Turbine on architectural merits (one dep, edge import swap, typed errors,withinference) rather than speed. Full writeup:benchmarks/RESULTS.md. - Added
benchmarks/seed-neon.tsso the benchmark harness is fully reproducible against any Postgres endpoint (Neon, Vercel, local). benchmarks/bench.tsgained two new scenarios: streaming (drain 50K rows three ways) and atomic counter (view_count + 1).
0.7.1 (2026-04-07)
This release is a hardening + DX pass on top of 0.7.0. CLI now reliably loads
TypeScript schema files, error messages are safer by default, two new typed
errors cover transient Postgres failures, composite primary keys are first
class, and the with clause is fully type-inferred at any nesting depth.
Added
DeadlockError(TURBINE_E012) andSerializationFailureError(TURBINE_E013) — both exposeisRetryable: truefor safe automatic retry on Postgres40P01and40001sqlstates. Surfaced throughwrapPgError()at every query chokepoint.- Composite primary keys:
defineSchema()now accepts a table-levelprimaryKey: ['col1', 'col2']field. The DDL generator emits aCONSTRAINT ... PRIMARY KEY (col1, col2)and the typedfindUniqueaccepts the composite key as an object. - Deep
with-clause type inference: theWithResultmapped type now recurses through arbitrarily nestedwithclauses, sodb.users.findMany({ with: { posts: { with: { comments: true } } } })narrows the return type toUser & { posts: (Post & { comments: Comment[] })[] }without manual assertions. - Typed
TransactionClienttable accessors: thetxargument inside$transaction(async (tx) => ...)now exposes the sametx.users/tx.poststyped accessors as the top-level client. Generated clients emit a typedTransactionClientsubclass alongside the mainTurbineClientsubclass. - Atomic update operator types in generated
*Updateinterfaces: numeric fields now allow{ increment | decrement | multiply | divide | set: number }at the type level, matching the runtime behaviour shipped in 0.6.2. findManyunlimited-query warning:findManycalls without alimit(and nodefaultLimitconfigured) now emit a one-time warning per table. Disable withwarnOnUnlimited: falseinTurbineConfig.- Strict operator validation: JSONB-only operators (
hasKey,path) and array-only operators (has,hasEvery,hasSome) now throwValidationErrorwhen applied to columns of the wrong Postgres type, instead of silently generating broken SQL. $transactiontimeout cleanup: when a transaction exceeds itstimeout, Turbine now destroys the underlying connection rather than returning it to the pool, freeing the slot immediately.- WHERE Operator Reference in README — every operator (equality, sets, comparison, string, relation, array, combinators) with a one-line description and example.
- Prisma migration guide at
docs/migrate-from-prisma.md— API mapping table, side-by-sidefindManyexample, and notes on the differences (include->with, code-first schema, typed errors, edge support). - Four serverless example apps under
examples/:neon-edge(Neon on Vercel Edge),cloudflare-worker(Hyperdrive +pg),vercel-postgres(@vercel/postgreson the Next.js app router), andsupabase(directpgto Supabase). Each is self-contained withschema.ts, entrypoint,package.json, and a setup README.
Fixed
- CLI
turbine pushfailed on.tsschema files: the loader now registerstsx/tsmas needed before importing the schema module, sonpx turbine push --schema ./schema.tsworks without a manual loader flag. - README contradicted runtime on atomic update operators: the "no incremental updates" bullet under Limitations falsely claimed
{ count: { increment: 1 } }was unsupported. Atomic operators have shipped since 0.6.2 — bullet removed and a worked example added under Usage Examples. NotFoundErrorno longer leakswherevalues into error messages by default. Messages are now[turbine] findUniqueOrThrow on "users" found no recordwith the originalwherestill attached on the error object for programmatic inspection. Opt back into the verbose form witherrorMessages: 'verbose'inTurbineConfigif you need the previous behaviour.
Docs
- README: corrected stale
70KBpackage size in the Next.js example to~110KB(matches the v0.6.3 fix). - Next.js example rewritten to use the generated typed accessor (
db.users.findMany) instead of the untypeddb.table<User>('users')lookup.
0.7.0 (2026-04-07)
This release is a quality + reach overhaul driven by a full product review and gold-standard OS audit. Biggest new capability: Turbine now runs on the edge via any pg-compatible driver (Neon, Vercel Postgres, Cloudflare Hyperdrive, etc.) without bundling a single extra dependency.
Added
- Serverless / edge support (
turbine-orm/serverless): newturbineHttp(pool, schema)factory andPgCompatPool/PgCompatPoolClient/PgCompatQueryResultinterfaces. Plug in@neondatabase/serverless,@vercel/postgres, or any other pg-API compatible pool and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, and similar environments. - New
TurbineConfig.pooloption — pass an external pg-compatible pool and Turbine will route all queries through it instead of creating its ownpg.Pool.disconnect()is a no-op for externally-owned pools. - New public subpath export:
turbine-orm/serverless(both ESM and CJS). with-clause type inference: optional second type parameterQueryInterface<T, R>surfaces included relations at the type level. Generated clients now emit{Entity}Relationsinterfaces, sodb.users.findMany({ with: { posts: true } })narrows the return type to includeposts: Post[].- New exports:
TypedWithClause,WithResult,PgCompatPool,PgCompatPoolClient,PgCompatQueryResult,turbineHttp,TurbineHttpOptions.
Changed
serverless.tsrewritten: the old custom HTTP-proxy protocol (which required a nonexistent Turbine proxy server) is gone. It is replaced with a thin, driver-agnostic factory that binds any pg-compatible pool to a schema. No new runtime dependencies.TurbineClient.statsnow returns zeros for pools that don't expose connection counts (HTTP drivers), instead ofundefined.pg.types.setTypeParser(20, ...)registration is now skipped when Turbine is given an external pool — prevents Turbine from mutating global state owned by the external driver.
Tests
- 308 unit tests passing (up from 254).
- New test file:
src/test/serverless.test.ts— 9 tests covering external pool integration, transaction routing, lifecycle ownership, and error propagation via a mockPgCompatPool. - New test file:
src/test/pipeline.test.ts— 5 tests coveringexecutePipeline: BEGIN/COMMIT wrapping, transform ordering, ROLLBACK on failure, parameter passing, and empty-input short-circuit.
Docs
- README: new "Serverless / Edge" section with Neon, Supabase, and Vercel Postgres examples.
src/serverless.ts: extensive JSDoc covering supported drivers, limitations over HTTP (streaming cursors, LISTEN/NOTIFY), and full usage examples for Neon on Vercel Edge and Cloudflare Workers.
0.6.3 (2026-04-07)
Security
- SSL/TLS support: Added
ssloption toTurbineConfigfor secure connections to cloud providers (RDS, Supabase, Neon, etc.) - Aggregate column aliases now quoted via
quoteIdent()inbuildGroupBy()andbuildAggregate()— prevents potential SQL syntax injection - Column validation added to
buildAggregate()matchingbuildGroupBy()— rejects unknown field names findManyStream()batch size coerced to safe positive integer
Changed
- README repositioned: Tagline and "Why Turbine?" section now lead with streaming, typed errors, pipeline, and middleware as primary differentiators. json_agg presented as shared approach rather than unique feature
- Removed stale "no WASM" claims about Prisma (Prisma 7 dropped Rust engine in Jan 2026)
- Package size claim corrected from "70KB" to "~110KB" (actual npm pack size)
pg.types.setTypeParser(20, ...)moved from module scope intoTurbineClientconstructor with once-guard — fixes incorrectsideEffects: falsein package.json
Tests
- 254 unit tests passing
- Shared test helpers extracted to
src/test/helpers.ts
0.6.2 (2026-04-06)
Added
- Typed constraint-violation errors:
UniqueConstraintError,ForeignKeyError,NotNullViolationError,CheckConstraintError— pg sqlstate codes (23505/23503/23502/23514) are translated automatically at every query chokepoint (CRUD, raw, transactions, pipelines, streaming) wrapPgError()helper translates pg driver errors into typed Turbine errors withcausechaining preserved for stack traces- Atomic update operators:
update/updateManynow support Prisma-style{ increment, decrement, multiply, divide, set }operators for race-free counter updatesawait db.posts.update({ where: { id: 5 }, data: { viewCount: { increment: 1 } } }) - New exported types
UpdateInput<T>andUpdateOperatorInput<V>with conditionalV extends numbernarrowing —incrementon a non-numeric column is a compile-time error - Operator detection uses strict single-key rule to avoid collisions with JSON column payloads
Changed
NotFoundErrornow carries query context:findFirstOrThrow,findUniqueOrThrow,update,delete,upsert, andcreatenow throwNotFoundErrorwith{table, where, operation}fields and Prisma-style messages:[turbine] findUniqueOrThrow on "users" found no record matching where: {"id":1}NotFoundErrorconstructor accepts either a string (back-compat) or{table?, where?, operation?, cause?, message?}options object- Removed dead
havingfield fromGroupByArgsinterface (was silently ignored)
Tests
- 514 integration tests + 239 unit tests, all passing
- 19 new tests for atomic update operators including 10-way concurrent atomicity proof
- 10 new NotFoundError unit tests covering back-compat, format, fields, override, cause chains
- New test file:
src/test/update-operators.test.ts
0.6.1 (2026-04-06)
Added
- Streaming cursors:
findManyStream()returnsAsyncGenerator<T>backed by PostgreSQLDECLARE CURSOR— constant memory for large result sets - Configurable
batchSize(default: 100) for internal FETCH batching - Streaming supports all
findManyoptions:where,orderBy,limit,with(nested relations) - Early termination via
breakautomatically cleans up cursor and connection - Next.js example app (
examples/nextjs/) — server-rendered demo with nested relations, code blocks, and streaming showcase - Auto-diff migrations:
npx turbine migrate create <name> --autogenerates UP + DOWN SQL from schema diff - Schema diff now detects DEFAULT value changes (SET DEFAULT / DROP DEFAULT)
- Schema diff now detects UNIQUE constraint changes (ADD / DROP CONSTRAINT)
- Schema diff generates reverse SQL for all operations (for DOWN migrations)
- Type changes now include USING clause for safe casting
- Fresh benchmark suite against Prisma 7.6 and Drizzle 0.45 (
benchmarks/) - README benchmarks updated with current numbers — Turbine 1.4–1.9x faster across all scenarios
- Reproducible benchmark harness:
cd benchmarks && npm install && npx prisma generate && npx tsx bench.ts
Changed
schemaDiff()now returnsreverseStatementsalongsidestatementscreateMigration()accepts optionalautoContentfor pre-populated UP/DOWN- README benchmark section replaced with fresh Prisma 7 / Drizzle v2 results (was Prisma 5.x / Drizzle v1)
- Comparison section updated to reflect all three ORMs now using single-query approaches
0.6.0 (2026-04-05)
Security
- CRITICAL: Fixed shell injection in seed command — replaced
execSyncstring interpolation withexecFileSyncarray args - Migration tracking table name now quoted via
quoteIdent()at all SQL interpolation sites - DEFAULT value validation rejects strings containing semicolons and SQL statement keywords
- Connection string redaction (
redactUrl()) applied to all CLI error output paths
Added
- Column validation in
orderBy— throwsValidationErrorfor unknown column names - Column validation in
groupBy— throwsValidationErrorfor unknown column names - Runtime type validation in
defineSchema()— throws for invalid column types not in TYPE_MAP - JSON parse warning in nested relation parsing — warns instead of silently falling back
- Error handling section in README with typed error examples and error code reference
- 20 new unit tests (validation, DEFAULT edge cases, schema type checks) — 171 total
Changed
- README messaging: "Prisma-inspired API" replaces "Prisma-compatible API"
- README tagline leads with Postgres-native positioning instead of speed claims
- Benchmark section now notes results are against Prisma 5.x / Drizzle v1 with context about modern versions
- Comparison table updated: Prisma now shown as "1 query (LATERAL JOIN + json_agg, since v5.8)"
- package.json description updated to factual positioning: "Postgres-native TypeScript ORM"
noExplicitAnylint rule changed from "off" to "warn" in biome.json- "Why Turbine?" section rewritten to lead with architectural simplicity, not speed claims
Fixed
- Stale "Prisma sends 3 separate queries" claim in README (Prisma 7+ uses single query)
- Stale "2-3x faster than Prisma" claim in package.json description
- Stale benchmark context in query.ts header comment
0.5.0 (2026-03-28)
Security
- DDL identifier quoting via
quoteIdent()on all CREATE/ALTER/DROP statements - DEFAULT value validation against strict allowlist
- Path traversal protection on
--outflag - Shell escaping for seed file paths
json_build_objectkey escaping
Added
- Full migration engine:
turbine migrate create/up/down/status- Advisory locking for concurrent migration safety
- Checksum validation for drift detection
- Per-migration transactions with rollback on failure
- LRU cache (1,000 entries) for SQL query templates
- Case-insensitive LIKE support (
mode: 'insensitive'on string filters) - Per-query timeout option
- Configurable
defaultLimitandwarnOnUnlimitedfor findMany - CJS output alongside ESM (dual publishing)
- Pre-computed column type lookups (O(1) instead of O(n))
- 89 unit tests (schema-builder + migrations)
Changed
- Node.js requirement lowered from >= 22 to >= 18
- Test runner changed from
node --experimental-strip-typestotsx numerictype now consistently maps tostring(removed runtime parser)- Middleware JSDoc clarifies that args are captured before middleware runs
Fixed
- Test suite was completely broken (import resolution mismatch)
numeric/biginttype mismatch between generated types and runtimeprocess.exit(0)in integration tests killed parallel test runner- CLI
showVersion()was hardcoded to v0.3.0 - Test files leaked into npm package via
dist/cjs/test/
0.4.0 (2026-03-26)
Added
findFirst,findFirstOrThrow,findUniqueOrThrowquery methods- Middleware system (
db.$use()) for query interception
0.3.0 (2026-03-25)
Added
- Initial public release
- Schema introspection from
information_schema+pg_catalog - Type generation (entity interfaces, create/update types, relation types)
- Query builder with
json_aggnested relations (L2-L4 depth) - 18+ WHERE operators (gt, gte, lt, lte, in, notIn, contains, startsWith, endsWith, OR, AND, NOT)
- JSONB operators (contains, equals, path, hasKey)
- Array operators (has, hasEvery, hasSome, isEmpty)
- Transactions with nested SAVEPOINTs and isolation levels
- Pipeline batching
- Raw SQL tagged templates
- Schema builder (
defineSchema()) with TypeScript objects - CLI: init, generate, push, migrate, seed, status
Rendered from the repository's CHANGELOG.md at build time (currently through v0.39.0).