turbine-orm 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -5
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/client.d.ts +106 -2
- package/dist/cjs/client.js +111 -5
- package/dist/cjs/dialect.d.ts +33 -0
- package/dist/cjs/dialect.js +14 -0
- package/dist/cjs/engine-config.d.ts +49 -0
- package/dist/cjs/engine-config.js +19 -0
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/mssql.d.ts +8 -3
- package/dist/cjs/mssql.js +22 -3
- package/dist/cjs/mysql.d.ts +7 -3
- package/dist/cjs/mysql.js +20 -3
- package/dist/cjs/nested-write.d.ts +31 -0
- package/dist/cjs/nested-write.js +80 -2
- package/dist/cjs/powdb-introspect.d.ts +10 -1
- package/dist/cjs/powdb-introspect.js +10 -1
- package/dist/cjs/powdb.d.ts +116 -6
- package/dist/cjs/powdb.js +169 -10
- package/dist/cjs/powql.d.ts +161 -1
- package/dist/cjs/powql.js +299 -19
- package/dist/cjs/prisma-compat.d.ts +54 -8
- package/dist/cjs/prisma-compat.js +136 -20
- package/dist/cjs/query/batched-loader.d.ts +7 -0
- package/dist/cjs/query/batched-loader.js +97 -15
- package/dist/cjs/query/builder.d.ts +131 -5
- package/dist/cjs/query/builder.js +223 -19
- package/dist/cjs/query/compound-unique.js +0 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/index.js +2 -1
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/writes.js +115 -7
- package/dist/cjs/sqlite.d.ts +10 -4
- package/dist/cjs/sqlite.js +18 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/client.d.ts +106 -2
- package/dist/client.js +111 -5
- package/dist/dialect.d.ts +33 -0
- package/dist/dialect.js +14 -0
- package/dist/engine-config.d.ts +49 -0
- package/dist/engine-config.js +18 -0
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.d.ts +8 -3
- package/dist/mssql.js +22 -3
- package/dist/mysql.d.ts +7 -3
- package/dist/mysql.js +20 -3
- package/dist/nested-write.d.ts +31 -0
- package/dist/nested-write.js +79 -2
- package/dist/powdb-introspect.d.ts +10 -1
- package/dist/powdb-introspect.js +10 -1
- package/dist/powdb.d.ts +116 -6
- package/dist/powdb.js +167 -9
- package/dist/powql.d.ts +161 -1
- package/dist/powql.js +299 -19
- package/dist/prisma-compat.d.ts +54 -8
- package/dist/prisma-compat.js +136 -20
- package/dist/query/batched-loader.d.ts +7 -0
- package/dist/query/batched-loader.js +98 -16
- package/dist/query/builder.d.ts +131 -5
- package/dist/query/builder.js +222 -18
- package/dist/query/compound-unique.js +0 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/writes.js +116 -8
- package/dist/sqlite.d.ts +10 -4
- package/dist/sqlite.js +19 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ First, what is **not** a reason. Resolving a nested `with` clause in one stateme
|
|
|
17
17
|
The reason to reach for Turbine is that every layer between you and a production database is built on one assumption: **the rows are real**. That plays out in five concrete places.
|
|
18
18
|
|
|
19
19
|
1. **The database UI is read-only, and writes are a per-launch decision.** `npx turbine studio` binds loopback, authenticates with a 192-bit per-process token, and runs every read inside `BEGIN READ ONLY`. In the default mode the write endpoints do not exist in the router at all (they 404), so there is nothing to bypass. `--write` opts a single launch in to edits, each addressed by its full primary key rather than a predicate, compiled by the same validated builder your app uses. There is no raw-SQL surface at all since v0.19.
|
|
20
|
-
2. **PII is a schema contract, enforced in the SQL.** Tag a column `pii: true` and it is excluded from every default projection
|
|
20
|
+
2. **PII is a schema contract, enforced in the SQL.** Tag a column `pii: true` and it is excluded from every default projection: top-level rows, `with` subqueries, batched loaders, write returns, and the Studio UI. On the SQL engines the exclusion is in the emitted statement, so the value never leaves the database. (PowDB is the one exception, and it is a weaker guarantee: its `returning` keyword takes no column list, so a write's PII is stripped in the client after crossing the wire. Its read projections are still column-explicit.) A tagged column is also **refused** as a `groupBy` key and as a `_min` / `_max` target, because both hand back a stored cell. `includePii: true` unlocks it explicitly per read. A schema with no tagged column emits byte-identical SQL.
|
|
21
21
|
3. **Errors carry keys, never values.** A `NotFoundError` says `where: { id, email }`. A `UniqueConstraintError` names the column that conflicted. Neither prints the user's data, so the error is safe to send straight to Sentry with no scrubbing rule in front of it. The full `where` object stays available as `err.where` in code.
|
|
22
22
|
4. **Data-destroying statements need consent.** `migrate up`, `migrate down` and `push` scan for `DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, unqualified `DELETE` / `UPDATE`, and `ALTER COLUMN … TYPE`, print an itemized report, and refuse to run. Interactively you type `destroy my data` and then `yes`; in CI you pass `--allow-destructive`. A refused batch applies nothing.
|
|
23
23
|
5. **The review a DBA would have given you, offline.** `npx turbine doctor` derives every column set the ORM's relation subqueries probe and reports the ones with no covering index, with a cost tier per finding. `--fix` writes the migration. No cloud service, no telemetry, no account: it reads your introspected schema.
|
|
@@ -238,6 +238,9 @@ const newUser = await db.users.create({
|
|
|
238
238
|
|
|
239
239
|
### createMany (batch insert with UNNEST)
|
|
240
240
|
|
|
241
|
+
One exception to the UNNEST shape: rows of pure defaults (`data: [{}, {}]`) name no column to unnest, so Postgres emits `INSERT INTO t SELECT FROM generate_series(1, N)` instead. `create({ data: {} })` inserts a single defaults row on every engine; the multi-row form is Postgres/MySQL only (SQLite and SQL Server throw `UnsupportedFeatureError`, as does MySQL when `skipDuplicates` is combined with it).
|
|
242
|
+
|
|
243
|
+
|
|
241
244
|
```typescript
|
|
242
245
|
const users = await db.users.createMany({
|
|
243
246
|
data: [
|
|
@@ -249,6 +252,8 @@ const users = await db.users.createMany({
|
|
|
249
252
|
// Single INSERT with UNNEST -- not 3 separate inserts
|
|
250
253
|
```
|
|
251
254
|
|
|
255
|
+
Because it is one statement, the column list comes from the **first** row, so every row must name the same fields. A field the first row names and a later row omits would be bound as `NULL` over that column's default, and a field only a later row names would be dropped, so a ragged call throws `ValidationError` (`TURBINE_E003`) naming the row index and the differing columns instead. This holds on every engine including PowDB. A field set to `undefined` counts as omitted, exactly as in `create`; split the call into one `createMany` per field set.
|
|
256
|
+
|
|
252
257
|
### update / delete
|
|
253
258
|
|
|
254
259
|
```typescript
|
|
@@ -300,7 +305,11 @@ await db.$transaction(async (tx) => {
|
|
|
300
305
|
// Fully typed -- tx.users and tx.posts have the same API as db.users and db.posts
|
|
301
306
|
```
|
|
302
307
|
|
|
303
|
-
`tx` is a `TransactionClient`, which is deliberately a **smaller** object than `db`.
|
|
308
|
+
`tx` is a `TransactionClient`, which is deliberately a **smaller** object than `db`. Its application surface is table accessors (`tx.users`, or `tx.table('users')`), `tx.$transaction(fn)` for SAVEPOINT-nested blocks, `tx.raw` for tagged-template raw SQL, and `tx.schema`. It does **not** have `tx.sql`, `tx.pipeline`, `tx.$use`, `tx.$listen` / `tx.$notify`, `tx.$observe`, `tx.$on` / `tx.$off`, `tx.$retry`, `tx.$primary`, `tx.$withSession`, `tx.pipelineSupported()`, `tx.transaction()`, `tx.connect()`, `tx.pool`, `tx.stats`, `tx.disconnect()` or `tx.end()`. `TransactionClient` declares `table()`, `$transaction()`, `raw`, `schema` and one `@internal` member (see below), plus the generated per-table accessors, and has no index signature, so reaching for any of the others is a **compile error** (`TS2339`), caught in your editor rather than in production. Reach for `tx.raw`: the tagged template turns every `${value}` into a placeholder, so a value cannot be concatenated into the SQL text by accident.
|
|
309
|
+
|
|
310
|
+
`tx.rawQuery(text, params)` is the one `@internal` member. It is the seam `turbine-orm/prisma-compat` detects by shape so it can run compat raw SQL on the transaction's own connection, and it takes the SQL as a plain string, which puts the escaping discipline back on the caller, exactly what `tx.raw` exists to remove. It still exists at runtime and still typechecks, so nothing calling it breaks, it is simply no longer part of the documented API.
|
|
311
|
+
|
|
312
|
+
Note that transaction-scoped raw SQL (`tx.raw`, and `tx.rawQuery`) bypasses the instrumentation layer: it emits no `$on('query')` event, runs no middleware, and records no timing, so it is invisible to query listeners and to `$observe` metrics. Table-scoped queries inside the transaction (`tx.users.findMany(...)`) are instrumented as usual. See [Transactions & Pipelines](https://turbineorm.dev/transactions#transactionclient-reference) for the full surface and the reason each omission exists.
|
|
304
313
|
|
|
305
314
|
### Pipeline (batch queries in one round-trip)
|
|
306
315
|
|
|
@@ -409,13 +418,21 @@ const db = turbine({
|
|
|
409
418
|
// subqueries, ~39% fewer wire bytes on wide relations, byte-identical output.
|
|
410
419
|
// Default 'object'.
|
|
411
420
|
jsonEncoding: 'object',
|
|
412
|
-
//
|
|
413
|
-
// convention, so results don't shift with the server's local zone.
|
|
421
|
+
// Treat zone-less `date` / `timestamp` columns as UTC, the Prisma/Rails/
|
|
422
|
+
// Django convention, so results don't shift with the server's local zone.
|
|
423
|
+
// Since v0.52 it reaches the WRITE side too (the `Date` values that
|
|
424
|
+
// create/update/upsert and where clauses bind on Postgres); before v0.52 it
|
|
425
|
+
// reached reads only, so a client that set `false` was still binding UTC.
|
|
426
|
+
// PER PROCESS, NOT PER CLIENT: see the note below.
|
|
414
427
|
// Default true; set false for the legacy local-time interpretation.
|
|
415
428
|
utcTimestamps: true,
|
|
416
429
|
});
|
|
417
430
|
```
|
|
418
431
|
|
|
432
|
+
> **`utcTimestamps` is a process-wide decision, not a per-client one.** The two halves settle differently. The WRITE half is per client: a `Date` bound to a zone-less `date` / `timestamp` column is rewritten to a UTC literal unless that client opted out. The READ half is a pg type parser on OID 1114, and `pg.types.setTypeParser` installs one parser per OID for the whole pg module, shared by every pool, every raw query, and any other library on the same `pg`. The first Turbine-owned client in the process settles it for all the rest. Constructing a second Turbine-owned client with the **opposite** value therefore throws `ValidationError` (`TURBINE_E003`) at construction, rather than handing back a client that writes UTC and reads local (or the reverse) and so does not round-trip its own values. The message names both values and the two ways out: give every client in the process the same `utcTimestamps`, or run the odd one out in its own process. Clients built on an external pool (`pool: ...`, `turbineHttp()`) never register a parser, settle nothing, and are exempt from the check, they inherit whatever parser configuration the caller set up.
|
|
433
|
+
|
|
434
|
+
> **Upgrading with `utcTimestamps: false`.** If you already set `false`, this release changes the **stored text** of your writes: the write path now honors the flag where it previously ignored it, so zone-less columns receive local-calendar literals instead of UTC ones. Rows written before the upgrade and rows written after carry two conventions in the same column until you backfill.
|
|
435
|
+
|
|
419
436
|
Run `npx turbine doctor` to catch relations whose child-side FK lacks a covering index, the correlated-subquery strategy probes the child once per parent row, so a missing FK index costs a full scan per parent.
|
|
420
437
|
|
|
421
438
|
### Pool and statement configuration
|
|
@@ -842,6 +859,17 @@ db.$on('query', (e) => {
|
|
|
842
859
|
});
|
|
843
860
|
```
|
|
844
861
|
|
|
862
|
+
**`event.params` is redacted by default**: every value arrives as `'[REDACTED]'` so a query log can't carry user data into a log sink. Opt in with `logQueryParams: true` on the client (`errorMessages: 'verbose'` also reveals them, and keeps doing so). Nothing else about the event changes.
|
|
863
|
+
|
|
864
|
+
```typescript
|
|
865
|
+
const db = turbine({
|
|
866
|
+
connectionString: process.env.DATABASE_URL,
|
|
867
|
+
logQueryParams: process.env.NODE_ENV !== 'production',
|
|
868
|
+
});
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
See [Observability](https://turbineorm.dev/observability#seeing-the-parameter-values-logqueryparams) for the full behavior.
|
|
872
|
+
|
|
845
873
|
View the collected metrics in a local dashboard:
|
|
846
874
|
|
|
847
875
|
```bash
|
|
@@ -1064,7 +1092,7 @@ Turbine maps Postgres types to TypeScript:
|
|
|
1064
1092
|
| `int8` / `bigint` | `number` | Values > `Number.MAX_SAFE_INTEGER` (2^53 - 1) are returned as `string` at runtime to avoid precision loss. This affects < 0.01% of use cases (auto-increment IDs, counts, etc. are all safe). |
|
|
1065
1093
|
| `numeric`, `money` | `string` | Arbitrary precision, kept as string to avoid JS float issues |
|
|
1066
1094
|
| `text`, `varchar`, `uuid`, `citext` | `string` | |
|
|
1067
|
-
| `timestamptz`, `timestamp`, `date` | `Date` | `timestamp` (without time zone) is parsed as UTC by default (Prisma/Rails/Django convention), so the same row yields the same instant in every region. Opt out with `utcTimestamps: false`. |
|
|
1095
|
+
| `timestamptz`, `timestamp`, `date` | `Date` | `timestamp` (without time zone) is parsed as UTC by default (Prisma/Rails/Django convention), so the same row yields the same instant in every region. Opt out with `utcTimestamps: false`. Since v0.52 the flag also reaches the WRITE side on Postgres: it governs the `Date` values that `create` / `update` / `upsert` and `where` clauses bind to zone-less `date` / `timestamp` columns. Before v0.52 it reached the read path only, so a client that had set `false` was still binding UTC, and those statements (and the text they store) change on upgrade. The two halves settle at different scopes: the write half is per client, the read half is a process-global pg type parser, so two Turbine-owned clients in one process must agree or construction throws `ValidationError`. See [Relation loading and wire encoding](#relation-loading-and-wire-encoding). |
|
|
1068
1096
|
| `boolean` | `boolean` | |
|
|
1069
1097
|
| `json`, `jsonb` | `unknown` | |
|
|
1070
1098
|
| `bytea` | `Buffer` | |
|