turbine-orm 0.90.0-next.25511aa → 0.90.1

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 CHANGED
@@ -272,7 +272,7 @@ Going deep on one database means the parts other ORMs push to raw SQL are typed
272
272
 
273
273
  ## Serverless and edge
274
274
 
275
- The core is driver-agnostic: hand any pg-compatible pool to `turbineHttp()` and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under **147 kB brotli** (edge entry under **116 kB**) with `pg` external, enforced by `size-limit` in CI at those exact numbers; run `npm run size` for the current figure.
275
+ The core is driver-agnostic: hand any pg-compatible pool to `turbineHttp()` and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under **146 kB brotli** (edge entry under **114 kB**) with `pg` external, enforced by `size-limit` in CI at those exact numbers; run `npm run size` for the current figure.
276
276
 
277
277
  ```typescript
278
278
  import { Pool } from '@neondatabase/serverless';
@@ -287,7 +287,7 @@ HTTP drivers cannot hold a cursor or a LISTEN connection, so `findManyStream` an
287
287
 
288
288
  ## Database engines
289
289
 
290
- Postgres is the default and primary target. The same typed API also runs on **SQLite** (Node's built-in `node:sqlite`, zero extra installs, Node ≥ 22.5), **MySQL 8** (`mysql2`), **SQL Server 2016+** (`mssql`), and **PowDB** (embedded or networked), each behind a subpath export with its driver as an optional peer. All four are **Experimental** in the [stability policy](STABILITY.md): the query API they expose is the shared one, but each engine's capability boundaries and factory options may still change in a minor release.
290
+ Postgres is the default and primary target. The same typed API also runs on **SQLite** (Node's built-in `node:sqlite`, zero extra installs, Node ≥ 22.5), **MySQL 8** (`mysql2`), **SQL Server 2016+** (`mssql`), and **PowDB** (embedded or networked), each behind a subpath export with its driver as an optional peer. All four are **Experimental** in the [stability policy](STABILITY.md): the query API they expose is the shared one, but each engine's capability boundaries and factory options may still change in a minor release. PowDB is in maintenance mode: bug and data-safety fixes, no new features.
291
291
 
292
292
  ```bash
293
293
  npm install turbine-orm # SQLite needs nothing else
@@ -359,11 +359,11 @@ It is also built to be extended rather than wrapped. All SQL generation routes t
359
359
  |---|---|---|---|---|---|
360
360
  | **Engine / runtime** | No engine binary (`pg` only) | Client + TS/WASM query compiler | No engine | No engine | Kysely as query runner; SQL built by MikroORM (v7) |
361
361
  | **Runtime deps** | 1 (`pg`) | `@prisma/client` + required driver adapter | 0 | 0 | `@mikro-orm/core` (0 deps) + a driver package |
362
- | **Main bundle (brotli)** | under 147 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small | |
362
+ | **Main bundle (brotli)** | under 146 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small | |
363
363
  | **Studio** | Read-only by default | Full CRUD, cloud-hosted | Full CRUD; [Gateway](https://gateway.drizzle.team/) self-hosted, free | None | AdminJS integration guide (CRUD panel) |
364
364
  | **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors | |
365
365
  | **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None | Generated by diffing entities against a snapshot or the database; TS/JS classes |
366
- | **Edge runtime** | One import swap, under 116 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native | Cloudflare Workers and Deno (v7) |
366
+ | **Edge runtime** | One import swap, under 114 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native | Cloudflare Workers and Deno (v7) |
367
367
  | **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual | `flush()` runs queued changes in one transaction |
368
368
  | **Typed errors** | `isRetryable` discriminant | Error codes only | None | None | `DriverException` subclasses (`UniqueConstraintViolationException`, ...) |
369
369
  | **Nested relations** | 1 query, deep type inference | 1 query per relation by default; single-query `relationJoins` is Preview | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) | `populate`; default `balanced`: joins to-one, one query per to-many relation |
@@ -50,6 +50,7 @@ const warn_registry_js_1 = require("./query/warn-registry.js");
50
50
  const query_events_js_1 = require("./query-events.js");
51
51
  const realtime_js_1 = require("./realtime.js");
52
52
  const supplied_pool_types_js_1 = require("./supplied-pool-types.js");
53
+ const transaction_client_slot_js_1 = require("./transaction-client-slot.js");
53
54
  const transaction_state_js_1 = require("./transaction-state.js");
54
55
  const typed_sql_js_1 = require("./typed-sql.js");
55
56
  async function withRetry(fn, options) {
@@ -563,19 +564,6 @@ function resolveNonNegativeInteger(value, name, unit) {
563
564
  }
564
565
  return value;
565
566
  }
566
- /**
567
- * Refuse a dialect with no `capabilities` object (E003): every code path asks
568
- * it what the engine can do, so a hand-built dialect without one would fail on
569
- * its first query with a TypeError instead of here.
570
- */
571
- function assertDialectCapabilities(dialect) {
572
- const capabilities = dialect.capabilities;
573
- if (typeof capabilities === 'object' && capabilities !== null)
574
- return;
575
- throw new errors_js_1.ValidationError(`The dialect "${String(dialect.name)}" has no \`capabilities\` object, which says what the engine can do. ` +
576
- 'Build it from an engine dialect (`{ ...postgresDialect, name, capabilities: { ... } }`) and state every ' +
577
- 'EngineCapabilities key.');
578
- }
579
567
  /**
580
568
  * The statement that opens a transaction, read-only when asked. PostgreSQL and
581
569
  * MySQL both accept `READ ONLY` after their own BEGIN form (`BEGIN [ISOLATION
@@ -1027,6 +1015,10 @@ class TransactionClient {
1027
1015
  }
1028
1016
  }
1029
1017
  exports.TransactionClient = TransactionClient;
1018
+ // query/builder.ts opens an implicit transaction with this class but sits
1019
+ // below this module in the import graph, so the class is handed over here,
1020
+ // once, when this module is evaluated (see transaction-client-slot.ts).
1021
+ (0, transaction_client_slot_js_1.registerTransactionClient)(TransactionClient);
1030
1022
  /**
1031
1023
  * Give each multi-statement write on a transaction-scoped accessor its own
1032
1024
  * savepoint: `create` / `update` carrying nested relation writes, and an
@@ -1251,7 +1243,7 @@ class TurbineClient {
1251
1243
  // settling the process-global parser mode and then rejecting the config
1252
1244
  // would poison the next, valid, TurbineClient with a phantom conflict.
1253
1245
  const dialect = config.dialect ?? dialect_js_1.postgresDialect;
1254
- assertDialectCapabilities(dialect);
1246
+ (0, dialect_js_1.assertDialectCapabilities)(dialect);
1255
1247
  const planCacheMode = TurbineClient.resolvePlanCacheMode(config.planCacheMode, dialect);
1256
1248
  const statementTimeoutMs = TurbineClient.resolveStatementTimeout(config.statementTimeoutMs, dialect);
1257
1249
  const maxLifetimeSeconds = resolveNonNegativeInteger(config.maxLifetimeSeconds, 'maxLifetimeSeconds', 'seconds');
@@ -389,10 +389,11 @@ export interface RelationSubqueryContext {
389
389
  * refuses an engine whose `capabilities` is inherited or shared.
390
390
  *
391
391
  * Code asks these questions instead of testing `dialect.name`; a source scan
392
- * (src/test/capabilities-guard.test.ts) refuses a new engine-name comparison
393
- * outside a short, commented allowlist. Rendering differences are not
394
- * capabilities: they belong in the dialect's hooks (a few still switch on the
395
- * name, allowlisted, until engines stop spreading `postgresDialect`).
392
+ * (src/test/capabilities-guard.test.ts) refuses a new engine-name test, of any
393
+ * shape, outside a short, commented allowlist that pins each site it exempts.
394
+ * Rendering differences are not capabilities: they belong in the dialect's
395
+ * hooks (a few still switch on the name, allowlisted, until engines stop
396
+ * spreading `postgresDialect`).
396
397
  */
397
398
  export interface EngineCapabilities {
398
399
  /** `SELECT DISTINCT ON (...)`: findMany `distinct` and groupBy `distinctOn`. Else E017. */
@@ -586,6 +587,16 @@ export interface EngineCapabilities {
586
587
  */
587
588
  readonly unboundedTextKeys: boolean;
588
589
  }
590
+ /**
591
+ * Refuse a dialect with no `capabilities` object (E003). Every path that builds
592
+ * SQL asks it what the engine can do, so a hand-built dialect written before
593
+ * the object existed would otherwise fail on first use with a TypeError
594
+ * ("cannot read properties of undefined"). Every public entry point that takes
595
+ * a dialect calls this (`TurbineClient`, `QueryInterface`, `schemaToSQL`,
596
+ * `introspect`), so the refusal and its words are the same whichever one a
597
+ * caller reaches first. One `typeof`: `QueryInterface` is built per table.
598
+ */
599
+ export declare function assertDialectCapabilities(dialect: Dialect): void;
589
600
  export interface Dialect {
590
601
  /** Dialect identifier. */
591
602
  readonly name: DialectName;
@@ -8,11 +8,29 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.postgresDialect = void 0;
11
+ exports.assertDialectCapabilities = assertDialectCapabilities;
11
12
  exports.maxBindParameters = maxBindParameters;
12
13
  exports.onConflictAction = onConflictAction;
13
14
  const errors_js_1 = require("./errors.js");
14
15
  const utils_js_1 = require("./query/utils.js");
15
16
  const schema_js_1 = require("./schema.js");
17
+ /**
18
+ * Refuse a dialect with no `capabilities` object (E003). Every path that builds
19
+ * SQL asks it what the engine can do, so a hand-built dialect written before
20
+ * the object existed would otherwise fail on first use with a TypeError
21
+ * ("cannot read properties of undefined"). Every public entry point that takes
22
+ * a dialect calls this (`TurbineClient`, `QueryInterface`, `schemaToSQL`,
23
+ * `introspect`), so the refusal and its words are the same whichever one a
24
+ * caller reaches first. One `typeof`: `QueryInterface` is built per table.
25
+ */
26
+ function assertDialectCapabilities(dialect) {
27
+ const capabilities = dialect.capabilities;
28
+ if (typeof capabilities === 'object' && capabilities !== null)
29
+ return;
30
+ throw new errors_js_1.ValidationError(`The dialect "${String(dialect.name)}" has no \`capabilities\` object, which says what the engine can do. ` +
31
+ 'Build it from an engine dialect (`{ ...postgresDialect, name, capabilities: { ... } }`) and state every ' +
32
+ 'EngineCapabilities key.');
33
+ }
16
34
  /** The bind-parameter limit for one statement on this dialect, or `undefined` when it has none. */
17
35
  function maxBindParameters(dialect) {
18
36
  const limit = dialect.capabilities.maxBindParameters;
@@ -485,6 +485,7 @@ function assertSafeCatalogSchema(schema) {
485
485
  */
486
486
  async function introspect(options) {
487
487
  const dialect = options.dialect ?? dialect_js_1.postgresDialect;
488
+ (0, dialect_js_1.assertDialectCapabilities)(dialect);
488
489
  const introspector = dialect.introspector;
489
490
  const schema = introspector
490
491
  ? await introspector.introspect(options)
package/dist/cjs/powdb.js CHANGED
@@ -53,39 +53,6 @@
53
53
  *
54
54
  * @module
55
55
  */
56
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
57
- if (k2 === undefined) k2 = k;
58
- var desc = Object.getOwnPropertyDescriptor(m, k);
59
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
60
- desc = { enumerable: true, get: function() { return m[k]; } };
61
- }
62
- Object.defineProperty(o, k2, desc);
63
- }) : (function(o, m, k, k2) {
64
- if (k2 === undefined) k2 = k;
65
- o[k2] = m[k];
66
- }));
67
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
68
- Object.defineProperty(o, "default", { enumerable: true, value: v });
69
- }) : function(o, v) {
70
- o["default"] = v;
71
- });
72
- var __importStar = (this && this.__importStar) || (function () {
73
- var ownKeys = function(o) {
74
- ownKeys = Object.getOwnPropertyNames || function (o) {
75
- var ar = [];
76
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
77
- return ar;
78
- };
79
- return ownKeys(o);
80
- };
81
- return function (mod) {
82
- if (mod && mod.__esModule) return mod;
83
- var result = {};
84
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
85
- __setModuleDefault(result, mod);
86
- return result;
87
- };
88
- })();
89
56
  var __importDefault = (this && this.__importDefault) || function (mod) {
90
57
  return (mod && mod.__esModule) ? mod : { "default": mod };
91
58
  };
@@ -109,6 +76,7 @@ const errors_js_1 = require("./errors.js");
109
76
  const logger_js_1 = require("./logger.js");
110
77
  const optional_peer_import_cjs_1 = __importDefault(require("./optional-peer-import.cjs"));
111
78
  const powdb_shared_js_1 = require("./powdb-shared.js");
79
+ const powql_js_1 = require("./powql.js");
112
80
  const warn_registry_js_1 = require("./query/warn-registry.js");
113
81
  const schema_js_1 = require("./schema.js");
114
82
  // The shared PowDB primitives live in a leaf module (see powdb-shared.ts): this
@@ -1861,8 +1829,8 @@ exports.PowdbEmbeddedPool = PowdbEmbeddedPool;
1861
1829
  // `describe`-based introspection (programmatic API; see powdb-introspect.ts).
1862
1830
  var powdb_introspect_js_1 = require("./powdb-introspect.js");
1863
1831
  Object.defineProperty(exports, "introspectPowdbDatabase", { enumerable: true, get: function () { return powdb_introspect_js_1.introspectPowdbDatabase; } });
1864
- var powql_js_1 = require("./powql.js");
1865
- Object.defineProperty(exports, "PowqlInterface", { enumerable: true, get: function () { return powql_js_1.PowqlInterface; } });
1832
+ var powql_js_2 = require("./powql.js");
1833
+ Object.defineProperty(exports, "PowqlInterface", { enumerable: true, get: function () { return powql_js_2.PowqlInterface; } });
1866
1834
  /**
1867
1835
  * Dynamically load `@zvndev/powdb-client`. Kept out of the static import graph so
1868
1836
  * `import 'turbine-orm/powdb'` never throws when the optional peer is absent.
@@ -2064,9 +2032,7 @@ async function turbinePowDB(target, schema, options = {}) {
2064
2032
  }
2065
2033
  let client;
2066
2034
  try {
2067
- // The PowQL generator is loaded here to keep client.ts free of any PowDB import.
2068
- const { PowqlInterface } = await Promise.resolve().then(() => __importStar(require('./powql.js')));
2069
- const queryInterfaceFactory = (p, table, sch, middlewares, opts) => new PowqlInterface(p, table, sch, middlewares, opts);
2035
+ const queryInterfaceFactory = (p, table, sch, middlewares, opts) => new powql_js_1.PowqlInterface(p, table, sch, middlewares, opts);
2070
2036
  // Forward every client-level option EXCEPT the ones this factory owns (they
2071
2037
  // configure the pool / transport above, and TurbineClient warns on a config
2072
2038
  // key it does not recognize). Destructured rather than hand-listed on the way
@@ -486,7 +486,9 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
486
486
  * `gt` / `gte` / `lt` / `lte` compile to `in` over the labels on that side of
487
487
  * the operand in DECLARATION order, which is what PostgreSQL compares; the
488
488
  * text comparison PowDB would run is alphabetical, a different answer. A null
489
- * operand matches no row, as `col > NULL` does. Case-insensitive matching and
489
+ * operand is refused before this (assertOperatorOperands, as on the SQL
490
+ * engines) and again by {@link assertEnumLabel}: compiled to a constant false,
491
+ * `NOT` would turn it into every row. Case-insensitive matching and
490
492
  * `contains` / `startsWith` / `endsWith` are E003: PostgreSQL has no `lower`
491
493
  * or `LIKE` on an enum (42883).
492
494
  */
package/dist/cjs/powql.js CHANGED
@@ -35,39 +35,6 @@
35
35
  *
36
36
  * @module
37
37
  */
38
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
39
- if (k2 === undefined) k2 = k;
40
- var desc = Object.getOwnPropertyDescriptor(m, k);
41
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
42
- desc = { enumerable: true, get: function() { return m[k]; } };
43
- }
44
- Object.defineProperty(o, k2, desc);
45
- }) : (function(o, m, k, k2) {
46
- if (k2 === undefined) k2 = k;
47
- o[k2] = m[k];
48
- }));
49
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
50
- Object.defineProperty(o, "default", { enumerable: true, value: v });
51
- }) : function(o, v) {
52
- o["default"] = v;
53
- });
54
- var __importStar = (this && this.__importStar) || (function () {
55
- var ownKeys = function(o) {
56
- ownKeys = Object.getOwnPropertyNames || function (o) {
57
- var ar = [];
58
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
59
- return ar;
60
- };
61
- return ownKeys(o);
62
- };
63
- return function (mod) {
64
- if (mod && mod.__esModule) return mod;
65
- var result = {};
66
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
67
- __setModuleDefault(result, mod);
68
- return result;
69
- };
70
- })();
71
38
  Object.defineProperty(exports, "__esModule", { value: true });
72
39
  exports.PowqlInterface = exports.MAX_RELATION_FILTER_KEYS = exports.MAX_POWQL_DATETIME_TERMS = void 0;
73
40
  const node_crypto_1 = require("node:crypto");
@@ -99,6 +66,7 @@ const where_js_1 = require("./query/where.js");
99
66
  const where_compile_js_1 = require("./query/where-compile.js");
100
67
  const writes_js_1 = require("./query/writes.js");
101
68
  const schema_js_1 = require("./schema.js");
69
+ const transaction_client_slot_js_1 = require("./transaction-client-slot.js");
102
70
  /**
103
71
  * Max parent keys per relation-loader `in (…)` query. A `with` over a large
104
72
  * parent set is split into batches of this size so a single query never exceeds
@@ -1083,17 +1051,15 @@ class PowqlInterface {
1083
1051
  // The SQL engines' operand rules and messages, so the engines agree on
1084
1052
  // which operator objects are valid; one naming no comparison
1085
1053
  // (`{ mode: 'insensitive' }`) is refused instead of compiling to nothing.
1086
- const operand = {
1054
+ // JSON columns included, as on the SQL engines: skipping them let a
1055
+ // mode-only filter fall through to the constant-false fallback below, which
1056
+ // `NOT` turned into every non-null row (a deleteMany wrote all of them).
1057
+ // The same call refuses a JSON `not` object made only of filter operators.
1058
+ (0, filters_js_1.assertOperatorOperands)(op, {
1087
1059
  column: colMeta.name,
1088
1060
  table: this.table,
1089
1061
  kind: (0, filters_js_1.operandColumnKind)(this.meta.pgTypes[colMeta.name]),
1090
- };
1091
- if (!(0, powdb_shared_js_1.isJsonColumn)(colMeta, this.schema.enums))
1092
- (0, filters_js_1.assertOperatorOperands)(op, operand);
1093
- // On a JSON column a `not` object made only of filter operators is refused,
1094
- // as on the SQL engines (one spelling, one meaning on every engine).
1095
- else
1096
- (0, filters_js_1.assertJsonNotOperand)(op.not, operand);
1062
+ });
1097
1063
  const insensitive = op.mode === 'insensitive';
1098
1064
  const lhs = insensitive ? `lower(${ref})` : ref;
1099
1065
  const labels = this.enumLabels(colMeta);
@@ -1179,7 +1145,14 @@ class PowqlInterface {
1179
1145
  throw new errors_js_1.ValidationError(`Unsupported operator "${opName}" on PowDB.`);
1180
1146
  }
1181
1147
  }
1182
- return conds.length > 1 ? `(${conds.join(' and ')})` : (conds[0] ?? this.alwaysFalse());
1148
+ // assertOperatorOperands refuses an operator object that names no
1149
+ // comparison, and every comparison above pushes a condition or throws, so
1150
+ // an empty list is a generator bug. It must not become a constant: under
1151
+ // `NOT` a constant false is every row, and a mutation would write them.
1152
+ if (!conds.length) {
1153
+ throw new errors_js_1.InvariantError('powql-filter-empty', `the filter on "${field}" compiled to no condition.`);
1154
+ }
1155
+ return conds.length > 1 ? `(${conds.join(' and ')})` : conds[0];
1183
1156
  }
1184
1157
  /**
1185
1158
  * PowQL JSON path expression `.col->$a->$b…`, binding EVERY path segment as a
@@ -1354,7 +1327,9 @@ class PowqlInterface {
1354
1327
  * `gt` / `gte` / `lt` / `lte` compile to `in` over the labels on that side of
1355
1328
  * the operand in DECLARATION order, which is what PostgreSQL compares; the
1356
1329
  * text comparison PowDB would run is alphabetical, a different answer. A null
1357
- * operand matches no row, as `col > NULL` does. Case-insensitive matching and
1330
+ * operand is refused before this (assertOperatorOperands, as on the SQL
1331
+ * engines) and again by {@link assertEnumLabel}: compiled to a constant false,
1332
+ * `NOT` would turn it into every row. Case-insensitive matching and
1358
1333
  * `contains` / `startsWith` / `endsWith` are E003: PostgreSQL has no `lower`
1359
1334
  * or `LIKE` on an enum (42883).
1360
1335
  */
@@ -1365,8 +1340,6 @@ class PowqlInterface {
1365
1340
  }
1366
1341
  if (opName !== 'gt' && opName !== 'gte' && opName !== 'lt' && opName !== 'lte')
1367
1342
  return undefined;
1368
- if (opVal === null)
1369
- return this.alwaysFalse();
1370
1343
  this.assertEnumLabel(col, labels, opVal);
1371
1344
  const at = labels.indexOf(opVal);
1372
1345
  const side = labels.filter((_, i) => opName === 'gt' ? i > at : opName === 'gte' ? i >= at : opName === 'lt' ? i < at : i <= at);
@@ -4102,6 +4075,9 @@ class PowqlInterface {
4102
4075
  // Route tx keywords through the dialect (like the SQL path) so this never
4103
4076
  // drifts from `powdbDialect`; falls back to the literal lowercase keywords.
4104
4077
  const d = this.options.dialect;
4078
+ // Read before the checkout, so an empty slot (client.ts never loaded in this
4079
+ // copy of the package) refuses with no connection taken and no `begin` sent.
4080
+ const TransactionClient = (0, transaction_client_slot_js_1.transactionClientFor)(`A nested write or lookup-first upsert on "${this.table}"`);
4105
4081
  const client = await this.pool.connect();
4106
4082
  // A no-op today (PowDB checkouts have no event surface), kept so this path
4107
4083
  // cannot become the one unguarded checkout if a transport ever emits 'error'.
@@ -4110,7 +4086,6 @@ class PowqlInterface {
4110
4086
  try {
4111
4087
  await client.query(d?.beginStatement?.() ?? 'begin');
4112
4088
  began = true;
4113
- const { TransactionClient } = await Promise.resolve().then(() => __importStar(require('./client.js')));
4114
4089
  const tx = new TransactionClient(client, this.schema, this.middlewares, this.options,
4115
4090
  // Pass the PowDB pool so its read-only guard + capabilities carry into
4116
4091
  // the transaction-scoped proxy pool (see createTxPool).
@@ -56,6 +56,7 @@ const index_advisor_js_1 = require("../index-advisor.js");
56
56
  const logger_js_1 = require("../logger.js");
57
57
  const nested_write_js_1 = require("../nested-write.js");
58
58
  const schema_js_1 = require("../schema.js");
59
+ const transaction_client_slot_js_1 = require("../transaction-client-slot.js");
59
60
  const transaction_state_js_1 = require("../transaction-state.js");
60
61
  const aggMod = __importStar(require("./aggregates.js"));
61
62
  const batched_loader_js_1 = require("./batched-loader.js");
@@ -833,6 +834,7 @@ class QueryInterface {
833
834
  this.sqlCacheEnabled = options?.sqlCache !== false && sqlCacheSize !== 0;
834
835
  this.sqlTemplateCache = new utils_js_1.LRUCache(sqlCacheSize !== undefined && sqlCacheSize > 0 ? Math.floor(sqlCacheSize) : 1000);
835
836
  this.dialect = options?.dialect ?? dialect_js_1.postgresDialect;
837
+ (0, dialect_js_1.assertDialectCapabilities)(this.dialect);
836
838
  this.sqlCommenter =
837
839
  options?._sqlCommenter ??
838
840
  (0, sql_comment_js_1.resolveSqlComment)(options?.sqlComment, this.logger, {
@@ -4447,6 +4449,11 @@ class QueryInterface {
4447
4449
  return (0, checkout_js_1.acquireConnection)(this.pool);
4448
4450
  }
4449
4451
  async runInImplicitTx(fn) {
4452
+ // client.ts registers TransactionClient when it is evaluated (it cannot be
4453
+ // imported from here, see transaction-client-slot.ts). Read before the
4454
+ // checkout, so a program that never loaded client.ts gets a typed error
4455
+ // without a connection having been touched.
4456
+ const TransactionClient = (0, transaction_client_slot_js_1.transactionClientFor)(`A nested write or lookup-first upsert on "${this.table}"`);
4450
4457
  // BEGIN runs inside openCheckout, which sends it once more on a fresh
4451
4458
  // connection when the first one turns out to be dead and releases the
4452
4459
  // connection itself when BEGIN fails for good. The catch below therefore
@@ -4454,7 +4461,6 @@ class QueryInterface {
4454
4461
  // a connection that opened none.
4455
4462
  const { client, checkout } = await (0, checkout_js_1.openCheckout)(this.pool, (c) => c.query(this.dialect.beginStatement()));
4456
4463
  try {
4457
- const { TransactionClient } = await Promise.resolve().then(() => __importStar(require('../client.js')));
4458
4464
  const tx = new TransactionClient(
4459
4465
  // biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
4460
4466
  client, this.schema,
@@ -113,6 +113,7 @@ function resolveDdlType(config, dialect, columnName) {
113
113
  */
114
114
  function schemaToSQL(schema, options) {
115
115
  const dialect = options?.dialect ?? dialect_js_1.postgresDialect;
116
+ (0, dialect_js_1.assertDialectCapabilities)(dialect);
116
117
  const extensions = options?.extensions ?? 'auto';
117
118
  const statements = [];
118
119
  // Topologically sort tables by their foreign key references
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Where the code that opens an implicit transaction finds `TransactionClient`.
3
+ *
4
+ * A nested write (or an upsert that looks its row up first) issued outside a
5
+ * transaction opens one of its own and runs the nested-write engine against a
6
+ * `TransactionClient` (query/builder.ts; powql.ts, the PowDB generator, does
7
+ * the same and can read this slot too). Those modules sit BELOW client.ts in
8
+ * the import graph: client.ts imports query/, so a static value import back
9
+ * would be a cycle, and scripts/check-import-cycles.mjs refuses one.
10
+ *
11
+ * builder.ts used to reach the class with a dynamic import of client.ts.
12
+ * That works, but a bundler that does not split code (esbuild, which is also
13
+ * what the size budget in .size-limit.js measures, and Wrangler) inlines the
14
+ * dynamic target and, to keep its evaluation lazy, wraps client.ts and every
15
+ * module it reaches in lazy-init closures: about 1 kB brotli on every entry,
16
+ * for a module that is always already loaded by the time a query runs.
17
+ * client.ts instead registers the class here once, when it is evaluated, and
18
+ * every public entry point evaluates client.ts.
19
+ *
20
+ * ## One slot per package copy, deliberately not `Symbol.for`
21
+ *
22
+ * The ESM and CJS builds can both be loaded in one process. Shared state that
23
+ * must agree across them (the warn-once registry, the prepared-statement
24
+ * budget) lives under a `Symbol.for` key; this slot must NOT. A builder has to
25
+ * construct the `TransactionClient` of its OWN copy, exactly as the dynamic
26
+ * import did: the other copy's class builds the other copy's accessors and
27
+ * transaction state and throws the other copy's error classes, so `instanceof`
28
+ * checks in the caller's copy would stop matching. Each copy's entry points
29
+ * load that copy's client.ts, so each copy's slot is filled.
30
+ *
31
+ * ## An empty slot
32
+ *
33
+ * Reachable only when a `QueryInterface` is built directly and client.ts was
34
+ * never loaded in that copy: a test or tool that deep-imports query/, or a
35
+ * bundled application that uses the exported `QueryInterface` without ever
36
+ * importing `TurbineClient` or an engine factory (with `sideEffects: false` a
37
+ * bundler may then drop client.ts). Only the implicit-transaction path reads
38
+ * the slot, so everything else keeps working; that path throws a
39
+ * `ValidationError` (E003) naming the fix, before any connection is checked out.
40
+ *
41
+ * The import of client.ts below is type-only, so this file adds no edge to the
42
+ * runtime graph. errors.ts is a leaf.
43
+ */
44
+ import type { TransactionClient } from './client.js';
45
+ /** The `TransactionClient` constructor, as client.ts registers it. */
46
+ export type TransactionClientClass = typeof TransactionClient;
47
+ /** @internal Called once, by client.ts, when it is evaluated. */
48
+ export declare function registerTransactionClient(ctor: TransactionClientClass): void;
49
+ /** @internal The registered class, or `undefined` when client.ts was never loaded in this copy. */
50
+ export declare function registeredTransactionClient(): TransactionClientClass | undefined;
51
+ /**
52
+ * @internal The registered class for an implicit transaction. Throws a
53
+ * `ValidationError` when client.ts was never loaded in this copy of the package.
54
+ * `operation` starts the message, for example `A nested write on "users"`.
55
+ */
56
+ export declare function transactionClientFor(operation: string): TransactionClientClass;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerTransactionClient = registerTransactionClient;
4
+ exports.registeredTransactionClient = registeredTransactionClient;
5
+ exports.transactionClientFor = transactionClientFor;
6
+ const errors_js_1 = require("./errors.js");
7
+ let registered;
8
+ /** @internal Called once, by client.ts, when it is evaluated. */
9
+ function registerTransactionClient(ctor) {
10
+ registered = ctor;
11
+ }
12
+ /** @internal The registered class, or `undefined` when client.ts was never loaded in this copy. */
13
+ function registeredTransactionClient() {
14
+ return registered;
15
+ }
16
+ /**
17
+ * @internal The registered class for an implicit transaction. Throws a
18
+ * `ValidationError` when client.ts was never loaded in this copy of the package.
19
+ * `operation` starts the message, for example `A nested write on "users"`.
20
+ */
21
+ function transactionClientFor(operation) {
22
+ if (registered !== undefined)
23
+ return registered;
24
+ throw new errors_js_1.ValidationError(`${operation} outside a transaction opens one of its own through TurbineClient, ` +
25
+ 'and TurbineClient was never loaded in this program. Import TurbineClient (or an engine factory such as ' +
26
+ "turbineHttp) from 'turbine-orm' before running it, or run the write inside db.$transaction().");
27
+ }
package/dist/client.js CHANGED
@@ -26,7 +26,7 @@ import { openCheckout, typeCheckoutTimeouts } from './checkout.js';
26
26
  import { absorbCheckedOutErrors, settleLongIdleCheckouts, watchCheckouts, } from './connection-guard.js';
27
27
  import { detectPoolerTarget, fillTargetFromEnvironment, parseConnectionTarget, statementTimeoutSetting, withStartupSetting, } from './connection-url.js';
28
28
  import { resolveDiagnostics } from './diagnostics.js';
29
- import { postgresDialect } from './dialect.js';
29
+ import { assertDialectCapabilities, postgresDialect } from './dialect.js';
30
30
  import { ConnectionError, errorMessageModesDiverged, explainConnectionLoss, PipelineError, registerClientErrorMessageMode, runWithErrorMessageMode, setErrorMessageMode, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
31
31
  import { normalizeGlobalFilters, tableAccessorName } from './global-filters-config.js';
32
32
  import { CLIENT_LOG_CONTEXT, logTo, resolveLogger } from './logger.js';
@@ -43,6 +43,7 @@ import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
43
43
  import { currentQueryTag, deferredIdentity, emitStatementEvent, rawIdentity, resultRowCount, runReportedStatement, runWithQueryTag, } from './query-events.js';
44
44
  import { createSubscription, validateChannel, } from './realtime.js';
45
45
  import { parseInt8, rawClientOf, typedQueryPool } from './supplied-pool-types.js';
46
+ import { registerTransactionClient } from './transaction-client-slot.js';
46
47
  import { commitTransaction, rolledBackError, TRANSACTION_STATE, TransactionState, unawaitedWorkError, } from './transaction-state.js';
47
48
  import { buildTypedSql, TypedSqlQuery, utcDateParams } from './typed-sql.js';
48
49
  export async function withRetry(fn, options) {
@@ -556,19 +557,6 @@ function resolveNonNegativeInteger(value, name, unit) {
556
557
  }
557
558
  return value;
558
559
  }
559
- /**
560
- * Refuse a dialect with no `capabilities` object (E003): every code path asks
561
- * it what the engine can do, so a hand-built dialect without one would fail on
562
- * its first query with a TypeError instead of here.
563
- */
564
- function assertDialectCapabilities(dialect) {
565
- const capabilities = dialect.capabilities;
566
- if (typeof capabilities === 'object' && capabilities !== null)
567
- return;
568
- throw new ValidationError(`The dialect "${String(dialect.name)}" has no \`capabilities\` object, which says what the engine can do. ` +
569
- 'Build it from an engine dialect (`{ ...postgresDialect, name, capabilities: { ... } }`) and state every ' +
570
- 'EngineCapabilities key.');
571
- }
572
560
  /**
573
561
  * The statement that opens a transaction, read-only when asked. PostgreSQL and
574
562
  * MySQL both accept `READ ONLY` after their own BEGIN form (`BEGIN [ISOLATION
@@ -1019,6 +1007,10 @@ export class TransactionClient {
1019
1007
  return txPool;
1020
1008
  }
1021
1009
  }
1010
+ // query/builder.ts opens an implicit transaction with this class but sits
1011
+ // below this module in the import graph, so the class is handed over here,
1012
+ // once, when this module is evaluated (see transaction-client-slot.ts).
1013
+ registerTransactionClient(TransactionClient);
1022
1014
  /**
1023
1015
  * Give each multi-statement write on a transaction-scoped accessor its own
1024
1016
  * savepoint: `create` / `update` carrying nested relation writes, and an
package/dist/dialect.d.ts CHANGED
@@ -389,10 +389,11 @@ export interface RelationSubqueryContext {
389
389
  * refuses an engine whose `capabilities` is inherited or shared.
390
390
  *
391
391
  * Code asks these questions instead of testing `dialect.name`; a source scan
392
- * (src/test/capabilities-guard.test.ts) refuses a new engine-name comparison
393
- * outside a short, commented allowlist. Rendering differences are not
394
- * capabilities: they belong in the dialect's hooks (a few still switch on the
395
- * name, allowlisted, until engines stop spreading `postgresDialect`).
392
+ * (src/test/capabilities-guard.test.ts) refuses a new engine-name test, of any
393
+ * shape, outside a short, commented allowlist that pins each site it exempts.
394
+ * Rendering differences are not capabilities: they belong in the dialect's
395
+ * hooks (a few still switch on the name, allowlisted, until engines stop
396
+ * spreading `postgresDialect`).
396
397
  */
397
398
  export interface EngineCapabilities {
398
399
  /** `SELECT DISTINCT ON (...)`: findMany `distinct` and groupBy `distinctOn`. Else E017. */
@@ -586,6 +587,16 @@ export interface EngineCapabilities {
586
587
  */
587
588
  readonly unboundedTextKeys: boolean;
588
589
  }
590
+ /**
591
+ * Refuse a dialect with no `capabilities` object (E003). Every path that builds
592
+ * SQL asks it what the engine can do, so a hand-built dialect written before
593
+ * the object existed would otherwise fail on first use with a TypeError
594
+ * ("cannot read properties of undefined"). Every public entry point that takes
595
+ * a dialect calls this (`TurbineClient`, `QueryInterface`, `schemaToSQL`,
596
+ * `introspect`), so the refusal and its words are the same whichever one a
597
+ * caller reaches first. One `typeof`: `QueryInterface` is built per table.
598
+ */
599
+ export declare function assertDialectCapabilities(dialect: Dialect): void;
589
600
  export interface Dialect {
590
601
  /** Dialect identifier. */
591
602
  readonly name: DialectName;
package/dist/dialect.js CHANGED
@@ -8,6 +8,23 @@
8
8
  import { ValidationError } from './errors.js';
9
9
  import { coerceJsonWireValue, jsonWireCoercionOid } from './query/utils.js';
10
10
  import { pgArrayType, pgTypeToTs } from './schema.js';
11
+ /**
12
+ * Refuse a dialect with no `capabilities` object (E003). Every path that builds
13
+ * SQL asks it what the engine can do, so a hand-built dialect written before
14
+ * the object existed would otherwise fail on first use with a TypeError
15
+ * ("cannot read properties of undefined"). Every public entry point that takes
16
+ * a dialect calls this (`TurbineClient`, `QueryInterface`, `schemaToSQL`,
17
+ * `introspect`), so the refusal and its words are the same whichever one a
18
+ * caller reaches first. One `typeof`: `QueryInterface` is built per table.
19
+ */
20
+ export function assertDialectCapabilities(dialect) {
21
+ const capabilities = dialect.capabilities;
22
+ if (typeof capabilities === 'object' && capabilities !== null)
23
+ return;
24
+ throw new ValidationError(`The dialect "${String(dialect.name)}" has no \`capabilities\` object, which says what the engine can do. ` +
25
+ 'Build it from an engine dialect (`{ ...postgresDialect, name, capabilities: { ... } }`) and state every ' +
26
+ 'EngineCapabilities key.');
27
+ }
11
28
  /** The bind-parameter limit for one statement on this dialect, or `undefined` when it has none. */
12
29
  export function maxBindParameters(dialect) {
13
30
  const limit = dialect.capabilities.maxBindParameters;
@@ -8,7 +8,7 @@
8
8
  * This is the foundation of `npx turbine generate`.
9
9
  */
10
10
  import pg from '#pg';
11
- import { postgresDialect } from './dialect.js';
11
+ import { assertDialectCapabilities, postgresDialect } from './dialect.js';
12
12
  import { ValidationError } from './errors.js';
13
13
  import { consoleLogger, logTo } from './logger.js';
14
14
  import { isDateType, pgTypeToTs, singularize, snakeToCamel, } from './schema.js';
@@ -455,6 +455,7 @@ export function assertSafeCatalogSchema(schema) {
455
455
  */
456
456
  export async function introspect(options) {
457
457
  const dialect = options.dialect ?? postgresDialect;
458
+ assertDialectCapabilities(dialect);
458
459
  const introspector = dialect.introspector;
459
460
  const schema = introspector
460
461
  ? await introspector.introspect(options)
package/dist/powdb.js CHANGED
@@ -59,6 +59,7 @@ import { ConnectionError, malformedConnectionStringMessage, NotNullViolationErro
59
59
  import { consoleLogger, logTo } from './logger.js';
60
60
  import importOptionalPeer from './optional-peer-import.cjs';
61
61
  import { ALL_POWDB_CAPABILITIES, atLeastVersion, isDateColumn, PowdbFloatParam, PowdbJsonParam, parsePowdbSemver, powqlColumnType, quotePowqlIdent, requireCapability, } from './powdb-shared.js';
62
+ import { PowqlInterface } from './powql.js';
62
63
  import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
63
64
  import { normalizeKeyColumns } from './schema.js';
64
65
  // The shared PowDB primitives live in a leaf module (see powdb-shared.ts): this
@@ -1996,8 +1997,6 @@ export async function turbinePowDB(target, schema, options = {}) {
1996
1997
  }
1997
1998
  let client;
1998
1999
  try {
1999
- // The PowQL generator is loaded here to keep client.ts free of any PowDB import.
2000
- const { PowqlInterface } = await import('./powql.js');
2001
2000
  const queryInterfaceFactory = (p, table, sch, middlewares, opts) => new PowqlInterface(p, table, sch, middlewares, opts);
2002
2001
  // Forward every client-level option EXCEPT the ones this factory owns (they
2003
2002
  // configure the pool / transport above, and TurbineClient warns on a config
package/dist/powql.d.ts CHANGED
@@ -486,7 +486,9 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
486
486
  * `gt` / `gte` / `lt` / `lte` compile to `in` over the labels on that side of
487
487
  * the operand in DECLARATION order, which is what PostgreSQL compares; the
488
488
  * text comparison PowDB would run is alphabetical, a different answer. A null
489
- * operand matches no row, as `col > NULL` does. Case-insensitive matching and
489
+ * operand is refused before this (assertOperatorOperands, as on the SQL
490
+ * engines) and again by {@link assertEnumLabel}: compiled to a constant false,
491
+ * `NOT` would turn it into every row. Case-insensitive matching and
490
492
  * `contains` / `startsWith` / `endsWith` are E003: PostgreSQL has no `lower`
491
493
  * or `LIKE` on an enum (42883).
492
494
  */
package/dist/powql.js CHANGED
@@ -47,7 +47,7 @@ import { unlockNestedWriteTx } from './query/builder.js';
47
47
  import { isColumnRef } from './query/column-ref.js';
48
48
  import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, uniqueKeyNames, upsertWhereMatchesCreate, whereIdentifiesOneRow, } from './query/compound-unique.js';
49
49
  import { exprNotAllowedMessage, isSqlExpr } from './query/expr.js';
50
- import { ARRAY_OPERATOR_KEYS, assertJsonNotOperand, assertOperatorOperands, cursorFieldValue, dedupeColumnList, isJsonFilter, isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isUnmatchedPlainObject, isVectorOrderBy, normalizeOrderBy, operandColumnKind, orderByEntries, UPDATE_OPERATOR_KEYS, } from './query/filters.js';
50
+ import { ARRAY_OPERATOR_KEYS, assertOperatorOperands, cursorFieldValue, dedupeColumnList, isJsonFilter, isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isUnmatchedPlainObject, isVectorOrderBy, normalizeOrderBy, operandColumnKind, orderByEntries, UPDATE_OPERATOR_KEYS, } from './query/filters.js';
51
51
  import { isJsonValue } from './query/json-value.js';
52
52
  import { assertCursorNotEmptied, assertCursorWritten, assertWhereNotEmptiedByPruning, hasCursor, pruneUndefinedArgs, } from './query/normalize-args.js';
53
53
  import { warnUnknownQueryOptions } from './query/option-surface.js';
@@ -63,6 +63,7 @@ import { assertJsonFilterKeys, emptyMutationWhereMessage, jsonStringEntries, nor
63
63
  import { combinatorBranches, foldWhereFor, relationCardinalityProblem, relationFilterValueProblem, } from './query/where-compile.js';
64
64
  import { assertNoBulkWritePaging, assertNoOptimisticLock } from './query/writes.js';
65
65
  import { normalizeKeyColumns, snakeToCamel, } from './schema.js';
66
+ import { transactionClientFor } from './transaction-client-slot.js';
66
67
  /**
67
68
  * Max parent keys per relation-loader `in (…)` query. A `with` over a large
68
69
  * parent set is split into batches of this size so a single query never exceeds
@@ -1047,17 +1048,15 @@ export class PowqlInterface {
1047
1048
  // The SQL engines' operand rules and messages, so the engines agree on
1048
1049
  // which operator objects are valid; one naming no comparison
1049
1050
  // (`{ mode: 'insensitive' }`) is refused instead of compiling to nothing.
1050
- const operand = {
1051
+ // JSON columns included, as on the SQL engines: skipping them let a
1052
+ // mode-only filter fall through to the constant-false fallback below, which
1053
+ // `NOT` turned into every non-null row (a deleteMany wrote all of them).
1054
+ // The same call refuses a JSON `not` object made only of filter operators.
1055
+ assertOperatorOperands(op, {
1051
1056
  column: colMeta.name,
1052
1057
  table: this.table,
1053
1058
  kind: operandColumnKind(this.meta.pgTypes[colMeta.name]),
1054
- };
1055
- if (!isJsonColumn(colMeta, this.schema.enums))
1056
- assertOperatorOperands(op, operand);
1057
- // On a JSON column a `not` object made only of filter operators is refused,
1058
- // as on the SQL engines (one spelling, one meaning on every engine).
1059
- else
1060
- assertJsonNotOperand(op.not, operand);
1059
+ });
1061
1060
  const insensitive = op.mode === 'insensitive';
1062
1061
  const lhs = insensitive ? `lower(${ref})` : ref;
1063
1062
  const labels = this.enumLabels(colMeta);
@@ -1143,7 +1142,14 @@ export class PowqlInterface {
1143
1142
  throw new ValidationError(`Unsupported operator "${opName}" on PowDB.`);
1144
1143
  }
1145
1144
  }
1146
- return conds.length > 1 ? `(${conds.join(' and ')})` : (conds[0] ?? this.alwaysFalse());
1145
+ // assertOperatorOperands refuses an operator object that names no
1146
+ // comparison, and every comparison above pushes a condition or throws, so
1147
+ // an empty list is a generator bug. It must not become a constant: under
1148
+ // `NOT` a constant false is every row, and a mutation would write them.
1149
+ if (!conds.length) {
1150
+ throw new InvariantError('powql-filter-empty', `the filter on "${field}" compiled to no condition.`);
1151
+ }
1152
+ return conds.length > 1 ? `(${conds.join(' and ')})` : conds[0];
1147
1153
  }
1148
1154
  /**
1149
1155
  * PowQL JSON path expression `.col->$a->$b…`, binding EVERY path segment as a
@@ -1318,7 +1324,9 @@ export class PowqlInterface {
1318
1324
  * `gt` / `gte` / `lt` / `lte` compile to `in` over the labels on that side of
1319
1325
  * the operand in DECLARATION order, which is what PostgreSQL compares; the
1320
1326
  * text comparison PowDB would run is alphabetical, a different answer. A null
1321
- * operand matches no row, as `col > NULL` does. Case-insensitive matching and
1327
+ * operand is refused before this (assertOperatorOperands, as on the SQL
1328
+ * engines) and again by {@link assertEnumLabel}: compiled to a constant false,
1329
+ * `NOT` would turn it into every row. Case-insensitive matching and
1322
1330
  * `contains` / `startsWith` / `endsWith` are E003: PostgreSQL has no `lower`
1323
1331
  * or `LIKE` on an enum (42883).
1324
1332
  */
@@ -1329,8 +1337,6 @@ export class PowqlInterface {
1329
1337
  }
1330
1338
  if (opName !== 'gt' && opName !== 'gte' && opName !== 'lt' && opName !== 'lte')
1331
1339
  return undefined;
1332
- if (opVal === null)
1333
- return this.alwaysFalse();
1334
1340
  this.assertEnumLabel(col, labels, opVal);
1335
1341
  const at = labels.indexOf(opVal);
1336
1342
  const side = labels.filter((_, i) => opName === 'gt' ? i > at : opName === 'gte' ? i >= at : opName === 'lt' ? i < at : i <= at);
@@ -4066,6 +4072,9 @@ export class PowqlInterface {
4066
4072
  // Route tx keywords through the dialect (like the SQL path) so this never
4067
4073
  // drifts from `powdbDialect`; falls back to the literal lowercase keywords.
4068
4074
  const d = this.options.dialect;
4075
+ // Read before the checkout, so an empty slot (client.ts never loaded in this
4076
+ // copy of the package) refuses with no connection taken and no `begin` sent.
4077
+ const TransactionClient = transactionClientFor(`A nested write or lookup-first upsert on "${this.table}"`);
4069
4078
  const client = await this.pool.connect();
4070
4079
  // A no-op today (PowDB checkouts have no event surface), kept so this path
4071
4080
  // cannot become the one unguarded checkout if a transport ever emits 'error'.
@@ -4074,7 +4083,6 @@ export class PowqlInterface {
4074
4083
  try {
4075
4084
  await client.query(d?.beginStatement?.() ?? 'begin');
4076
4085
  began = true;
4077
- const { TransactionClient } = await import('./client.js');
4078
4086
  const tx = new TransactionClient(client, this.schema, this.middlewares, this.options,
4079
4087
  // Pass the PowDB pool so its read-only guard + capabilities carry into
4080
4088
  // the transaction-scoped proxy pool (see createTxPool).
@@ -13,12 +13,13 @@
13
13
  import { acquireConnection, openCheckout } from '../checkout.js';
14
14
  import { settleEventLoop } from '../connection-guard.js';
15
15
  import { resolveDiagnostics } from '../diagnostics.js';
16
- import { postgresDialect } from '../dialect.js';
16
+ import { assertDialectCapabilities, postgresDialect } from '../dialect.js';
17
17
  import { explainConnectionLoss, INVARIANT_REPORT_HINT, InvariantError, isStaleConnectionError, NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from '../errors.js';
18
18
  import { missingIndexForRelation, schemaHasIndexInfo } from '../index-advisor.js';
19
19
  import { consoleLogger, logTo } from '../logger.js';
20
20
  import { executeNestedCreate, executeNestedUpdate, extractRelationFields, hasRelationFields, } from '../nested-write.js';
21
21
  import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
22
+ import { transactionClientFor } from '../transaction-client-slot.js';
22
23
  import { transactionStateOf, withSavepoint } from '../transaction-state.js';
23
24
  import * as aggMod from './aggregates.js';
24
25
  import { assertProjectionShape, defaultProjectionFields, includeKeysForBatching, loadRelationsBatched, neededParentKeyFields, rejectNestedPickOrder, resolveCountRelations, stripFields, } from './batched-loader.js';
@@ -796,6 +797,7 @@ export class QueryInterface {
796
797
  this.sqlCacheEnabled = options?.sqlCache !== false && sqlCacheSize !== 0;
797
798
  this.sqlTemplateCache = new LRUCache(sqlCacheSize !== undefined && sqlCacheSize > 0 ? Math.floor(sqlCacheSize) : 1000);
798
799
  this.dialect = options?.dialect ?? postgresDialect;
800
+ assertDialectCapabilities(this.dialect);
799
801
  this.sqlCommenter =
800
802
  options?._sqlCommenter ??
801
803
  resolveSqlComment(options?.sqlComment, this.logger, {
@@ -4410,6 +4412,11 @@ export class QueryInterface {
4410
4412
  return acquireConnection(this.pool);
4411
4413
  }
4412
4414
  async runInImplicitTx(fn) {
4415
+ // client.ts registers TransactionClient when it is evaluated (it cannot be
4416
+ // imported from here, see transaction-client-slot.ts). Read before the
4417
+ // checkout, so a program that never loaded client.ts gets a typed error
4418
+ // without a connection having been touched.
4419
+ const TransactionClient = transactionClientFor(`A nested write or lookup-first upsert on "${this.table}"`);
4413
4420
  // BEGIN runs inside openCheckout, which sends it once more on a fresh
4414
4421
  // connection when the first one turns out to be dead and releases the
4415
4422
  // connection itself when BEGIN fails for good. The catch below therefore
@@ -4417,7 +4424,6 @@ export class QueryInterface {
4417
4424
  // a connection that opened none.
4418
4425
  const { client, checkout } = await openCheckout(this.pool, (c) => c.query(this.dialect.beginStatement()));
4419
4426
  try {
4420
- const { TransactionClient } = await import('../client.js');
4421
4427
  const tx = new TransactionClient(
4422
4428
  // biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
4423
4429
  client, this.schema,
@@ -7,7 +7,7 @@
7
7
  import pg from 'pg';
8
8
  import { DESTRUCTIVE_KIND_LABEL, scanDestructiveSql } from './cli/destructive.js';
9
9
  import { guardConnection } from './connection-guard.js';
10
- import { postgresDialect } from './dialect.js';
10
+ import { assertDialectCapabilities, postgresDialect } from './dialect.js';
11
11
  import { UnsupportedFeatureError, ValidationError } from './errors.js';
12
12
  import { pgConfActionToReferential, stripCheckWrapper } from './introspect.js';
13
13
  import { camelToSnake } from './schema.js';
@@ -92,6 +92,7 @@ function resolveDdlType(config, dialect, columnName) {
92
92
  */
93
93
  export function schemaToSQL(schema, options) {
94
94
  const dialect = options?.dialect ?? postgresDialect;
95
+ assertDialectCapabilities(dialect);
95
96
  const extensions = options?.extensions ?? 'auto';
96
97
  const statements = [];
97
98
  // Topologically sort tables by their foreign key references
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Where the code that opens an implicit transaction finds `TransactionClient`.
3
+ *
4
+ * A nested write (or an upsert that looks its row up first) issued outside a
5
+ * transaction opens one of its own and runs the nested-write engine against a
6
+ * `TransactionClient` (query/builder.ts; powql.ts, the PowDB generator, does
7
+ * the same and can read this slot too). Those modules sit BELOW client.ts in
8
+ * the import graph: client.ts imports query/, so a static value import back
9
+ * would be a cycle, and scripts/check-import-cycles.mjs refuses one.
10
+ *
11
+ * builder.ts used to reach the class with a dynamic import of client.ts.
12
+ * That works, but a bundler that does not split code (esbuild, which is also
13
+ * what the size budget in .size-limit.js measures, and Wrangler) inlines the
14
+ * dynamic target and, to keep its evaluation lazy, wraps client.ts and every
15
+ * module it reaches in lazy-init closures: about 1 kB brotli on every entry,
16
+ * for a module that is always already loaded by the time a query runs.
17
+ * client.ts instead registers the class here once, when it is evaluated, and
18
+ * every public entry point evaluates client.ts.
19
+ *
20
+ * ## One slot per package copy, deliberately not `Symbol.for`
21
+ *
22
+ * The ESM and CJS builds can both be loaded in one process. Shared state that
23
+ * must agree across them (the warn-once registry, the prepared-statement
24
+ * budget) lives under a `Symbol.for` key; this slot must NOT. A builder has to
25
+ * construct the `TransactionClient` of its OWN copy, exactly as the dynamic
26
+ * import did: the other copy's class builds the other copy's accessors and
27
+ * transaction state and throws the other copy's error classes, so `instanceof`
28
+ * checks in the caller's copy would stop matching. Each copy's entry points
29
+ * load that copy's client.ts, so each copy's slot is filled.
30
+ *
31
+ * ## An empty slot
32
+ *
33
+ * Reachable only when a `QueryInterface` is built directly and client.ts was
34
+ * never loaded in that copy: a test or tool that deep-imports query/, or a
35
+ * bundled application that uses the exported `QueryInterface` without ever
36
+ * importing `TurbineClient` or an engine factory (with `sideEffects: false` a
37
+ * bundler may then drop client.ts). Only the implicit-transaction path reads
38
+ * the slot, so everything else keeps working; that path throws a
39
+ * `ValidationError` (E003) naming the fix, before any connection is checked out.
40
+ *
41
+ * The import of client.ts below is type-only, so this file adds no edge to the
42
+ * runtime graph. errors.ts is a leaf.
43
+ */
44
+ import type { TransactionClient } from './client.js';
45
+ /** The `TransactionClient` constructor, as client.ts registers it. */
46
+ export type TransactionClientClass = typeof TransactionClient;
47
+ /** @internal Called once, by client.ts, when it is evaluated. */
48
+ export declare function registerTransactionClient(ctor: TransactionClientClass): void;
49
+ /** @internal The registered class, or `undefined` when client.ts was never loaded in this copy. */
50
+ export declare function registeredTransactionClient(): TransactionClientClass | undefined;
51
+ /**
52
+ * @internal The registered class for an implicit transaction. Throws a
53
+ * `ValidationError` when client.ts was never loaded in this copy of the package.
54
+ * `operation` starts the message, for example `A nested write on "users"`.
55
+ */
56
+ export declare function transactionClientFor(operation: string): TransactionClientClass;
@@ -0,0 +1,22 @@
1
+ import { ValidationError } from './errors.js';
2
+ let registered;
3
+ /** @internal Called once, by client.ts, when it is evaluated. */
4
+ export function registerTransactionClient(ctor) {
5
+ registered = ctor;
6
+ }
7
+ /** @internal The registered class, or `undefined` when client.ts was never loaded in this copy. */
8
+ export function registeredTransactionClient() {
9
+ return registered;
10
+ }
11
+ /**
12
+ * @internal The registered class for an implicit transaction. Throws a
13
+ * `ValidationError` when client.ts was never loaded in this copy of the package.
14
+ * `operation` starts the message, for example `A nested write on "users"`.
15
+ */
16
+ export function transactionClientFor(operation) {
17
+ if (registered !== undefined)
18
+ return registered;
19
+ throw new ValidationError(`${operation} outside a transaction opens one of its own through TurbineClient, ` +
20
+ 'and TurbineClient was never loaded in this program. Import TurbineClient (or an engine factory such as ' +
21
+ "turbineHttp) from 'turbine-orm' before running it, or run the write inside db.$transaction().");
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.90.0-next.25511aa",
3
+ "version": "0.90.1",
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).",
@@ -208,23 +208,23 @@
208
208
  },
209
209
  "devDependencies": {
210
210
  "@arethetypeswrong/cli": "^0.18.2",
211
- "@biomejs/biome": "^2.4.10",
211
+ "@biomejs/biome": "^2.5.14",
212
212
  "@size-limit/esbuild": "^12.1.0",
213
213
  "@size-limit/file": "^12.1.0",
214
214
  "@swc/core": "1.16.2",
215
215
  "@types/node": "^22.20.4",
216
- "@types/pg": "^8.11.11",
216
+ "@types/pg": "^8.23.1",
217
217
  "@zvndev/powdb-client": "^0.20.0",
218
218
  "@zvndev/powdb-embedded": "^0.20.0",
219
219
  "c8": "^12.0.0",
220
220
  "esbuild": "^0.28.2",
221
221
  "husky": "^9.1.7",
222
- "lint-staged": "^17.0.8",
223
- "mssql": "^12.7.0",
224
- "mysql2": "^3.22.5",
225
- "publint": "^0.3.16",
222
+ "lint-staged": "^17.6.0",
223
+ "mssql": "^12.7.2",
224
+ "mysql2": "^3.24.5",
225
+ "publint": "^0.3.24",
226
226
  "size-limit": "^12.1.0",
227
- "tsx": "^4.19.0",
227
+ "tsx": "^4.23.15",
228
228
  "typescript": "^6.0.3"
229
229
  },
230
230
  "peerDependencies": {