turbine-orm 0.71.0 → 0.72.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 +2 -2
- package/dist/cjs/client.d.ts +0 -18
- package/dist/cjs/client.js +2 -29
- package/dist/cjs/connection-url.d.ts +30 -0
- package/dist/cjs/connection-url.js +15 -17
- package/dist/cjs/powql.d.ts +38 -1
- package/dist/cjs/powql.js +106 -18
- package/dist/cjs/query/aggregates.d.ts +0 -13
- package/dist/cjs/query/aggregates.js +81 -33
- package/dist/cjs/query/batched-loader.d.ts +13 -1
- package/dist/cjs/query/batched-loader.js +46 -11
- package/dist/cjs/query/builder.d.ts +13 -0
- package/dist/cjs/query/builder.js +104 -14
- package/dist/cjs/query/compound-unique.js +29 -5
- package/dist/cjs/query/relation-names.d.ts +52 -0
- package/dist/cjs/query/relation-names.js +120 -0
- package/dist/cjs/query/relations.d.ts +11 -6
- package/dist/cjs/query/relations.js +45 -27
- package/dist/cjs/query/utils.d.ts +107 -3
- package/dist/cjs/query/utils.js +408 -7
- package/dist/cjs/query/where-compile.js +9 -4
- package/dist/cjs/query/where.js +9 -5
- package/dist/client.d.ts +0 -18
- package/dist/client.js +2 -29
- package/dist/connection-url.d.ts +30 -0
- package/dist/connection-url.js +15 -18
- package/dist/powql.d.ts +38 -1
- package/dist/powql.js +107 -19
- package/dist/query/aggregates.d.ts +0 -13
- package/dist/query/aggregates.js +82 -34
- package/dist/query/batched-loader.d.ts +13 -1
- package/dist/query/batched-loader.js +47 -12
- package/dist/query/builder.d.ts +13 -0
- package/dist/query/builder.js +105 -15
- package/dist/query/compound-unique.js +30 -6
- package/dist/query/relation-names.d.ts +52 -0
- package/dist/query/relation-names.js +117 -0
- package/dist/query/relations.d.ts +11 -6
- package/dist/query/relations.js +47 -29
- package/dist/query/utils.d.ts +107 -3
- package/dist/query/utils.js +404 -8
- package/dist/query/where-compile.js +10 -5
- package/dist/query/where.js +10 -6
- package/package.json +5 -3
package/dist/query/relations.js
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { CircularRelationError, RelationError, UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
16
16
|
import { missingIndexForRelation } from '../index-advisor.js';
|
|
17
|
-
import {
|
|
17
|
+
import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
18
18
|
import { resolveCountRelations } from './batched-loader.js';
|
|
19
19
|
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, MAX_NAMED_ORDER_KEYS, normalizeOrderBy, orderByEntries, sortedEntries, } from './filters.js';
|
|
20
20
|
import { assertDirectionToken, assertOrderDirection } from './types.js';
|
|
21
|
-
import { canonicalColumnOrder, ownLookup, relationInProjectionMessage, resolveColumnName, selectNamesNothingMessage, selectOmitExclusiveMessage, unknownFieldMessage, } from './utils.js';
|
|
21
|
+
import { canonicalColumnOrder, ownLookup, relationInProjectionMessage, resolveColumnName, resolveRelation, resolveRelationDef, selectNamesNothingMessage, selectOmitExclusiveMessage, unknownFieldMessage, } from './utils.js';
|
|
22
22
|
import { hasWarnedOnce, shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
23
23
|
import * as whereMod from './where.js';
|
|
24
24
|
import * as writesMod from './writes.js';
|
|
@@ -38,7 +38,10 @@ function projectionColumn(table, meta, field, clause) {
|
|
|
38
38
|
// A relation named in a projection is a habit, not a typo, so it gets its own
|
|
39
39
|
// message pointing at `with`. Checked BEFORE the generic throw because the
|
|
40
40
|
// generic one degrades into "Did you mean <exactly what you typed>?".
|
|
41
|
-
|
|
41
|
+
// Resolved, so `select: { ripening_checks: true }` gets the "that is a
|
|
42
|
+
// relation, use `with`" message rather than degrading to an unknown-field
|
|
43
|
+
// suggestion that names the relation back at the caller.
|
|
44
|
+
if (resolveRelationDef(meta.relations, field))
|
|
42
45
|
throw new ValidationError(relationInProjectionMessage(table, field, clause));
|
|
43
46
|
throw new ValidationError(unknownFieldMessage(table, field, meta));
|
|
44
47
|
}
|
|
@@ -462,10 +465,8 @@ export function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
462
465
|
.map(([key, value]) => {
|
|
463
466
|
// Vector KNN ordering: { distance: { to, metric, direction? } }
|
|
464
467
|
if (isVectorOrderBy(value)) {
|
|
465
|
-
if (meta
|
|
466
|
-
|
|
467
|
-
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
468
|
-
}
|
|
468
|
+
if (meta)
|
|
469
|
+
resolveOrderByColumn(qi, qi.table, meta, key);
|
|
469
470
|
if (!params) {
|
|
470
471
|
throw new ValidationError(`[turbine] Vector distance ordering on "${key}" is only supported in a top-level findMany orderBy.`);
|
|
471
472
|
}
|
|
@@ -488,10 +489,13 @@ export function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
488
489
|
return buildRelationOrderBy(qi, key, value, `ord${relOrdCounter++}`, params, undefined, lateralSink);
|
|
489
490
|
}
|
|
490
491
|
// Scalar column ordering, a plain direction or an OrderBySpec (nulls).
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
492
|
+
// Through `resolveOrderByColumn`, which is the same resolve-or-E003 the
|
|
493
|
+
// nested orderBy paths below already used and emits the same message
|
|
494
|
+
// this used to inline. It used to test `key in meta.columnMap`, which
|
|
495
|
+
// knows only the FIELD spelling, and so rejected the snake_case COLUMN
|
|
496
|
+
// name that `where` / `select` / `distinct` accept on the same table.
|
|
497
|
+
if (meta)
|
|
498
|
+
resolveOrderByColumn(qi, qi.table, meta, key);
|
|
495
499
|
// Refuse a direction that is neither asc nor desc. normalizeOrderBy is
|
|
496
500
|
// `=== 'desc' ? DESC : ASC`, so without this every typo sorted ASCENDING
|
|
497
501
|
// and returned a correct-looking page in the reverse order.
|
|
@@ -529,16 +533,21 @@ export function nullsSuffix(qi, nulls) {
|
|
|
529
533
|
return nulls === 'first' ? ' NULLS FIRST' : ' NULLS LAST';
|
|
530
534
|
}
|
|
531
535
|
/**
|
|
532
|
-
* Resolve an orderBy key to its snake_case column via
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
536
|
+
* Resolve an orderBy key to its snake_case column via {@link resolveColumnName},
|
|
537
|
+
* throwing the SAME unknown-field E003 the top-level where path uses. Shared by
|
|
538
|
+
* top-level JSON-path ordering and every nested relation orderBy path so nested
|
|
539
|
+
* orderBy accepts exactly what top-level accepts (the 0.30.x bug: nested orderBy
|
|
540
|
+
* skipped the columnMap and rejected camelCase-named DB columns like
|
|
541
|
+
* "sortOrder").
|
|
542
|
+
*
|
|
543
|
+
* The rule is REACHED here, never restated: this used to inline
|
|
544
|
+
* `columnMap ?? camelToSnake` + an `allColumns` check, which agreed with
|
|
545
|
+
* `resolveColumnName` while the top-level scalar path a few lines up disagreed
|
|
546
|
+
* with both.
|
|
538
547
|
*/
|
|
539
548
|
export function resolveOrderByColumn(_qi, table, meta, key) {
|
|
540
|
-
const col =
|
|
541
|
-
if (
|
|
549
|
+
const col = resolveColumnName(meta, key);
|
|
550
|
+
if (col === undefined) {
|
|
542
551
|
throw new ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${table}". ` +
|
|
543
552
|
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
544
553
|
}
|
|
@@ -667,14 +676,18 @@ function buildChainedToOneOrderBy(qi, head, nextRelName, nextValue, params) {
|
|
|
667
676
|
`(got: ${entries.map(([k]) => k).join(', ') || '(empty)'}).`);
|
|
668
677
|
}
|
|
669
678
|
const [key, entryValue] = entries[0];
|
|
670
|
-
|
|
671
|
-
|
|
679
|
+
// Resolved, so a chained orderBy descends through the snake_case spelling
|
|
680
|
+
// of a relation exactly as `with` does; `relName` carries the DECLARED
|
|
681
|
+
// name onward so the path reported in errors is the canonical one.
|
|
682
|
+
const chained = resolveRelation(currentMeta.relations, key);
|
|
683
|
+
if (chained && isRelationOrderByValue(qi, entryValue)) {
|
|
684
|
+
relName = chained.name;
|
|
672
685
|
value = entryValue;
|
|
673
686
|
continue;
|
|
674
687
|
}
|
|
675
688
|
// Terminal: a column on the last table in the chain.
|
|
676
|
-
const snakeCol =
|
|
677
|
-
if (
|
|
689
|
+
const snakeCol = resolveColumnName(currentMeta, key);
|
|
690
|
+
if (snakeCol === undefined) {
|
|
678
691
|
throw new ValidationError(`[turbine] Unknown column "${key}" in orderBy on relation "${path.join('.')}" (table "${currentMeta.name}").`);
|
|
679
692
|
}
|
|
680
693
|
assertOrderDirection(entryValue, `orderBy on relation path "${path.join('.')}"`);
|
|
@@ -709,7 +722,12 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
709
722
|
const ownerMeta = ctx?.meta ?? qi.tableMeta;
|
|
710
723
|
const ownerTable = ctx?.table ?? qi.table;
|
|
711
724
|
const parentRef = ctx?.parentRef ?? qi.table;
|
|
712
|
-
|
|
725
|
+
// Resolved, then `relName` is rebound to the declared spelling so the alias
|
|
726
|
+
// and every message below use one name.
|
|
727
|
+
const resolvedOwner = resolveRelation(ownerMeta.relations, relName);
|
|
728
|
+
if (resolvedOwner)
|
|
729
|
+
relName = resolvedOwner.name;
|
|
730
|
+
const relDef = resolvedOwner?.def;
|
|
713
731
|
if (!relDef) {
|
|
714
732
|
// A table with no relations at all would otherwise render a dangling
|
|
715
733
|
// "Available: " and read as a broken message; and the most likely cause of
|
|
@@ -717,7 +735,7 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
717
735
|
// scalar column, which deserves to be named rather than reported as a
|
|
718
736
|
// missing relation.
|
|
719
737
|
const known = Object.keys(ownerMeta.relations);
|
|
720
|
-
const isColumn =
|
|
738
|
+
const isColumn = resolveColumnName(ownerMeta, relName) !== undefined;
|
|
721
739
|
throw new RelationError(isColumn
|
|
722
740
|
? `[turbine] orderBy on "${ownerTable}.${relName}" got a relation-shaped value, but "${relName}" is a ` +
|
|
723
741
|
`column. Order a column with 'asc' / 'desc' (or { sort, nulls }); the object form is for relations.`
|
|
@@ -768,10 +786,10 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
768
786
|
if (nestedRel && isRelationOrderByValue(qi, dirValue)) {
|
|
769
787
|
return buildChainedToOneOrderBy(qi, { relName, relDef, alias, correlation }, col, dirValue, params);
|
|
770
788
|
}
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
const snakeCol =
|
|
774
|
-
if (
|
|
789
|
+
// The ONE key-resolution rule ({@link resolveColumnName}), so a target
|
|
790
|
+
// column resolves here exactly as it does in the top-level orderBy.
|
|
791
|
+
const snakeCol = resolveColumnName(targetMeta, col);
|
|
792
|
+
if (snakeCol === undefined) {
|
|
775
793
|
const relationHint = ownLookup(targetMeta.relations, col)
|
|
776
794
|
? ` "${col}" is a relation on "${relDef.to}": order by one of ITS columns, e.g. ` +
|
|
777
795
|
`{ ${relName}: { ${col}: { <column>: 'asc' } } }.`
|
package/dist/query/utils.d.ts
CHANGED
|
@@ -59,6 +59,47 @@ export interface ColumnNameSource {
|
|
|
59
59
|
* name (see {@link ownLookup}).
|
|
60
60
|
*/
|
|
61
61
|
export declare function resolveColumnName(meta: ColumnNameSource, key: string): string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a user-supplied key to a relation's CANONICAL name and definition, or
|
|
64
|
+
* `undefined` when the key names no relation on the table.
|
|
65
|
+
*
|
|
66
|
+
* The relation-name half of the rule {@link resolveColumnName} states for
|
|
67
|
+
* columns, and deliberately the same shape: the declared name first, else
|
|
68
|
+
* `snakeToCamel(key)` accepted ONLY when that names a real relation.
|
|
69
|
+
* `snakeToCamel` is idempotent on an already-camel string, so a canonical key
|
|
70
|
+
* takes the first branch and this is a no-op for every existing caller.
|
|
71
|
+
*
|
|
72
|
+
* WHY IT EXISTS. A relation has one declared name, `ripeningChecks`, while the
|
|
73
|
+
* DDL anyone reads has only the TABLE name, `ripening_checks`. Writing back
|
|
74
|
+
* what the schema shows therefore failed with E005 in `with`, E003 in a
|
|
75
|
+
* relation filter, and E005 in `orderBy`, on names the error text was already
|
|
76
|
+
* computing correctly ("Did you mean ...?"). A system that can name the
|
|
77
|
+
* intended relation can accept it.
|
|
78
|
+
*
|
|
79
|
+
* NOT A GUESS, for the same reason the column rule is not: the transformed name
|
|
80
|
+
* is accepted only when it is a real declared relation, so an unknown key still
|
|
81
|
+
* fails and a typo is still a typo. Exact match wins first, so a schema that
|
|
82
|
+
* literally declares `ripening_checks` keeps it, even alongside a
|
|
83
|
+
* `ripeningChecks`.
|
|
84
|
+
*
|
|
85
|
+
* The RESULT KEY is the canonical name, not the caller's spelling, matching the
|
|
86
|
+
* column side (`select: { ledger_handle: true }` already returns
|
|
87
|
+
* `{ ledgerHandle }`). Resolving here rather than normalizing the args up front
|
|
88
|
+
* also means both spellings share one SQL-cache entry instead of minting two
|
|
89
|
+
* templates for one query.
|
|
90
|
+
*
|
|
91
|
+
* Prototype-safe via {@link ownLookup}, so `__proto__` cannot name a relation.
|
|
92
|
+
*/
|
|
93
|
+
export declare function resolveRelation<R>(relations: Record<string, R>, key: string): {
|
|
94
|
+
name: string;
|
|
95
|
+
def: R;
|
|
96
|
+
} | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* {@link resolveRelation} when only the definition is wanted: a drop-in for the
|
|
99
|
+
* `ownLookup(meta.relations, key)` it replaces, with the same signature and the
|
|
100
|
+
* same `undefined` on a miss.
|
|
101
|
+
*/
|
|
102
|
+
export declare function resolveRelationDef<R>(relations: Record<string, R>, key: string): R | undefined;
|
|
62
103
|
/**
|
|
63
104
|
* THE canonical order for a caller-supplied set of columns: the table's own
|
|
64
105
|
* `allColumns` order, which is the order `omit` and the default projection
|
|
@@ -356,6 +397,19 @@ export declare function isTemporalInfinity(value: unknown): boolean;
|
|
|
356
397
|
* astronomical year (`0044 BC` → -43) the way the driver's own parser does.
|
|
357
398
|
*/
|
|
358
399
|
export declare function createUtcDateParser(fallback: (text: string) => unknown): (text: string) => unknown;
|
|
400
|
+
/**
|
|
401
|
+
* The `date` (OID 1082) parser WITHOUT the fast scan in front of it: the regex
|
|
402
|
+
* implementation described by {@link createUtcDateParser}, and the reference
|
|
403
|
+
* side of the differential test.
|
|
404
|
+
*
|
|
405
|
+
* Exported so "what the fast path must agree with" is the running code rather
|
|
406
|
+
* than a transcription of it in a test file. Two hand-synced copies of a
|
|
407
|
+
* parser is the drift class this repo has been bitten by before; there is one
|
|
408
|
+
* copy, and the fast path delegates to it.
|
|
409
|
+
*
|
|
410
|
+
* @internal
|
|
411
|
+
*/
|
|
412
|
+
export declare function createUtcDateParserGeneral(fallback: (text: string) => unknown): (text: string) => unknown;
|
|
359
413
|
/**
|
|
360
414
|
* Build the driver parser for Postgres `timestamp` (OID 1114) that reads an
|
|
361
415
|
* offset-less date-time as UTC. Also lifted to the `_timestamp` array OID
|
|
@@ -376,6 +430,34 @@ export declare function createUtcDateParser(fallback: (text: string) => unknown)
|
|
|
376
430
|
* `Date`-string parsing did too.
|
|
377
431
|
*/
|
|
378
432
|
export declare function createUtcTimestampParser(fallback: (text: string) => unknown): (text: string) => unknown;
|
|
433
|
+
/**
|
|
434
|
+
* The `timestamp` (OID 1114) parser WITHOUT the fast scan in front of it: the
|
|
435
|
+
* regex implementation described by {@link createUtcTimestampParser}, and the
|
|
436
|
+
* reference side of the differential test. Same reasoning as
|
|
437
|
+
* {@link createUtcDateParserGeneral}.
|
|
438
|
+
*
|
|
439
|
+
* @internal
|
|
440
|
+
*/
|
|
441
|
+
export declare function createUtcTimestampParserGeneral(fallback: (text: string) => unknown): (text: string) => unknown;
|
|
442
|
+
/**
|
|
443
|
+
* Build the driver parser for Postgres `timestamptz` (OID 1184): the ISO wire
|
|
444
|
+
* shape decoded by {@link scanIsoTemporal}, everything else handed straight to
|
|
445
|
+
* `fallback`.
|
|
446
|
+
*
|
|
447
|
+
* UNLIKE the `date` and `timestamp` parsers beside it, this one changes NO
|
|
448
|
+
* READING. A `timestamptz` arrives with an explicit offset, so its instant is
|
|
449
|
+
* unambiguous and both this and `postgres-date` produce the same `Date`; the
|
|
450
|
+
* only difference is how long it takes. That is also why it is not governed by
|
|
451
|
+
* a semantic decision the way `utcTimestamps` governs the zone-less types: there
|
|
452
|
+
* is no second interpretation to choose between.
|
|
453
|
+
*
|
|
454
|
+
* `fallback` must be captured with `pg.types.getTypeParser(1184, 'text')`
|
|
455
|
+
* BEFORE registration, for the same reason as the parsers above: reading it
|
|
456
|
+
* afterwards hands back this function and recurses forever. It is what keeps
|
|
457
|
+
* `infinity` / `-infinity`, ` BC`, wide and low years, and every non-ISO
|
|
458
|
+
* `DateStyle` on `postgres-date`, which already handles them.
|
|
459
|
+
*/
|
|
460
|
+
export declare function createFastTimestamptzParser(fallback: (text: string) => unknown): (text: string) => unknown;
|
|
379
461
|
/**
|
|
380
462
|
* The offset-less-timestamp-as-UTC reading, with no fallback: `text` must be a
|
|
381
463
|
* plain `YYYY-MM-DD HH:MM:SS[.ffffff]`. Used where the input shape is already
|
|
@@ -395,8 +477,17 @@ export declare function parseUtcTimestampText(text: string): Date;
|
|
|
395
477
|
* `pg.types.arrayParser` is a public member of the `pg` module (it is what the
|
|
396
478
|
* driver's own `_text` / `_date` parsers are built from), so this adds no
|
|
397
479
|
* dependency. NULL elements stay `null` and are never handed to `element`.
|
|
480
|
+
*
|
|
481
|
+
* The empty-string guard mirrors pg's own `parseDateArray`, which opens
|
|
482
|
+
* `if (!value) return null`. Turbine's copy did not, and answered `[]` where
|
|
483
|
+
* the driver answers `null` for the same input. No column produces it (a SQL
|
|
484
|
+
* NULL never reaches a parser, and an empty array is `{}`), so this is parity
|
|
485
|
+
* for its own sake rather than a bug report; it matters because these parsers
|
|
486
|
+
* are registered process-globally over pg's, and a shape where Turbine's
|
|
487
|
+
* answer differs from the driver's is a difference somebody eventually finds
|
|
488
|
+
* the hard way.
|
|
398
489
|
*/
|
|
399
|
-
export declare function createPgArrayParser(element: (text: string) => unknown): (text: string) => unknown[];
|
|
490
|
+
export declare function createPgArrayParser(element: (text: string) => unknown): (text: string) => unknown[] | null;
|
|
400
491
|
/** Tag `parser` as Turbine's own and return it (see {@link TURBINE_PARSER}). */
|
|
401
492
|
export declare function markTurbineParser<F extends (text: string) => unknown>(parser: F): F;
|
|
402
493
|
/**
|
|
@@ -458,8 +549,21 @@ export declare function isDefaultTextParser(oid: number, parser: (text: string)
|
|
|
458
549
|
*/
|
|
459
550
|
export declare function warnParserOverwrite(oid: number, typeName: string): void;
|
|
460
551
|
/**
|
|
461
|
-
* Register
|
|
462
|
-
*
|
|
552
|
+
* Register Turbine's temporal text parsers on the pg module. SIX OIDs, doing
|
|
553
|
+
* two different jobs:
|
|
554
|
+
*
|
|
555
|
+
* 1114 / 1082 / 1115 / 1182 the UTC READING of the zone-less types,
|
|
556
|
+
* `timestamp`, `date` and their array forms.
|
|
557
|
+
* This changes what a column means and is what
|
|
558
|
+
* `utcTimestamps` is named for.
|
|
559
|
+
* 1184 / 1185 the fast decode path for `timestamptz` and
|
|
560
|
+
* `timestamptz[]`. This changes NOTHING about
|
|
561
|
+
* what a column means: an offset-carrying value
|
|
562
|
+
* has one instant and this reads the same one.
|
|
563
|
+
* It is here for speed, `timestamptz` being ~88%
|
|
564
|
+
* of the client-side decode cost of a wide row
|
|
565
|
+
* drain, and it DECLINES rather than overwrites
|
|
566
|
+
* (see the comment at the call site).
|
|
463
567
|
*
|
|
464
568
|
* ONE place, because `pg.types.setTypeParser` is process-global and the pairing
|
|
465
569
|
* matters: registering a scalar without its array form, or a `date` without the
|