turbine-orm 0.34.0 → 0.36.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 (76) hide show
  1. package/README.md +18 -16
  2. package/dist/cjs/cli/index.js +109 -16
  3. package/dist/cjs/cli/migrate.js +78 -3
  4. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  5. package/dist/cjs/cli/studio.js +333 -22
  6. package/dist/cjs/cli/ui.js +7 -1
  7. package/dist/cjs/client.js +26 -4
  8. package/dist/cjs/dialect.js +2 -1
  9. package/dist/cjs/errors.js +41 -1
  10. package/dist/cjs/generate.js +23 -2
  11. package/dist/cjs/index.js +4 -2
  12. package/dist/cjs/mssql.js +27 -5
  13. package/dist/cjs/mysql.js +4 -0
  14. package/dist/cjs/powdb.js +197 -25
  15. package/dist/cjs/powql.js +515 -51
  16. package/dist/cjs/query/aggregates.js +683 -0
  17. package/dist/cjs/query/batched-loader.js +2 -0
  18. package/dist/cjs/query/builder.js +361 -4508
  19. package/dist/cjs/query/filters.js +12 -0
  20. package/dist/cjs/query/relations.js +1698 -0
  21. package/dist/cjs/query/where-compile.js +180 -0
  22. package/dist/cjs/query/where.js +1491 -0
  23. package/dist/cjs/query/writes.js +680 -0
  24. package/dist/cjs/schema-builder.js +6 -0
  25. package/dist/cjs/schema-metadata.js +4 -0
  26. package/dist/cjs/schema-sql.js +265 -3
  27. package/dist/cjs/sqlite.js +4 -1
  28. package/dist/cli/index.d.ts +8 -2
  29. package/dist/cli/index.js +111 -18
  30. package/dist/cli/migrate.d.ts +24 -1
  31. package/dist/cli/migrate.js +77 -3
  32. package/dist/cli/studio-ui.generated.js +1 -1
  33. package/dist/cli/studio.d.ts +46 -13
  34. package/dist/cli/studio.js +331 -23
  35. package/dist/cli/ui.js +7 -1
  36. package/dist/client.d.ts +32 -5
  37. package/dist/client.js +26 -4
  38. package/dist/dialect.d.ts +28 -6
  39. package/dist/dialect.js +2 -1
  40. package/dist/errors.d.ts +36 -0
  41. package/dist/errors.js +39 -0
  42. package/dist/generate.js +23 -2
  43. package/dist/index.d.ts +3 -3
  44. package/dist/index.js +2 -2
  45. package/dist/mssql.js +27 -5
  46. package/dist/mysql.js +4 -0
  47. package/dist/powdb.d.ts +135 -9
  48. package/dist/powdb.js +197 -25
  49. package/dist/powql.d.ts +166 -4
  50. package/dist/powql.js +516 -52
  51. package/dist/query/aggregates.d.ts +74 -0
  52. package/dist/query/aggregates.js +641 -0
  53. package/dist/query/batched-loader.d.ts +6 -0
  54. package/dist/query/batched-loader.js +2 -0
  55. package/dist/query/builder.d.ts +98 -830
  56. package/dist/query/builder.js +366 -4513
  57. package/dist/query/deferred.d.ts +13 -2
  58. package/dist/query/filters.d.ts +7 -0
  59. package/dist/query/filters.js +11 -0
  60. package/dist/query/relations.d.ts +441 -0
  61. package/dist/query/relations.js +1627 -0
  62. package/dist/query/types.d.ts +25 -6
  63. package/dist/query/where-compile.d.ts +139 -0
  64. package/dist/query/where-compile.js +175 -0
  65. package/dist/query/where.d.ts +494 -0
  66. package/dist/query/where.js +1431 -0
  67. package/dist/query/writes.d.ts +131 -0
  68. package/dist/query/writes.js +626 -0
  69. package/dist/schema-builder.d.ts +18 -3
  70. package/dist/schema-builder.js +6 -0
  71. package/dist/schema-metadata.js +4 -0
  72. package/dist/schema-sql.d.ts +60 -3
  73. package/dist/schema-sql.js +261 -4
  74. package/dist/schema.d.ts +10 -0
  75. package/dist/sqlite.js +4 -1
  76. package/package.json +4 -4
@@ -7,15 +7,19 @@ export type OrderDirection = 'asc' | 'desc';
7
7
  /**
8
8
  * How a query resolves its `with` relations.
9
9
  *
10
- * - `'join'` (default) — one SQL statement with correlated
11
- * `json_agg(json_build_object(...))` subqueries. One round-trip; an index
12
- * seek per parent row when the child FK is indexed.
13
- * - `'batched'` run the base query, then ONE flat follow-up query per
10
+ * - `'join'`: one SQL statement with correlated
11
+ * `json_agg(json_build_object(...))` subqueries. One round-trip, an index
12
+ * seek per parent row when the child FK is indexed. On PowDB, `'join'`
13
+ * instead opts into native PowQL server-side joins where eligible.
14
+ * - `'batched'`: run the base query, then ONE flat follow-up query per
14
15
  * relation (`WHERE fk = ANY($1)`), stitching children client-side. D levels
15
16
  * cost D extra round-trips, but each is a single key-set lookup and rows come
16
- * back flat a win when FK columns are unindexed or result sets are huge.
17
+ * back flat (a win when FK columns are unindexed or result sets are huge).
17
18
  *
18
- * Precedence: per-query arg > client `relationLoadStrategy` config > `'join'`.
19
+ * Precedence: per-query arg > client `relationLoadStrategy` config > the engine
20
+ * default. On SQL engines the default is `'join'`; on PowDB the default is the
21
+ * batched loaders (an ineligible relation falls back to them per-relation and
22
+ * silently even when `'join'` is requested).
19
23
  */
20
24
  export type RelationLoadStrategy = 'join' | 'batched';
21
25
  /**
@@ -317,6 +321,8 @@ export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithCla
317
321
  relationLoadStrategy?: RelationLoadStrategy;
318
322
  /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
319
323
  skipGlobalFilters?: SkipGlobalFilters;
324
+ /** Include PII-tagged columns in the result. See {@link FindManyArgs.includePii}. */
325
+ includePii?: boolean;
320
326
  }
321
327
  export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
322
328
  where?: WhereClause<T>;
@@ -345,6 +351,19 @@ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClaus
345
351
  * force the warning even when it is disabled in config.
346
352
  */
347
353
  warnOnUnlimited?: boolean;
354
+ /**
355
+ * Include PII-tagged columns (`defineSchema` `pii: true`) in the result.
356
+ *
357
+ * PII columns are EXCLUDED from default projections: they come back only when
358
+ * explicitly named in `select`, or when this flag is `true`. Set it to `true`
359
+ * to return every PII column at the top level AND at every nested `with`
360
+ * level of this query. Default `false`. Schemas with no PII-tagged columns are
361
+ * unaffected (the emitted SQL is byte-identical either way).
362
+ *
363
+ * Referencing a PII column in `where` / `orderBy` / `groupBy` / aggregates is
364
+ * always allowed regardless of this flag (the reference is explicit).
365
+ */
366
+ includePii?: boolean;
348
367
  }
349
368
  export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> extends FindManyArgs<T, R, W, S, O> {
350
369
  /**
@@ -0,0 +1,139 @@
1
+ /**
2
+ * turbine-orm: Shared WHERE-clause walk
3
+ *
4
+ * The SQL template cache requires three code paths over a table-scoped WHERE
5
+ * object to stay in perfect lockstep:
6
+ * - `fingerprintWhere` : the value-invariant cache KEY,
7
+ * - `buildWhereClause` : the SQL text + `$N` params on a cache MISS,
8
+ * - `collectWhereParams` : the params ONLY on a cache HIT (no SQL rebuild).
9
+ *
10
+ * If any two of them enumerate the WHERE keys in a different order, or classify
11
+ * a key's value into a different filter shape, the cached SQL's `$N`
12
+ * placeholders bind the wrong values: a silent cross-value (and, with tenant
13
+ * columns, cross-tenant) leak. That drift shipped twice historically (permuted
14
+ * where-key order; an orderBy fingerprint collision).
15
+ *
16
+ * This module removes the drift BY CONSTRUCTION:
17
+ * - {@link walkWhere} is the ONE enumeration. It sorts keys canonically,
18
+ * skips `undefined`, dispatches the `OR`/`AND`/`NOT` combinators and
19
+ * relation filters, and yields a flat, ordered {@link WhereEvent} stream.
20
+ * All three consumers iterate this same stream, so their key order and
21
+ * combinator structure can never diverge again.
22
+ * - {@link classifyScalarForSql} is the ONE scalar-shape decision the SQL
23
+ * paths use. `buildWhereClause` and `collectWhereParams` BOTH call it with
24
+ * the same `(rawColumn, value)`, so they always take the same branch and
25
+ * therefore push params in the same order.
26
+ * - {@link fingerprintScalarToken} is the fingerprint's own (deliberately
27
+ * column-blind) scalar token. It over-distinguishes relative to the SQL
28
+ * classifier (which is always safe), so a fingerprint match still implies
29
+ * an identical SQL shape.
30
+ *
31
+ * The dev-mode / sampled-production cross-check in `builder.ts` stays as the
32
+ * tripwire: with this shared walk it should never fire, but it remains the
33
+ * last-line guard against a future leaf builder / collect mirror falling out of
34
+ * step.
35
+ */
36
+ import type { RelationDef, TableMetadata } from '../schema.js';
37
+ /** A table-scoped WHERE object (or an `OR`/`AND`/`NOT` branch of one). */
38
+ export type WhereRecord = Record<string, unknown>;
39
+ /**
40
+ * Everything the shared walk needs from the owning {@link QueryInterface}. Bound
41
+ * once per instance (see `QueryInterface`'s `whereHost` field) so the walk stays
42
+ * a pure function of the instance's schema state without widening the class's
43
+ * public surface.
44
+ */
45
+ export interface WhereHost {
46
+ readonly tableMeta: TableMetadata;
47
+ /** Wrap a bare belongsTo/hasOne relation filter in `{ is: … }`. */
48
+ normalizeRelationFilter(relDef: RelationDef, filterObj: WhereRecord): WhereRecord;
49
+ /** Resolve a column's Postgres type token (defaults to `text`). */
50
+ getColumnPgType(column: string): string;
51
+ /** True for `json` / `jsonb` column types. */
52
+ isJsonColumnType(colType: string): boolean;
53
+ }
54
+ /**
55
+ * The SQL-relevant classification of a scalar WHERE value, decided column-aware
56
+ * in the SAME linear fall-through order the SQL builder has always used
57
+ * (null → vector → json → array → text-search → operator → equality). Shared by
58
+ * the build and collect paths so their branch choice (and thus param order)
59
+ * is identical by construction. The `*Throw` variants preserve the build path's
60
+ * strict-validation errors for a JSON/array operator on a non-JSON/array column.
61
+ */
62
+ export type ScalarSqlClass = {
63
+ kind: 'null';
64
+ } | {
65
+ kind: 'vector';
66
+ } | {
67
+ kind: 'json';
68
+ } | {
69
+ kind: 'jsonThrow';
70
+ jsonKey: string;
71
+ } | {
72
+ kind: 'array';
73
+ colType: string;
74
+ } | {
75
+ kind: 'arrayThrow';
76
+ arrayKey: string;
77
+ } | {
78
+ kind: 'textsearch';
79
+ } | {
80
+ kind: 'operator';
81
+ } | {
82
+ kind: 'equality';
83
+ };
84
+ /**
85
+ * One node of the canonical WHERE walk. `scalar` carries only `key` + `value`;
86
+ * each consumer resolves the column/shape itself (the SQL paths via
87
+ * {@link classifyScalarForSql}, the fingerprint via
88
+ * {@link fingerprintScalarToken}) so the fingerprint stays column-blind exactly
89
+ * as before.
90
+ */
91
+ export type WhereEvent = {
92
+ kind: 'or';
93
+ conditions: WhereRecord[];
94
+ } | {
95
+ kind: 'and';
96
+ conditions: WhereRecord[];
97
+ } | {
98
+ kind: 'not';
99
+ condition: WhereRecord;
100
+ } | {
101
+ kind: 'relation';
102
+ key: string;
103
+ relDef: RelationDef;
104
+ filterObj: WhereRecord;
105
+ } | {
106
+ kind: 'scalar';
107
+ key: string;
108
+ value: unknown;
109
+ };
110
+ /**
111
+ * THE canonical WHERE enumeration. Yields events in sorted-key order (skipping
112
+ * `undefined`), dispatching combinators and relation filters, so every consumer
113
+ * (fingerprint, SQL build, param collect) walks identically.
114
+ *
115
+ * Combinator SKIP rules match the historical code exactly: an `OR`/`AND` whose
116
+ * value is a non-array or an empty array is skipped entirely (it contributes
117
+ * neither SQL, params, nor a fingerprint token); `NOT` is always emitted.
118
+ * A key that names a relation but whose value is not a `{ some/every/none/is/
119
+ * isNot }` filter falls through to the scalar path, exactly as before.
120
+ */
121
+ export declare function walkWhere(host: WhereHost, where: WhereRecord): WhereEvent[];
122
+ /**
123
+ * Column-aware SQL classification of a scalar WHERE value. Reproduces the SQL
124
+ * builder's linear fall-through: a JSON/array-shaped value on a non-JSON/array
125
+ * column falls THROUGH to the next shape (and ultimately equality) unless it
126
+ * carries a shape-unique key, in which case the build path reports a typed
127
+ * error (`*Throw`). Both the build and collect paths call this, so they can
128
+ * never classify the same value differently.
129
+ */
130
+ export declare function classifyScalarForSql(host: WhereHost, rawColumn: string, value: unknown): ScalarSqlClass;
131
+ /**
132
+ * The fingerprint's scalar token: deliberately COLUMN-BLIND and in the
133
+ * historical fingerprint precedence (operator before vector/json/array), so a
134
+ * value that both looks like an operator and a JSON filter (`equals`/`contains`
135
+ * overlap) tokenizes as an operator exactly as it did before. Column-blindness
136
+ * only ever over-distinguishes versus {@link classifyScalarForSql}, which is
137
+ * safe: it can cause an extra cache MISS, never a wrong-value HIT.
138
+ */
139
+ export declare function fingerprintScalarToken(value: unknown): string;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * turbine-orm: Shared WHERE-clause walk
3
+ *
4
+ * The SQL template cache requires three code paths over a table-scoped WHERE
5
+ * object to stay in perfect lockstep:
6
+ * - `fingerprintWhere` : the value-invariant cache KEY,
7
+ * - `buildWhereClause` : the SQL text + `$N` params on a cache MISS,
8
+ * - `collectWhereParams` : the params ONLY on a cache HIT (no SQL rebuild).
9
+ *
10
+ * If any two of them enumerate the WHERE keys in a different order, or classify
11
+ * a key's value into a different filter shape, the cached SQL's `$N`
12
+ * placeholders bind the wrong values: a silent cross-value (and, with tenant
13
+ * columns, cross-tenant) leak. That drift shipped twice historically (permuted
14
+ * where-key order; an orderBy fingerprint collision).
15
+ *
16
+ * This module removes the drift BY CONSTRUCTION:
17
+ * - {@link walkWhere} is the ONE enumeration. It sorts keys canonically,
18
+ * skips `undefined`, dispatches the `OR`/`AND`/`NOT` combinators and
19
+ * relation filters, and yields a flat, ordered {@link WhereEvent} stream.
20
+ * All three consumers iterate this same stream, so their key order and
21
+ * combinator structure can never diverge again.
22
+ * - {@link classifyScalarForSql} is the ONE scalar-shape decision the SQL
23
+ * paths use. `buildWhereClause` and `collectWhereParams` BOTH call it with
24
+ * the same `(rawColumn, value)`, so they always take the same branch and
25
+ * therefore push params in the same order.
26
+ * - {@link fingerprintScalarToken} is the fingerprint's own (deliberately
27
+ * column-blind) scalar token. It over-distinguishes relative to the SQL
28
+ * classifier (which is always safe), so a fingerprint match still implies
29
+ * an identical SQL shape.
30
+ *
31
+ * The dev-mode / sampled-production cross-check in `builder.ts` stays as the
32
+ * tripwire: with this shared walk it should never fire, but it remains the
33
+ * last-line guard against a future leaf builder / collect mirror falling out of
34
+ * step.
35
+ */
36
+ import { findArrayUniqueKey, findJsonUniqueKey, fingerprintArrayFilterShape, fingerprintJsonFilterShape, fingerprintOperatorShape, isArrayFilter, isJsonFilter, isTextSearchFilter, isUnmatchedPlainObject, isVectorFilter, isWhereOperator, sortedKeys, VECTOR_DISTANCE_COMPARATORS, } from './filters.js';
37
+ /** True when a normalized relation filter carries at least one cardinality key. */
38
+ function isRelationFilterObj(filterObj) {
39
+ return ('some' in filterObj || 'every' in filterObj || 'none' in filterObj || 'is' in filterObj || 'isNot' in filterObj);
40
+ }
41
+ /**
42
+ * THE canonical WHERE enumeration. Yields events in sorted-key order (skipping
43
+ * `undefined`), dispatching combinators and relation filters, so every consumer
44
+ * (fingerprint, SQL build, param collect) walks identically.
45
+ *
46
+ * Combinator SKIP rules match the historical code exactly: an `OR`/`AND` whose
47
+ * value is a non-array or an empty array is skipped entirely (it contributes
48
+ * neither SQL, params, nor a fingerprint token); `NOT` is always emitted.
49
+ * A key that names a relation but whose value is not a `{ some/every/none/is/
50
+ * isNot }` filter falls through to the scalar path, exactly as before.
51
+ */
52
+ export function walkWhere(host, where) {
53
+ const events = [];
54
+ for (const key of sortedKeys(where)) {
55
+ const value = where[key];
56
+ if (value === undefined)
57
+ continue;
58
+ if (key === 'OR') {
59
+ const arr = value;
60
+ if (!Array.isArray(arr) || arr.length === 0)
61
+ continue;
62
+ events.push({ kind: 'or', conditions: arr });
63
+ continue;
64
+ }
65
+ if (key === 'AND') {
66
+ const arr = value;
67
+ if (!Array.isArray(arr) || arr.length === 0)
68
+ continue;
69
+ events.push({ kind: 'and', conditions: arr });
70
+ continue;
71
+ }
72
+ if (key === 'NOT') {
73
+ events.push({ kind: 'not', condition: value });
74
+ continue;
75
+ }
76
+ const relDef = host.tableMeta.relations[key];
77
+ if (relDef && typeof value === 'object' && value !== null && !Array.isArray(value)) {
78
+ const filterObj = host.normalizeRelationFilter(relDef, value);
79
+ if (isRelationFilterObj(filterObj)) {
80
+ events.push({ kind: 'relation', key, relDef, filterObj });
81
+ continue;
82
+ }
83
+ }
84
+ events.push({ kind: 'scalar', key, value });
85
+ }
86
+ return events;
87
+ }
88
+ /**
89
+ * Column-aware SQL classification of a scalar WHERE value. Reproduces the SQL
90
+ * builder's linear fall-through: a JSON/array-shaped value on a non-JSON/array
91
+ * column falls THROUGH to the next shape (and ultimately equality) unless it
92
+ * carries a shape-unique key, in which case the build path reports a typed
93
+ * error (`*Throw`). Both the build and collect paths call this, so they can
94
+ * never classify the same value differently.
95
+ */
96
+ export function classifyScalarForSql(host, rawColumn, value) {
97
+ if (value === null)
98
+ return { kind: 'null' };
99
+ if (typeof value === 'object' && !Array.isArray(value) && isVectorFilter(value)) {
100
+ return { kind: 'vector' };
101
+ }
102
+ if (typeof value === 'object' && !Array.isArray(value) && isJsonFilter(value)) {
103
+ const colType = host.getColumnPgType(rawColumn);
104
+ if (host.isJsonColumnType(colType))
105
+ return { kind: 'json' };
106
+ const jsonKey = findJsonUniqueKey(value);
107
+ if (jsonKey)
108
+ return { kind: 'jsonThrow', jsonKey };
109
+ // else: fall through, `equals`/`contains` on a non-JSON column keep their
110
+ // WhereOperator meaning (matches builder.ts: no `continue`).
111
+ }
112
+ if (typeof value === 'object' && !Array.isArray(value) && isArrayFilter(value)) {
113
+ const colType = host.getColumnPgType(rawColumn);
114
+ if (colType.startsWith('_'))
115
+ return { kind: 'array', colType };
116
+ const arrayKey = findArrayUniqueKey(value);
117
+ if (arrayKey)
118
+ return { kind: 'arrayThrow', arrayKey };
119
+ // else: fall through.
120
+ }
121
+ if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
122
+ return { kind: 'textsearch' };
123
+ }
124
+ if (isWhereOperator(value))
125
+ return { kind: 'operator' };
126
+ return { kind: 'equality' };
127
+ }
128
+ /**
129
+ * The fingerprint's scalar token: deliberately COLUMN-BLIND and in the
130
+ * historical fingerprint precedence (operator before vector/json/array), so a
131
+ * value that both looks like an operator and a JSON filter (`equals`/`contains`
132
+ * overlap) tokenizes as an operator exactly as it did before. Column-blindness
133
+ * only ever over-distinguishes versus {@link classifyScalarForSql}, which is
134
+ * safe: it can cause an extra cache MISS, never a wrong-value HIT.
135
+ */
136
+ export function fingerprintScalarToken(value) {
137
+ // null → distinct from any value token.
138
+ if (value === null)
139
+ return 'null';
140
+ // Operator objects, checked first (column-blind precedence).
141
+ if (isWhereOperator(value))
142
+ return fingerprintOperatorShape(value);
143
+ // Vector distance filter: metric (operator) and present comparators change
144
+ // the SQL shape, so both go in the token.
145
+ if (typeof value === 'object' && !Array.isArray(value) && isVectorFilter(value)) {
146
+ const dist = value.distance;
147
+ const cmps = Object.keys(VECTOR_DISTANCE_COMPARATORS)
148
+ .filter((c) => dist[c] !== undefined)
149
+ .sort()
150
+ .join('|');
151
+ return `vec(${dist.metric},${cmps})`;
152
+ }
153
+ // JSON filter: range ops carry a numeric/string annotation (different cast).
154
+ if (typeof value === 'object' && !Array.isArray(value) && isJsonFilter(value)) {
155
+ return fingerprintJsonFilterShape(value);
156
+ }
157
+ // Array filter.
158
+ if (typeof value === 'object' && !Array.isArray(value) && isArrayFilter(value)) {
159
+ return `arr(${fingerprintArrayFilterShape(value)})`;
160
+ }
161
+ // Text search filter.
162
+ if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
163
+ const cfg = value.config ?? 'english';
164
+ return `fts(${cfg})`;
165
+ }
166
+ // Plain object literal that matched no filter shape: a token distinct from
167
+ // real equality so a cache entry warmed by genuine equality can't serve it.
168
+ if (isUnmatchedPlainObject(value)) {
169
+ return `obj(${Object.keys(value)
170
+ .sort()
171
+ .join(',')})`;
172
+ }
173
+ // Plain equality.
174
+ return 'eq';
175
+ }