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
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Caller-supplied RELATION names, normalized to their declared spelling once,
3
+ * before anything reads them.
4
+ *
5
+ * ## The rule
6
+ *
7
+ * A relation has one declared name (`ripeningChecks`), while the DDL anyone
8
+ * reads has only the table name (`ripening_checks`). Writing back what the
9
+ * schema shows therefore failed: E005 in `with`, E003 in a relation filter,
10
+ * E005 in `orderBy`, on names the error text was already computing correctly
11
+ * ("Did you mean ...?"). This accepts the snake_case spelling wherever the
12
+ * declared name is accepted, by the same resolve-then-validate rule
13
+ * {@link resolveRelation} states, which is the relation-level twin of
14
+ * `resolveColumnName`. It is not a guess: `snakeToCamel(key)` is accepted ONLY
15
+ * when it names a real declared relation, so a typo is still a typo, and an
16
+ * exact declared name always wins first.
17
+ *
18
+ * ## Why this is ONE pass up front and not a fix at each lookup
19
+ *
20
+ * The `with` tree is walked by SIX independent functions, each of which decides
21
+ * for itself which keys name relations: `withFingerprint`, `collectWithParams`,
22
+ * `buildRelationShapes`, `planFlattenWith`, `buildSelectWithRelations` and the
23
+ * batched loader, plus the positional row parser. Teaching each of them that a
24
+ * key has two spellings would make seven places that must agree about it, and
25
+ * disagreement is not a clean failure: the fingerprint is the SQL-cache key, so
26
+ * a walker that resolved differently from the builder would serve one query's
27
+ * template to another, silently. That is the drift class this repo has paid for
28
+ * repeatedly (the where-clause walkers, the two projection resolvers).
29
+ *
30
+ * Normalizing before any walker runs makes all seven correct with no knowledge
31
+ * of the second spelling, and keeps ONE authority for the rule.
32
+ *
33
+ * ## Shape
34
+ *
35
+ * Returns the SAME object when nothing needed rewriting, which is every query
36
+ * that already spells its relations the declared way, so the common path
37
+ * allocates nothing and is reference-identical to its input.
38
+ *
39
+ * An UNRESOLVABLE key is left exactly as written, deliberately. Reporting it is
40
+ * the builder's job, and it already names the offending key and lists the
41
+ * available relations; rejecting it here would move that error away from its
42
+ * context and change which error type callers see.
43
+ */
44
+ import type { SchemaMetadata } from '../schema.js';
45
+ import type { WithClause } from './types.js';
46
+ /**
47
+ * `withClause` with every relation key replaced by its declared spelling,
48
+ * recursively, including the relation names inside a `_count`.
49
+ *
50
+ * Returns the input by reference when no key changed.
51
+ */
52
+ export declare function normalizeWithClause(schema: SchemaMetadata, table: string, withClause: WithClause | undefined, depth?: number): WithClause | undefined;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * Caller-supplied RELATION names, normalized to their declared spelling once,
4
+ * before anything reads them.
5
+ *
6
+ * ## The rule
7
+ *
8
+ * A relation has one declared name (`ripeningChecks`), while the DDL anyone
9
+ * reads has only the table name (`ripening_checks`). Writing back what the
10
+ * schema shows therefore failed: E005 in `with`, E003 in a relation filter,
11
+ * E005 in `orderBy`, on names the error text was already computing correctly
12
+ * ("Did you mean ...?"). This accepts the snake_case spelling wherever the
13
+ * declared name is accepted, by the same resolve-then-validate rule
14
+ * {@link resolveRelation} states, which is the relation-level twin of
15
+ * `resolveColumnName`. It is not a guess: `snakeToCamel(key)` is accepted ONLY
16
+ * when it names a real declared relation, so a typo is still a typo, and an
17
+ * exact declared name always wins first.
18
+ *
19
+ * ## Why this is ONE pass up front and not a fix at each lookup
20
+ *
21
+ * The `with` tree is walked by SIX independent functions, each of which decides
22
+ * for itself which keys name relations: `withFingerprint`, `collectWithParams`,
23
+ * `buildRelationShapes`, `planFlattenWith`, `buildSelectWithRelations` and the
24
+ * batched loader, plus the positional row parser. Teaching each of them that a
25
+ * key has two spellings would make seven places that must agree about it, and
26
+ * disagreement is not a clean failure: the fingerprint is the SQL-cache key, so
27
+ * a walker that resolved differently from the builder would serve one query's
28
+ * template to another, silently. That is the drift class this repo has paid for
29
+ * repeatedly (the where-clause walkers, the two projection resolvers).
30
+ *
31
+ * Normalizing before any walker runs makes all seven correct with no knowledge
32
+ * of the second spelling, and keeps ONE authority for the rule.
33
+ *
34
+ * ## Shape
35
+ *
36
+ * Returns the SAME object when nothing needed rewriting, which is every query
37
+ * that already spells its relations the declared way, so the common path
38
+ * allocates nothing and is reference-identical to its input.
39
+ *
40
+ * An UNRESOLVABLE key is left exactly as written, deliberately. Reporting it is
41
+ * the builder's job, and it already names the offending key and lists the
42
+ * available relations; rejecting it here would move that error away from its
43
+ * context and change which error type callers see.
44
+ */
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.normalizeWithClause = normalizeWithClause;
47
+ const utils_js_1 = require("./utils.js");
48
+ /** Depth cap mirroring the builder's own, so a cyclic `with` cannot spin here. */
49
+ const MAX_DEPTH = 12;
50
+ /**
51
+ * `withClause` with every relation key replaced by its declared spelling,
52
+ * recursively, including the relation names inside a `_count`.
53
+ *
54
+ * Returns the input by reference when no key changed.
55
+ */
56
+ function normalizeWithClause(schema, table, withClause, depth = 0) {
57
+ if (!withClause || typeof withClause !== 'object' || depth > MAX_DEPTH)
58
+ return withClause;
59
+ const meta = schema.tables[table];
60
+ if (!meta)
61
+ return withClause;
62
+ let changed = false;
63
+ const out = {};
64
+ for (const [key, spec] of Object.entries(withClause)) {
65
+ if (key === '_count') {
66
+ const nextCount = normalizeCount(meta, spec);
67
+ if (nextCount !== spec)
68
+ changed = true;
69
+ out[key] = nextCount;
70
+ continue;
71
+ }
72
+ const resolved = (0, utils_js_1.resolveRelation)(meta.relations, key);
73
+ // Unknown key: keep it verbatim and let the builder raise E005 by name.
74
+ const name = resolved?.name ?? key;
75
+ if (name !== key)
76
+ changed = true;
77
+ const nextSpec = resolved ? normalizeSpec(schema, resolved.def.to, spec, depth) : spec;
78
+ if (nextSpec !== spec)
79
+ changed = true;
80
+ out[name] = nextSpec;
81
+ }
82
+ return changed ? out : withClause;
83
+ }
84
+ /** A relation's `with` options, normalizing its nested `with` against the TARGET table. */
85
+ function normalizeSpec(schema, target, spec, depth) {
86
+ if (spec === true || spec === false || spec === null || typeof spec !== 'object')
87
+ return spec;
88
+ const opts = spec;
89
+ if (!opts.with)
90
+ return spec;
91
+ const nested = normalizeWithClause(schema, target, opts.with, depth + 1);
92
+ return nested === opts.with ? spec : { ...opts, with: nested };
93
+ }
94
+ /**
95
+ * `_count` names relations too. `true` means every relation and has nothing to
96
+ * rename; the record form has one key per counted relation.
97
+ */
98
+ function normalizeCount(meta, count) {
99
+ if (!count || typeof count !== 'object')
100
+ return count;
101
+ let changed = false;
102
+ const out = {};
103
+ for (const [key, value] of Object.entries(count)) {
104
+ const name = (0, utils_js_1.resolveRelation)(meta.relations, key)?.name ?? key;
105
+ if (name !== key)
106
+ changed = true;
107
+ out[name] = value;
108
+ }
109
+ return changed ? out : count;
110
+ }
111
+ /*
112
+ * NOTE for the next person: there are deliberately no `declaredRelationName` /
113
+ * `namesRelation` wrappers here. The argument positions where a relation key
114
+ * sits INTERLEAVED with column keys (`where`'s some/every/none, `orderBy`'s
115
+ * relation targets, the simple-where fast path) cannot be normalized up front
116
+ * the way `with` can, so they call `resolveRelation` / `resolveRelationDef`
117
+ * from query/utils.ts directly at their branch point. Each of those branches is
118
+ * already a documented single authority; wrapping them here would add a second
119
+ * name for the same rule without removing a caller.
120
+ */
@@ -117,12 +117,17 @@ export declare function isRelationOrderByValue(_qi: BuilderCtx, value: unknown):
117
117
  */
118
118
  export declare function nullsSuffix(qi: BuilderCtx, nulls: 'first' | 'last' | undefined): string;
119
119
  /**
120
- * Resolve an orderBy key to its snake_case column via the table's columnMap
121
- * (camelToSnake fallback), throwing the SAME unknown-field E003 the top-level
122
- * where path uses. Shared by top-level JSON-path ordering and every nested
123
- * relation orderBy path so nested orderBy accepts exactly what top-level
124
- * accepts (the 0.30.x bug: nested orderBy skipped the columnMap and rejected
125
- * camelCase-named DB columns like "sortOrder").
120
+ * Resolve an orderBy key to its snake_case column via {@link resolveColumnName},
121
+ * throwing the SAME unknown-field E003 the top-level where path uses. Shared by
122
+ * top-level JSON-path ordering and every nested relation orderBy path so nested
123
+ * orderBy accepts exactly what top-level accepts (the 0.30.x bug: nested orderBy
124
+ * skipped the columnMap and rejected camelCase-named DB columns like
125
+ * "sortOrder").
126
+ *
127
+ * The rule is REACHED here, never restated: this used to inline
128
+ * `columnMap ?? camelToSnake` + an `allColumns` check, which agreed with
129
+ * `resolveColumnName` while the top-level scalar path a few lines up disagreed
130
+ * with both.
126
131
  */
127
132
  export declare function resolveOrderByColumn(_qi: BuilderCtx, table: string, meta: TableMetadata, key: string): string;
128
133
  /**
@@ -117,7 +117,10 @@ function projectionColumn(table, meta, field, clause) {
117
117
  // A relation named in a projection is a habit, not a typo, so it gets its own
118
118
  // message pointing at `with`. Checked BEFORE the generic throw because the
119
119
  // generic one degrades into "Did you mean <exactly what you typed>?".
120
- if ((0, utils_js_1.ownLookup)(meta.relations, field))
120
+ // Resolved, so `select: { ripening_checks: true }` gets the "that is a
121
+ // relation, use `with`" message rather than degrading to an unknown-field
122
+ // suggestion that names the relation back at the caller.
123
+ if ((0, utils_js_1.resolveRelationDef)(meta.relations, field))
121
124
  throw new errors_js_1.ValidationError((0, utils_js_1.relationInProjectionMessage)(table, field, clause));
122
125
  throw new errors_js_1.ValidationError((0, utils_js_1.unknownFieldMessage)(table, field, meta));
123
126
  }
@@ -541,10 +544,8 @@ function buildOrderBy(qi, orderBy, params, lateralSink) {
541
544
  .map(([key, value]) => {
542
545
  // Vector KNN ordering: { distance: { to, metric, direction? } }
543
546
  if ((0, filters_js_1.isVectorOrderBy)(value)) {
544
- if (meta && !(key in meta.columnMap)) {
545
- throw new errors_js_1.ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${qi.table}". ` +
546
- `Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
547
- }
547
+ if (meta)
548
+ resolveOrderByColumn(qi, qi.table, meta, key);
548
549
  if (!params) {
549
550
  throw new errors_js_1.ValidationError(`[turbine] Vector distance ordering on "${key}" is only supported in a top-level findMany orderBy.`);
550
551
  }
@@ -567,10 +568,13 @@ function buildOrderBy(qi, orderBy, params, lateralSink) {
567
568
  return buildRelationOrderBy(qi, key, value, `ord${relOrdCounter++}`, params, undefined, lateralSink);
568
569
  }
569
570
  // Scalar column ordering, a plain direction or an OrderBySpec (nulls).
570
- if (meta && !(key in meta.columnMap)) {
571
- throw new errors_js_1.ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${qi.table}". ` +
572
- `Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
573
- }
571
+ // Through `resolveOrderByColumn`, which is the same resolve-or-E003 the
572
+ // nested orderBy paths below already used and emits the same message
573
+ // this used to inline. It used to test `key in meta.columnMap`, which
574
+ // knows only the FIELD spelling, and so rejected the snake_case COLUMN
575
+ // name that `where` / `select` / `distinct` accept on the same table.
576
+ if (meta)
577
+ resolveOrderByColumn(qi, qi.table, meta, key);
574
578
  // Refuse a direction that is neither asc nor desc. normalizeOrderBy is
575
579
  // `=== 'desc' ? DESC : ASC`, so without this every typo sorted ASCENDING
576
580
  // and returned a correct-looking page in the reverse order.
@@ -608,16 +612,21 @@ function nullsSuffix(qi, nulls) {
608
612
  return nulls === 'first' ? ' NULLS FIRST' : ' NULLS LAST';
609
613
  }
610
614
  /**
611
- * Resolve an orderBy key to its snake_case column via the table's columnMap
612
- * (camelToSnake fallback), throwing the SAME unknown-field E003 the top-level
613
- * where path uses. Shared by top-level JSON-path ordering and every nested
614
- * relation orderBy path so nested orderBy accepts exactly what top-level
615
- * accepts (the 0.30.x bug: nested orderBy skipped the columnMap and rejected
616
- * camelCase-named DB columns like "sortOrder").
615
+ * Resolve an orderBy key to its snake_case column via {@link resolveColumnName},
616
+ * throwing the SAME unknown-field E003 the top-level where path uses. Shared by
617
+ * top-level JSON-path ordering and every nested relation orderBy path so nested
618
+ * orderBy accepts exactly what top-level accepts (the 0.30.x bug: nested orderBy
619
+ * skipped the columnMap and rejected camelCase-named DB columns like
620
+ * "sortOrder").
621
+ *
622
+ * The rule is REACHED here, never restated: this used to inline
623
+ * `columnMap ?? camelToSnake` + an `allColumns` check, which agreed with
624
+ * `resolveColumnName` while the top-level scalar path a few lines up disagreed
625
+ * with both.
617
626
  */
618
627
  function resolveOrderByColumn(_qi, table, meta, key) {
619
- const col = (0, utils_js_1.ownLookup)(meta.columnMap, key) ?? (0, schema_js_1.camelToSnake)(key);
620
- if (!meta.allColumns.includes(col)) {
628
+ const col = (0, utils_js_1.resolveColumnName)(meta, key);
629
+ if (col === undefined) {
621
630
  throw new errors_js_1.ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${table}". ` +
622
631
  `Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
623
632
  }
@@ -746,14 +755,18 @@ function buildChainedToOneOrderBy(qi, head, nextRelName, nextValue, params) {
746
755
  `(got: ${entries.map(([k]) => k).join(', ') || '(empty)'}).`);
747
756
  }
748
757
  const [key, entryValue] = entries[0];
749
- if ((0, utils_js_1.ownLookup)(currentMeta.relations, key) && isRelationOrderByValue(qi, entryValue)) {
750
- relName = key;
758
+ // Resolved, so a chained orderBy descends through the snake_case spelling
759
+ // of a relation exactly as `with` does; `relName` carries the DECLARED
760
+ // name onward so the path reported in errors is the canonical one.
761
+ const chained = (0, utils_js_1.resolveRelation)(currentMeta.relations, key);
762
+ if (chained && isRelationOrderByValue(qi, entryValue)) {
763
+ relName = chained.name;
751
764
  value = entryValue;
752
765
  continue;
753
766
  }
754
767
  // Terminal: a column on the last table in the chain.
755
- const snakeCol = (0, utils_js_1.ownLookup)(currentMeta.columnMap, key) ?? (0, schema_js_1.camelToSnake)(key);
756
- if (!currentMeta.allColumns.includes(snakeCol)) {
768
+ const snakeCol = (0, utils_js_1.resolveColumnName)(currentMeta, key);
769
+ if (snakeCol === undefined) {
757
770
  throw new errors_js_1.ValidationError(`[turbine] Unknown column "${key}" in orderBy on relation "${path.join('.')}" (table "${currentMeta.name}").`);
758
771
  }
759
772
  (0, types_js_1.assertOrderDirection)(entryValue, `orderBy on relation path "${path.join('.')}"`);
@@ -788,7 +801,12 @@ function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lateralSin
788
801
  const ownerMeta = ctx?.meta ?? qi.tableMeta;
789
802
  const ownerTable = ctx?.table ?? qi.table;
790
803
  const parentRef = ctx?.parentRef ?? qi.table;
791
- const relDef = (0, utils_js_1.ownLookup)(ownerMeta.relations, relName);
804
+ // Resolved, then `relName` is rebound to the declared spelling so the alias
805
+ // and every message below use one name.
806
+ const resolvedOwner = (0, utils_js_1.resolveRelation)(ownerMeta.relations, relName);
807
+ if (resolvedOwner)
808
+ relName = resolvedOwner.name;
809
+ const relDef = resolvedOwner?.def;
792
810
  if (!relDef) {
793
811
  // A table with no relations at all would otherwise render a dangling
794
812
  // "Available: " and read as a broken message; and the most likely cause of
@@ -796,7 +814,7 @@ function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lateralSin
796
814
  // scalar column, which deserves to be named rather than reported as a
797
815
  // missing relation.
798
816
  const known = Object.keys(ownerMeta.relations);
799
- const isColumn = Object.hasOwn(ownerMeta.columnMap, relName) || ownerMeta.allColumns.includes(relName);
817
+ const isColumn = (0, utils_js_1.resolveColumnName)(ownerMeta, relName) !== undefined;
800
818
  throw new errors_js_1.RelationError(isColumn
801
819
  ? `[turbine] orderBy on "${ownerTable}.${relName}" got a relation-shaped value, but "${relName}" is a ` +
802
820
  `column. Order a column with 'asc' / 'desc' (or { sort, nulls }); the object form is for relations.`
@@ -847,10 +865,10 @@ function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lateralSin
847
865
  if (nestedRel && isRelationOrderByValue(qi, dirValue)) {
848
866
  return buildChainedToOneOrderBy(qi, { relName, relDef, alias, correlation }, col, dirValue, params);
849
867
  }
850
- // columnMap-first resolution (camelToSnake fallback): mirrors the
851
- // scalar orderBy path so camelCase-named DB columns resolve here too.
852
- const snakeCol = (0, utils_js_1.ownLookup)(targetMeta.columnMap, col) ?? (0, schema_js_1.camelToSnake)(col);
853
- if (!targetMeta.allColumns.includes(snakeCol)) {
868
+ // The ONE key-resolution rule ({@link resolveColumnName}), so a target
869
+ // column resolves here exactly as it does in the top-level orderBy.
870
+ const snakeCol = (0, utils_js_1.resolveColumnName)(targetMeta, col);
871
+ if (snakeCol === undefined) {
854
872
  const relationHint = (0, utils_js_1.ownLookup)(targetMeta.relations, col)
855
873
  ? ` "${col}" is a relation on "${relDef.to}": order by one of ITS columns, e.g. ` +
856
874
  `{ ${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