turbine-orm 0.65.0 → 0.66.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.
Files changed (142) hide show
  1. package/README.md +34 -32
  2. package/dist/adapters/cockroachdb.js +21 -3
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/yugabytedb.js +20 -3
  5. package/dist/cjs/adapters/cockroachdb.js +21 -3
  6. package/dist/cjs/adapters/index.d.ts +15 -0
  7. package/dist/cjs/adapters/yugabytedb.js +20 -3
  8. package/dist/cjs/cli/destructive.d.ts +18 -4
  9. package/dist/cjs/cli/destructive.js +230 -122
  10. package/dist/cjs/cli/index.d.ts +21 -4
  11. package/dist/cjs/cli/index.js +119 -22
  12. package/dist/cjs/cli/mcp.d.ts +28 -8
  13. package/dist/cjs/cli/mcp.js +170 -127
  14. package/dist/cjs/cli/migrate.d.ts +134 -13
  15. package/dist/cjs/cli/migrate.js +349 -241
  16. package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
  17. package/dist/cjs/cli/pii-predicate-guard.js +390 -0
  18. package/dist/cjs/cli/prisma-resolve.js +75 -4
  19. package/dist/cjs/cli/prisma-schema.d.ts +17 -1
  20. package/dist/cjs/cli/prisma-schema.js +83 -17
  21. package/dist/cjs/cli/sql-statements.d.ts +125 -0
  22. package/dist/cjs/cli/sql-statements.js +378 -0
  23. package/dist/cjs/cli/studio.js +49 -118
  24. package/dist/cjs/cli/ui.d.ts +1 -1
  25. package/dist/cjs/client.d.ts +43 -0
  26. package/dist/cjs/client.js +125 -6
  27. package/dist/cjs/dialect.d.ts +123 -0
  28. package/dist/cjs/dialect.js +33 -0
  29. package/dist/cjs/errors.d.ts +74 -1
  30. package/dist/cjs/errors.js +239 -25
  31. package/dist/cjs/index-advisor.d.ts +33 -1
  32. package/dist/cjs/index-advisor.js +32 -1
  33. package/dist/cjs/introspect.d.ts +48 -0
  34. package/dist/cjs/introspect.js +222 -91
  35. package/dist/cjs/mssql.js +43 -1
  36. package/dist/cjs/mysql.d.ts +5 -2
  37. package/dist/cjs/mysql.js +202 -17
  38. package/dist/cjs/nested-write.js +6 -1
  39. package/dist/cjs/pipeline-submittable.js +17 -3
  40. package/dist/cjs/pipeline.js +75 -9
  41. package/dist/cjs/powdb.d.ts +23 -0
  42. package/dist/cjs/powdb.js +33 -1
  43. package/dist/cjs/powql.d.ts +61 -9
  44. package/dist/cjs/powql.js +186 -49
  45. package/dist/cjs/prisma-compat.js +160 -41
  46. package/dist/cjs/query/aggregates.d.ts +1 -1
  47. package/dist/cjs/query/aggregates.js +80 -18
  48. package/dist/cjs/query/batched-loader.d.ts +10 -0
  49. package/dist/cjs/query/batched-loader.js +268 -7
  50. package/dist/cjs/query/builder.d.ts +73 -0
  51. package/dist/cjs/query/builder.js +225 -28
  52. package/dist/cjs/query/filters.d.ts +162 -0
  53. package/dist/cjs/query/filters.js +250 -1
  54. package/dist/cjs/query/relations.d.ts +10 -10
  55. package/dist/cjs/query/relations.js +93 -12
  56. package/dist/cjs/query/types.d.ts +14 -1
  57. package/dist/cjs/query/utils.d.ts +146 -2
  58. package/dist/cjs/query/utils.js +210 -4
  59. package/dist/cjs/query/warn-registry.d.ts +10 -0
  60. package/dist/cjs/query/warn-registry.js +10 -0
  61. package/dist/cjs/query/where-compile.d.ts +30 -0
  62. package/dist/cjs/query/where-compile.js +41 -0
  63. package/dist/cjs/query/where.d.ts +128 -13
  64. package/dist/cjs/query/where.js +215 -77
  65. package/dist/cjs/query/writes.d.ts +1 -1
  66. package/dist/cjs/query/writes.js +39 -15
  67. package/dist/cjs/schema-builder.d.ts +2 -1
  68. package/dist/cjs/schema-sql.d.ts +94 -4
  69. package/dist/cjs/schema-sql.js +506 -30
  70. package/dist/cjs/schema.d.ts +3 -1
  71. package/dist/cjs/sqlite.d.ts +6 -0
  72. package/dist/cjs/sqlite.js +151 -10
  73. package/dist/cjs/typed-sql.d.ts +29 -1
  74. package/dist/cjs/typed-sql.js +30 -12
  75. package/dist/cli/destructive.d.ts +18 -4
  76. package/dist/cli/destructive.js +229 -121
  77. package/dist/cli/index.d.ts +21 -4
  78. package/dist/cli/index.js +120 -24
  79. package/dist/cli/mcp.d.ts +28 -8
  80. package/dist/cli/mcp.js +172 -129
  81. package/dist/cli/migrate.d.ts +134 -13
  82. package/dist/cli/migrate.js +347 -238
  83. package/dist/cli/pii-predicate-guard.d.ts +112 -0
  84. package/dist/cli/pii-predicate-guard.js +386 -0
  85. package/dist/cli/prisma-resolve.js +75 -4
  86. package/dist/cli/prisma-schema.d.ts +17 -1
  87. package/dist/cli/prisma-schema.js +83 -17
  88. package/dist/cli/sql-statements.d.ts +125 -0
  89. package/dist/cli/sql-statements.js +373 -0
  90. package/dist/cli/studio.js +49 -118
  91. package/dist/cli/ui.d.ts +1 -1
  92. package/dist/client.d.ts +43 -0
  93. package/dist/client.js +126 -7
  94. package/dist/dialect.d.ts +123 -0
  95. package/dist/dialect.js +33 -0
  96. package/dist/errors.d.ts +74 -1
  97. package/dist/errors.js +228 -19
  98. package/dist/index-advisor.d.ts +33 -1
  99. package/dist/index-advisor.js +31 -1
  100. package/dist/introspect.d.ts +48 -0
  101. package/dist/introspect.js +221 -91
  102. package/dist/mssql.js +44 -2
  103. package/dist/mysql.d.ts +5 -2
  104. package/dist/mysql.js +203 -18
  105. package/dist/nested-write.js +7 -2
  106. package/dist/pipeline-submittable.js +18 -4
  107. package/dist/pipeline.js +76 -10
  108. package/dist/powdb.d.ts +23 -0
  109. package/dist/powdb.js +33 -2
  110. package/dist/powql.d.ts +61 -9
  111. package/dist/powql.js +187 -50
  112. package/dist/prisma-compat.js +160 -41
  113. package/dist/query/aggregates.d.ts +1 -1
  114. package/dist/query/aggregates.js +82 -20
  115. package/dist/query/batched-loader.d.ts +10 -0
  116. package/dist/query/batched-loader.js +270 -9
  117. package/dist/query/builder.d.ts +73 -0
  118. package/dist/query/builder.js +226 -30
  119. package/dist/query/filters.d.ts +162 -0
  120. package/dist/query/filters.js +246 -1
  121. package/dist/query/relations.d.ts +10 -10
  122. package/dist/query/relations.js +94 -14
  123. package/dist/query/types.d.ts +14 -1
  124. package/dist/query/utils.d.ts +146 -2
  125. package/dist/query/utils.js +204 -3
  126. package/dist/query/warn-registry.d.ts +10 -0
  127. package/dist/query/warn-registry.js +10 -0
  128. package/dist/query/where-compile.d.ts +30 -0
  129. package/dist/query/where-compile.js +40 -1
  130. package/dist/query/where.d.ts +128 -13
  131. package/dist/query/where.js +216 -80
  132. package/dist/query/writes.d.ts +1 -1
  133. package/dist/query/writes.js +40 -16
  134. package/dist/schema-builder.d.ts +2 -1
  135. package/dist/schema-sql.d.ts +94 -4
  136. package/dist/schema-sql.js +505 -30
  137. package/dist/schema.d.ts +3 -1
  138. package/dist/sqlite.d.ts +6 -0
  139. package/dist/sqlite.js +151 -10
  140. package/dist/typed-sql.d.ts +29 -1
  141. package/dist/typed-sql.js +30 -12
  142. package/package.json +6 -4
package/dist/schema.d.ts CHANGED
@@ -115,7 +115,9 @@ export interface ColumnMetadata {
115
115
  * Tagged in `defineSchema` (`pii: true`) and carried through generated
116
116
  * metadata. A PII column is EXCLUDED from default projections: it comes back
117
117
  * only when explicitly named in `select` or when the query passes
118
- * `includePii: true` (full opt-in). Studio redacts PII cells by default.
118
+ * `includePii: UNSAFE` (full opt-in, the imported sentinel symbol, since a
119
+ * literal `true` is what a stray JSON body deserializes to). Studio redacts
120
+ * PII cells by default.
119
121
  * Optional / defaults to `false`; untagged schemas behave exactly as before.
120
122
  * Introspection never auto-tags PII (it is a code-first declaration).
121
123
  */
package/dist/sqlite.d.ts CHANGED
@@ -67,6 +67,12 @@ export declare class SqlitePool implements PgCompatPool {
67
67
  /** The underlying `node:sqlite` handle, exposed as an escape hatch (seed/DDL). */
68
68
  readonly db: DatabaseSync;
69
69
  private closed;
70
+ /**
71
+ * `sql -> declared-boolean result columns` (or `null` for none). Bounded like
72
+ * the query builder's SQL-template cache, since a caller can generate
73
+ * unbounded distinct statements (raw SQL, an IN-list that inlines).
74
+ */
75
+ private readonly booleanCache;
70
76
  constructor(db: DatabaseSync);
71
77
  query(text: QueryArg, values?: unknown[]): Promise<any>;
72
78
  connect(): Promise<PgCompatPoolClient>;
package/dist/sqlite.js CHANGED
@@ -50,6 +50,7 @@ import { TurbineClient } from './client.js';
50
50
  import { postgresDialect, } from './dialect.js';
51
51
  import { ConnectionError, UnsupportedFeatureError } from './errors.js';
52
52
  import { applyTableFilters, deriveEngineRelations } from './introspect.js';
53
+ import { LRUCache } from './query/utils.js';
53
54
  import { isDateType, snakeToCamel, } from './schema.js';
54
55
  let cachedDatabaseSync;
55
56
  /**
@@ -123,14 +124,101 @@ function normalizeValue(value) {
123
124
  }
124
125
  return value;
125
126
  }
126
- /** Convert a `node:sqlite` null-prototype row into a normalized plain object. */
127
- function normalizeRow(row) {
127
+ /**
128
+ * Convert a `node:sqlite` null-prototype row into a normalized plain object.
129
+ *
130
+ * `booleanKeys`, when present, names the result columns whose ORIGIN column is
131
+ * declared with a boolean affinity; their 1/0 storage is turned back into
132
+ * `true`/`false` (see {@link booleanResultColumns}). Anything else in such a
133
+ * column, and every column not listed, passes through untouched.
134
+ */
135
+ function normalizeRow(row, booleanKeys) {
128
136
  const out = {};
129
137
  for (const key of Object.keys(row)) {
130
- out[key] = normalizeValue(row[key]);
138
+ const value = normalizeValue(row[key]);
139
+ out[key] = booleanKeys?.has(key) && (value === 1 || value === 0) ? value === 1 : value;
131
140
  }
132
141
  return out;
133
142
  }
143
+ /**
144
+ * The result columns of a prepared statement whose ORIGIN column is declared
145
+ * boolean, or `null` when there are none (the overwhelmingly common case).
146
+ *
147
+ * WHY THIS EXISTS. SQLite has no boolean storage class: a `BOOLEAN` column
148
+ * holds 1/0 integers. Turbine still generates `ok: boolean` for it
149
+ * ({@link sqliteTypeToTs} maps `/bool/` to `boolean`), so `row.ok === true` was
150
+ * false, `JSON.stringify(row)` emitted `1`, and a value the ORM itself wrote as
151
+ * `true` came back as a number. The declared type is the ONLY thing that says
152
+ * "this integer is a boolean", and it lives in the schema, not in the value.
153
+ *
154
+ * The decltype comes from the driver rather than from Turbine's metadata
155
+ * because the driver knows which TABLE each result column came from.
156
+ * `StatementSync.columns()` reports `type` as `sqlite3_column_decltype`, which
157
+ * is non-null only for a direct table-column reference and null for any
158
+ * expression, so a computed column can never be coerced by accident.
159
+ *
160
+ * Node gained `columns()` in 22.13 / 23.4 while this engine's floor is 22.5, so
161
+ * it is feature-detected: on the narrow band without it, booleans keep reading
162
+ * back as 1/0 exactly as before rather than the engine refusing to run.
163
+ */
164
+ function booleanResultColumns(stmt) {
165
+ const columnsFn = stmt.columns;
166
+ if (typeof columnsFn !== 'function')
167
+ return null;
168
+ let described;
169
+ try {
170
+ described = columnsFn.call(stmt);
171
+ }
172
+ catch {
173
+ // A statement the driver cannot describe (some PRAGMA forms) is not worth
174
+ // failing a query over; skip the coercion for it.
175
+ return null;
176
+ }
177
+ let out = null;
178
+ for (const c of described) {
179
+ if (typeof c.name !== 'string' || typeof c.type !== 'string')
180
+ continue;
181
+ if (!/bool/i.test(c.type))
182
+ continue;
183
+ if (out === null)
184
+ out = new Set();
185
+ out.add(c.name);
186
+ }
187
+ return out;
188
+ }
189
+ /**
190
+ * The `node:sqlite` namespace, looked up once, or `null` when this Node build
191
+ * has no such builtin. Deliberately separate from {@link loadDatabaseSync},
192
+ * which THROWS: this is a capability question asked from the pure dialect,
193
+ * which must stay importable on Node 20.
194
+ */
195
+ let cachedSqliteNamespace;
196
+ /**
197
+ * Can the driver report a result column's DECLARED type?
198
+ *
199
+ * This is the same capability {@link booleanResultColumns} feature-detects, and
200
+ * the two have to be asked the SAME question or they split: that helper is what
201
+ * turns 1/0 into `true`/`false` on every direct read, and the boolean
202
+ * `jsonWireRule` is what does it on the join path. On the Node band with
203
+ * `node:sqlite` but without `StatementSync.columns()` (>= 22.5, < 22.13 / 23.4)
204
+ * the helper returns `null` and every direct read keeps 1/0, so a join that
205
+ * still converted would produce exactly the strategy-dependent type flip the
206
+ * rule exists to prevent, on the one runtime where nothing else can catch it.
207
+ *
208
+ * The prototype property is re-read on each call rather than memoized as a
209
+ * boolean, so the answer cannot outlive the capability.
210
+ */
211
+ function driverReportsDeclaredTypes() {
212
+ if (cachedSqliteNamespace === undefined) {
213
+ try {
214
+ cachedSqliteNamespace = createRequire(process.cwd())('node:sqlite');
215
+ }
216
+ catch {
217
+ cachedSqliteNamespace = null;
218
+ }
219
+ }
220
+ return typeof cachedSqliteNamespace?.StatementSync?.prototype?.columns === 'function';
221
+ }
134
222
  // ---------------------------------------------------------------------------
135
223
  // Statement classification + error translation
136
224
  // ---------------------------------------------------------------------------
@@ -234,7 +322,7 @@ function toNamedBinding(values) {
234
322
  }
235
323
  return named;
236
324
  }
237
- function runStatement(db, sql, values) {
325
+ function runStatement(db, sql, values, booleanCache) {
238
326
  const binding = toNamedBinding(values);
239
327
  let stmt;
240
328
  try {
@@ -246,7 +334,15 @@ function runStatement(db, sql, values) {
246
334
  stmt.setReadBigInts(true);
247
335
  try {
248
336
  if (statementReturnsRows(sql)) {
249
- const rows = (binding ? stmt.all(binding) : stmt.all()).map(normalizeRow);
337
+ // Describe each distinct statement ONCE. `null` (no boolean column) is a
338
+ // cached answer too, so the ordinary query pays a single Map hit per call
339
+ // and nothing per row.
340
+ let booleanKeys = booleanCache?.get(sql);
341
+ if (booleanKeys === undefined) {
342
+ booleanKeys = booleanResultColumns(stmt);
343
+ booleanCache?.set(sql, booleanKeys);
344
+ }
345
+ const rows = (binding ? stmt.all(binding) : stmt.all()).map((r) => normalizeRow(r, booleanKeys ?? undefined));
250
346
  return { rows, rowCount: rows.length };
251
347
  }
252
348
  const info = binding ? stmt.run(binding) : stmt.run();
@@ -269,21 +365,28 @@ export class SqlitePool {
269
365
  /** The underlying `node:sqlite` handle, exposed as an escape hatch (seed/DDL). */
270
366
  db;
271
367
  closed = false;
368
+ /**
369
+ * `sql -> declared-boolean result columns` (or `null` for none). Bounded like
370
+ * the query builder's SQL-template cache, since a caller can generate
371
+ * unbounded distinct statements (raw SQL, an IN-list that inlines).
372
+ */
373
+ booleanCache = new LRUCache(1000);
272
374
  constructor(db) {
273
375
  this.db = db;
274
376
  }
275
377
  // biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape; runStatement returns plain objects.
276
378
  async query(text, values) {
277
379
  const { text: sql, params } = normalizeQueryArgs(text, values);
278
- return runStatement(this.db, sql, params);
380
+ return runStatement(this.db, sql, params, this.booleanCache);
279
381
  }
280
382
  async connect() {
281
383
  const db = this.db;
384
+ const booleanCache = this.booleanCache;
282
385
  return {
283
386
  // biome-ignore lint/suspicious/noExplicitAny: see query() above.
284
387
  query: async (text, values) => {
285
388
  const { text: sql, params } = normalizeQueryArgs(text, values);
286
- return runStatement(db, sql, params);
389
+ return runStatement(db, sql, params, booleanCache);
287
390
  },
288
391
  release: () => {
289
392
  // Single shared connection, nothing to return to a pool.
@@ -367,6 +470,7 @@ export const sqliteDialect = {
367
470
  supportsReturning: true,
368
471
  supportsILike: false,
369
472
  supportsVector: false,
473
+ supportsJsonContains: false,
370
474
  // FTS5 is a virtual-table feature with its own MATCH syntax, not the
371
475
  // `to_tsvector @@ to_tsquery` shape Turbine's `search` filter emits.
372
476
  supportsFullTextSearch: false,
@@ -439,6 +543,33 @@ export const sqliteDialect = {
439
543
  },
440
544
  };
441
545
  }
546
+ // A declared-boolean column is 1/0 in storage, and the driver shim turns
547
+ // that back into true/false on every direct read (see
548
+ // booleanResultColumns). `json_object` sees the raw integer and has no
549
+ // decltype to consult, so the join strategy alone would have kept handing
550
+ // back 1/0 while the top-level read, the batched loader and the flatten
551
+ // plan returned booleans, which is the same strategy-dependent type flip
552
+ // the bigint rule above exists to prevent. No cast is needed, only the
553
+ // decode: the JSON number IS the stored value, it just has to be read the
554
+ // same way.
555
+ //
556
+ // Gated on the SAME capability the direct-read half feature-detects (see
557
+ // driverReportsDeclaredTypes). Without it the shim cannot name a boolean
558
+ // result column, so 1/0 is the answer everywhere; converting here alone
559
+ // would put the flip back, pointing the other way.
560
+ if (t.includes('BOOL') && driverReportsDeclaredTypes()) {
561
+ return {
562
+ // Carried as TEXT because that is how the decode half is reached at
563
+ // all: the parser only runs `decode` on a string cell, so a rule that
564
+ // left the JSON number alone would never fire. The CASE narrows the
565
+ // carrier to EXACTLY the two values that mean true/false, so anything
566
+ // else a declared-boolean column happens to hold (SQLite does not
567
+ // enforce the type) stays in its own JSON form and reads back as the
568
+ // driver would have returned it, rather than being stringified.
569
+ sql: (ref) => `CASE WHEN ${ref} IN (0, 1) THEN CAST(${ref} AS TEXT) ELSE ${ref} END`,
570
+ decode: (value) => (value === '1' ? true : value === '0' ? false : value),
571
+ };
572
+ }
442
573
  // A BLOB cannot go into JSON at all: SQLite raises "JSON cannot hold BLOB
443
574
  // values" and the whole query fails with a raw SQL logic error, where the
444
575
  // batched loader returns the row fine. Carry hex and rebuild the bytes.
@@ -523,9 +654,14 @@ export const sqliteDialect = {
523
654
  // COLLATE NOCASE is ASCII-only (no Unicode case folding), documented limit.
524
655
  return `${column} LIKE ${paramRef} COLLATE NOCASE`;
525
656
  },
657
+ // UNREACHABLE while `supportsJsonContains` is false, and kept deliberately:
658
+ // it is the emulation this dialect would use if the encoding mismatch were
659
+ // repaired, and deleting it would delete the record of what was tried. The
660
+ // param is bound as JSON text (`'"gold"'`) while `json_each.value` yields the
661
+ // decoded SQL value (`gold`), so this predicate never holds for ANY operand.
662
+ // See `supportsJsonContains` in dialect.ts for the measurements and the shape
663
+ // a faithful version needs (`json_each.type` alongside the value).
526
664
  buildJsonContains(column, paramRef) {
527
- // Emulated containment: true when any top-level JSON value equals the param.
528
- // Limited vs Postgres `@>` (no deep/object containment), jsonPathSupport='function'.
529
665
  return `EXISTS (SELECT 1 FROM json_each(${column}) WHERE json_each.value = ${paramRef})`;
530
666
  },
531
667
  buildJsonPathExtract(column, pathParamRef) {
@@ -608,7 +744,12 @@ export const sqliteDialect = {
608
744
  function pragma(db, sql) {
609
745
  // PRAGMA / SELECT against sqlite_master, read-only, identifiers are SQLite
610
746
  // catalog names (never user input here), values normalized for safe ints.
611
- return db.prepare(sql).all().map(normalizeRow);
747
+ // No boolean-key set: PRAGMA output columns are catalog metadata (`notnull`,
748
+ // `pk`, `unique`), which are 0/1 integers the introspector reads AS integers.
749
+ return db
750
+ .prepare(sql)
751
+ .all()
752
+ .map((r) => normalizeRow(r));
612
753
  }
613
754
  /**
614
755
  * Read a live SQLite database (an open `DatabaseSync` handle) into the same
@@ -79,7 +79,35 @@ export declare class TypedSqlQuery<T extends Record<string, unknown>> implements
79
79
  private readonly sql;
80
80
  private readonly params;
81
81
  private readonly logging;
82
- constructor(pool: PgCompatPool, sql: string, params: unknown[], logging: boolean);
82
+ /**
83
+ * Runs the execution under the owning client's `errorMessages` mode. Passed
84
+ * in rather than read here because this module must not import the client.
85
+ *
86
+ * It has to reach the EXECUTION, not the construction, and that is the whole
87
+ * reason it is a constructor parameter instead of a wrapper around
88
+ * `client.sql\`...\``: this builder is lazy, so a scope established around
89
+ * the tagged-template call is torn down before a single row is fetched.
90
+ * Wrapping the template call compiled, ran, and protected nothing.
91
+ *
92
+ * Defaults to calling through, so a `TypedSqlQuery` built outside a client
93
+ * behaves exactly as before.
94
+ */
95
+ private readonly runScoped;
96
+ constructor(pool: PgCompatPool, sql: string, params: unknown[], logging: boolean,
97
+ /**
98
+ * Runs the execution under the owning client's `errorMessages` mode. Passed
99
+ * in rather than read here because this module must not import the client.
100
+ *
101
+ * It has to reach the EXECUTION, not the construction, and that is the whole
102
+ * reason it is a constructor parameter instead of a wrapper around
103
+ * `client.sql\`...\``: this builder is lazy, so a scope established around
104
+ * the tagged-template call is torn down before a single row is fetched.
105
+ * Wrapping the template call compiled, ran, and protected nothing.
106
+ *
107
+ * Defaults to calling through, so a `TypedSqlQuery` built outside a client
108
+ * behaves exactly as before.
109
+ */
110
+ runScoped?: <R>(fn: () => R) => R);
83
111
  /** Execute and return all rows. Internal; powers `then`, `one`, and `scalar`. */
84
112
  private run;
85
113
  /**
package/dist/typed-sql.js CHANGED
@@ -92,24 +92,42 @@ export class TypedSqlQuery {
92
92
  sql;
93
93
  params;
94
94
  logging;
95
- constructor(pool, sql, params, logging) {
95
+ runScoped;
96
+ constructor(pool, sql, params, logging,
97
+ /**
98
+ * Runs the execution under the owning client's `errorMessages` mode. Passed
99
+ * in rather than read here because this module must not import the client.
100
+ *
101
+ * It has to reach the EXECUTION, not the construction, and that is the whole
102
+ * reason it is a constructor parameter instead of a wrapper around
103
+ * `client.sql\`...\``: this builder is lazy, so a scope established around
104
+ * the tagged-template call is torn down before a single row is fetched.
105
+ * Wrapping the template call compiled, ran, and protected nothing.
106
+ *
107
+ * Defaults to calling through, so a `TypedSqlQuery` built outside a client
108
+ * behaves exactly as before.
109
+ */
110
+ runScoped = (fn) => fn()) {
96
111
  this.pool = pool;
97
112
  this.sql = sql;
98
113
  this.params = params;
99
114
  this.logging = logging;
115
+ this.runScoped = runScoped;
100
116
  }
101
117
  /** Execute and return all rows. Internal; powers `then`, `one`, and `scalar`. */
102
- async run() {
103
- if (this.logging) {
104
- console.log(`[turbine] Typed SQL: ${this.sql.trim().substring(0, 120)}...`);
105
- }
106
- try {
107
- const result = await this.pool.query(this.sql, this.params);
108
- return result.rows;
109
- }
110
- catch (err) {
111
- throw wrapPgError(err);
112
- }
118
+ run() {
119
+ return this.runScoped(async () => {
120
+ if (this.logging) {
121
+ console.log(`[turbine] Typed SQL: ${this.sql.trim().substring(0, 120)}...`);
122
+ }
123
+ try {
124
+ const result = await this.pool.query(this.sql, this.params);
125
+ return result.rows;
126
+ }
127
+ catch (err) {
128
+ throw wrapPgError(err);
129
+ }
130
+ });
113
131
  }
114
132
  /**
115
133
  * PromiseLike implementation: `await db.sql<T>\`...\`` resolves to `T[]`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
5
5
  "type": "module",
6
6
  "//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
@@ -124,13 +124,15 @@
124
124
  "test:unit": "DATABASE_URL= tsx --test src/test/*.test.ts",
125
125
  "test:coverage": "c8 tsx --test --test-concurrency=1 src/test/*.test.ts",
126
126
  "//coverage:cli": "The CLI coverage gate, split into ONE collection run plus FOUR threshold checks. c8 enforces a single threshold set per invocation and its --per-file applies the SAME numbers to every file, neither of which can express 'destructive.ts holds 100 while migrate.ts holds 70'. An aggregate-only floor lets the least-covered file spend the whole slack the best-covered file earned: at the measured 3623/2951 lines, migrate.ts could fall from 70.3% to 66.6% with the aggregate still green. So each file gets its OWN floor, checked by re-reporting the coverage already on disk (c8 report re-reads ./coverage/tmp, so this costs no extra test run). The aggregate check is kept as well: it catches all three sagging together inside their individual margins. Per-file gates run FIRST because their failure names the file. DATABASE_URL is neutralized on the collection run: these test files include live migration tests that create and drop tables, and this script runs from prepublishOnly.",
127
- "test:coverage:cli": "npm run coverage:cli:collect && npm run coverage:cli:gate:destructive && npm run coverage:cli:gate:studio && npm run coverage:cli:gate:migrate && npm run coverage:cli:gate:aggregate",
128
- "coverage:cli:collect": "DATABASE_URL= c8 --all --reporter text --exclude 'src/test/**' --include src/cli/studio.ts --include src/cli/migrate.ts --include src/cli/destructive.ts tsx --test src/test/studio-write.test.ts src/test/studio-demo.test.ts src/test/studio.test.ts src/test/studio-security.test.ts src/test/migrate.test.ts src/test/migrate-deploy.test.ts src/test/migrate-smoke-fixes.test.ts src/test/destructive-migrations.test.ts src/test/backfill-recipe.test.ts src/test/cli.test.ts src/test/cli-diff-migration.test.ts src/test/cli-flags.test.ts src/test/cli-first-run.test.ts",
127
+ "test:coverage:cli": "npm run coverage:cli:collect && npm run coverage:cli:gate:destructive && npm run coverage:cli:gate:sql-statements && npm run coverage:cli:gate:pii-guard && npm run coverage:cli:gate:studio && npm run coverage:cli:gate:migrate && npm run coverage:cli:gate:aggregate",
128
+ "coverage:cli:collect": "DATABASE_URL= c8 --all --reporter text --exclude 'src/test/**' --include src/cli/studio.ts --include src/cli/migrate.ts --include src/cli/destructive.ts --include src/cli/sql-statements.ts --include src/cli/pii-predicate-guard.ts tsx --test src/test/studio-write.test.ts src/test/studio-demo.test.ts src/test/studio.test.ts src/test/studio-security.test.ts src/test/migrate.test.ts src/test/migrate-deploy.test.ts src/test/migrate-smoke-fixes.test.ts src/test/destructive-migrations.test.ts src/test/backfill-recipe.test.ts src/test/cli.test.ts src/test/cli-diff-migration.test.ts src/test/cli-flags.test.ts src/test/cli-first-run.test.ts",
129
129
  "coverage:cli:gate": "c8 report --all --exclude 'src/test/**' --reporter text --check-coverage",
130
130
  "coverage:cli:gate:destructive": "npm run coverage:cli:gate -- --include src/cli/destructive.ts --lines 98 --statements 98 --branches 84 --functions 98",
131
+ "coverage:cli:gate:sql-statements": "npm run coverage:cli:gate -- --include src/cli/sql-statements.ts --lines 100 --statements 100 --branches 98 --functions 100",
132
+ "coverage:cli:gate:pii-guard": "npm run coverage:cli:gate -- --include src/cli/pii-predicate-guard.ts --lines 97 --statements 97 --branches 88 --functions 100",
131
133
  "coverage:cli:gate:studio": "npm run coverage:cli:gate -- --include src/cli/studio.ts --lines 82 --statements 82 --branches 81 --functions 86",
132
134
  "coverage:cli:gate:migrate": "npm run coverage:cli:gate -- --include src/cli/migrate.ts --lines 69 --statements 69 --branches 91 --functions 73",
133
- "coverage:cli:gate:aggregate": "npm run coverage:cli:gate -- --include src/cli/studio.ts --include src/cli/migrate.ts --include src/cli/destructive.ts --lines 78 --statements 78 --branches 84 --functions 82",
135
+ "coverage:cli:gate:aggregate": "npm run coverage:cli:gate -- --include src/cli/studio.ts --include src/cli/migrate.ts --include src/cli/destructive.ts --include src/cli/sql-statements.ts --include src/cli/pii-predicate-guard.ts --lines 78 --statements 78 --branches 84 --functions 82",
134
136
  "lint": "biome check src/",
135
137
  "lint:fix": "biome check --write src/",
136
138
  "format": "biome format --write src/",