turbine-orm 0.35.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 (68) 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/dialect.js +1 -1
  8. package/dist/cjs/generate.js +23 -2
  9. package/dist/cjs/index.js +2 -1
  10. package/dist/cjs/mssql.js +22 -5
  11. package/dist/cjs/powdb.js +41 -1
  12. package/dist/cjs/powql.js +80 -25
  13. package/dist/cjs/query/aggregates.js +683 -0
  14. package/dist/cjs/query/batched-loader.js +2 -0
  15. package/dist/cjs/query/builder.js +297 -4504
  16. package/dist/cjs/query/filters.js +12 -0
  17. package/dist/cjs/query/relations.js +1698 -0
  18. package/dist/cjs/query/where-compile.js +180 -0
  19. package/dist/cjs/query/where.js +1491 -0
  20. package/dist/cjs/query/writes.js +680 -0
  21. package/dist/cjs/schema-builder.js +6 -0
  22. package/dist/cjs/schema-metadata.js +4 -0
  23. package/dist/cjs/schema-sql.js +265 -3
  24. package/dist/cjs/sqlite.js +1 -1
  25. package/dist/cli/index.d.ts +8 -2
  26. package/dist/cli/index.js +111 -18
  27. package/dist/cli/migrate.d.ts +24 -1
  28. package/dist/cli/migrate.js +77 -3
  29. package/dist/cli/studio-ui.generated.js +1 -1
  30. package/dist/cli/studio.d.ts +46 -13
  31. package/dist/cli/studio.js +331 -23
  32. package/dist/cli/ui.js +7 -1
  33. package/dist/dialect.d.ts +15 -6
  34. package/dist/dialect.js +1 -1
  35. package/dist/generate.js +23 -2
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/mssql.js +22 -5
  39. package/dist/powdb.d.ts +20 -0
  40. package/dist/powdb.js +40 -0
  41. package/dist/powql.d.ts +33 -1
  42. package/dist/powql.js +80 -25
  43. package/dist/query/aggregates.d.ts +74 -0
  44. package/dist/query/aggregates.js +641 -0
  45. package/dist/query/batched-loader.d.ts +6 -0
  46. package/dist/query/batched-loader.js +2 -0
  47. package/dist/query/builder.d.ts +62 -829
  48. package/dist/query/builder.js +302 -4509
  49. package/dist/query/deferred.d.ts +7 -0
  50. package/dist/query/filters.d.ts +7 -0
  51. package/dist/query/filters.js +11 -0
  52. package/dist/query/relations.d.ts +441 -0
  53. package/dist/query/relations.js +1627 -0
  54. package/dist/query/types.d.ts +15 -0
  55. package/dist/query/where-compile.d.ts +139 -0
  56. package/dist/query/where-compile.js +175 -0
  57. package/dist/query/where.d.ts +494 -0
  58. package/dist/query/where.js +1431 -0
  59. package/dist/query/writes.d.ts +131 -0
  60. package/dist/query/writes.js +626 -0
  61. package/dist/schema-builder.d.ts +18 -3
  62. package/dist/schema-builder.js +6 -0
  63. package/dist/schema-metadata.js +4 -0
  64. package/dist/schema-sql.d.ts +60 -3
  65. package/dist/schema-sql.js +261 -4
  66. package/dist/schema.d.ts +10 -0
  67. package/dist/sqlite.js +1 -1
  68. package/package.json +2 -2
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm: Shared WHERE-clause walk
4
+ *
5
+ * The SQL template cache requires three code paths over a table-scoped WHERE
6
+ * object to stay in perfect lockstep:
7
+ * - `fingerprintWhere` : the value-invariant cache KEY,
8
+ * - `buildWhereClause` : the SQL text + `$N` params on a cache MISS,
9
+ * - `collectWhereParams` : the params ONLY on a cache HIT (no SQL rebuild).
10
+ *
11
+ * If any two of them enumerate the WHERE keys in a different order, or classify
12
+ * a key's value into a different filter shape, the cached SQL's `$N`
13
+ * placeholders bind the wrong values: a silent cross-value (and, with tenant
14
+ * columns, cross-tenant) leak. That drift shipped twice historically (permuted
15
+ * where-key order; an orderBy fingerprint collision).
16
+ *
17
+ * This module removes the drift BY CONSTRUCTION:
18
+ * - {@link walkWhere} is the ONE enumeration. It sorts keys canonically,
19
+ * skips `undefined`, dispatches the `OR`/`AND`/`NOT` combinators and
20
+ * relation filters, and yields a flat, ordered {@link WhereEvent} stream.
21
+ * All three consumers iterate this same stream, so their key order and
22
+ * combinator structure can never diverge again.
23
+ * - {@link classifyScalarForSql} is the ONE scalar-shape decision the SQL
24
+ * paths use. `buildWhereClause` and `collectWhereParams` BOTH call it with
25
+ * the same `(rawColumn, value)`, so they always take the same branch and
26
+ * therefore push params in the same order.
27
+ * - {@link fingerprintScalarToken} is the fingerprint's own (deliberately
28
+ * column-blind) scalar token. It over-distinguishes relative to the SQL
29
+ * classifier (which is always safe), so a fingerprint match still implies
30
+ * an identical SQL shape.
31
+ *
32
+ * The dev-mode / sampled-production cross-check in `builder.ts` stays as the
33
+ * tripwire: with this shared walk it should never fire, but it remains the
34
+ * last-line guard against a future leaf builder / collect mirror falling out of
35
+ * step.
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.walkWhere = walkWhere;
39
+ exports.classifyScalarForSql = classifyScalarForSql;
40
+ exports.fingerprintScalarToken = fingerprintScalarToken;
41
+ const filters_js_1 = require("./filters.js");
42
+ /** True when a normalized relation filter carries at least one cardinality key. */
43
+ function isRelationFilterObj(filterObj) {
44
+ return ('some' in filterObj || 'every' in filterObj || 'none' in filterObj || 'is' in filterObj || 'isNot' in filterObj);
45
+ }
46
+ /**
47
+ * THE canonical WHERE enumeration. Yields events in sorted-key order (skipping
48
+ * `undefined`), dispatching combinators and relation filters, so every consumer
49
+ * (fingerprint, SQL build, param collect) walks identically.
50
+ *
51
+ * Combinator SKIP rules match the historical code exactly: an `OR`/`AND` whose
52
+ * value is a non-array or an empty array is skipped entirely (it contributes
53
+ * neither SQL, params, nor a fingerprint token); `NOT` is always emitted.
54
+ * A key that names a relation but whose value is not a `{ some/every/none/is/
55
+ * isNot }` filter falls through to the scalar path, exactly as before.
56
+ */
57
+ function walkWhere(host, where) {
58
+ const events = [];
59
+ for (const key of (0, filters_js_1.sortedKeys)(where)) {
60
+ const value = where[key];
61
+ if (value === undefined)
62
+ continue;
63
+ if (key === 'OR') {
64
+ const arr = value;
65
+ if (!Array.isArray(arr) || arr.length === 0)
66
+ continue;
67
+ events.push({ kind: 'or', conditions: arr });
68
+ continue;
69
+ }
70
+ if (key === 'AND') {
71
+ const arr = value;
72
+ if (!Array.isArray(arr) || arr.length === 0)
73
+ continue;
74
+ events.push({ kind: 'and', conditions: arr });
75
+ continue;
76
+ }
77
+ if (key === 'NOT') {
78
+ events.push({ kind: 'not', condition: value });
79
+ continue;
80
+ }
81
+ const relDef = host.tableMeta.relations[key];
82
+ if (relDef && typeof value === 'object' && value !== null && !Array.isArray(value)) {
83
+ const filterObj = host.normalizeRelationFilter(relDef, value);
84
+ if (isRelationFilterObj(filterObj)) {
85
+ events.push({ kind: 'relation', key, relDef, filterObj });
86
+ continue;
87
+ }
88
+ }
89
+ events.push({ kind: 'scalar', key, value });
90
+ }
91
+ return events;
92
+ }
93
+ /**
94
+ * Column-aware SQL classification of a scalar WHERE value. Reproduces the SQL
95
+ * builder's linear fall-through: a JSON/array-shaped value on a non-JSON/array
96
+ * column falls THROUGH to the next shape (and ultimately equality) unless it
97
+ * carries a shape-unique key, in which case the build path reports a typed
98
+ * error (`*Throw`). Both the build and collect paths call this, so they can
99
+ * never classify the same value differently.
100
+ */
101
+ function classifyScalarForSql(host, rawColumn, value) {
102
+ if (value === null)
103
+ return { kind: 'null' };
104
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isVectorFilter)(value)) {
105
+ return { kind: 'vector' };
106
+ }
107
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isJsonFilter)(value)) {
108
+ const colType = host.getColumnPgType(rawColumn);
109
+ if (host.isJsonColumnType(colType))
110
+ return { kind: 'json' };
111
+ const jsonKey = (0, filters_js_1.findJsonUniqueKey)(value);
112
+ if (jsonKey)
113
+ return { kind: 'jsonThrow', jsonKey };
114
+ // else: fall through, `equals`/`contains` on a non-JSON column keep their
115
+ // WhereOperator meaning (matches builder.ts: no `continue`).
116
+ }
117
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isArrayFilter)(value)) {
118
+ const colType = host.getColumnPgType(rawColumn);
119
+ if (colType.startsWith('_'))
120
+ return { kind: 'array', colType };
121
+ const arrayKey = (0, filters_js_1.findArrayUniqueKey)(value);
122
+ if (arrayKey)
123
+ return { kind: 'arrayThrow', arrayKey };
124
+ // else: fall through.
125
+ }
126
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isTextSearchFilter)(value)) {
127
+ return { kind: 'textsearch' };
128
+ }
129
+ if ((0, filters_js_1.isWhereOperator)(value))
130
+ return { kind: 'operator' };
131
+ return { kind: 'equality' };
132
+ }
133
+ /**
134
+ * The fingerprint's scalar token: deliberately COLUMN-BLIND and in the
135
+ * historical fingerprint precedence (operator before vector/json/array), so a
136
+ * value that both looks like an operator and a JSON filter (`equals`/`contains`
137
+ * overlap) tokenizes as an operator exactly as it did before. Column-blindness
138
+ * only ever over-distinguishes versus {@link classifyScalarForSql}, which is
139
+ * safe: it can cause an extra cache MISS, never a wrong-value HIT.
140
+ */
141
+ function fingerprintScalarToken(value) {
142
+ // null → distinct from any value token.
143
+ if (value === null)
144
+ return 'null';
145
+ // Operator objects, checked first (column-blind precedence).
146
+ if ((0, filters_js_1.isWhereOperator)(value))
147
+ return (0, filters_js_1.fingerprintOperatorShape)(value);
148
+ // Vector distance filter: metric (operator) and present comparators change
149
+ // the SQL shape, so both go in the token.
150
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isVectorFilter)(value)) {
151
+ const dist = value.distance;
152
+ const cmps = Object.keys(filters_js_1.VECTOR_DISTANCE_COMPARATORS)
153
+ .filter((c) => dist[c] !== undefined)
154
+ .sort()
155
+ .join('|');
156
+ return `vec(${dist.metric},${cmps})`;
157
+ }
158
+ // JSON filter: range ops carry a numeric/string annotation (different cast).
159
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isJsonFilter)(value)) {
160
+ return (0, filters_js_1.fingerprintJsonFilterShape)(value);
161
+ }
162
+ // Array filter.
163
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isArrayFilter)(value)) {
164
+ return `arr(${(0, filters_js_1.fingerprintArrayFilterShape)(value)})`;
165
+ }
166
+ // Text search filter.
167
+ if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isTextSearchFilter)(value)) {
168
+ const cfg = value.config ?? 'english';
169
+ return `fts(${cfg})`;
170
+ }
171
+ // Plain object literal that matched no filter shape: a token distinct from
172
+ // real equality so a cache entry warmed by genuine equality can't serve it.
173
+ if ((0, filters_js_1.isUnmatchedPlainObject)(value)) {
174
+ return `obj(${Object.keys(value)
175
+ .sort()
176
+ .join(',')})`;
177
+ }
178
+ // Plain equality.
179
+ return 'eq';
180
+ }