@rudderjs/database 1.1.0 → 1.2.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 +70 -0
- package/dist/db.d.ts +21 -3
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +27 -5
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/dist/native/adapter.d.ts +202 -0
- package/dist/native/adapter.d.ts.map +1 -0
- package/dist/native/adapter.js +440 -0
- package/dist/native/adapter.js.map +1 -0
- package/dist/native/compiler.d.ts +371 -0
- package/dist/native/compiler.d.ts.map +1 -0
- package/dist/native/compiler.js +978 -0
- package/dist/native/compiler.js.map +1 -0
- package/dist/native/dialect-mysql.d.ts +26 -0
- package/dist/native/dialect-mysql.d.ts.map +1 -0
- package/dist/native/dialect-mysql.js +188 -0
- package/dist/native/dialect-mysql.js.map +1 -0
- package/dist/native/dialect-pg.d.ts +26 -0
- package/dist/native/dialect-pg.d.ts.map +1 -0
- package/dist/native/dialect-pg.js +192 -0
- package/dist/native/dialect-pg.js.map +1 -0
- package/dist/native/dialect.d.ts +255 -0
- package/dist/native/dialect.d.ts.map +1 -0
- package/dist/native/dialect.js +237 -0
- package/dist/native/dialect.js.map +1 -0
- package/dist/native/driver.d.ts +37 -0
- package/dist/native/driver.d.ts.map +1 -0
- package/dist/native/driver.js +19 -0
- package/dist/native/driver.js.map +1 -0
- package/dist/native/drivers/better-sqlite3.d.ts +56 -0
- package/dist/native/drivers/better-sqlite3.d.ts.map +1 -0
- package/dist/native/drivers/better-sqlite3.js +171 -0
- package/dist/native/drivers/better-sqlite3.js.map +1 -0
- package/dist/native/drivers/mysql.d.ts +30 -0
- package/dist/native/drivers/mysql.d.ts.map +1 -0
- package/dist/native/drivers/mysql.js +176 -0
- package/dist/native/drivers/mysql.js.map +1 -0
- package/dist/native/drivers/postgres.d.ts +57 -0
- package/dist/native/drivers/postgres.d.ts.map +1 -0
- package/dist/native/drivers/postgres.js +155 -0
- package/dist/native/drivers/postgres.js.map +1 -0
- package/dist/native/errors.d.ts +43 -0
- package/dist/native/errors.d.ts.map +1 -0
- package/dist/native/errors.js +64 -0
- package/dist/native/errors.js.map +1 -0
- package/dist/native/index.d.ts +27 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +55 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/isolation.d.ts +14 -0
- package/dist/native/isolation.d.ts.map +1 -0
- package/dist/native/isolation.js +37 -0
- package/dist/native/isolation.js.map +1 -0
- package/dist/native/query-builder.d.ts +303 -0
- package/dist/native/query-builder.d.ts.map +1 -0
- package/dist/native/query-builder.js +984 -0
- package/dist/native/query-builder.js.map +1 -0
- package/dist/native/replica-picker.d.ts +22 -0
- package/dist/native/replica-picker.d.ts.map +1 -0
- package/dist/native/replica-picker.js +65 -0
- package/dist/native/replica-picker.js.map +1 -0
- package/dist/native/schema/alter-blueprint.d.ts +37 -0
- package/dist/native/schema/alter-blueprint.d.ts.map +1 -0
- package/dist/native/schema/alter-blueprint.js +56 -0
- package/dist/native/schema/alter-blueprint.js.map +1 -0
- package/dist/native/schema/blueprint.d.ts +151 -0
- package/dist/native/schema/blueprint.d.ts.map +1 -0
- package/dist/native/schema/blueprint.js +286 -0
- package/dist/native/schema/blueprint.js.map +1 -0
- package/dist/native/schema/column.d.ts +168 -0
- package/dist/native/schema/column.d.ts.map +1 -0
- package/dist/native/schema/column.js +190 -0
- package/dist/native/schema/column.js.map +1 -0
- package/dist/native/schema/ddl-compiler.d.ts +34 -0
- package/dist/native/schema/ddl-compiler.d.ts.map +1 -0
- package/dist/native/schema/ddl-compiler.js +352 -0
- package/dist/native/schema/ddl-compiler.js.map +1 -0
- package/dist/native/schema/inspect.d.ts +67 -0
- package/dist/native/schema/inspect.d.ts.map +1 -0
- package/dist/native/schema/inspect.js +312 -0
- package/dist/native/schema/inspect.js.map +1 -0
- package/dist/native/schema/introspect.d.ts +34 -0
- package/dist/native/schema/introspect.d.ts.map +1 -0
- package/dist/native/schema/introspect.js +101 -0
- package/dist/native/schema/introspect.js.map +1 -0
- package/dist/native/schema/migration.d.ts +8 -0
- package/dist/native/schema/migration.d.ts.map +1 -0
- package/dist/native/schema/migration.js +19 -0
- package/dist/native/schema/migration.js.map +1 -0
- package/dist/native/schema/migrator.d.ts +144 -0
- package/dist/native/schema/migrator.d.ts.map +1 -0
- package/dist/native/schema/migrator.js +239 -0
- package/dist/native/schema/migrator.js.map +1 -0
- package/dist/native/schema/rebuild.d.ts +11 -0
- package/dist/native/schema/rebuild.d.ts.map +1 -0
- package/dist/native/schema/rebuild.js +92 -0
- package/dist/native/schema/rebuild.js.map +1 -0
- package/dist/native/schema/schema-builder.d.ts +46 -0
- package/dist/native/schema/schema-builder.d.ts.map +1 -0
- package/dist/native/schema/schema-builder.js +153 -0
- package/dist/native/schema/schema-builder.js.map +1 -0
- package/dist/native/schema/schema-facade.d.ts +63 -0
- package/dist/native/schema/schema-facade.d.ts.map +1 -0
- package/dist/native/schema/schema-facade.js +124 -0
- package/dist/native/schema/schema-facade.js.map +1 -0
- package/dist/native/schema/schema-types.d.ts +27 -0
- package/dist/native/schema/schema-types.d.ts.map +1 -0
- package/dist/native/schema/schema-types.js +52 -0
- package/dist/native/schema/schema-types.js.map +1 -0
- package/dist/native/schema/types-generator.d.ts +73 -0
- package/dist/native/schema/types-generator.d.ts.map +1 -0
- package/dist/native/schema/types-generator.js +181 -0
- package/dist/native/schema/types-generator.js.map +1 -0
- package/dist/registry-bridge.d.ts +24 -4
- package/dist/registry-bridge.d.ts.map +1 -1
- package/dist/registry-bridge.js +20 -0
- package/dist/registry-bridge.js.map +1 -1
- package/dist/sticky.d.ts +22 -0
- package/dist/sticky.d.ts.map +1 -0
- package/dist/sticky.js +61 -0
- package/dist/sticky.js.map +1 -0
- package/package.json +32 -2
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
// ─── SQL compiler (read path) ──────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// PURE: turns a {@link NativeQueryState} into a `{ sql, bindings }` pair via a
|
|
4
|
+
// {@link Dialect}. No driver, no `node:`, no I/O — this is the portable half of
|
|
5
|
+
// the engine (cross-phase rule 7). Every value is emitted as a bound parameter;
|
|
6
|
+
// only identifiers (validated + quoted by the dialect) ever reach the SQL text
|
|
7
|
+
// (cross-phase rule 2 — security gate).
|
|
8
|
+
import { Expression } from '@rudderjs/contracts';
|
|
9
|
+
import { parseJsonPath } from './dialect.js';
|
|
10
|
+
import { NativeOrmError } from './errors.js';
|
|
11
|
+
const WINDOW_FUNCTION_SQL = {
|
|
12
|
+
rowNumber: 'ROW_NUMBER',
|
|
13
|
+
rank: 'RANK',
|
|
14
|
+
denseRank: 'DENSE_RANK',
|
|
15
|
+
percentRank: 'PERCENT_RANK',
|
|
16
|
+
cumeDist: 'CUME_DIST',
|
|
17
|
+
};
|
|
18
|
+
/** Runtime membership check for {@link WindowFunction} — the builder's
|
|
19
|
+
* injection gate for the spliced function name (JS callers bypass the TS
|
|
20
|
+
* union). */
|
|
21
|
+
export function isWindowFunction(fn) {
|
|
22
|
+
return Object.prototype.hasOwnProperty.call(WINDOW_FUNCTION_SQL, fn);
|
|
23
|
+
}
|
|
24
|
+
/** SQL text for each {@link WhereOperator}. `IN`/`NOT IN` are handled
|
|
25
|
+
* separately (they expand to a placeholder list), as are null comparisons. */
|
|
26
|
+
const OPERATOR_SQL = {
|
|
27
|
+
'=': '=',
|
|
28
|
+
'!=': '!=',
|
|
29
|
+
'>': '>',
|
|
30
|
+
'>=': '>=',
|
|
31
|
+
'<': '<',
|
|
32
|
+
'<=': '<=',
|
|
33
|
+
'LIKE': 'LIKE',
|
|
34
|
+
'NOT LIKE': 'NOT LIKE',
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Accumulates positional bindings and hands out the matching placeholder. One
|
|
38
|
+
* instance per compile so `$n` indices (Postgres, later) stay correct across
|
|
39
|
+
* the whole statement, not per-fragment.
|
|
40
|
+
*/
|
|
41
|
+
class Bindings {
|
|
42
|
+
dialect;
|
|
43
|
+
values = [];
|
|
44
|
+
constructor(dialect) {
|
|
45
|
+
this.dialect = dialect;
|
|
46
|
+
}
|
|
47
|
+
add(value) {
|
|
48
|
+
const ph = this.dialect.placeholder(this.values.length);
|
|
49
|
+
this.values.push(value);
|
|
50
|
+
return ph;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Splice a raw SQL fragment, rebinding its `?` placeholders to the dialect's
|
|
55
|
+
* form via the shared {@link Bindings} (so `$n` indices stay correct on
|
|
56
|
+
* Postgres and the values land in positional order across the whole statement).
|
|
57
|
+
* The fragment's identifiers are NOT quoted — raw means raw, the caller owns it.
|
|
58
|
+
* `?` count must equal `bindings.length` or we throw (a silent off-by-one would
|
|
59
|
+
* misalign every subsequent placeholder).
|
|
60
|
+
*/
|
|
61
|
+
function compileRaw(frag, b) {
|
|
62
|
+
const parts = frag.sql.split('?');
|
|
63
|
+
const holes = parts.length - 1;
|
|
64
|
+
if (holes !== frag.bindings.length) {
|
|
65
|
+
throw new Error(`[RudderJS ORM native] Raw SQL expects ${holes} binding(s) for its '?' placeholders but got ${frag.bindings.length}: ${frag.sql}`);
|
|
66
|
+
}
|
|
67
|
+
let out = parts[0] ?? '';
|
|
68
|
+
for (let i = 0; i < holes; i++) {
|
|
69
|
+
out += b.add(frag.bindings[i]) + (parts[i + 1] ?? '');
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render one `WhereClause` to SQL. Null values route through `IS NULL` /
|
|
75
|
+
* `IS NOT NULL` (a `= NULL` never matches in SQL); `IN`/`NOT IN` expand to a
|
|
76
|
+
* parenthesized placeholder list (empty list → constant false/true).
|
|
77
|
+
*/
|
|
78
|
+
function compileClause(clause, dialect, b) {
|
|
79
|
+
return compileComparison(dialect.quoteId(clause.column), clause.operator, clause.value, b);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Render `<lhs> <op> <value>` for an already-built left-hand expression — the
|
|
83
|
+
* shared comparison tail of `compileClause` and the `json` condition kind.
|
|
84
|
+
* Handles the `Expression` splice, `IN`/`NOT IN` list expansion, and
|
|
85
|
+
* `IS [NOT] NULL` semantics; everything else binds positionally.
|
|
86
|
+
*/
|
|
87
|
+
function compileComparison(lhs, operator, value, b) {
|
|
88
|
+
// `where(col, op, raw('NOW()'))` — splice the expression verbatim, no binding.
|
|
89
|
+
if (value instanceof Expression) {
|
|
90
|
+
const op = OPERATOR_SQL[operator];
|
|
91
|
+
if (!op)
|
|
92
|
+
throw new Error(`[RudderJS ORM native] Unsupported operator: ${String(operator)}`);
|
|
93
|
+
return `${lhs} ${op} ${value.getValue()}`;
|
|
94
|
+
}
|
|
95
|
+
if (operator === 'IN' || operator === 'NOT IN') {
|
|
96
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
97
|
+
if (arr.length === 0) {
|
|
98
|
+
// `x IN ()` is a syntax error in SQLite; emit the equivalent constant.
|
|
99
|
+
return operator === 'IN' ? '1 = 0' : '1 = 1';
|
|
100
|
+
}
|
|
101
|
+
const list = arr.map(v => b.add(v)).join(', ');
|
|
102
|
+
return `${lhs} ${operator} (${list})`;
|
|
103
|
+
}
|
|
104
|
+
// Null equality/inequality must use IS [NOT] NULL semantics.
|
|
105
|
+
if (value === null && (operator === '=' || operator === '!=')) {
|
|
106
|
+
return `${lhs} IS ${operator === '=' ? '' : 'NOT '}NULL`;
|
|
107
|
+
}
|
|
108
|
+
const op = OPERATOR_SQL[operator];
|
|
109
|
+
if (!op) {
|
|
110
|
+
// Unreachable for a well-typed WhereOperator; guard keeps the compiler
|
|
111
|
+
// honest if the contract grows an operator the native engine hasn't mapped.
|
|
112
|
+
throw new Error(`[RudderJS ORM native] Unsupported operator: ${String(operator)}`);
|
|
113
|
+
}
|
|
114
|
+
return `${lhs} ${op} ${b.add(value)}`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Render a JSON arrow-path comparison for an already-quoted column expression —
|
|
118
|
+
* the shared body of the `json` condition kind and arrow-path constraint wheres
|
|
119
|
+
* inside `compileExists`. The value's JS type picks the extraction shape (pg
|
|
120
|
+
* casts; mysql booleans skip UNQUOTE), and booleans normalize per dialect via
|
|
121
|
+
* the jsonBoolean seam. `IN` probes its first element so a list of numbers
|
|
122
|
+
* compares against the typed extraction. Null equality routes through the
|
|
123
|
+
* {@link Dialect.jsonNullComparison} seam (mysql needs Laravel's
|
|
124
|
+
* IS NULL OR JSON_TYPE = 'NULL' shape — a missing key and an explicit json
|
|
125
|
+
* null both count as null on every dialect). Other comparison semantics
|
|
126
|
+
* (Expression / IN) ride the shared {@link compileComparison} tail.
|
|
127
|
+
*/
|
|
128
|
+
function compileJsonComparison(columnExpr, segments, operator, value, dialect, b) {
|
|
129
|
+
if (value === null && (operator === '=' || operator === '!=')) {
|
|
130
|
+
return dialect.jsonNullComparison(columnExpr, segments, operator === '!=');
|
|
131
|
+
}
|
|
132
|
+
const probe = (operator === 'IN' || operator === 'NOT IN') && Array.isArray(value)
|
|
133
|
+
? value[0]
|
|
134
|
+
: value;
|
|
135
|
+
const valueKind = typeof probe === 'number' ? 'number' : typeof probe === 'boolean' ? 'boolean' : 'text';
|
|
136
|
+
const norm = (v) => (typeof v === 'boolean' ? dialect.jsonBoolean(v) : v);
|
|
137
|
+
const normalized = Array.isArray(value) ? value.map(norm) : norm(value);
|
|
138
|
+
const expr = dialect.jsonExtract(columnExpr, segments, valueKind);
|
|
139
|
+
return compileComparison(expr, operator, normalized, b);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Render a list of sibling condition nodes into a single boolean expression,
|
|
143
|
+
* inserting each node's `boolean` connector between siblings. Returns `''` for
|
|
144
|
+
* an empty list (caller omits the WHERE/parens).
|
|
145
|
+
*/
|
|
146
|
+
function compileNodes(nodes, dialect, b) {
|
|
147
|
+
const parts = [];
|
|
148
|
+
for (const node of nodes) {
|
|
149
|
+
let frag;
|
|
150
|
+
if (node.kind === 'clause') {
|
|
151
|
+
frag = compileClause(node.clause, dialect, b);
|
|
152
|
+
}
|
|
153
|
+
else if (node.kind === 'raw') {
|
|
154
|
+
frag = compileRaw(node.raw, b);
|
|
155
|
+
}
|
|
156
|
+
else if (node.kind === 'column') {
|
|
157
|
+
// Column-vs-column (`whereColumn`) — both sides are identifiers, quoted
|
|
158
|
+
// per dialect; nothing is bound. This is exactly why whereColumn can't
|
|
159
|
+
// ride on whereRaw, which leaves identifiers un-quoted.
|
|
160
|
+
const op = OPERATOR_SQL[node.operator];
|
|
161
|
+
if (!op)
|
|
162
|
+
throw new Error(`[RudderJS ORM native] Unsupported operator: ${String(node.operator)}`);
|
|
163
|
+
frag = `${dialect.quoteId(node.left)} ${op} ${dialect.quoteId(node.right)}`;
|
|
164
|
+
}
|
|
165
|
+
else if (node.kind === 'date') {
|
|
166
|
+
// Date-component predicate (`whereDate`/`whereTime`/`whereDay`/…) — the
|
|
167
|
+
// column is quoted then run through the dialect's extraction seam; the
|
|
168
|
+
// value binds through the shared positional Bindings like any clause.
|
|
169
|
+
const op = OPERATOR_SQL[node.operator];
|
|
170
|
+
if (!op)
|
|
171
|
+
throw new Error(`[RudderJS ORM native] Unsupported operator: ${String(node.operator)}`);
|
|
172
|
+
frag = `${dialect.dateExtract(node.part, dialect.quoteId(node.column))} ${op} ${b.add(node.value)}`;
|
|
173
|
+
}
|
|
174
|
+
else if (node.kind === 'json') {
|
|
175
|
+
// JSON arrow-path comparison (`where('meta->prefs->lang', …)`) — shared
|
|
176
|
+
// body in compileJsonComparison (also serves arrow-path constraint
|
|
177
|
+
// wheres inside compileExists).
|
|
178
|
+
frag = compileJsonComparison(dialect.quoteId(node.column), node.segments, node.operator, node.value, dialect, b);
|
|
179
|
+
}
|
|
180
|
+
else if (node.kind === 'jsonContains') {
|
|
181
|
+
// whereJsonContains / whereJsonDoesntContain — the dialect seam owns the
|
|
182
|
+
// whole predicate (pg @>, mysql JSON_CONTAINS, sqlite json_each EXISTS)
|
|
183
|
+
// and binds through the shared Bindings via the callback.
|
|
184
|
+
const expr = dialect.jsonContains(dialect.quoteId(node.column), node.segments, node.value, v => b.add(v));
|
|
185
|
+
frag = node.negated ? `NOT (${expr})` : expr;
|
|
186
|
+
}
|
|
187
|
+
else if (node.kind === 'jsonLength') {
|
|
188
|
+
// whereJsonLength — array length via the dialect seam, the count binds.
|
|
189
|
+
const op = OPERATOR_SQL[node.operator];
|
|
190
|
+
if (!op)
|
|
191
|
+
throw new Error(`[RudderJS ORM native] Unsupported operator: ${String(node.operator)}`);
|
|
192
|
+
frag = `${dialect.jsonLength(dialect.quoteId(node.column), node.segments)} ${op} ${b.add(node.value)}`;
|
|
193
|
+
}
|
|
194
|
+
else if (node.kind === 'exists') {
|
|
195
|
+
// whereExists / whereNotExists — an arbitrary [NOT] EXISTS subquery.
|
|
196
|
+
// Builder-backed bodies correlate to the outer query via qualified
|
|
197
|
+
// whereColumn refs ('orders.userId' = 'users.id'); raw bodies rebind
|
|
198
|
+
// their ? placeholders through the shared Bindings (text order — the
|
|
199
|
+
// subquery sits exactly here in the WHERE).
|
|
200
|
+
frag = `${node.negated ? 'NOT ' : ''}EXISTS (${compileSubqueryBody(node.body, dialect, b)})`;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const inner = compileNodes(node.children, dialect, b);
|
|
204
|
+
// An empty group contributes nothing — skip it entirely so it doesn't
|
|
205
|
+
// emit dangling `AND ()` (or a constant `NOT ()`). The connector is keyed
|
|
206
|
+
// off whether anything has been emitted yet (parts.length), not the
|
|
207
|
+
// source index, so a leading skipped group never leaves a dangling
|
|
208
|
+
// `AND`/`OR`.
|
|
209
|
+
if (inner === '')
|
|
210
|
+
continue;
|
|
211
|
+
// `negated` (whereNot / orWhereNot) wraps the parenthesized sub-tree.
|
|
212
|
+
frag = node.negated ? `NOT (${inner})` : `(${inner})`;
|
|
213
|
+
}
|
|
214
|
+
parts.push(parts.length === 0 ? frag : `${node.boolean} ${frag}`);
|
|
215
|
+
}
|
|
216
|
+
return parts.join(' ');
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Build the WHERE expression for a query, folding in the soft-delete filter.
|
|
220
|
+
* The soft-delete predicate is AND-ed at the top level — Laravel scopes it
|
|
221
|
+
* around the whole user predicate, matching orm-drizzle/orm-prisma.
|
|
222
|
+
*/
|
|
223
|
+
function compileWhere(state, dialect, b) {
|
|
224
|
+
// The user predicate binds first (positional order), so compile it up front —
|
|
225
|
+
// but we only know whether to parenthesize it after seeing the other
|
|
226
|
+
// top-level AND-ed components (soft-delete, EXISTS).
|
|
227
|
+
const userExpr = compileNodes(state.conditions, dialect, b);
|
|
228
|
+
const softExpr = compileSoftDelete(state, dialect);
|
|
229
|
+
// Compile the existence predicates in order (shares the positional Bindings).
|
|
230
|
+
const existsParts = (state.relationExists ?? []).map(pred => ({
|
|
231
|
+
boolean: pred.boolean ?? 'AND',
|
|
232
|
+
sql: compileExists(state.table, pred, dialect, b),
|
|
233
|
+
}));
|
|
234
|
+
const hasOr = existsParts.some(p => p.boolean === 'OR');
|
|
235
|
+
if (!hasOr) {
|
|
236
|
+
// ── all-AND path (unchanged text) — whereHas/whereDoesntHave AND-ed at the
|
|
237
|
+
// top level alongside soft-delete. Preserved verbatim so existing SQL holds.
|
|
238
|
+
const others = [];
|
|
239
|
+
if (softExpr)
|
|
240
|
+
others.push(softExpr);
|
|
241
|
+
for (const part of existsParts)
|
|
242
|
+
others.push(part.sql);
|
|
243
|
+
if (!userExpr)
|
|
244
|
+
return others.join(' AND ');
|
|
245
|
+
if (others.length === 0)
|
|
246
|
+
return userExpr;
|
|
247
|
+
// Parenthesize the user predicate when it has more than one top-level clause
|
|
248
|
+
// so an inner top-level OR can't escape the AND. A single clause needs none.
|
|
249
|
+
const wrapped = state.conditions.length > 1 ? `(${userExpr})` : userExpr;
|
|
250
|
+
return [wrapped, ...others].join(' AND ');
|
|
251
|
+
}
|
|
252
|
+
// ── OR-rooted existence predicate present ──
|
|
253
|
+
// Fold the user clauses and the existence predicates into ONE predicate by
|
|
254
|
+
// their booleans, then AND the soft-delete scope around the whole group so an
|
|
255
|
+
// `orWhereHas` can't leak past the soft-delete filter.
|
|
256
|
+
let predicate = userExpr;
|
|
257
|
+
for (const part of existsParts) {
|
|
258
|
+
predicate = predicate === '' ? part.sql : `${predicate} ${part.boolean} ${part.sql}`;
|
|
259
|
+
}
|
|
260
|
+
if (!softExpr)
|
|
261
|
+
return predicate;
|
|
262
|
+
return `${softExpr} AND (${predicate})`;
|
|
263
|
+
}
|
|
264
|
+
/** The `deletedAt IS [NOT] NULL` fragment, or `''` when not scoping. */
|
|
265
|
+
function compileSoftDelete(state, dialect) {
|
|
266
|
+
if (state.softDelete === 'with')
|
|
267
|
+
return '';
|
|
268
|
+
const col = dialect.quoteId(state.deletedAtColumn);
|
|
269
|
+
return state.softDelete === 'only' ? `${col} IS NOT NULL` : `${col} IS NULL`;
|
|
270
|
+
}
|
|
271
|
+
/** ORDER BY fragment (without the keyword), or `''` when no orders. Raw order
|
|
272
|
+
* items splice verbatim (and may carry bindings — ORDER BY follows WHERE in the
|
|
273
|
+
* SQL text, so its placeholders bind after the WHERE's via the shared `b`). */
|
|
274
|
+
function compileOrderBy(orders, dialect, b) {
|
|
275
|
+
return orders
|
|
276
|
+
.map(o => ('kind' in o ? compileRaw(o.raw, b) : `${dialect.quoteId(o.column)} ${o.direction === 'DESC' ? 'DESC' : 'ASC'}`))
|
|
277
|
+
.join(', ');
|
|
278
|
+
}
|
|
279
|
+
/** SQL keyword for each {@link JoinType}. */
|
|
280
|
+
const JOIN_KEYWORD = {
|
|
281
|
+
inner: 'INNER JOIN',
|
|
282
|
+
left: 'LEFT JOIN',
|
|
283
|
+
right: 'RIGHT JOIN',
|
|
284
|
+
cross: 'CROSS JOIN',
|
|
285
|
+
};
|
|
286
|
+
/** Render a join's ON condition list into one boolean expression. `on` nodes are
|
|
287
|
+
* column-vs-column (both sides quoted, nothing bound); `where` nodes bind their
|
|
288
|
+
* value through the shared {@link Bindings}. */
|
|
289
|
+
function compileJoinConditions(conditions, dialect, b) {
|
|
290
|
+
const parts = [];
|
|
291
|
+
for (const c of conditions) {
|
|
292
|
+
let frag;
|
|
293
|
+
if (c.kind === 'on') {
|
|
294
|
+
const op = OPERATOR_SQL[c.operator];
|
|
295
|
+
if (!op)
|
|
296
|
+
throw new Error(`[RudderJS ORM native] Unsupported operator: ${String(c.operator)}`);
|
|
297
|
+
frag = `${dialect.quoteId(c.left)} ${op} ${dialect.quoteId(c.right)}`;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
frag = compileClause(c.clause, dialect, b);
|
|
301
|
+
}
|
|
302
|
+
parts.push(parts.length === 0 ? frag : `${c.boolean} ${frag}`);
|
|
303
|
+
}
|
|
304
|
+
return parts.join(' ');
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Compile the JOIN clauses (`''` when none). Emitted after FROM and before
|
|
308
|
+
* WHERE — so any bound values in a join's `where` condition land in positional
|
|
309
|
+
* order after the SELECT-list bindings (rawSelects/aggregates) and before the
|
|
310
|
+
* WHERE's. Shares the caller's {@link Bindings} to keep that order correct.
|
|
311
|
+
*/
|
|
312
|
+
function compileJoins(joins, dialect, b) {
|
|
313
|
+
return joins
|
|
314
|
+
.map(j => {
|
|
315
|
+
const table = dialect.quoteId(j.table);
|
|
316
|
+
const keyword = JOIN_KEYWORD[j.type];
|
|
317
|
+
if (j.type === 'cross')
|
|
318
|
+
return `${keyword} ${table}`;
|
|
319
|
+
const on = compileJoinConditions(j.conditions, dialect, b);
|
|
320
|
+
if (on === '') {
|
|
321
|
+
throw new Error(`[RudderJS ORM native] ${keyword} ${j.table} requires at least one ON condition.`);
|
|
322
|
+
}
|
|
323
|
+
return `${keyword} ${table} ON ${on}`;
|
|
324
|
+
})
|
|
325
|
+
.join(' ');
|
|
326
|
+
}
|
|
327
|
+
/** `GROUP BY` column list (without the keyword), or `''` when none. Each column
|
|
328
|
+
* is identifier-quoted (qualified `table.col` supported); no values bind. */
|
|
329
|
+
function compileGroupBy(groupBy, dialect) {
|
|
330
|
+
return groupBy.map(c => dialect.quoteId(c)).join(', ');
|
|
331
|
+
}
|
|
332
|
+
/** HAVING expression (without the keyword), or `''` when none. `clause` entries
|
|
333
|
+
* bind their value; `raw` entries splice verbatim (and may bind via the shared
|
|
334
|
+
* {@link Bindings}). Booleans connect siblings, first ignored — mirrors WHERE. */
|
|
335
|
+
function compileHaving(having, dialect, b) {
|
|
336
|
+
const parts = [];
|
|
337
|
+
for (const node of having) {
|
|
338
|
+
const frag = node.kind === 'raw' ? compileRaw(node.raw, b) : compileClause(node.clause, dialect, b);
|
|
339
|
+
parts.push(parts.length === 0 ? frag : `${node.boolean} ${frag}`);
|
|
340
|
+
}
|
|
341
|
+
return parts.join(' ');
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Compile a SELECT for the read terminals (`get`/`all`/`first`/`find`).
|
|
345
|
+
*
|
|
346
|
+
* `overrides` lets terminals tweak the shape without mutating state:
|
|
347
|
+
* - `limit` — force a LIMIT (e.g. `first()` → 1), overriding `state.limitN`.
|
|
348
|
+
* - `selectColumns` — projection list; defaults to `*`.
|
|
349
|
+
* - `extraConditions` — additional clauses AND-ed in (e.g. `find(id)` → PK match),
|
|
350
|
+
* applied at the top level *outside* the user predicate parens.
|
|
351
|
+
*/
|
|
352
|
+
export function compileSelect(state, dialect, overrides = {}) {
|
|
353
|
+
const b = new Bindings(dialect);
|
|
354
|
+
// WITH prefix first — CTE-body bindings precede every other parameter (the
|
|
355
|
+
// WITH clause is the first SQL text). '' when the query declares no CTEs.
|
|
356
|
+
let sql = compileCtePrefix(state.ctes ?? [], dialect, b);
|
|
357
|
+
// Base SELECT body (projection → HAVING; no ORDER BY / LIMIT / lock — those
|
|
358
|
+
// apply to the whole result, after any UNION). `overrides` only touch the base.
|
|
359
|
+
sql += compileSelectBody(state, dialect, b, overrides);
|
|
360
|
+
// UNION / UNION ALL members. Each member's body shares the same `Bindings`, so
|
|
361
|
+
// its parameters land positionally after the base body's. Member ORDER BY /
|
|
362
|
+
// LIMIT are intentionally dropped (compileSelectBody emits neither).
|
|
363
|
+
for (const u of state.unions ?? []) {
|
|
364
|
+
sql += ` UNION ${u.all ? 'ALL ' : ''}${compileSelectBody(u.state, dialect, b)}`;
|
|
365
|
+
}
|
|
366
|
+
// ORDER BY / LIMIT / OFFSET / lock come from the BASE state and apply to the
|
|
367
|
+
// combined result. Binds after every union member's parameters (SQL text order).
|
|
368
|
+
const orderBy = compileOrderBy(state.orders, dialect, b);
|
|
369
|
+
if (orderBy)
|
|
370
|
+
sql += ` ORDER BY ${orderBy}`;
|
|
371
|
+
const limit = overrides.limit !== undefined ? overrides.limit : state.limitN;
|
|
372
|
+
if (limit !== null && limit !== undefined)
|
|
373
|
+
sql += ` LIMIT ${asInt(limit)}`;
|
|
374
|
+
if (state.offsetN !== null) {
|
|
375
|
+
// SQLite requires a LIMIT before OFFSET; supply -1 (unbounded) when the
|
|
376
|
+
// caller set an offset without a limit.
|
|
377
|
+
if (limit === null || limit === undefined)
|
|
378
|
+
sql += ` LIMIT -1`;
|
|
379
|
+
sql += ` OFFSET ${asInt(state.offsetN)}`;
|
|
380
|
+
}
|
|
381
|
+
// Pessimistic lock trails everything (standard SQL puts the locking clause
|
|
382
|
+
// last). dialect.lockSql returns '' on engines without row locks (SQLite).
|
|
383
|
+
if (state.lock)
|
|
384
|
+
sql += dialect.lockSql(state.lock, state.lockOptions ?? undefined);
|
|
385
|
+
return { sql, bindings: b.values };
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Compile the `WITH [RECURSIVE] name [(cols)] AS (body), …` prefix (trailing
|
|
389
|
+
* space included) — or `''` when `ctes` is empty. `RECURSIVE` is a property of
|
|
390
|
+
* the whole WITH list (standard SQL): one recursive member marks the list.
|
|
391
|
+
* Builder-backed bodies compile through {@link compileSelectBody} (+ their own
|
|
392
|
+
* UNION members — recursive-style bodies built from two queries `union`ed work);
|
|
393
|
+
* raw bodies rebind their `?` placeholders through the shared {@link Bindings}.
|
|
394
|
+
* Body ORDER BY / LIMIT are dropped, same rule as UNION members.
|
|
395
|
+
*/
|
|
396
|
+
function compileCtePrefix(ctes, dialect, b) {
|
|
397
|
+
if (ctes.length === 0)
|
|
398
|
+
return '';
|
|
399
|
+
const parts = ctes.map(cte => {
|
|
400
|
+
const name = dialect.quoteId(cte.name);
|
|
401
|
+
const cols = cte.columns && cte.columns.length > 0
|
|
402
|
+
? ` (${cte.columns.map(c => dialect.quoteId(c)).join(', ')})`
|
|
403
|
+
: '';
|
|
404
|
+
return `${name}${cols} AS (${compileSubqueryBody(cte.body, dialect, b)})`;
|
|
405
|
+
});
|
|
406
|
+
const recursive = ctes.some(c => c.recursive);
|
|
407
|
+
return `WITH ${recursive ? 'RECURSIVE ' : ''}${parts.join(', ')} `;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Compile a {@link SubqueryBody} (CTE body / `whereExists` subquery) through
|
|
411
|
+
* the caller's shared {@link Bindings}. Builder-backed bodies compile via
|
|
412
|
+
* {@link compileSelectBody} plus their own UNION members (ORDER BY / LIMIT
|
|
413
|
+
* dropped — the UNION-member rule); raw bodies rebind their `?` placeholders.
|
|
414
|
+
*/
|
|
415
|
+
function compileSubqueryBody(body, dialect, b) {
|
|
416
|
+
if (body.kind === 'raw')
|
|
417
|
+
return compileRaw(body.raw, b);
|
|
418
|
+
let sql = compileSelectBody(body.state, dialect, b);
|
|
419
|
+
for (const u of body.state.unions ?? []) {
|
|
420
|
+
sql += ` UNION ${u.all ? 'ALL ' : ''}${compileSelectBody(u.state, dialect, b)}`;
|
|
421
|
+
}
|
|
422
|
+
return sql;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The SELECT body up to and including HAVING — projection, FROM, JOINs, WHERE,
|
|
426
|
+
* GROUP BY, HAVING — with NO ORDER BY / LIMIT / OFFSET / lock. Shared by
|
|
427
|
+
* {@link compileSelect} (which appends those) and the UNION members + the
|
|
428
|
+
* wrapped {@link compileCount}. Uses the caller's {@link Bindings} so positional
|
|
429
|
+
* parameters stay aligned across the whole (possibly unioned) statement.
|
|
430
|
+
*/
|
|
431
|
+
function compileSelectBody(state, dialect, b, overrides = {}) {
|
|
432
|
+
const table = dialect.quoteId(state.table);
|
|
433
|
+
// `select(...)` / `selectRaw` REPLACE the default `*` projection (Laravel
|
|
434
|
+
// semantics). Structured columns (quoted) come first, then raw fragments —
|
|
435
|
+
// compiled before the WHERE so any `?` bindings land first (SELECT precedes
|
|
436
|
+
// WHERE in SQL text). `overrides.selectColumns` (terminal-injected) still wins.
|
|
437
|
+
const structuredSelects = (state.selects ?? []).map(c => dialect.quoteId(c));
|
|
438
|
+
const rawSelects = state.rawSelects ?? [];
|
|
439
|
+
const projection = [...structuredSelects, ...rawSelects.map(frag => compileRaw(frag, b))];
|
|
440
|
+
const baseSelect = overrides.selectColumns
|
|
441
|
+
?? (projection.length > 0 ? projection.join(', ') : '*');
|
|
442
|
+
// Aggregate subselects (withCount/withSum/…) join the SELECT list. They're
|
|
443
|
+
// compiled BEFORE the WHERE so their bindings land first — matching the SQL
|
|
444
|
+
// text order (SELECT list precedes WHERE).
|
|
445
|
+
const aggParts = (state.aggregates ?? []).map(req => compileAggregateSubselect(state.table, req, dialect, b));
|
|
446
|
+
// Window projections are ADDITIVE (appended after the base projection +
|
|
447
|
+
// aggregates) and bind-free — pure identifiers and keywords.
|
|
448
|
+
const windowParts = (state.windows ?? []).map(w => compileWindowSelect(w, dialect));
|
|
449
|
+
const extraParts = [...aggParts, ...windowParts];
|
|
450
|
+
const selectList = extraParts.length > 0 ? [baseSelect, ...extraParts].join(', ') : baseSelect;
|
|
451
|
+
let sql = `SELECT ${state.distinct ? 'DISTINCT ' : ''}${selectList} FROM ${table}`;
|
|
452
|
+
// JOINs sit between FROM and WHERE; their `where`-condition bindings (if any)
|
|
453
|
+
// land after the SELECT-list bindings and before the WHERE's — SQL text order.
|
|
454
|
+
const joins = compileJoins(state.joins ?? [], dialect, b);
|
|
455
|
+
if (joins)
|
|
456
|
+
sql += ` ${joins}`;
|
|
457
|
+
const where = compileWhereWithExtra(state, dialect, b, overrides.extraConditions);
|
|
458
|
+
if (where)
|
|
459
|
+
sql += ` WHERE ${where}`;
|
|
460
|
+
// GROUP BY (no bindings) then HAVING (binds after WHERE, before ORDER BY).
|
|
461
|
+
const groupBy = compileGroupBy(state.groupBy ?? [], dialect);
|
|
462
|
+
if (groupBy)
|
|
463
|
+
sql += ` GROUP BY ${groupBy}`;
|
|
464
|
+
const having = compileHaving(state.having ?? [], dialect, b);
|
|
465
|
+
if (having)
|
|
466
|
+
sql += ` HAVING ${having}`;
|
|
467
|
+
return sql;
|
|
468
|
+
}
|
|
469
|
+
/** `ROW_NUMBER() OVER (PARTITION BY "a" ORDER BY "b" DESC) AS "alias"`. The
|
|
470
|
+
* function name comes from the closed {@link WINDOW_FUNCTION_SQL} map (the
|
|
471
|
+
* injection gate — `selectWindow` validates membership); every identifier is
|
|
472
|
+
* quoted; directions are pre-validated to `asc`/`desc`. */
|
|
473
|
+
function compileWindowSelect(w, dialect) {
|
|
474
|
+
const parts = [];
|
|
475
|
+
if (w.partitionBy.length > 0) {
|
|
476
|
+
parts.push(`PARTITION BY ${w.partitionBy.map(c => dialect.quoteId(c)).join(', ')}`);
|
|
477
|
+
}
|
|
478
|
+
if (w.orderBy.length > 0) {
|
|
479
|
+
parts.push(`ORDER BY ${w.orderBy.map(o => `${dialect.quoteId(o.column)} ${o.direction === 'desc' ? 'DESC' : 'ASC'}`).join(', ')}`);
|
|
480
|
+
}
|
|
481
|
+
return `${WINDOW_FUNCTION_SQL[w.fn]}() OVER (${parts.join(' ')}) AS ${dialect.quoteId(w.as)}`;
|
|
482
|
+
}
|
|
483
|
+
/** Compile `SELECT COUNT(*) AS count FROM ... WHERE ...` for `count()` /
|
|
484
|
+
* `paginate()` totals. Orders/limit/offset are irrelevant to a count. */
|
|
485
|
+
export function compileCount(state, dialect) {
|
|
486
|
+
const b = new Bindings(dialect);
|
|
487
|
+
const table = dialect.quoteId(state.table);
|
|
488
|
+
const countCol = dialect.quoteId('count');
|
|
489
|
+
// WITH prefix first (same rule as compileSelect) — CTE bindings precede all.
|
|
490
|
+
const cte = compileCtePrefix(state.ctes ?? [], dialect, b);
|
|
491
|
+
// A UNION counts the rows of the COMBINED result — wrap the whole union body
|
|
492
|
+
// (each member carries its own GROUP BY/HAVING). Takes precedence over the
|
|
493
|
+
// GROUP BY wrap below; member ORDER BY/LIMIT are irrelevant to a count.
|
|
494
|
+
const unions = state.unions ?? [];
|
|
495
|
+
if (unions.length > 0) {
|
|
496
|
+
let inner = compileSelectBody(state, dialect, b);
|
|
497
|
+
for (const u of unions)
|
|
498
|
+
inner += ` UNION ${u.all ? 'ALL ' : ''}${compileSelectBody(u.state, dialect, b)}`;
|
|
499
|
+
const sql = `${cte}SELECT COUNT(*) AS ${countCol} FROM (${inner}) AS ${dialect.quoteId('aggregate')}`;
|
|
500
|
+
return { sql, bindings: b.values };
|
|
501
|
+
}
|
|
502
|
+
// DISTINCT counts the number of DISTINCT projected rows — wrap the SELECT
|
|
503
|
+
// DISTINCT body (a bare `COUNT(DISTINCT *)` isn't valid SQL).
|
|
504
|
+
if (state.distinct) {
|
|
505
|
+
const inner = compileSelectBody(state, dialect, b);
|
|
506
|
+
const sql = `${cte}SELECT COUNT(*) AS ${countCol} FROM (${inner}) AS ${dialect.quoteId('aggregate')}`;
|
|
507
|
+
return { sql, bindings: b.values };
|
|
508
|
+
}
|
|
509
|
+
const joins = compileJoins(state.joins ?? [], dialect, b);
|
|
510
|
+
const where = compileWhere(state, dialect, b);
|
|
511
|
+
const groupBy = state.groupBy ?? [];
|
|
512
|
+
// No GROUP BY → a plain scalar COUNT(*).
|
|
513
|
+
if (groupBy.length === 0) {
|
|
514
|
+
let sql = `${cte}SELECT COUNT(*) AS ${countCol} FROM ${table}`;
|
|
515
|
+
if (joins)
|
|
516
|
+
sql += ` ${joins}`;
|
|
517
|
+
if (where)
|
|
518
|
+
sql += ` WHERE ${where}`;
|
|
519
|
+
return { sql, bindings: b.values };
|
|
520
|
+
}
|
|
521
|
+
// With GROUP BY, `COUNT(*)` would return one row per group. Laravel counts the
|
|
522
|
+
// NUMBER OF GROUPS by wrapping the grouped query in a subquery — so paginate()
|
|
523
|
+
// totals and count() agree. WHERE binds before HAVING (text order) via shared b.
|
|
524
|
+
let inner = `SELECT 1 FROM ${table}`;
|
|
525
|
+
if (joins)
|
|
526
|
+
inner += ` ${joins}`;
|
|
527
|
+
if (where)
|
|
528
|
+
inner += ` WHERE ${where}`;
|
|
529
|
+
inner += ` GROUP BY ${compileGroupBy(groupBy, dialect)}`;
|
|
530
|
+
const having = compileHaving(state.having ?? [], dialect, b);
|
|
531
|
+
if (having)
|
|
532
|
+
inner += ` HAVING ${having}`;
|
|
533
|
+
const sql = `${cte}SELECT COUNT(*) AS ${countCol} FROM (${inner}) AS ${dialect.quoteId('aggregate')}`;
|
|
534
|
+
return { sql, bindings: b.values };
|
|
535
|
+
}
|
|
536
|
+
/** Drop keys whose value is `undefined` — better-sqlite3 rejects `undefined`
|
|
537
|
+
* bindings, and an absent column should fall to its DB default, not error.
|
|
538
|
+
* `null` is kept (it's a real SQL value). Mirrors the Model layer's
|
|
539
|
+
* `_toData()` undefined-filtering for the `query().create()` bypass path. */
|
|
540
|
+
function definedEntries(data) {
|
|
541
|
+
return Object.entries(data).filter(([, v]) => v !== undefined);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Compile an INSERT for one or more rows. Single-row (`create`) and multi-row
|
|
545
|
+
* (`insertMany`) share this. The column list is the first-seen union of every
|
|
546
|
+
* row's defined keys; a row missing a union column binds `null`. With
|
|
547
|
+
* `returning`, appends `RETURNING *` (single-row `create` reads the inserted
|
|
548
|
+
* row back). Throws on an empty `rows` array — callers guard the no-op.
|
|
549
|
+
*/
|
|
550
|
+
export function compileInsert(state, dialect, rows, opts = {}) {
|
|
551
|
+
if (rows.length === 0) {
|
|
552
|
+
throw new Error('[RudderJS ORM native] compileInsert called with no rows.');
|
|
553
|
+
}
|
|
554
|
+
const table = dialect.quoteId(state.table);
|
|
555
|
+
// Conflict suffix (before RETURNING) for an upsert. Identifiers only — quoted
|
|
556
|
+
// by the dialect; values stay parameterized in the VALUES tuples.
|
|
557
|
+
const conflict = opts.upsert
|
|
558
|
+
? ` ${dialect.upsertClause(opts.upsert.uniqueBy, opts.upsert.update)}`
|
|
559
|
+
: '';
|
|
560
|
+
// First-seen union of defined columns across all rows.
|
|
561
|
+
const columns = [];
|
|
562
|
+
const seen = new Set();
|
|
563
|
+
for (const row of rows) {
|
|
564
|
+
for (const [k, v] of Object.entries(row)) {
|
|
565
|
+
if (v !== undefined && !seen.has(k)) {
|
|
566
|
+
seen.add(k);
|
|
567
|
+
columns.push(k);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// No columns at all → rely entirely on DB defaults.
|
|
572
|
+
if (columns.length === 0) {
|
|
573
|
+
let sql = `INSERT INTO ${table} DEFAULT VALUES${conflict}`;
|
|
574
|
+
if (opts.returning)
|
|
575
|
+
sql += ` RETURNING *`;
|
|
576
|
+
return { sql, bindings: [] };
|
|
577
|
+
}
|
|
578
|
+
const b = new Bindings(dialect);
|
|
579
|
+
const quotedCols = columns.map(c => dialect.quoteId(c)).join(', ');
|
|
580
|
+
const tuples = rows.map(row => {
|
|
581
|
+
const placeholders = columns.map(c => {
|
|
582
|
+
const v = row[c];
|
|
583
|
+
return b.add(v === undefined ? null : v);
|
|
584
|
+
});
|
|
585
|
+
return `(${placeholders.join(', ')})`;
|
|
586
|
+
}).join(', ');
|
|
587
|
+
let sql = `INSERT INTO ${table} (${quotedCols}) VALUES ${tuples}${conflict}`;
|
|
588
|
+
if (opts.returning)
|
|
589
|
+
sql += ` RETURNING *`;
|
|
590
|
+
return { sql, bindings: b.values };
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Compile `INSERT INTO table (cols) SELECT …` for `insertUsing(columns, query)`
|
|
594
|
+
* — the rows come from a subquery (builder state or raw SQL), not VALUES
|
|
595
|
+
* tuples. Column names are identifier-quoted; the subquery compiles through
|
|
596
|
+
* the shared {@link Bindings} (raw `?` placeholders rebound per dialect). The
|
|
597
|
+
* column list is REQUIRED — the subquery's projection order must be pinned to
|
|
598
|
+
* named target columns, a bare `INSERT INTO t SELECT …` is a column-order
|
|
599
|
+
* footgun.
|
|
600
|
+
*/
|
|
601
|
+
export function compileInsertUsing(state, dialect, columns, body, opts = {}) {
|
|
602
|
+
if (columns.length === 0) {
|
|
603
|
+
throw new NativeOrmError('NATIVE_INSERT_USING_COLUMNS', 'insertUsing() requires an explicit target column list — the subquery projection maps to it positionally.');
|
|
604
|
+
}
|
|
605
|
+
const b = new Bindings(dialect);
|
|
606
|
+
const cols = columns.map(c => dialect.quoteId(c)).join(', ');
|
|
607
|
+
let sql = `INSERT INTO ${dialect.quoteId(state.table)} (${cols}) ${compileSubqueryBody(body, dialect, b)}`;
|
|
608
|
+
if (opts.returning)
|
|
609
|
+
sql += ` RETURNING *`;
|
|
610
|
+
return { sql, bindings: b.values };
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Render the SET clause for an UPDATE payload containing arrow-path keys
|
|
614
|
+
* (`'meta->prefs->lang': 'en'` → `"meta" = json_set("meta", '$."prefs"."lang"', json(?))`).
|
|
615
|
+
*
|
|
616
|
+
* All arrow writes on one base column merge into a single assignment (the
|
|
617
|
+
* dialect's `jsonSet` seam takes the write list) — SQL forbids assigning the
|
|
618
|
+
* same column twice in one SET. Plain keys keep their original form. SET items
|
|
619
|
+
* appear in first-seen key order, values binding left-to-right in SQL-text
|
|
620
|
+
* order. Mixing a plain write and an arrow write to the same column throws —
|
|
621
|
+
* the two assignments would silently race, last-one-wins, per dialect.
|
|
622
|
+
*/
|
|
623
|
+
function compileJsonSetClause(entries, dialect, b) {
|
|
624
|
+
const items = [];
|
|
625
|
+
const jsonByColumn = new Map();
|
|
626
|
+
const plainColumns = new Set();
|
|
627
|
+
const conflict = (column) => {
|
|
628
|
+
throw new NativeOrmError('NATIVE_JSON_SET_CONFLICT', `[RudderJS ORM native] Update payload writes both the whole column "${column}" and a JSON path inside it — ` +
|
|
629
|
+
`pick one (the two assignments would conflict in a single SET).`);
|
|
630
|
+
};
|
|
631
|
+
for (const [key, value] of entries) {
|
|
632
|
+
if (key.includes('->')) {
|
|
633
|
+
const { column, segments } = parseJsonPath(key);
|
|
634
|
+
if (plainColumns.has(column))
|
|
635
|
+
conflict(column);
|
|
636
|
+
let item = jsonByColumn.get(column);
|
|
637
|
+
if (!item) {
|
|
638
|
+
item = { kind: 'json', column, writes: [] };
|
|
639
|
+
jsonByColumn.set(column, item);
|
|
640
|
+
items.push(item);
|
|
641
|
+
}
|
|
642
|
+
item.writes.push({ segments, value });
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
if (jsonByColumn.has(key))
|
|
646
|
+
conflict(key);
|
|
647
|
+
plainColumns.add(key);
|
|
648
|
+
items.push({ kind: 'plain', column: key, value });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return items.map(item => {
|
|
652
|
+
const col = dialect.quoteId(item.column);
|
|
653
|
+
return item.kind === 'plain'
|
|
654
|
+
? `${col} = ${b.add(item.value)}`
|
|
655
|
+
: `${col} = ${dialect.jsonSet(col, item.writes, v => b.add(v))}`;
|
|
656
|
+
}).join(', ');
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Compile `UPDATE <table> SET col = ? [, …] [WHERE …] [RETURNING *]`.
|
|
660
|
+
*
|
|
661
|
+
* SET bindings are emitted before WHERE bindings, matching positional `?`
|
|
662
|
+
* order. `undefined`-valued columns are dropped (see {@link definedEntries}).
|
|
663
|
+
* Throws when there's nothing to set.
|
|
664
|
+
*
|
|
665
|
+
* Arrow-path keys (`'meta->prefs->lang'`) write into a JSON column via the
|
|
666
|
+
* dialect's `jsonSet` seam — see {@link compileJsonSetClause}. Payloads with
|
|
667
|
+
* no arrow key take the original plain path (byte-identical SQL).
|
|
668
|
+
*/
|
|
669
|
+
export function compileUpdate(state, dialect, data, opts = {}) {
|
|
670
|
+
const entries = definedEntries(data);
|
|
671
|
+
if (entries.length === 0) {
|
|
672
|
+
throw new Error('[RudderJS ORM native] compileUpdate called with no columns to set.');
|
|
673
|
+
}
|
|
674
|
+
const b = new Bindings(dialect);
|
|
675
|
+
const table = dialect.quoteId(state.table);
|
|
676
|
+
const setClause = entries.some(([col]) => col.includes('->'))
|
|
677
|
+
? compileJsonSetClause(entries, dialect, b)
|
|
678
|
+
: entries.map(([col, v]) => `${dialect.quoteId(col)} = ${b.add(v)}`).join(', ');
|
|
679
|
+
let sql = `UPDATE ${table} SET ${setClause}`;
|
|
680
|
+
const where = compileWhereWithExtra(state, dialect, b, opts.extraConditions);
|
|
681
|
+
if (where)
|
|
682
|
+
sql += ` WHERE ${where}`;
|
|
683
|
+
if (opts.returning)
|
|
684
|
+
sql += ` RETURNING *`;
|
|
685
|
+
return { sql, bindings: b.values };
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Compile an atomic counter update: `UPDATE <table> SET col = col + ? [, extra = ?]
|
|
689
|
+
* [WHERE …] [RETURNING *]`. `delta` is the signed amount (decrement passes a
|
|
690
|
+
* negative). `extra` columns are written in the same statement. Pure SQL —
|
|
691
|
+
* `col = col + ?` reads and writes atomically at the DB, safe under concurrency.
|
|
692
|
+
*/
|
|
693
|
+
export function compileIncrement(state, dialect, column, delta, extra, opts = {}) {
|
|
694
|
+
const b = new Bindings(dialect);
|
|
695
|
+
const table = dialect.quoteId(state.table);
|
|
696
|
+
const col = dialect.quoteId(column);
|
|
697
|
+
const assignments = [`${col} = ${col} + ${b.add(delta)}`];
|
|
698
|
+
for (const [k, v] of definedEntries(extra)) {
|
|
699
|
+
assignments.push(`${dialect.quoteId(k)} = ${b.add(v)}`);
|
|
700
|
+
}
|
|
701
|
+
let sql = `UPDATE ${table} SET ${assignments.join(', ')}`;
|
|
702
|
+
const where = compileWhereWithExtra(state, dialect, b, opts.extraConditions);
|
|
703
|
+
if (where)
|
|
704
|
+
sql += ` WHERE ${where}`;
|
|
705
|
+
if (opts.returning)
|
|
706
|
+
sql += ` RETURNING *`;
|
|
707
|
+
return { sql, bindings: b.values };
|
|
708
|
+
}
|
|
709
|
+
/** Compile `DELETE FROM <table> [WHERE …] [RETURNING *]`. With `returning`,
|
|
710
|
+
* the executor returns the deleted rows so the caller can take `rows.length`
|
|
711
|
+
* as the affected count (no driver `changes` metadata needed). */
|
|
712
|
+
export function compileDelete(state, dialect, opts = {}) {
|
|
713
|
+
const b = new Bindings(dialect);
|
|
714
|
+
const table = dialect.quoteId(state.table);
|
|
715
|
+
let sql = `DELETE FROM ${table}`;
|
|
716
|
+
const where = compileWhereWithExtra(state, dialect, b, opts.extraConditions);
|
|
717
|
+
if (where)
|
|
718
|
+
sql += ` WHERE ${where}`;
|
|
719
|
+
if (opts.returning)
|
|
720
|
+
sql += ` RETURNING *`;
|
|
721
|
+
return { sql, bindings: b.values };
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* WHERE builder that also folds in terminal-supplied `extraConditions` (e.g.
|
|
725
|
+
* the primary-key match for `find(id)`). The extras are AND-ed at the top
|
|
726
|
+
* level, *outside* the parenthesized user predicate, so `find` composes with
|
|
727
|
+
* an existing `where('tenantId', t)` without crossing tenants.
|
|
728
|
+
*/
|
|
729
|
+
function compileWhereWithExtra(state, dialect, b, extra) {
|
|
730
|
+
const base = compileWhere(state, dialect, b);
|
|
731
|
+
if (!extra || extra.length === 0)
|
|
732
|
+
return base;
|
|
733
|
+
const extraExpr = compileNodes(extra, dialect, b);
|
|
734
|
+
if (!extraExpr)
|
|
735
|
+
return base;
|
|
736
|
+
if (!base)
|
|
737
|
+
return extraExpr;
|
|
738
|
+
// base may itself be `userPred AND deletedAt IS NULL`; AND the extra on.
|
|
739
|
+
return `${base} AND ${extraExpr}`;
|
|
740
|
+
}
|
|
741
|
+
/** Coerce a limit/offset to a safe non-negative integer for inlining. LIMIT/
|
|
742
|
+
* OFFSET take integer literals (not all SQLite builds bind them cleanly), so
|
|
743
|
+
* we inline — but only after `Number.isInteger` + clamp, never user strings. */
|
|
744
|
+
function asInt(n) {
|
|
745
|
+
if (!Number.isInteger(n) || n < -1) {
|
|
746
|
+
throw new Error(`[RudderJS ORM native] LIMIT/OFFSET must be a non-negative integer, got ${String(n)}.`);
|
|
747
|
+
}
|
|
748
|
+
return n;
|
|
749
|
+
}
|
|
750
|
+
// ─── Relations + aggregates (Phase 3) ──────────────────────
|
|
751
|
+
//
|
|
752
|
+
// Correlated EXISTS / NOT EXISTS subqueries (whereHas) and aggregate subselects
|
|
753
|
+
// (withCount/Sum/Min/Max/Avg/Exists). Both reference the OUTER query's table via
|
|
754
|
+
// a qualified column (`"outer"."parentColumn"`) and qualify every inner column
|
|
755
|
+
// with its own table so a column name shared between outer and inner can't go
|
|
756
|
+
// ambiguous. Same purity + parameterization rules as the rest of the compiler.
|
|
757
|
+
/** Qualified `"table"."column"` — both segments validated + quoted. */
|
|
758
|
+
function qcol(table, column, dialect) {
|
|
759
|
+
return `${dialect.quoteId(table)}.${dialect.quoteId(column)}`;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Render a `WhereClause` with its column qualified by `table`. Mirrors
|
|
763
|
+
* {@link compileClause} (operator map, `IS [NOT] NULL`, `IN`/`NOT IN` expansion)
|
|
764
|
+
* but every column reference is `"table"."col"` — required inside a correlated
|
|
765
|
+
* subquery where an unqualified name could resolve to the outer table.
|
|
766
|
+
*
|
|
767
|
+
* Arrow-path columns (`meta->prefs->lang`, from a `where()` inside a whereHas
|
|
768
|
+
* constrain callback) route through the same {@link compileJsonComparison}
|
|
769
|
+
* body as top-level `json` condition nodes — the base column is qualified +
|
|
770
|
+
* quoted, the path segments are validated by {@link parseJsonPath}, and the
|
|
771
|
+
* value binds through the shared positional {@link Bindings} so it lands in
|
|
772
|
+
* SQL-text order within the EXISTS body.
|
|
773
|
+
*/
|
|
774
|
+
function compileClauseOn(table, clause, dialect, b) {
|
|
775
|
+
if (clause.column.includes('->')) {
|
|
776
|
+
const { column, segments } = parseJsonPath(clause.column);
|
|
777
|
+
return compileJsonComparison(qcol(table, column, dialect), segments, clause.operator, clause.value, dialect, b);
|
|
778
|
+
}
|
|
779
|
+
const col = qcol(table, clause.column, dialect);
|
|
780
|
+
const { operator, value } = clause;
|
|
781
|
+
if (operator === 'IN' || operator === 'NOT IN') {
|
|
782
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
783
|
+
if (arr.length === 0)
|
|
784
|
+
return operator === 'IN' ? '1 = 0' : '1 = 1';
|
|
785
|
+
const list = arr.map(v => b.add(v)).join(', ');
|
|
786
|
+
return `${col} ${operator} (${list})`;
|
|
787
|
+
}
|
|
788
|
+
if (value === null && (operator === '=' || operator === '!=')) {
|
|
789
|
+
return `${col} IS ${operator === '=' ? '' : 'NOT '}NULL`;
|
|
790
|
+
}
|
|
791
|
+
const op = OPERATOR_SQL[operator];
|
|
792
|
+
if (!op)
|
|
793
|
+
throw new Error(`[RudderJS ORM native] Unsupported operator: ${String(operator)}`);
|
|
794
|
+
return `${col} ${op} ${b.add(value)}`;
|
|
795
|
+
}
|
|
796
|
+
/** AND-join non-empty fragments; `'1 = 1'` when there are none (keeps a bare
|
|
797
|
+
* `WHERE` valid for the rare all-empty case). */
|
|
798
|
+
function andAll(fragments) {
|
|
799
|
+
const parts = fragments.filter(f => f !== '');
|
|
800
|
+
return parts.length === 0 ? '1 = 1' : parts.join(' AND ');
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Compile a correlated `EXISTS (…)` / `NOT EXISTS (…)` fragment for a
|
|
804
|
+
* {@link RelationExistencePredicate}. Shares the caller's {@link Bindings} so
|
|
805
|
+
* its parameters stay in positional order with the surrounding WHERE.
|
|
806
|
+
*
|
|
807
|
+
* - **Direct** (hasMany/hasOne/belongsTo/morphMany/morphOne): single subquery
|
|
808
|
+
* joining `related.relatedColumn = outer.parentColumn`, plus `extraEquals`
|
|
809
|
+
* (morph discriminator) on the related table and the constraint wheres.
|
|
810
|
+
* - **Through-pivot** (belongsToMany/morphToMany/morphedByMany): nested EXISTS —
|
|
811
|
+
* pivot rows for this parent (+ `extraEquals` on the pivot) whose related row
|
|
812
|
+
* exists (+ constraint wheres on the related table).
|
|
813
|
+
*
|
|
814
|
+
* Soft-delete scoping is intentionally NOT applied here — it's the constrain
|
|
815
|
+
* callback's responsibility (documented), matching the other adapters.
|
|
816
|
+
*/
|
|
817
|
+
export function compileExists(outerTable, predicate, dialect, b) {
|
|
818
|
+
const related = predicate.relatedTable;
|
|
819
|
+
// The subquery's FROM table + WHERE body — shared by the EXISTS and the
|
|
820
|
+
// `COUNT(*) op N` wrappers below.
|
|
821
|
+
let fromTable;
|
|
822
|
+
let whereBody;
|
|
823
|
+
if (predicate.through) {
|
|
824
|
+
const pivot = predicate.through.pivotTable;
|
|
825
|
+
// Compile in SQL-TEXT order so the shared `Bindings` stays positionally
|
|
826
|
+
// aligned: the pivot's `extraEquals` appears in the text BEFORE the nested
|
|
827
|
+
// inner EXISTS, so its parameters must bind first. (Building the inner
|
|
828
|
+
// EXISTS first would swap `taggableType` and the related constraint.)
|
|
829
|
+
const pivotKeyExpr = `${qcol(pivot, predicate.through.foreignPivotKey, dialect)} = ${qcol(outerTable, predicate.parentColumn, dialect)}`;
|
|
830
|
+
const extraExprs = extraEqualsOn(pivot, predicate.extraEquals, dialect, b);
|
|
831
|
+
// Inner: the related row joined to this pivot row, plus constraint wheres,
|
|
832
|
+
// plus the child predicate of a nested path (correlated against the
|
|
833
|
+
// related table, so it lives inside the related row's EXISTS — not the
|
|
834
|
+
// pivot's). Recursion handles arbitrarily deep chains; compiled LAST so
|
|
835
|
+
// its bindings follow the constraint values (SQL-text order).
|
|
836
|
+
const innerExprs = [
|
|
837
|
+
`${qcol(related, predicate.relatedColumn, dialect)} = ${qcol(pivot, predicate.through.relatedPivotKey, dialect)}`,
|
|
838
|
+
...predicate.constraintWheres.map(w => compileClauseOn(related, w, dialect, b)),
|
|
839
|
+
...(predicate.nested ? [compileExists(related, predicate.nested, dialect, b)] : []),
|
|
840
|
+
];
|
|
841
|
+
const innerExists = `EXISTS (SELECT 1 FROM ${dialect.quoteId(related)} WHERE ${andAll(innerExprs)})`;
|
|
842
|
+
fromTable = pivot;
|
|
843
|
+
whereBody = andAll([pivotKeyExpr, ...extraExprs, innerExists]);
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
// Direct: one correlated subquery on the related table. A nested child
|
|
847
|
+
// predicate (`whereHas('posts.comments')`) appends its own correlated
|
|
848
|
+
// EXISTS to the body — compiled LAST in text order, after the constraint
|
|
849
|
+
// wheres, so the shared Bindings stay positionally aligned.
|
|
850
|
+
fromTable = related;
|
|
851
|
+
whereBody = andAll([
|
|
852
|
+
`${qcol(related, predicate.relatedColumn, dialect)} = ${qcol(outerTable, predicate.parentColumn, dialect)}`,
|
|
853
|
+
...extraEqualsOn(related, predicate.extraEquals, dialect, b),
|
|
854
|
+
...predicate.constraintWheres.map(w => compileClauseOn(related, w, dialect, b)),
|
|
855
|
+
...(predicate.nested ? [compileExists(related, predicate.nested, dialect, b)] : []),
|
|
856
|
+
]);
|
|
857
|
+
}
|
|
858
|
+
// `has(relation, op, n)` — count the matching rows instead of testing
|
|
859
|
+
// existence. The integer is validated + inlined (not bound) so it can't shift
|
|
860
|
+
// the surrounding WHERE's positional bindings.
|
|
861
|
+
if (predicate.count) {
|
|
862
|
+
const op = OPERATOR_SQL[predicate.count.operator];
|
|
863
|
+
if (!op)
|
|
864
|
+
throw new Error(`[RudderJS ORM native] Unsupported operator: ${String(predicate.count.operator)}`);
|
|
865
|
+
return `(SELECT COUNT(*) FROM ${dialect.quoteId(fromTable)} WHERE ${whereBody}) ${op} ${asInt(predicate.count.value)}`;
|
|
866
|
+
}
|
|
867
|
+
const keyword = predicate.exists ? 'EXISTS' : 'NOT EXISTS';
|
|
868
|
+
return `${keyword} (SELECT 1 FROM ${dialect.quoteId(fromTable)} WHERE ${whereBody})`;
|
|
869
|
+
}
|
|
870
|
+
/** Render each `extraEquals` entry as a bound `"table"."k" = ?` fragment. */
|
|
871
|
+
function extraEqualsOn(table, extraEquals, dialect, b) {
|
|
872
|
+
if (!extraEquals)
|
|
873
|
+
return [];
|
|
874
|
+
return Object.entries(extraEquals).map(([k, v]) => `${qcol(table, k, dialect)} = ${b.add(v)}`);
|
|
875
|
+
}
|
|
876
|
+
/** The `fn(col)` SQL for an aggregate, with COALESCE on sum so an empty match
|
|
877
|
+
* set yields 0 not NULL. `count`/`exists` ignore the column. */
|
|
878
|
+
function aggregateFnSql(req, relatedTable, dialect) {
|
|
879
|
+
if (req.fn === 'count' || req.fn === 'exists')
|
|
880
|
+
return 'COUNT(*)';
|
|
881
|
+
const col = qcol(relatedTable, requireColumn(req.fn, req.column), dialect);
|
|
882
|
+
switch (req.fn) {
|
|
883
|
+
case 'sum': return `COALESCE(SUM(${col}), 0)`;
|
|
884
|
+
case 'min': return `MIN(${col})`;
|
|
885
|
+
case 'max': return `MAX(${col})`;
|
|
886
|
+
case 'avg': return `AVG(${col})`;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/** Resolve the required column for a numeric aggregate, or throw a clear error.
|
|
890
|
+
* The Model layer always supplies it for sum/min/max/avg; this guards the
|
|
891
|
+
* contract boundary instead of a bare `!`. */
|
|
892
|
+
function requireColumn(fn, column) {
|
|
893
|
+
if (column === undefined) {
|
|
894
|
+
throw new Error(`[RudderJS ORM native] Aggregate "${fn}" requires a column.`);
|
|
895
|
+
}
|
|
896
|
+
return column;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Compile one aggregate request into a correlated subselect expression, aliased
|
|
900
|
+
* as `(…) AS "alias"`, for injection into the main SELECT list. `exists` wraps
|
|
901
|
+
* the COUNT in `(… ) > 0`. Mirrors the orm-drizzle aggregate-subquery shape.
|
|
902
|
+
*/
|
|
903
|
+
export function compileAggregateSubselect(outerTable, req, dialect, b) {
|
|
904
|
+
const js = req.joinShape;
|
|
905
|
+
const related = js.relatedTable;
|
|
906
|
+
const fnSql = aggregateFnSql(req, related, dialect);
|
|
907
|
+
const alias = dialect.quoteId(req.alias);
|
|
908
|
+
let subquery;
|
|
909
|
+
if (js.through) {
|
|
910
|
+
const pivot = js.through.pivotTable;
|
|
911
|
+
const pivotExprs = [
|
|
912
|
+
`${qcol(pivot, js.through.foreignPivotKey, dialect)} = ${qcol(outerTable, js.parentColumn, dialect)}`,
|
|
913
|
+
...extraEqualsOn(pivot, js.extraEquals, dialect, b),
|
|
914
|
+
];
|
|
915
|
+
// A join to the related table is needed only when the aggregate reads a
|
|
916
|
+
// related column, filters on it, or must honor its soft-delete flag.
|
|
917
|
+
const needJoin = req.fn === 'sum' || req.fn === 'min' || req.fn === 'max' || req.fn === 'avg'
|
|
918
|
+
|| req.constraintWheres.length > 0
|
|
919
|
+
|| js.softDeletes === true;
|
|
920
|
+
if (!needJoin) {
|
|
921
|
+
subquery = `(SELECT ${fnSql} FROM ${dialect.quoteId(pivot)} WHERE ${andAll(pivotExprs)})`;
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
const joined = [
|
|
925
|
+
...pivotExprs,
|
|
926
|
+
...req.constraintWheres.map(w => compileClauseOn(related, w, dialect, b)),
|
|
927
|
+
...softDeleteOn(related, js.softDeletes, dialect),
|
|
928
|
+
];
|
|
929
|
+
subquery =
|
|
930
|
+
`(SELECT ${fnSql} FROM ${dialect.quoteId(pivot)} ` +
|
|
931
|
+
`INNER JOIN ${dialect.quoteId(related)} ON ${qcol(related, js.relatedColumn, dialect)} = ${qcol(pivot, js.through.relatedPivotKey, dialect)} ` +
|
|
932
|
+
`WHERE ${andAll(joined)})`;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
const exprs = [
|
|
937
|
+
`${qcol(related, js.relatedColumn, dialect)} = ${qcol(outerTable, js.parentColumn, dialect)}`,
|
|
938
|
+
...extraEqualsOn(related, js.extraEquals, dialect, b),
|
|
939
|
+
...req.constraintWheres.map(w => compileClauseOn(related, w, dialect, b)),
|
|
940
|
+
...softDeleteOn(related, js.softDeletes, dialect),
|
|
941
|
+
];
|
|
942
|
+
subquery = `(SELECT ${fnSql} FROM ${dialect.quoteId(related)} WHERE ${andAll(exprs)})`;
|
|
943
|
+
}
|
|
944
|
+
const valueExpr = req.fn === 'exists' ? `(${subquery} > 0)` : subquery;
|
|
945
|
+
return `${valueExpr} AS ${alias}`;
|
|
946
|
+
}
|
|
947
|
+
/** `["related"."deletedAt" IS NULL]` when the related Model soft-deletes, else `[]`. */
|
|
948
|
+
function softDeleteOn(table, softDeletes, dialect) {
|
|
949
|
+
return softDeletes ? [`${qcol(table, 'deletedAt', dialect)} IS NULL`] : [];
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Compile a single-scalar aggregate terminal — `SELECT fn(col) AS value FROM
|
|
953
|
+
* table WHERE <wheres>` — for the `_aggregate(fn, column?)` contract method
|
|
954
|
+
* (powers `instance.loadSum`/`loadMin`/etc.). `count`/`exists` ignore the
|
|
955
|
+
* column and use `COUNT(*)`; the builder coerces the scalar afterward.
|
|
956
|
+
*/
|
|
957
|
+
export function compileScalarAggregate(state, dialect, fn, column) {
|
|
958
|
+
const b = new Bindings(dialect);
|
|
959
|
+
const table = dialect.quoteId(state.table);
|
|
960
|
+
const expr = fn === 'count' || fn === 'exists' ? 'COUNT(*)'
|
|
961
|
+
: fn === 'sum' ? `COALESCE(SUM(${dialect.quoteId(requireColumn(fn, column))}), 0)`
|
|
962
|
+
: `${fn.toUpperCase()}(${dialect.quoteId(requireColumn(fn, column))})`;
|
|
963
|
+
let sql = `SELECT ${expr} AS ${dialect.quoteId('value')} FROM ${table}`;
|
|
964
|
+
const where = compileWhere(state, dialect, b);
|
|
965
|
+
if (where)
|
|
966
|
+
sql += ` WHERE ${where}`;
|
|
967
|
+
return { sql, bindings: b.values };
|
|
968
|
+
}
|
|
969
|
+
/** Engine-internal seam — exported for the query builder so it can share one
|
|
970
|
+
* `Bindings` run across the WHERE (clauses + EXISTS fragments) and the
|
|
971
|
+
* SELECT-list aggregate subselects. Construction is otherwise module-private.
|
|
972
|
+
* (Deliberately NOT tagged internal: `stripInternal` would drop it from the
|
|
973
|
+
* emitted d.ts, and it is consumed cross-package by @rudderjs/orm's engine
|
|
974
|
+
* suites until PR-A3.) */
|
|
975
|
+
export function makeBindings(dialect) {
|
|
976
|
+
return new Bindings(dialect);
|
|
977
|
+
}
|
|
978
|
+
//# sourceMappingURL=compiler.js.map
|