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.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/client.d.ts +0 -18
  3. package/dist/cjs/client.js +2 -29
  4. package/dist/cjs/connection-url.d.ts +30 -0
  5. package/dist/cjs/connection-url.js +15 -17
  6. package/dist/cjs/powql.d.ts +38 -1
  7. package/dist/cjs/powql.js +106 -18
  8. package/dist/cjs/query/aggregates.d.ts +0 -13
  9. package/dist/cjs/query/aggregates.js +81 -33
  10. package/dist/cjs/query/batched-loader.d.ts +13 -1
  11. package/dist/cjs/query/batched-loader.js +46 -11
  12. package/dist/cjs/query/builder.d.ts +13 -0
  13. package/dist/cjs/query/builder.js +104 -14
  14. package/dist/cjs/query/compound-unique.js +29 -5
  15. package/dist/cjs/query/relation-names.d.ts +52 -0
  16. package/dist/cjs/query/relation-names.js +120 -0
  17. package/dist/cjs/query/relations.d.ts +11 -6
  18. package/dist/cjs/query/relations.js +45 -27
  19. package/dist/cjs/query/utils.d.ts +107 -3
  20. package/dist/cjs/query/utils.js +408 -7
  21. package/dist/cjs/query/where-compile.js +9 -4
  22. package/dist/cjs/query/where.js +9 -5
  23. package/dist/client.d.ts +0 -18
  24. package/dist/client.js +2 -29
  25. package/dist/connection-url.d.ts +30 -0
  26. package/dist/connection-url.js +15 -18
  27. package/dist/powql.d.ts +38 -1
  28. package/dist/powql.js +107 -19
  29. package/dist/query/aggregates.d.ts +0 -13
  30. package/dist/query/aggregates.js +82 -34
  31. package/dist/query/batched-loader.d.ts +13 -1
  32. package/dist/query/batched-loader.js +47 -12
  33. package/dist/query/builder.d.ts +13 -0
  34. package/dist/query/builder.js +105 -15
  35. package/dist/query/compound-unique.js +30 -6
  36. package/dist/query/relation-names.d.ts +52 -0
  37. package/dist/query/relation-names.js +117 -0
  38. package/dist/query/relations.d.ts +11 -6
  39. package/dist/query/relations.js +47 -29
  40. package/dist/query/utils.d.ts +107 -3
  41. package/dist/query/utils.js +404 -8
  42. package/dist/query/where-compile.js +10 -5
  43. package/dist/query/where.js +10 -6
  44. package/package.json +5 -3
@@ -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 { camelToSnake, normalizeKeyColumns, snakeToCamel } from '../schema.js';
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
- if (ownLookup(meta.relations, field))
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 && !(key in meta.columnMap)) {
466
- throw new ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${qi.table}". ` +
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
- if (meta && !(key in meta.columnMap)) {
492
- throw new ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${qi.table}". ` +
493
- `Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
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 the table's columnMap
533
- * (camelToSnake fallback), throwing the SAME unknown-field E003 the top-level
534
- * where path uses. Shared by top-level JSON-path ordering and every nested
535
- * relation orderBy path so nested orderBy accepts exactly what top-level
536
- * accepts (the 0.30.x bug: nested orderBy skipped the columnMap and rejected
537
- * camelCase-named DB columns like "sortOrder").
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 = ownLookup(meta.columnMap, key) ?? camelToSnake(key);
541
- if (!meta.allColumns.includes(col)) {
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
- if (ownLookup(currentMeta.relations, key) && isRelationOrderByValue(qi, entryValue)) {
671
- relName = key;
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 = ownLookup(currentMeta.columnMap, key) ?? camelToSnake(key);
677
- if (!currentMeta.allColumns.includes(snakeCol)) {
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
- const relDef = ownLookup(ownerMeta.relations, relName);
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 = Object.hasOwn(ownerMeta.columnMap, relName) || ownerMeta.allColumns.includes(relName);
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
- // columnMap-first resolution (camelToSnake fallback): mirrors the
772
- // scalar orderBy path so camelCase-named DB columns resolve here too.
773
- const snakeCol = ownLookup(targetMeta.columnMap, col) ?? camelToSnake(col);
774
- if (!targetMeta.allColumns.includes(snakeCol)) {
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' } } }.`
@@ -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 the UTC readings of the four zone-less temporal OIDs on the pg
462
- * module: `timestamp` (1114), `date` (1082) and their array forms (1115, 1182).
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