turbine-orm 0.33.0 → 0.35.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 (47) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/client.js +26 -4
  3. package/dist/cjs/dialect.js +1 -0
  4. package/dist/cjs/errors.js +41 -1
  5. package/dist/cjs/index-advisor.js +0 -0
  6. package/dist/cjs/index.js +4 -2
  7. package/dist/cjs/mssql.js +5 -0
  8. package/dist/cjs/mysql.js +4 -0
  9. package/dist/cjs/optional-peer-import.cjs +28 -0
  10. package/dist/cjs/powdb-introspect.js +222 -0
  11. package/dist/cjs/powdb.js +592 -72
  12. package/dist/cjs/powql.js +998 -134
  13. package/dist/cjs/query/builder.js +72 -1
  14. package/dist/cjs/schema-builder.js +16 -0
  15. package/dist/cjs/schema-metadata.js +81 -10
  16. package/dist/cjs/sqlite.js +3 -0
  17. package/dist/client.d.ts +32 -5
  18. package/dist/client.js +26 -4
  19. package/dist/dialect.d.ts +13 -0
  20. package/dist/dialect.js +1 -0
  21. package/dist/errors.d.ts +36 -0
  22. package/dist/errors.js +39 -0
  23. package/dist/index-advisor.d.ts +15 -1
  24. package/dist/index-advisor.js +0 -0
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +2 -2
  27. package/dist/mssql.js +5 -0
  28. package/dist/mysql.js +4 -0
  29. package/dist/optional-peer-import.cjs +28 -0
  30. package/dist/optional-peer-import.d.cts +19 -0
  31. package/dist/powdb-introspect.d.ts +84 -0
  32. package/dist/powdb-introspect.js +219 -0
  33. package/dist/powdb.d.ts +361 -19
  34. package/dist/powdb.js +585 -72
  35. package/dist/powql.d.ts +245 -8
  36. package/dist/powql.js +1001 -137
  37. package/dist/query/builder.d.ts +36 -1
  38. package/dist/query/builder.js +72 -1
  39. package/dist/query/deferred.d.ts +6 -2
  40. package/dist/query/types.d.ts +49 -12
  41. package/dist/schema-builder.d.ts +46 -1
  42. package/dist/schema-builder.js +15 -0
  43. package/dist/schema-metadata.d.ts +13 -7
  44. package/dist/schema-metadata.js +82 -11
  45. package/dist/schema.d.ts +25 -0
  46. package/dist/sqlite.js +3 -0
  47. package/package.json +3 -3
package/dist/powql.d.ts CHANGED
@@ -53,13 +53,20 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
53
53
  private readonly defaultLimit?;
54
54
  private readonly warnOnUnlimited;
55
55
  private readonly onQuery?;
56
- private currentAction;
57
56
  private warnedUnlimited;
58
57
  constructor(pool: PowdbPool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
59
58
  /** Resolve a camelCase field name (or raw snake) to its column metadata. */
60
59
  private column;
61
- /** PowQL column reference (`.snake_name`) for a field. */
60
+ /**
61
+ * PowQL column reference for a field. Unqualified it is a dotted field
62
+ * reference (`.snake_name`), which bypasses keyword lookup. When an `alias`
63
+ * is supplied (the F2 join path) it is qualified (`alias.snake_name`) and the
64
+ * column name is backtick-quoted if it is a reserved word (a qualified
65
+ * `p.order` does NOT bypass keyword lookup, unlike the dotted `.order`).
66
+ */
62
67
  private ref;
68
+ /** Render a raw column name as a PowQL reference, qualified with `alias` when given. */
69
+ private colRefName;
63
70
  /**
64
71
  * Push a value into the param array and return its `$N` placeholder. When the
65
72
  * value targets a `float` column it is wrapped in {@link PowdbFloatParam} so
@@ -81,15 +88,67 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
81
88
  */
82
89
  private writeRef;
83
90
  private isFloatCol;
91
+ /**
92
+ * The bound pool's {@link PowdbCapabilities}. Falls back to the trusted-caller
93
+ * default (all feature gates on, `nativeRaw` off) when a directly-constructed
94
+ * pool did not carry them, matching {@link PowdbPool}'s own constructor
95
+ * default so a hand-built test pool never crashes the version gates.
96
+ */
97
+ private get capabilities();
84
98
  /** A predicate that is always false — the empty-`in` / contradiction sentinel. */
85
99
  private alwaysFalse;
86
100
  /**
87
101
  * Compile a {@link WhereClause} into a PowQL filter expression, pushing every
88
102
  * value as a positional `$N` param. Returns `''` when there are no conditions.
103
+ *
104
+ * When `alias` is supplied (the F2 native-join path) every field reference is
105
+ * qualified with it (`.col` → `alias.col`, JSON path bases too); params bind
106
+ * exactly as in the unqualified path. The caller only ever passes an alias for
107
+ * an already-RESOLVED where (relation filters pre-resolved to literal in-lists
108
+ * by {@link resolveRelationFilters}): the relation-key branch below still
109
+ * throws, so an unresolved relation filter can never leak into a join.
89
110
  */
90
111
  private buildWhere;
91
112
  /** Build a single `field: value | operator` condition. */
92
113
  private buildFieldCondition;
114
+ /**
115
+ * PowQL JSON path expression `.col->$a->$b…`, binding EVERY path segment as a
116
+ * positional param (a string segment as a `str` token, an integer index as an
117
+ * `int` token). `->` binds tighter than every operator, so no parens are
118
+ * needed around the path in a comparison. Segments are bound (never inlined)
119
+ * to keep {@link materializePowql}'s `$N`-scan invariant intact: a segment
120
+ * that literally contained `$1` would otherwise be rewritten. Shared by the
121
+ * F1 where-filter path and the F2 orderBy / groupBy path emitters.
122
+ *
123
+ * A digit-only STRING segment (`'0'`) binds as an `int` array index, matching
124
+ * the SQL engines: `JsonFilter.path` is typed `string[]`, so an array index
125
+ * can only be expressed as a digit string, and the SQL builder converts it the
126
+ * same way (`/^\d+$/ → [n]`, query/builder.ts). Without this, PowDB's typed
127
+ * `->` treats `'0'` as a string KEY and silently matches nothing on an array
128
+ * (a wrong result, not an error). Same object-key-`'0'` caveat SQL accepts: a
129
+ * json object whose key is literally `"0"` is addressed as an array index.
130
+ */
131
+ private jsonPathExpr;
132
+ /**
133
+ * Compile a {@link JsonFilter} on a json document column into a PowQL filter
134
+ * (≥ 0.12). Operators PowQL cannot express EXACTLY throw a per-operator E017
135
+ * (never a wrong result): containment (`contains`, and `equals` without a
136
+ * `path`) has no PowQL operator. The mapped shapes:
137
+ * - `{ path, equals: v }` → `P = $n` (typed: string→str, bool→bool,
138
+ * integral number→int, fractional→float; NOT stringified)
139
+ * - `{ path, equals: null }` → `P is null` (matches JSON null AND a missing
140
+ * key, a deliberate divergence from the PG driver, documented on
141
+ * {@link JsonFilter})
142
+ * - `{ path, gt|gte|lt|lte: v }` → `P > $n` … (range ops require `path`; the
143
+ * engine coerces int/float numerically)
144
+ * - `{ hasKey: k }` → `json_type(.col->$n) is not null` (top-level key test,
145
+ * ignoring `path`, mirroring PG `col ? key`; includes keys holding JSON
146
+ * null)
147
+ * A bare `{ path }` with no operators compiles to zero clauses (byte-parity
148
+ * with SQL), so a mutation whose only `where` is a bare `{ path }` is refused
149
+ * by the empty-where guard.
150
+ */
151
+ private buildJsonPathCondition;
93
152
  /** Bind a value, lowercasing for case-insensitive comparisons. */
94
153
  private bind;
95
154
  /** Bind a LIKE pattern (already escaped), lowercasing for insensitive mode. */
@@ -120,23 +179,99 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
120
179
  private projectedColumns;
121
180
  /** `{ .c1, .c2, … }` projection clause. */
122
181
  private projection;
123
- /** `order .c1 asc, .c2 desc` clause (empty string when no orderBy). */
182
+ /**
183
+ * `order .c1 asc, .c2 desc` clause (empty string when no orderBy). Supports,
184
+ * besides a plain direction:
185
+ * - {@link JsonPathOrderBy} on a json column (≥ 0.12): `{ data: { path: […],
186
+ * type?, direction? } }` → `order .data->$n asc` (or
187
+ * `cast(.data->$n, "float")` for `type: 'numeric'`);
188
+ * - {@link OrderBySpec} `{ sort, nulls }`: `nulls: 'last'` is accepted as a
189
+ * no-op (PowDB is always nulls-last), `nulls: 'first'` throws E017.
190
+ *
191
+ * PowDB orders missing / JSON-null keys LAST in BOTH directions (an engine
192
+ * contract): for identical cross-engine results pass `nulls: 'last'`
193
+ * explicitly on Postgres, which defaults nulls-first for `desc`.
194
+ */
124
195
  private buildOrder;
125
- /** Run PowQL with optional timeout, emitting a query event either way. */
196
+ /** Compile one {@link JsonPathOrderBy} entry to `order .col->$n asc` (+ optional numeric cast). */
197
+ private buildJsonPathOrder;
198
+ /**
199
+ * Run PowQL with optional timeout, emitting a query event either way. The
200
+ * `action` is passed PER CALL (never read from shared instance state) so the
201
+ * retry-eligibility and the emitted event action stay correct even when a
202
+ * concurrent operation runs on the same cached interface: a WRITE statement
203
+ * carries a write action and can therefore never be mistaken for a replayable
204
+ * read. Read statements pass a read-shaped action from {@link POWQL_READ_ACTIONS}.
205
+ */
126
206
  private exec;
207
+ /** Build the E018 refusal for a write / `begin` on a read-only pool. */
208
+ private readOnlyError;
209
+ /**
210
+ * Execute one statement, with the opt-in single stale-frame READ replay. When
211
+ * `retryStaleReads` is on and a first-statement READ fails with the stale-wire
212
+ * {@link isStaleFramePowdbError} ConnectionError (a socket idle-gap "received
213
+ * unexpected frame" that the client cannot recover), the statement is retried
214
+ * exactly once on a fresh pooled connection (the broken one was destroyed).
215
+ * The replay is refused for writes (an ambiguous mutation reply is unsafe to
216
+ * replay) and inside a transaction (a mid-tx statement cannot move connection),
217
+ * so only the read-shaped actions in {@link POWQL_READ_ACTIONS}, outside a
218
+ * `_txScoped` interface, are eligible. `action` is a per-call argument (never
219
+ * `this`-state), so a concurrent op flipping instance fields cannot turn a
220
+ * write into a retryable read.
221
+ */
222
+ private execOnce;
223
+ /** Is `err` a replayable stale-frame failure for THIS (per-call) read-shaped, non-tx action? */
224
+ private shouldRetryStaleRead;
127
225
  private emit;
128
226
  /** Run a method body through the middleware chain (mirrors QueryInterface). */
129
227
  private withMiddleware;
130
- /** Map raw rows to typed entities. */
228
+ /** Map raw rows to typed entities. `native` is the wire that ACTUALLY served
229
+ * this result (threaded from {@link execOnce}, not the pool-level capability),
230
+ * so cells that arrived pre-typed over `queryNativeRaw` (F3) skip the legacy
231
+ * string coercion (a genuine str `"null"` stays `"null"` instead of collapsing
232
+ * to null) while a per-call legacy fallback on a native-capable pool still
233
+ * coerces its string cells correctly. Defaults to the pool capability for the
234
+ * rare caller with no per-result flag (hand-built test pools). */
131
235
  private shape;
132
236
  findMany(args?: FindManyArgs<T>): Promise<T[]>;
133
- /** Build + run the flat findMany select; returns raw rows. */
237
+ /**
238
+ * Compile the flat findMany select into PowQL (no execution), pushing values
239
+ * into `params`. Returns the query plus the RESOLVED where (relation filters
240
+ * already collapsed to literal in-lists) so the F2 join path can re-emit the
241
+ * exact parent predicate alias-qualified, and so {@link explain} can wrap it.
242
+ */
243
+ private buildFind;
244
+ /** Build + run the flat findMany select; returns raw rows, the serving wire, and the resolved where. */
134
245
  private runFind;
246
+ /**
247
+ * Diagnostic surface: compile the same PowQL {@link findMany} would run for
248
+ * `args` (no cache) and return the engine's plan as one string per line.
249
+ *
250
+ * Runs as a READ (`explain <query>`), so it is safe on a read-only pool and
251
+ * eligible for the stale-read replay. The line content is engine-owned and is
252
+ * NOT covered by semver (match plan node names / tree shape, never exact
253
+ * bytes; mirrors PowDB's own `explain` contract).
254
+ *
255
+ * Does NOT run through the middleware chain: plan text is a diagnostic, not
256
+ * entity rows, and `QueryInterface.explain` deliberately bypasses middleware
257
+ * too, so both engines agree.
258
+ */
259
+ explain(args?: FindManyArgs<T>): Promise<string[]>;
135
260
  findUnique(args: FindUniqueArgs<T>): Promise<T | null>;
136
261
  findFirst(args?: FindManyArgs<T>): Promise<T | null>;
137
262
  findUniqueOrThrow(args: FindUniqueArgs<T>): Promise<T>;
138
263
  findFirstOrThrow(args?: FindManyArgs<T>): Promise<T>;
139
- /** Load each requested relation for `parents` and attach it onto each row. */
264
+ /**
265
+ * Load each requested relation for `parents` and attach it onto each row.
266
+ *
267
+ * `parent` is supplied ONLY by the top-level {@link findMany} (its args +
268
+ * resolved where). When the effective `relationLoadStrategy` resolves to an
269
+ * explicit `'join'` and the pool advertises `serverJoins`, an eligible
270
+ * top-level relation is loaded with a native PowQL join instead of the keyed
271
+ * loaders (F2); everything else (nested `with` levels, ineligible shapes, and
272
+ * the default `'batched'` strategy) keeps the loaders. Output is byte-equal
273
+ * either way (the join reuses the same stitch / shape helpers).
274
+ */
140
275
  private loadRelations;
141
276
  /**
142
277
  * manyToMany nested read — a three-hop batched loader (no `json_agg`/join
@@ -147,6 +282,88 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
147
282
  * (composite junction keys would need PowQL tuple-`in`, which it lacks).
148
283
  */
149
284
  private loadManyToMany;
285
+ /**
286
+ * Resolve the effective relation-load strategy: the per-query arg wins, then
287
+ * the client config, then the PowDB default of `'batched'` (the keyed
288
+ * loaders). PowDB deliberately does NOT inherit the SQL-side implicit `'join'`
289
+ * default (that would silently flip every existing PowDB user onto brand-new
290
+ * join generation). Only a value the user actually set to `'join'` activates it.
291
+ */
292
+ private resolveStrategy;
293
+ /**
294
+ * Per-relation eligibility for the join path (checked before the serverJoins
295
+ * capability). Any `false` here is a SILENT fallback to the keyed loaders (it
296
+ * is never an error), so an off-page or nested-`with` shape still returns
297
+ * correct rows:
298
+ * - the parent query must not be paged (`limit`/`offset`/`take`, including the
299
+ * configured `defaultLimit`): a parent-filter join under a page would scan
300
+ * children of off-page parents, where the loaders are strictly better;
301
+ * - the relation must not request a nested `with` (its subtree stays on the
302
+ * loaders this round) or a `distinct`;
303
+ * - single-column relation keys only (a composite key falls to the loader,
304
+ * which throws the same E017 as today);
305
+ * - the PARENT-SIDE correlation column must be a single-column PK or unique
306
+ * column, or the INNER join would re-emit one child copy per matching
307
+ * parent row (a non-unique correlation key produces duplicate children the
308
+ * loader never would). For hasMany/hasOne/m2m that column is the relation's
309
+ * `referenceKey` on THIS (fetched) table; for belongsTo it is the
310
+ * `referenceKey` on the TARGET table (the join's non-fetched side);
311
+ * - m2m keeps any `orderBy`/`limit`/`offset` on the loader (the junction-order
312
+ * stitch can't be reproduced by the 3-table join deterministically);
313
+ * - a to-one relation `limit`/`offset` (meaningless) stays on the loader, as
314
+ * does a to-many relation `limit`/`offset` when the parent set spills past
315
+ * one loader chunk (the loader limits per chunk, the join once globally).
316
+ */
317
+ private joinEligible;
318
+ /**
319
+ * True when `col` is a single-column unique key of `tableMeta`: the sole
320
+ * primary-key column, a single-column entry in `uniqueColumns` (where a
321
+ * per-column `unique: true` and an introspected single-column unique constraint
322
+ * both land), or a single-column unique index. Used by {@link joinEligible} to
323
+ * keep the INNER-join path off relations whose parent-side correlation column
324
+ * can repeat (which would duplicate children).
325
+ */
326
+ private isSingleColumnUnique;
327
+ /** Dispatch one eligible relation to the correct native-join loader. */
328
+ private loadRelationViaJoin;
329
+ /**
330
+ * manyToMany via chained joins: the target (alias `t`) → junction (alias `j`)
331
+ * → the already-fetched side (alias `p`), correlating `__tpk` from the
332
+ * junction's source key. Always a list, stitched exactly like the loader.
333
+ */
334
+ private loadManyToManyViaJoin;
335
+ /**
336
+ * The target column list to project through the join (honouring select/omit),
337
+ * with a loud guard: a real column named `__tpk` would collide with the
338
+ * reserved correlation alias, so refuse rather than silently mis-stitch.
339
+ */
340
+ private joinChildCols;
341
+ /**
342
+ * `{ __tpk: <tpkExpr>, <col>: <childAlias>.<col>, … }`. Each child column is
343
+ * ALIASED to its bare name (a bare qualified ref `c.col` would come back named
344
+ * `c.col`, not `col`) so the stitched rows shape identically to a flat select.
345
+ */
346
+ private joinProjection;
347
+ /**
348
+ * `filter <parentWhere qualified p> [and <relationWhere qualified childAlias>]`.
349
+ * The parent where is the ALREADY-RESOLVED predicate (relation filters collapsed
350
+ * to literal in-lists before the base query ran); the relation where is resolved
351
+ * on the target the same way before qualifying, so a nested relation filter in
352
+ * the relation `where` never reaches the join unresolved. Params bind in order.
353
+ */
354
+ private joinFilter;
355
+ /** Group join rows by their (normalized) `__tpk`, stripping it and shaping each child. */
356
+ private bucketByTpk;
357
+ /**
358
+ * Normalize a correlation key to a stable string map key so a parent's key
359
+ * value (a shaped entity field) and a child row's `__tpk` cell match across
360
+ * wires and column types. A `Date` maps to microseconds
361
+ * (`getTime()` ms times 1000), because a datetime correlation cell arrives as
362
+ * raw micros (bigint on the native wire, a micros string on the legacy wire),
363
+ * never as ms. bigint / number / string all stringify to the same digits, so
364
+ * an int key matches whether it came back typed or as text.
365
+ */
366
+ private joinKey;
150
367
  /** Split `data` into scalar assignments; reject relation (nested-write) keys. */
151
368
  private scalarData;
152
369
  /**
@@ -192,8 +409,28 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
192
409
  count(args?: CountArgs<T>): Promise<number>;
193
410
  aggregate(args: AggregateArgs<T>): Promise<AggregateResult<T>>;
194
411
  groupBy(args: GroupByArgs<T>): Promise<Record<string, unknown>[]>;
195
- /** `having <expr>` over group aggregates (count/sum/avg/min/max). */
412
+ /** Validate a JSON-path target (group key / aggregate target): non-empty array of keys/indexes. */
413
+ private assertJsonPath;
414
+ /**
415
+ * `having <expr>` over group aggregates. `_count` compares `count(*)` (parity
416
+ * with the projection); a per-field aggregate re-emits its inner expression
417
+ * (from `aggInner` when the field is a requested aggregate, so a JSON-path
418
+ * aggregate reuses its bound placeholders, else `.field` for a plain column).
419
+ */
196
420
  private buildHaving;
421
+ /**
422
+ * Compile a groupBy `orderBy` into a PowQL `order` body over the group RESULT
423
+ * columns (by-fields, JSON group-key aliases, and requested aggregates). PowQL
424
+ * cannot re-emit an aggregate EXPRESSION in `order` (engine error), but CAN
425
+ * order by a projection alias on a grouped query (probed), so each key maps to
426
+ * its projected alias (`.agg_N` / `.gk_N` / `.col`). Semantics and error
427
+ * surface mirror the SQL `buildGroupByOrderBy` (0.32.2 R3-1): an aggregate not
428
+ * requested in this call, or an unknown by-key, throws E003 listing the valid
429
+ * keys. `nulls: 'first'` stays E017 (PowDB has no NULLS placement grammar).
430
+ */
431
+ private buildGroupOrder;
432
+ /** Resolve a groupBy order direction, refusing `nulls: 'first'` (E017); `nulls: 'last'` is a no-op. */
433
+ private groupOrderDir;
197
434
  findManyStream(): AsyncGenerator<T>;
198
435
  /** Reselect a single row by its single-column primary key value. */
199
436
  private reselectByPk;