turbine-orm 0.91.0-next.e72e9b4 → 0.92.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.
@@ -21,7 +21,9 @@
21
21
  * writable types (`str`/`int`/`float`/`bool`); `Date` → `int` epoch micros;
22
22
  * `string` PKs hold UUID strings.
23
23
  * - **No JSON aggregation / link navigation**, single-query nested `with` is
24
- * impossible → it degrades to batched N+1 loaders (Phase B).
24
+ * not available → on PowDB 0.18+ an eligible `with` compiles to nested
25
+ * projections inside the parent statement; older engines and ineligible
26
+ * relations use batched loaders.
25
27
  * - **Single global write lock; no savepoints/isolation**, nested
26
28
  * transactions / isolation / vector / LISTEN-NOTIFY / RLS throw.
27
29
  * Independent concurrent `db.$transaction` calls do NOT throw: they queue
@@ -667,8 +669,10 @@ export { PowqlInterface } from './powql.js';
667
669
  */
668
670
  export interface TurbinePowdbOptions extends EngineClientConfig {
669
671
  /**
670
- * Client-level default `with`-relation load strategy. On PowDB the default is
671
- * the batched N+1 loaders; setting `'join'` opts INTO native PowQL server-side
672
+ * Client-level default `with`-relation load strategy. On PowDB 0.18+ the default
673
+ * compiles eligible relations to nested projections in one statement (older
674
+ * engines and ineligible relations use the batched loaders); `'batched'` forces
675
+ * the loaders, and setting `'join'` opts INTO native PowQL server-side
672
676
  * joins for eligible top-level relations (ineligible ones, e.g. a paged parent
673
677
  * or a nested `with`, fall back to the loaders per-relation and silently). A
674
678
  * per-query `relationLoadStrategy` arg still overrides this. Requires an engine
@@ -743,9 +747,9 @@ export interface TurbinePowdbOptions extends EngineClientConfig {
743
747
  /**
744
748
  * Selects the **embedded** transport, an in-process `@zvndev/powdb-embedded`
745
749
  * database at the given data directory (no server, no socket). The value is the
746
- * data dir path. Preview: Full-durability checkpoint-bound; built binaries ship
747
- * for macOS (arm64/x64) and Unix-glibc only (Intel-mac/musl/Windows fall back to
748
- * a from-source `npm run build`).
750
+ * data dir path. Preview: Full-durability checkpoint-bound; prebuilt binaries ship
751
+ * for macOS arm64 and Linux glibc x64/arm64 only (Intel macOS, musl and Windows
752
+ * fall back to a from-source `npm run build`).
749
753
  *
750
754
  * @example
751
755
  * ```ts
package/dist/cjs/powdb.js CHANGED
@@ -22,7 +22,9 @@
22
22
  * writable types (`str`/`int`/`float`/`bool`); `Date` → `int` epoch micros;
23
23
  * `string` PKs hold UUID strings.
24
24
  * - **No JSON aggregation / link navigation**, single-query nested `with` is
25
- * impossible → it degrades to batched N+1 loaders (Phase B).
25
+ * not available → on PowDB 0.18+ an eligible `with` compiles to nested
26
+ * projections inside the parent statement; older engines and ineligible
27
+ * relations use batched loaders.
26
28
  * - **Single global write lock; no savepoints/isolation**, nested
27
29
  * transactions / isolation / vector / LISTEN-NOTIFY / RLS throw.
28
30
  * Independent concurrent `db.$transaction` calls do NOT throw: they queue
@@ -1867,7 +1869,7 @@ async function loadPowdbEmbedded() {
1867
1869
  catch (err) {
1868
1870
  throw new errors_js_1.ConnectionError("turbine-orm/powdb embedded mode requires the optional peer '@zvndev/powdb-embedded'. " +
1869
1871
  'Install it: npm i @zvndev/powdb-embedded. If install succeeded but loading failed, your platform has no ' +
1870
- 'prebuilt binary (prebuilts ship for macOS arm64/x64 and Linux glibc x64/arm64; Intel-mac/musl/Windows ' +
1872
+ 'prebuilt binary (prebuilts ship for macOS arm64 and Linux glibc x64/arm64; Intel macOS, musl and Windows ' +
1871
1873
  'build from source), build it with `npm run build` in the addon, then retry. You can also construct the ' +
1872
1874
  'pool yourself and inject it: turbinePowDB(pool, schema). ' +
1873
1875
  `(${err.message})`);
@@ -1702,6 +1702,14 @@ export declare class QueryInterface<T extends object, R extends object = {}, M e
1702
1702
  * search down the wrong path.
1703
1703
  */
1704
1704
  private buildRowDecodePlan;
1705
+ /**
1706
+ * The cached decode plan for one exact column list, without touching the
1707
+ * per-table most-recent slot. {@link buildRowDecodePlan} is the per-row entry;
1708
+ * this is also what the positional nested-row plan in relations.ts reads
1709
+ * (through the ctx) when it is built, so a nested row decodes by exactly the
1710
+ * field names and cell kinds {@link parseRow} would give the same keys.
1711
+ */
1712
+ private rowDecodePlanFor;
1705
1713
  /**
1706
1714
  * Whether a cached plan describes exactly this row's column list, in order.
1707
1715
  * A pointer compare per column: both sides are the driver's own interned
@@ -277,14 +277,6 @@ const ROW_PLAN_CACHE_SIZE = 128;
277
277
  * probe's own limit.
278
278
  */
279
279
  const STREAM_SPECULATION_MAX_BATCH = 1000;
280
- /** Per-cell decode work in a {@link RowDecodePlan}. */
281
- const CELL_PLAIN = 0;
282
- const CELL_TEMPORAL = 1;
283
- const CELL_FLOAT = 2;
284
- const CELL_FLOAT_ARRAY = 3;
285
- const CELL_TEXT_ARRAY = 4;
286
- /** The dialect's own decode for a type whose storage differs (see `Dialect.decodeColumnValue`). */
287
- const CELL_DIALECT = 5;
288
280
  const FLOAT_TYPES = new Set(['float4', 'float8', 'real', 'double precision']);
289
281
  const FLOAT_ARRAY_TYPES = new Set(['_float4', '_float8', 'float4[]', 'float8[]', 'real[]', 'double precision[]']);
290
282
  /**
@@ -294,11 +286,11 @@ const FLOAT_ARRAY_TYPES = new Set(['_float4', '_float8', 'float4[]', 'float8[]',
294
286
  */
295
287
  function cellKindOf(pgType, enums) {
296
288
  if (FLOAT_TYPES.has(pgType))
297
- return CELL_FLOAT;
289
+ return utils_js_1.CELL_FLOAT;
298
290
  if (FLOAT_ARRAY_TYPES.has(pgType))
299
- return CELL_FLOAT_ARRAY;
291
+ return utils_js_1.CELL_FLOAT_ARRAY;
300
292
  const element = pgType.startsWith('_') ? pgType.slice(1) : pgType.endsWith('[]') ? pgType.slice(0, -2) : undefined;
301
- return element !== undefined && enums !== undefined && Object.hasOwn(enums, element) ? CELL_TEXT_ARRAY : CELL_PLAIN;
293
+ return element !== undefined && enums !== undefined && Object.hasOwn(enums, element) ? utils_js_1.CELL_TEXT_ARRAY : utils_js_1.CELL_PLAIN;
302
294
  }
303
295
  /**
304
296
  * Strict structural equality for a single SQL parameter value. Handles the
@@ -1020,6 +1012,11 @@ class QueryInterface {
1020
1012
  toColumn: (field) => this.toColumn(field),
1021
1013
  castAgg: (expr, target) => this.castAgg(expr, target),
1022
1014
  parseRow: (row, table) => this.parseRow(row, table),
1015
+ rowDecodePlan: (table, keys) => {
1016
+ const meta = this.schema.tables[table];
1017
+ return meta ? this.rowDecodePlanFor(table, meta, keys) : undefined;
1018
+ },
1019
+ decodeCell: (kind, value, table, field) => this.decodeCell(kind, value, table, field),
1023
1020
  nullsSuffix: (nulls) => this.nullsSuffix(nulls),
1024
1021
  isRelationOrderByValue: (value) => this.isRelationOrderByValue(value),
1025
1022
  resolveOrderByColumn: (table, meta, key) => this.resolveOrderByColumn(table, meta, key),
@@ -5266,9 +5263,9 @@ class QueryInterface {
5266
5263
  if (typeof pgType !== 'string')
5267
5264
  continue;
5268
5265
  let kind = cellKindOf(pgType, this.schema.enums);
5269
- const decode = kind === CELL_PLAIN ? this.dialect.decodeColumnValue?.(pgType) : undefined;
5266
+ const decode = kind === utils_js_1.CELL_PLAIN ? this.dialect.decodeColumnValue?.(pgType) : undefined;
5270
5267
  if (decode !== undefined) {
5271
- kind = CELL_DIALECT;
5268
+ kind = utils_js_1.CELL_DIALECT;
5272
5269
  let decoders = this.dialectDecodeCache.get(table);
5273
5270
  if (!decoders) {
5274
5271
  decoders = new Map();
@@ -5277,7 +5274,7 @@ class QueryInterface {
5277
5274
  decoders.set(column.name, decode);
5278
5275
  decoders.set(column.field, decode);
5279
5276
  }
5280
- if (kind === CELL_PLAIN)
5277
+ if (kind === utils_js_1.CELL_PLAIN)
5281
5278
  continue;
5282
5279
  kinds.set(column.name, kind);
5283
5280
  kinds.set(column.field, kind);
@@ -5289,15 +5286,15 @@ class QueryInterface {
5289
5286
  /** One cell's decode, for every kind but {@link CELL_PLAIN}. */
5290
5287
  decodeCell(kind, value, table, field) {
5291
5288
  switch (kind) {
5292
- case CELL_TEMPORAL:
5289
+ case utils_js_1.CELL_TEMPORAL:
5293
5290
  return this.decodeTemporalCell(value, table, field);
5294
- case CELL_FLOAT:
5291
+ case utils_js_1.CELL_FLOAT:
5295
5292
  return (0, utils_js_1.readJsonFloat)(value);
5296
- case CELL_FLOAT_ARRAY:
5293
+ case utils_js_1.CELL_FLOAT_ARRAY:
5297
5294
  return (0, utils_js_1.readJsonFloatArray)(value);
5298
- case CELL_TEXT_ARRAY:
5295
+ case utils_js_1.CELL_TEXT_ARRAY:
5299
5296
  return (0, utils_js_1.readTextArray)(value);
5300
- case CELL_DIALECT:
5297
+ case utils_js_1.CELL_DIALECT:
5301
5298
  return this.decodeDialectCell(value, table, field);
5302
5299
  default:
5303
5300
  return value;
@@ -5328,6 +5325,18 @@ class QueryInterface {
5328
5325
  * search down the wrong path.
5329
5326
  */
5330
5327
  buildRowDecodePlan(table, meta, keys) {
5328
+ const plan = this.rowDecodePlanFor(table, meta, keys);
5329
+ this.rowPlanLast.set(table, plan);
5330
+ return plan;
5331
+ }
5332
+ /**
5333
+ * The cached decode plan for one exact column list, without touching the
5334
+ * per-table most-recent slot. {@link buildRowDecodePlan} is the per-row entry;
5335
+ * this is also what the positional nested-row plan in relations.ts reads
5336
+ * (through the ctx) when it is built, so a nested row decodes by exactly the
5337
+ * field names and cell kinds {@link parseRow} would give the same keys.
5338
+ */
5339
+ rowDecodePlanFor(table, meta, keys) {
5331
5340
  const shapeKey = `${table}\u0000${keys.join('\u0000')}`;
5332
5341
  let plan = this.rowPlanCache.get(shapeKey);
5333
5342
  if (plan === undefined) {
@@ -5345,13 +5354,12 @@ class QueryInterface {
5345
5354
  fields.push(field);
5346
5355
  // Top-level rows are snake_case (dateCols); nested rows are camelCase.
5347
5356
  kinds.push(dateCols.has(col) || camelDateFields.has(field)
5348
- ? CELL_TEMPORAL
5349
- : (cellKinds.get(col) ?? cellKinds.get(field) ?? CELL_PLAIN));
5357
+ ? utils_js_1.CELL_TEMPORAL
5358
+ : (cellKinds.get(col) ?? cellKinds.get(field) ?? utils_js_1.CELL_PLAIN));
5350
5359
  }
5351
5360
  plan = { cols, fields, kinds };
5352
5361
  this.rowPlanCache.set(shapeKey, plan);
5353
5362
  }
5354
- this.rowPlanLast.set(table, plan);
5355
5363
  return plan;
5356
5364
  }
5357
5365
  /**
@@ -5399,7 +5407,7 @@ class QueryInterface {
5399
5407
  const kind = kinds[i];
5400
5408
  // A float from the driver is already a number; only JSON's strings need reading.
5401
5409
  parsed[field] =
5402
- kind === CELL_PLAIN || (kind === CELL_FLOAT && typeof value !== 'string')
5410
+ kind === utils_js_1.CELL_PLAIN || (kind === utils_js_1.CELL_FLOAT && typeof value !== 'string')
5403
5411
  ? value
5404
5412
  : this.decodeCell(kind, value, table, field);
5405
5413
  }
@@ -353,11 +353,22 @@ export declare function buildRelationShapes(qi: BuilderCtx, table: string, withC
353
353
  export declare function buildRelationShape(qi: BuilderCtx, relDef: RelationDef, spec: true | WithOptions, parentMeta: TableMetadata, includePii?: boolean): RelationShape;
354
354
  /**
355
355
  * Build the row parser for a `with` clause. In object mode this is just
356
- * {@link parseNestedRow}. In positional mode it decodes each relation's
357
- * positional arrays into the object form first (shapes built once, not per
358
- * row), then delegates to parseNestedRow for date/snake-camel coercion.
356
+ * {@link parseNestedRow}. In positional mode the relation shapes are built
357
+ * once (not per row) and compiled into a decode plan
358
+ * ({@link makePositionalRowParser}) that turns each positional array straight
359
+ * into its parsed row. A query whose shapes the plan declines takes the
360
+ * reference path instead, which decodes each positional array into the object
361
+ * form and hands it to parseNestedRow.
359
362
  */
360
363
  export declare function makeNestedParser(qi: BuilderCtx, withClause: WithClause, includePii?: boolean, flattenPlan?: FlattenPlan | null): (row: Record<string, unknown>) => Record<string, unknown>;
364
+ /**
365
+ * The positional parser the plan in {@link makePositionalRowParser} is held to:
366
+ * decode each relation's arrays into the object representation, then parse
367
+ * that exactly as an object-encoded row is parsed. It is the path for every
368
+ * query the plan declines, and is exported so the differential tests can
369
+ * compare the two.
370
+ */
371
+ export declare function makeReferencePositionalParser(qi: BuilderCtx, shapes: Record<string, RelationShape>): (row: Record<string, unknown>) => Record<string, unknown>;
361
372
  /**
362
373
  * Return a shallow copy of a top-level row with each relation column decoded
363
374
  * from its positional array(s) into the object representation. Only relation
@@ -374,6 +385,13 @@ export declare function decodePositionalRelations(qi: BuilderCtx, row: Record<st
374
385
  export declare function decodePositionalValue(qi: BuilderCtx, raw: unknown, shape: RelationShape): unknown;
375
386
  /** Map one positional array back to a keyed object using the shape's key order. */
376
387
  export declare function decodePositionalObject(qi: BuilderCtx, arr: unknown, shape: RelationShape): unknown;
388
+ /**
389
+ * Compile a top-level positional parser for `shapes`, or `undefined` when any
390
+ * part of it is a shape the plan does not take (the caller then uses
391
+ * {@link makeReferencePositionalParser}). Rows are identical to the
392
+ * reference's on every shape it does take.
393
+ */
394
+ export declare function makePositionalRowParser(qi: BuilderCtx, table: string, shapes: Record<string, RelationShape>): ((row: Record<string, unknown>) => Record<string, unknown>) | undefined;
377
395
  /** One relation `'flatten'` declined, with the reason, for the dev warning. */
378
396
  export interface FlattenReject {
379
397
  /** Dotted path from the root table's `with`, e.g. `order.customer`. */