turbine-orm 0.91.0 → 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.
- package/README.md +23 -24
- package/dist/cjs/cli/config.d.ts +5 -0
- package/dist/cjs/cli/config.js +1 -0
- package/dist/cjs/cli/index.d.ts +17 -17
- package/dist/cjs/cli/index.js +34 -27
- package/dist/cjs/cli/observe-ui.d.ts +1 -1
- package/dist/cjs/cli/observe-ui.js +1 -1
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/powdb.d.ts +10 -6
- package/dist/cjs/powdb.js +4 -2
- package/dist/cjs/query/builder.d.ts +8 -0
- package/dist/cjs/query/builder.js +31 -23
- package/dist/cjs/query/relations.d.ts +21 -3
- package/dist/cjs/query/relations.js +331 -68
- package/dist/cjs/query/utils.d.ts +13 -0
- package/dist/cjs/query/utils.js +44 -1
- package/dist/cjs/query/where.d.ts +15 -0
- package/dist/cjs/schema-builder.d.ts +8 -5
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +1 -0
- package/dist/cli/index.d.ts +17 -17
- package/dist/cli/index.js +35 -28
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +1 -1
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/powdb.d.ts +10 -6
- package/dist/powdb.js +4 -2
- package/dist/query/builder.d.ts +8 -0
- package/dist/query/builder.js +18 -10
- package/dist/query/relations.d.ts +21 -3
- package/dist/query/relations.js +330 -69
- package/dist/query/utils.d.ts +13 -0
- package/dist/query/utils.js +43 -0
- package/dist/query/where.d.ts +15 -0
- package/dist/schema-builder.d.ts +8 -5
- package/package.json +1 -1
package/dist/powdb.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
671
|
-
*
|
|
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;
|
|
747
|
-
* for macOS
|
|
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/powdb.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
@@ -1832,7 +1834,7 @@ async function loadPowdbEmbedded() {
|
|
|
1832
1834
|
catch (err) {
|
|
1833
1835
|
throw new ConnectionError("turbine-orm/powdb embedded mode requires the optional peer '@zvndev/powdb-embedded'. " +
|
|
1834
1836
|
'Install it: npm i @zvndev/powdb-embedded. If install succeeded but loading failed, your platform has no ' +
|
|
1835
|
-
'prebuilt binary (prebuilts ship for macOS arm64
|
|
1837
|
+
'prebuilt binary (prebuilts ship for macOS arm64 and Linux glibc x64/arm64; Intel macOS, musl and Windows ' +
|
|
1836
1838
|
'build from source), build it with `npm run build` in the addon, then retry. You can also construct the ' +
|
|
1837
1839
|
'pool yourself and inject it: turbinePowDB(pool, schema). ' +
|
|
1838
1840
|
`(${err.message})`);
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -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
|
package/dist/query/builder.js
CHANGED
|
@@ -34,7 +34,7 @@ import { lockOnStreamMessage, lockOutsideTransactionMessage, lockWithDistinctMes
|
|
|
34
34
|
import { resolveSqlComment } from './sql-comment.js';
|
|
35
35
|
import { DEFAULT_STREAM_IDLE_TIMEOUT_MS, newStreamIdleState, resolveStreamIdleTimeout, StreamIdleGuard, streamBackstopStatement, streamIdleExpiredError, watchStreamConnection, } from './stream-idle.js';
|
|
36
36
|
import { resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './types.js';
|
|
37
|
-
import { assertMiddlewareArgsKept, availableClause, boundedPreparedName, canCancelBackend, isNodePostgresPool, isTemporalInfinity, LRUCache, normalizePagination, ownLookup, parseDbDate, readJsonFloat, readJsonFloatArray, readOnlyMiddlewareParams, readTextArray, resolveColumnName, resolveRelation, resolveRelationDef, sendCancelRequest, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
|
|
37
|
+
import { assertMiddlewareArgsKept, availableClause, boundedPreparedName, CELL_DIALECT, CELL_FLOAT, CELL_FLOAT_ARRAY, CELL_PLAIN, CELL_TEMPORAL, CELL_TEXT_ARRAY, canCancelBackend, isNodePostgresPool, isTemporalInfinity, LRUCache, normalizePagination, ownLookup, parseDbDate, readJsonFloat, readJsonFloatArray, readOnlyMiddlewareParams, readTextArray, resolveColumnName, resolveRelation, resolveRelationDef, sendCancelRequest, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
|
|
38
38
|
import { resetWarnOnce, shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
39
39
|
import * as whereMod from './where.js';
|
|
40
40
|
import * as writesMod from './writes.js';
|
|
@@ -239,14 +239,6 @@ const ROW_PLAN_CACHE_SIZE = 128;
|
|
|
239
239
|
* probe's own limit.
|
|
240
240
|
*/
|
|
241
241
|
const STREAM_SPECULATION_MAX_BATCH = 1000;
|
|
242
|
-
/** Per-cell decode work in a {@link RowDecodePlan}. */
|
|
243
|
-
const CELL_PLAIN = 0;
|
|
244
|
-
const CELL_TEMPORAL = 1;
|
|
245
|
-
const CELL_FLOAT = 2;
|
|
246
|
-
const CELL_FLOAT_ARRAY = 3;
|
|
247
|
-
const CELL_TEXT_ARRAY = 4;
|
|
248
|
-
/** The dialect's own decode for a type whose storage differs (see `Dialect.decodeColumnValue`). */
|
|
249
|
-
const CELL_DIALECT = 5;
|
|
250
242
|
const FLOAT_TYPES = new Set(['float4', 'float8', 'real', 'double precision']);
|
|
251
243
|
const FLOAT_ARRAY_TYPES = new Set(['_float4', '_float8', 'float4[]', 'float8[]', 'real[]', 'double precision[]']);
|
|
252
244
|
/**
|
|
@@ -982,6 +974,11 @@ export class QueryInterface {
|
|
|
982
974
|
toColumn: (field) => this.toColumn(field),
|
|
983
975
|
castAgg: (expr, target) => this.castAgg(expr, target),
|
|
984
976
|
parseRow: (row, table) => this.parseRow(row, table),
|
|
977
|
+
rowDecodePlan: (table, keys) => {
|
|
978
|
+
const meta = this.schema.tables[table];
|
|
979
|
+
return meta ? this.rowDecodePlanFor(table, meta, keys) : undefined;
|
|
980
|
+
},
|
|
981
|
+
decodeCell: (kind, value, table, field) => this.decodeCell(kind, value, table, field),
|
|
985
982
|
nullsSuffix: (nulls) => this.nullsSuffix(nulls),
|
|
986
983
|
isRelationOrderByValue: (value) => this.isRelationOrderByValue(value),
|
|
987
984
|
resolveOrderByColumn: (table, meta, key) => this.resolveOrderByColumn(table, meta, key),
|
|
@@ -5290,6 +5287,18 @@ export class QueryInterface {
|
|
|
5290
5287
|
* search down the wrong path.
|
|
5291
5288
|
*/
|
|
5292
5289
|
buildRowDecodePlan(table, meta, keys) {
|
|
5290
|
+
const plan = this.rowDecodePlanFor(table, meta, keys);
|
|
5291
|
+
this.rowPlanLast.set(table, plan);
|
|
5292
|
+
return plan;
|
|
5293
|
+
}
|
|
5294
|
+
/**
|
|
5295
|
+
* The cached decode plan for one exact column list, without touching the
|
|
5296
|
+
* per-table most-recent slot. {@link buildRowDecodePlan} is the per-row entry;
|
|
5297
|
+
* this is also what the positional nested-row plan in relations.ts reads
|
|
5298
|
+
* (through the ctx) when it is built, so a nested row decodes by exactly the
|
|
5299
|
+
* field names and cell kinds {@link parseRow} would give the same keys.
|
|
5300
|
+
*/
|
|
5301
|
+
rowDecodePlanFor(table, meta, keys) {
|
|
5293
5302
|
const shapeKey = `${table}\u0000${keys.join('\u0000')}`;
|
|
5294
5303
|
let plan = this.rowPlanCache.get(shapeKey);
|
|
5295
5304
|
if (plan === undefined) {
|
|
@@ -5313,7 +5322,6 @@ export class QueryInterface {
|
|
|
5313
5322
|
plan = { cols, fields, kinds };
|
|
5314
5323
|
this.rowPlanCache.set(shapeKey, plan);
|
|
5315
5324
|
}
|
|
5316
|
-
this.rowPlanLast.set(table, plan);
|
|
5317
5325
|
return plan;
|
|
5318
5326
|
}
|
|
5319
5327
|
/**
|
|
@@ -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
|
|
357
|
-
*
|
|
358
|
-
*
|
|
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`. */
|
package/dist/query/relations.js
CHANGED
|
@@ -19,7 +19,7 @@ import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
|
19
19
|
import { resolveCountRelations, stitchOnlyFields } from './batched-loader.js';
|
|
20
20
|
import { describeOperand, isJsonPathOrderBy, isOrderBySpec, isPlainRecord, isRelationPickOrderBy, isVectorOrderBy, MAX_NAMED_ORDER_KEYS, normalizeOrderBy, orderByEntries, sortedEntries, } from './filters.js';
|
|
21
21
|
import { assertDirectionToken, assertOrderDirection } from './types.js';
|
|
22
|
-
import { availableClause, canonicalColumnOrder, ownLookup, relationInProjectionMessage, resolveColumnName, resolveRelation, resolveRelationDef, selectNamesNothingMessage, selectOmitExclusiveMessage, suggestKey, unknownFieldMessage, } from './utils.js';
|
|
22
|
+
import { availableClause, CELL_FLOAT, CELL_PLAIN, canonicalColumnOrder, ownLookup, relationInProjectionMessage, resolveColumnName, resolveRelation, resolveRelationDef, selectNamesNothingMessage, selectOmitExclusiveMessage, suggestKey, unknownFieldMessage, } from './utils.js';
|
|
23
23
|
import { hasWarnedOnce, shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
24
24
|
import * as whereMod from './where.js';
|
|
25
25
|
import * as writesMod from './writes.js';
|
|
@@ -1405,6 +1405,22 @@ export function parseNestedRow(qi, row, table, fromJson = false) {
|
|
|
1405
1405
|
const meta = qi.schema.tables[table];
|
|
1406
1406
|
if (!meta)
|
|
1407
1407
|
return qi.parseRow(row, table);
|
|
1408
|
+
const parsed = parseScalarColumns(qi, row, table, meta, fromJson);
|
|
1409
|
+
for (const [relName, relDef] of getRelationEntries(qi, table, meta)) {
|
|
1410
|
+
const rawValue = row[relName];
|
|
1411
|
+
if (rawValue === undefined)
|
|
1412
|
+
continue;
|
|
1413
|
+
parsed[relName] = parseRelationValue(qi, relName, relDef, rawValue);
|
|
1414
|
+
}
|
|
1415
|
+
return parsed;
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* The scalar half of {@link parseNestedRow}: the row through `parseRow` (after
|
|
1419
|
+
* the JSON-wire decode when `fromJson`), with any `_count__<rel>` columns
|
|
1420
|
+
* folded into a `_count` object. Relation columns are copied through as they
|
|
1421
|
+
* are; the caller replaces each one it parses.
|
|
1422
|
+
*/
|
|
1423
|
+
function parseScalarColumns(qi, row, table, meta, fromJson) {
|
|
1408
1424
|
const parsed = qi.parseRow(fromJson ? decodeJsonWireRow(qi, row, table, meta) : row, table);
|
|
1409
1425
|
// Assemble reserved `_count__<rel>` scalar columns into a `_count` object.
|
|
1410
1426
|
// parseRow copies these unknown columns through under their raw key.
|
|
@@ -1431,75 +1447,66 @@ export function parseNestedRow(qi, row, table, fromJson = false) {
|
|
|
1431
1447
|
delete parsed[`${COUNT_PREFIX}${rel}`];
|
|
1432
1448
|
parsed._count = countObj;
|
|
1433
1449
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
parsed[relName] = jsonVal.map((item) => typeof item === 'object' && item !== null
|
|
1460
|
-
? parseNestedRow(qi, item, relDef.to, true)
|
|
1461
|
-
: item);
|
|
1462
|
-
}
|
|
1463
|
-
else if (typeof jsonVal === 'object' && jsonVal !== null) {
|
|
1464
|
-
parsed[relName] = parseNestedRow(qi, jsonVal, relDef.to, true);
|
|
1465
|
-
}
|
|
1466
|
-
else {
|
|
1467
|
-
parsed[relName] = jsonVal;
|
|
1468
|
-
}
|
|
1450
|
+
return parsed;
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* The relation half of {@link parseNestedRow}: one relation column's value
|
|
1454
|
+
* (never `undefined`, which the caller skips) as the parsed relation.
|
|
1455
|
+
*/
|
|
1456
|
+
function parseRelationValue(qi, relName, relDef, rawValue) {
|
|
1457
|
+
// --- Short-circuit: skip JSON.parse for common empty/null cases ---
|
|
1458
|
+
// hasMany returns '[]' (from COALESCE(..., '[]'::json)); belongsTo/hasOne returns null
|
|
1459
|
+
if (rawValue === null || rawValue === 'null')
|
|
1460
|
+
return null;
|
|
1461
|
+
if (rawValue === '[]')
|
|
1462
|
+
return [];
|
|
1463
|
+
if (Array.isArray(rawValue) && rawValue.length === 0)
|
|
1464
|
+
return [];
|
|
1465
|
+
// --- Non-empty values: full parse path ---
|
|
1466
|
+
if (typeof rawValue === 'string') {
|
|
1467
|
+
try {
|
|
1468
|
+
const jsonVal = JSON.parse(rawValue);
|
|
1469
|
+
// After parsing, recurse via parseNestedRow so each item gets date
|
|
1470
|
+
// coercion AND its own sub-relations parsed at arbitrary depth.
|
|
1471
|
+
if (Array.isArray(jsonVal)) {
|
|
1472
|
+
return jsonVal.map((item) => typeof item === 'object' && item !== null
|
|
1473
|
+
? parseNestedRow(qi, item, relDef.to, true)
|
|
1474
|
+
: item);
|
|
1469
1475
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
// a driver-level corruption, not a recoverable condition. Handing back
|
|
1473
|
-
// the raw string satisfied the assignment and violated the generated
|
|
1474
|
-
// type: callers got a `string` where `Post[]` was promised, and found
|
|
1475
|
-
// out at the first `.map`. Throw instead, with the context needed to
|
|
1476
|
-
// find it.
|
|
1477
|
-
//
|
|
1478
|
-
// The warn is gated and deduped because this sits in parseNestedRow's
|
|
1479
|
-
// per-relation loop: one 10,000-row page was 10,000 warnings. The
|
|
1480
|
-
// parse error stays out of the entry: its message quotes the payload,
|
|
1481
|
-
// which is row data.
|
|
1482
|
-
const log = whereMod.adviceLogger(qi);
|
|
1483
|
-
if (log && shouldWarnOnce(WARN_NS.relationParseFailure, `${qi.table}.${relName}`)) {
|
|
1484
|
-
logTo(log, 'warn', `Relation "${relName}" on "${qi.table}" returned a payload that is not valid JSON. ` +
|
|
1485
|
-
`This is a bug; please report it with the emitted SQL.`, { code: 'relation-payload-json', table: qi.table, relation: relName });
|
|
1486
|
-
}
|
|
1487
|
-
throw new InvariantError('relation-payload-json', `Relation "${relName}" on table "${qi.table}" returned a payload that could not be parsed as JSON.`, { cause: err });
|
|
1476
|
+
if (typeof jsonVal === 'object' && jsonVal !== null) {
|
|
1477
|
+
return parseNestedRow(qi, jsonVal, relDef.to, true);
|
|
1488
1478
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1479
|
+
return jsonVal;
|
|
1480
|
+
}
|
|
1481
|
+
catch (err) {
|
|
1482
|
+
// A relation column that does not parse is a bug in the emitted SQL or
|
|
1483
|
+
// a driver-level corruption, not a recoverable condition. Handing back
|
|
1484
|
+
// the raw string satisfied the assignment and violated the generated
|
|
1485
|
+
// type: callers got a `string` where `Post[]` was promised, and found
|
|
1486
|
+
// out at the first `.map`. Throw instead, with the context needed to
|
|
1487
|
+
// find it.
|
|
1488
|
+
//
|
|
1489
|
+
// The warn is gated and deduped because this sits in parseNestedRow's
|
|
1490
|
+
// per-relation loop: one 10,000-row page was 10,000 warnings. The
|
|
1491
|
+
// parse error stays out of the entry: its message quotes the payload,
|
|
1492
|
+
// which is row data.
|
|
1493
|
+
const log = whereMod.adviceLogger(qi);
|
|
1494
|
+
if (log && shouldWarnOnce(WARN_NS.relationParseFailure, `${qi.table}.${relName}`)) {
|
|
1495
|
+
logTo(log, 'warn', `Relation "${relName}" on "${qi.table}" returned a payload that is not valid JSON. ` +
|
|
1496
|
+
`This is a bug; please report it with the emitted SQL.`, { code: 'relation-payload-json', table: qi.table, relation: relName });
|
|
1497
|
+
}
|
|
1498
|
+
throw new InvariantError('relation-payload-json', `Relation "${relName}" on table "${qi.table}" returned a payload that could not be parsed as JSON.`, { cause: err });
|
|
1500
1499
|
}
|
|
1501
1500
|
}
|
|
1502
|
-
|
|
1501
|
+
if (Array.isArray(rawValue)) {
|
|
1502
|
+
return rawValue.map((item) => typeof item === 'object' && item !== null
|
|
1503
|
+
? parseNestedRow(qi, item, relDef.to, true)
|
|
1504
|
+
: item);
|
|
1505
|
+
}
|
|
1506
|
+
if (typeof rawValue === 'object' && rawValue !== null) {
|
|
1507
|
+
return parseNestedRow(qi, rawValue, relDef.to, true);
|
|
1508
|
+
}
|
|
1509
|
+
return rawValue;
|
|
1503
1510
|
}
|
|
1504
1511
|
/**
|
|
1505
1512
|
* Resolve the emitted column list for a relation, honoring `select` / `omit`.
|
|
@@ -1685,9 +1692,12 @@ export function buildRelationShape(qi, relDef, spec, parentMeta, includePii) {
|
|
|
1685
1692
|
}
|
|
1686
1693
|
/**
|
|
1687
1694
|
* Build the row parser for a `with` clause. In object mode this is just
|
|
1688
|
-
* {@link parseNestedRow}. In positional mode
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1695
|
+
* {@link parseNestedRow}. In positional mode the relation shapes are built
|
|
1696
|
+
* once (not per row) and compiled into a decode plan
|
|
1697
|
+
* ({@link makePositionalRowParser}) that turns each positional array straight
|
|
1698
|
+
* into its parsed row. A query whose shapes the plan declines takes the
|
|
1699
|
+
* reference path instead, which decodes each positional array into the object
|
|
1700
|
+
* form and hands it to parseNestedRow.
|
|
1691
1701
|
*/
|
|
1692
1702
|
export function makeNestedParser(qi, withClause, includePii, flattenPlan) {
|
|
1693
1703
|
// A flatten plan and the positional encoding are mutually exclusive (the
|
|
@@ -1699,6 +1709,16 @@ export function makeNestedParser(qi, withClause, includePii, flattenPlan) {
|
|
|
1699
1709
|
return (row) => parseNestedRow(qi, row, qi.table);
|
|
1700
1710
|
}
|
|
1701
1711
|
const shapes = buildRelationShapes(qi, qi.table, withClause, includePii);
|
|
1712
|
+
return makePositionalRowParser(qi, qi.table, shapes) ?? makeReferencePositionalParser(qi, shapes);
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* The positional parser the plan in {@link makePositionalRowParser} is held to:
|
|
1716
|
+
* decode each relation's arrays into the object representation, then parse
|
|
1717
|
+
* that exactly as an object-encoded row is parsed. It is the path for every
|
|
1718
|
+
* query the plan declines, and is exported so the differential tests can
|
|
1719
|
+
* compare the two.
|
|
1720
|
+
*/
|
|
1721
|
+
export function makeReferencePositionalParser(qi, shapes) {
|
|
1702
1722
|
return (row) => parseNestedRow(qi, decodePositionalRelations(qi, row, shapes), qi.table);
|
|
1703
1723
|
}
|
|
1704
1724
|
/**
|
|
@@ -1753,6 +1773,247 @@ export function decodePositionalObject(qi, arr, shape) {
|
|
|
1753
1773
|
}
|
|
1754
1774
|
return obj;
|
|
1755
1775
|
}
|
|
1776
|
+
/**
|
|
1777
|
+
* A key the reference path treats as an ordinary own property: not one that
|
|
1778
|
+
* resolves through `Object.prototype` when read off a plain object (`__proto__`
|
|
1779
|
+
* included), and not an integer-like key, which `Object.keys` lists ahead of
|
|
1780
|
+
* every other key whatever order it was written in.
|
|
1781
|
+
*/
|
|
1782
|
+
function isPlainKey(key) {
|
|
1783
|
+
return typeof key === 'string' && !(key in Object.prototype) && !/^\d+$/.test(key);
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Does `name` reach `parseRow`'s answer for a top-level column? Any overlap
|
|
1787
|
+
* with a column name, field name, reverse-map key or date column means the
|
|
1788
|
+
* reference would give the relation column a field or cell kind of its own.
|
|
1789
|
+
*/
|
|
1790
|
+
function nameTouchesColumns(qi, table, meta, name) {
|
|
1791
|
+
if (Object.hasOwn(meta.reverseColumnMap, name) || meta.dateColumns.has(name))
|
|
1792
|
+
return true;
|
|
1793
|
+
if (getCamelDateFields(qi, table, meta).has(name))
|
|
1794
|
+
return true;
|
|
1795
|
+
return meta.columns.some((column) => column.name === name || column.field === name);
|
|
1796
|
+
}
|
|
1797
|
+
/** The indexes of `relations` in `entries` order, or `null` when that is their own order. */
|
|
1798
|
+
function relationDecodeOrder(relations, entries) {
|
|
1799
|
+
const order = [];
|
|
1800
|
+
for (const [name] of entries) {
|
|
1801
|
+
const index = relations.findIndex((rel) => rel.name === name);
|
|
1802
|
+
if (index !== -1)
|
|
1803
|
+
order.push(index);
|
|
1804
|
+
}
|
|
1805
|
+
return order.every((index, i) => index === i) ? null : order;
|
|
1806
|
+
}
|
|
1807
|
+
/** The plan for one relation shape, or `undefined` when the plan cannot take it. */
|
|
1808
|
+
function planPositionalRow(qi, table, shape) {
|
|
1809
|
+
const meta = qi.schema.tables[table];
|
|
1810
|
+
if (!meta || !qi.rowDecodePlan)
|
|
1811
|
+
return undefined;
|
|
1812
|
+
const keys = shape.keys;
|
|
1813
|
+
if (new Set(keys).size !== keys.length || !keys.every(isPlainKey))
|
|
1814
|
+
return undefined;
|
|
1815
|
+
const rowPlan = qi.rowDecodePlan(table, keys);
|
|
1816
|
+
if (!rowPlan)
|
|
1817
|
+
return undefined;
|
|
1818
|
+
const entries = getRelationEntries(qi, table, meta);
|
|
1819
|
+
if (!entries.every(([name]) => isPlainKey(name)))
|
|
1820
|
+
return undefined;
|
|
1821
|
+
const relationNames = new Set(entries.map(([name]) => name));
|
|
1822
|
+
const wireFields = jsonWireFields(qi, table, meta);
|
|
1823
|
+
const wireOrder = wireFields ? [...wireFields.keys()] : [];
|
|
1824
|
+
const fields = [];
|
|
1825
|
+
const wire = [];
|
|
1826
|
+
const cellPositions = [];
|
|
1827
|
+
const cellKinds = [];
|
|
1828
|
+
const relations = [];
|
|
1829
|
+
let lastWireRank = -1;
|
|
1830
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1831
|
+
const key = keys[i];
|
|
1832
|
+
const field = rowPlan.fields[i];
|
|
1833
|
+
const kind = rowPlan.kinds[i];
|
|
1834
|
+
// parseNestedRow folds a `_count__` field into `_count`; the plan does not.
|
|
1835
|
+
if (!isPlainKey(field) || field.startsWith(COUNT_PREFIX))
|
|
1836
|
+
return undefined;
|
|
1837
|
+
const nested = shape.nested[key];
|
|
1838
|
+
if (nested) {
|
|
1839
|
+
const def = ownLookup(meta.relations, key);
|
|
1840
|
+
// The reference copies the slot through parseRow before replacing it, so
|
|
1841
|
+
// the slot must be a plain cell under its own name, and no wire field.
|
|
1842
|
+
if (!def || field !== key || kind !== CELL_PLAIN || wireFields?.has(key))
|
|
1843
|
+
return undefined;
|
|
1844
|
+
const row = planPositionalRow(qi, def.to, nested);
|
|
1845
|
+
if (!row)
|
|
1846
|
+
return undefined;
|
|
1847
|
+
relations.push({ name: key, def, many: nested.cardinality === 'many', position: i, row });
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
// buildRelationShape puts every scalar ahead of every relation slot, and a
|
|
1851
|
+
// scalar named like one of the table's relations would be re-read as it.
|
|
1852
|
+
if (relations.length > 0 || relationNames.has(key))
|
|
1853
|
+
return undefined;
|
|
1854
|
+
const rule = wireFields?.get(key);
|
|
1855
|
+
if (rule !== undefined) {
|
|
1856
|
+
// The reference decodes wire columns in table-column order before any
|
|
1857
|
+
// cell decode; the plan does them in key order, so the two must agree.
|
|
1858
|
+
const rank = wireOrder.indexOf(key);
|
|
1859
|
+
if (rank < lastWireRank)
|
|
1860
|
+
return undefined;
|
|
1861
|
+
lastWireRank = rank;
|
|
1862
|
+
}
|
|
1863
|
+
fields.push(field);
|
|
1864
|
+
wire.push(rule);
|
|
1865
|
+
if (kind !== CELL_PLAIN) {
|
|
1866
|
+
cellPositions.push(i);
|
|
1867
|
+
cellKinds.push(kind);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
// Two keys landing on one field, or a scalar field named like a relation
|
|
1871
|
+
// slot, would collapse into one property in a different order.
|
|
1872
|
+
const written = new Set(fields);
|
|
1873
|
+
if (written.size !== fields.length || relations.some((rel) => written.has(rel.name)))
|
|
1874
|
+
return undefined;
|
|
1875
|
+
return {
|
|
1876
|
+
table,
|
|
1877
|
+
fields,
|
|
1878
|
+
wire,
|
|
1879
|
+
cellPositions,
|
|
1880
|
+
cellKinds,
|
|
1881
|
+
relations,
|
|
1882
|
+
relationOrder: relationDecodeOrder(relations, entries),
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Compile a top-level positional parser for `shapes`, or `undefined` when any
|
|
1887
|
+
* part of it is a shape the plan does not take (the caller then uses
|
|
1888
|
+
* {@link makeReferencePositionalParser}). Rows are identical to the
|
|
1889
|
+
* reference's on every shape it does take.
|
|
1890
|
+
*/
|
|
1891
|
+
export function makePositionalRowParser(qi, table, shapes) {
|
|
1892
|
+
const meta = qi.schema.tables[table];
|
|
1893
|
+
if (!meta || !qi.rowDecodePlan || !qi.decodeCell)
|
|
1894
|
+
return undefined;
|
|
1895
|
+
const entries = getRelationEntries(qi, table, meta);
|
|
1896
|
+
if (!entries.every(([name]) => isPlainKey(name)))
|
|
1897
|
+
return undefined;
|
|
1898
|
+
const plans = [];
|
|
1899
|
+
for (const [name, def] of entries) {
|
|
1900
|
+
const shape = Object.hasOwn(shapes, name) ? shapes[name] : undefined;
|
|
1901
|
+
if (!shape) {
|
|
1902
|
+
plans.push(null);
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
// The reference runs the top-level row through parseRow with the relation
|
|
1906
|
+
// columns already decoded; the plan runs it with them raw and replaces them
|
|
1907
|
+
// after. Those agree only while parseRow copies a relation column through
|
|
1908
|
+
// untouched, under its own name, and the `_count` fold never reads it.
|
|
1909
|
+
if (name.startsWith(COUNT_PREFIX) || nameTouchesColumns(qi, table, meta, name))
|
|
1910
|
+
return undefined;
|
|
1911
|
+
const row = planPositionalRow(qi, def.to, shape);
|
|
1912
|
+
if (!row)
|
|
1913
|
+
return undefined;
|
|
1914
|
+
plans.push({ name, def, many: shape.cardinality === 'many', position: -1, row });
|
|
1915
|
+
}
|
|
1916
|
+
const isOwnEnumerable = Object.prototype.propertyIsEnumerable;
|
|
1917
|
+
return (row) => {
|
|
1918
|
+
const parsed = parseScalarColumns(qi, row, table, meta, false);
|
|
1919
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1920
|
+
const [name, def] = entries[i];
|
|
1921
|
+
// The reference reads relation columns off a `{ ...row }` copy, which
|
|
1922
|
+
// holds exactly the row's own enumerable properties.
|
|
1923
|
+
if (!isOwnEnumerable.call(row, name))
|
|
1924
|
+
continue;
|
|
1925
|
+
const plan = plans[i];
|
|
1926
|
+
if (plan !== null) {
|
|
1927
|
+
parsed[name] = decodePlannedRelation(qi, row[name], plan);
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
const raw = row[name];
|
|
1931
|
+
if (raw !== undefined)
|
|
1932
|
+
parsed[name] = parseRelationValue(qi, name, def, raw);
|
|
1933
|
+
}
|
|
1934
|
+
return parsed;
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* One relation value through the plan: exactly what the reference produces by
|
|
1939
|
+
* {@link decodePositionalValue} followed by {@link parseRelationValue}. Every
|
|
1940
|
+
* value that is not the well-formed positional shape (a payload that is not
|
|
1941
|
+
* JSON, a to-many that is not an array, a to-one that is not an array) is
|
|
1942
|
+
* handed to `parseRelationValue` as the reference would hand it, so it
|
|
1943
|
+
* decodes, or throws, identically.
|
|
1944
|
+
*/
|
|
1945
|
+
function decodePlannedRelation(qi, raw, rel) {
|
|
1946
|
+
let value = raw;
|
|
1947
|
+
if (typeof value === 'string') {
|
|
1948
|
+
try {
|
|
1949
|
+
value = JSON.parse(value);
|
|
1950
|
+
}
|
|
1951
|
+
catch {
|
|
1952
|
+
return parseRelationValue(qi, rel.name, rel.def, raw);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
if (value === null || value === undefined)
|
|
1956
|
+
return rel.many ? [] : null;
|
|
1957
|
+
if (!rel.many) {
|
|
1958
|
+
return Array.isArray(value)
|
|
1959
|
+
? decodePlannedRow(qi, value, rel.row)
|
|
1960
|
+
: parseRelationValue(qi, rel.name, rel.def, value);
|
|
1961
|
+
}
|
|
1962
|
+
if (!Array.isArray(value))
|
|
1963
|
+
return parseRelationValue(qi, rel.name, rel.def, value);
|
|
1964
|
+
const out = [];
|
|
1965
|
+
for (let i = 0; i < value.length; i++) {
|
|
1966
|
+
const item = value[i];
|
|
1967
|
+
out.push(Array.isArray(item)
|
|
1968
|
+
? decodePlannedRow(qi, item, rel.row)
|
|
1969
|
+
: typeof item === 'object' && item !== null
|
|
1970
|
+
? parseNestedRow(qi, item, rel.def.to, true)
|
|
1971
|
+
: item);
|
|
1972
|
+
}
|
|
1973
|
+
return out;
|
|
1974
|
+
}
|
|
1975
|
+
/** One positional array through its row plan, into the parsed row. */
|
|
1976
|
+
function decodePlannedRow(qi, arr, plan) {
|
|
1977
|
+
const out = {};
|
|
1978
|
+
const { fields, wire, cellPositions, cellKinds, relations, table } = plan;
|
|
1979
|
+
for (let i = 0; i < fields.length; i++) {
|
|
1980
|
+
let value = arr[i];
|
|
1981
|
+
const rule = wire[i];
|
|
1982
|
+
if (rule !== undefined && typeof value === 'string')
|
|
1983
|
+
value = rule.decode(value);
|
|
1984
|
+
out[fields[i]] = value;
|
|
1985
|
+
}
|
|
1986
|
+
if (cellPositions.length > 0) {
|
|
1987
|
+
// `decodeCell` is present: the plan is only built when it is.
|
|
1988
|
+
const decodeCell = qi.decodeCell;
|
|
1989
|
+
for (let c = 0; c < cellPositions.length; c++) {
|
|
1990
|
+
const field = fields[cellPositions[c]];
|
|
1991
|
+
const kind = cellKinds[c];
|
|
1992
|
+
const value = out[field];
|
|
1993
|
+
// parseRow's own skip: a float the driver already made a number.
|
|
1994
|
+
if (kind === CELL_FLOAT && typeof value !== 'string')
|
|
1995
|
+
continue;
|
|
1996
|
+
out[field] = decodeCell(kind, value, table, field);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (relations.length > 0) {
|
|
2000
|
+
const order = plan.relationOrder;
|
|
2001
|
+
if (order === null) {
|
|
2002
|
+
for (const rel of relations)
|
|
2003
|
+
out[rel.name] = decodePlannedRelation(qi, arr[rel.position], rel);
|
|
2004
|
+
}
|
|
2005
|
+
else {
|
|
2006
|
+
// Claim the keys in key order first, then decode in the reference's order.
|
|
2007
|
+
for (const rel of relations)
|
|
2008
|
+
out[rel.name] = undefined;
|
|
2009
|
+
for (const index of order) {
|
|
2010
|
+
const rel = relations[index];
|
|
2011
|
+
out[rel.name] = decodePlannedRelation(qi, arr[rel.position], rel);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return out;
|
|
2016
|
+
}
|
|
1756
2017
|
// ---------------------------------------------------------------------------
|
|
1757
2018
|
// relationLoadStrategy: 'flatten', to-one relations compiled as LEFT JOINs
|
|
1758
2019
|
// ---------------------------------------------------------------------------
|
package/dist/query/utils.d.ts
CHANGED
|
@@ -423,6 +423,19 @@ export declare function coerceTemporalValue(dbType: string | undefined, value: u
|
|
|
423
423
|
* as-is.
|
|
424
424
|
*/
|
|
425
425
|
export declare function parseDbDate(value: string): Date;
|
|
426
|
+
/**
|
|
427
|
+
* Per-cell decode work in a row decode plan (`RowDecodePlan` in builder.ts).
|
|
428
|
+
* Defined here rather than in builder.ts because the positional nested-row
|
|
429
|
+
* plan in relations.ts reads the same codes, and relations.ts cannot import
|
|
430
|
+
* builder.ts.
|
|
431
|
+
*/
|
|
432
|
+
export declare const CELL_PLAIN = 0;
|
|
433
|
+
export declare const CELL_TEMPORAL = 1;
|
|
434
|
+
export declare const CELL_FLOAT = 2;
|
|
435
|
+
export declare const CELL_FLOAT_ARRAY = 3;
|
|
436
|
+
export declare const CELL_TEXT_ARRAY = 4;
|
|
437
|
+
/** The dialect's own decode for a type whose storage differs (see `Dialect.decodeColumnValue`). */
|
|
438
|
+
export declare const CELL_DIALECT = 5;
|
|
426
439
|
/**
|
|
427
440
|
* A float column's value as the driver reads it. JSON has no NaN or infinity,
|
|
428
441
|
* so `json_build_object` renders them as the strings "NaN", "Infinity" and
|