@rudderjs/database 1.1.0 → 1.2.1
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 +240 -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,984 @@
|
|
|
1
|
+
// ─── NativeQueryBuilder ────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Implements the `QueryBuilder<T>` contract over an {@link Executor} + {@link
|
|
4
|
+
// Dialect}. PHASE 1 shipped the read path (first/find/get/all/count/paginate);
|
|
5
|
+
// PHASE 2 adds the write path (create/update/delete/increment/… + soft deletes).
|
|
6
|
+
// Relation, aggregate, and vector terminals still throw
|
|
7
|
+
// {@link NativeNotImplementedError} until Phase 3.
|
|
8
|
+
//
|
|
9
|
+
// It talks ONLY to the compiler (pure) + the Executor interface — never a
|
|
10
|
+
// concrete driver, never `node:`. Because writes go through an `Executor` (not
|
|
11
|
+
// the top-level connection directly), a transaction scope (Phase 4) drops in
|
|
12
|
+
// without touching this class. Construction is cheap; one builder per query.
|
|
13
|
+
import { Expression } from '@rudderjs/contracts';
|
|
14
|
+
import { parseJsonPath } from './dialect.js';
|
|
15
|
+
import { compileSelect, compileCount, compileInsert, compileInsertUsing, compileUpdate, compileIncrement, compileDelete, compileScalarAggregate, isWindowFunction, } from './compiler.js';
|
|
16
|
+
/** One-time dev warning that native `with(<direct relation>)` doesn't eager-load
|
|
17
|
+
* yet (Phase 3 limitation). Keyed per relation name so each distinct call site
|
|
18
|
+
* warns once. No-op in production. */
|
|
19
|
+
const _warnedWith = new Set();
|
|
20
|
+
/** Global-registry symbol the `HydratingQueryBuilder` Proxy answers with its
|
|
21
|
+
* wrapped native builder. `union(other)` reads it to unwrap a passed proxy back
|
|
22
|
+
* to the underlying `NativeQueryBuilder` so it can read the member's state.
|
|
23
|
+
* `Symbol.for` (not an imported value) keeps the node-only native module out of
|
|
24
|
+
* the client-reachable `index.ts` import graph. */
|
|
25
|
+
const QB_TARGET = Symbol.for('rudderjs.orm.qb.target');
|
|
26
|
+
export class NativeQueryBuilder {
|
|
27
|
+
executor;
|
|
28
|
+
dialect;
|
|
29
|
+
table;
|
|
30
|
+
primaryKey;
|
|
31
|
+
readPick;
|
|
32
|
+
/** Capability marker read by the Model layer's hydrating proxy — arrow-path
|
|
33
|
+
* update keys (`'meta->prefs->lang'`) throw a clear Model-layer error on
|
|
34
|
+
* adapter QBs without it (Drizzle/Prisma until their follow-up). */
|
|
35
|
+
supportsJsonPathUpdates = true;
|
|
36
|
+
/** Capability marker read by the Model layer — nested relation paths
|
|
37
|
+
* (`whereHas('posts.comments')`) build a predicate chain (`nested` child
|
|
38
|
+
* predicates) that only adapters with this marker can compile; others get
|
|
39
|
+
* a clear Model-layer throw instead of a silently-ignored field. */
|
|
40
|
+
supportsNestedRelationPredicates = true;
|
|
41
|
+
_conditions = [];
|
|
42
|
+
_orders = [];
|
|
43
|
+
_selects = [];
|
|
44
|
+
_joins = [];
|
|
45
|
+
_groupBy = [];
|
|
46
|
+
_having = [];
|
|
47
|
+
_unions = [];
|
|
48
|
+
_ctes = [];
|
|
49
|
+
_rawSelects = [];
|
|
50
|
+
_windows = [];
|
|
51
|
+
_relationExists = [];
|
|
52
|
+
_aggregates = [];
|
|
53
|
+
_distinct = false;
|
|
54
|
+
_limitN = null;
|
|
55
|
+
_offsetN = null;
|
|
56
|
+
_softDeletes = false;
|
|
57
|
+
_withTrashed = false;
|
|
58
|
+
_onlyTrashed = false;
|
|
59
|
+
_lock = null;
|
|
60
|
+
/** Wait behavior for `_lock` (`skipLocked` / `noWait`) — validated mutually
|
|
61
|
+
* exclusive at the setter, threaded to `Dialect.lockSql` via the state. */
|
|
62
|
+
_lockOpts = null;
|
|
63
|
+
/** Marks a sub-builder created for whereGroup — terminals throw on it. */
|
|
64
|
+
_isSubBuilder = false;
|
|
65
|
+
constructor(executor, dialect, table, primaryKey,
|
|
66
|
+
/** Read-pool picker on a read/write-split connection (round-robin +
|
|
67
|
+
* sticky-aware, supplied by the adapter); `null` without a split. */
|
|
68
|
+
readPick = null) {
|
|
69
|
+
this.executor = executor;
|
|
70
|
+
this.dialect = dialect;
|
|
71
|
+
this.table = table;
|
|
72
|
+
this.primaryKey = primaryKey;
|
|
73
|
+
this.readPick = readPick;
|
|
74
|
+
}
|
|
75
|
+
// ── internal helpers ─────────────────────────────────────
|
|
76
|
+
/** The executor for a READ terminal: the read pool on a split connection,
|
|
77
|
+
* EXCEPT locked selects (`lockForUpdate`/`sharedLock`) — a lock is only
|
|
78
|
+
* meaningful on the write connection. Writes/`_reselect` never call this. */
|
|
79
|
+
_readExecutor() {
|
|
80
|
+
if (this._lock !== null || this.readPick === null)
|
|
81
|
+
return this.executor;
|
|
82
|
+
return this.readPick();
|
|
83
|
+
}
|
|
84
|
+
/** @internal — called by the Model layer to turn on soft-delete scoping. */
|
|
85
|
+
_enableSoftDeletes() { this._softDeletes = true; return this; }
|
|
86
|
+
/** @internal — mark as a where-group sub-builder so terminals throw. */
|
|
87
|
+
_markSubBuilder() { this._isSubBuilder = true; return this; }
|
|
88
|
+
_assertNotSubBuilder() {
|
|
89
|
+
if (this._isSubBuilder) {
|
|
90
|
+
throw new Error('[RudderJS ORM native] Sub-builder is for where* chaining only — call the terminal on the parent builder.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
_state() {
|
|
94
|
+
return {
|
|
95
|
+
table: this.table,
|
|
96
|
+
primaryKey: this.primaryKey,
|
|
97
|
+
conditions: this._conditions,
|
|
98
|
+
orders: this._orders,
|
|
99
|
+
limitN: this._limitN,
|
|
100
|
+
offsetN: this._offsetN,
|
|
101
|
+
softDelete: this._resolveSoftDelete(),
|
|
102
|
+
deletedAtColumn: 'deletedAt',
|
|
103
|
+
relationExists: this._relationExists,
|
|
104
|
+
aggregates: this._aggregates,
|
|
105
|
+
selects: this._selects,
|
|
106
|
+
rawSelects: this._rawSelects,
|
|
107
|
+
joins: this._joins,
|
|
108
|
+
groupBy: this._groupBy,
|
|
109
|
+
having: this._having,
|
|
110
|
+
unions: this._unions,
|
|
111
|
+
ctes: this._ctes,
|
|
112
|
+
distinct: this._distinct,
|
|
113
|
+
windows: this._windows,
|
|
114
|
+
lock: this._lock,
|
|
115
|
+
lockOptions: this._lockOpts,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
_resolveSoftDelete() {
|
|
119
|
+
if (!this._softDeletes || this._withTrashed)
|
|
120
|
+
return 'with';
|
|
121
|
+
return this._onlyTrashed ? 'only' : 'exclude';
|
|
122
|
+
}
|
|
123
|
+
_pushClause(boolean, column, operator, value) {
|
|
124
|
+
// Arrow column (`meta->prefs->lang`) = a JSON-path predicate, Laravel-style.
|
|
125
|
+
// Detection here covers where/orWhere AND everything composed from them:
|
|
126
|
+
// group callbacks, whereNot, and the whereIn/whereNull/whereBetween sugar.
|
|
127
|
+
if (column.includes('->')) {
|
|
128
|
+
const { column: col, segments } = parseJsonPath(column);
|
|
129
|
+
this._conditions.push({ kind: 'json', boolean, column: col, segments, operator, value });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const clause = { column, operator, value };
|
|
133
|
+
this._conditions.push({ kind: 'clause', boolean, clause });
|
|
134
|
+
}
|
|
135
|
+
// ── where chaining ───────────────────────────────────────
|
|
136
|
+
where(column, operatorOrValue, value) {
|
|
137
|
+
if (value === undefined) {
|
|
138
|
+
this._pushClause('AND', column, '=', operatorOrValue);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
this._pushClause('AND', column, operatorOrValue, value);
|
|
142
|
+
}
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
orWhere(column, operatorOrValue, value) {
|
|
146
|
+
if (value === undefined) {
|
|
147
|
+
this._pushClause('OR', column, '=', operatorOrValue);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
this._pushClause('OR', column, operatorOrValue, value);
|
|
151
|
+
}
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
whereColumn(left, operatorOrRight, right) {
|
|
155
|
+
this._pushColumn('AND', left, operatorOrRight, right);
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
orWhereColumn(left, operatorOrRight, right) {
|
|
159
|
+
this._pushColumn('OR', left, operatorOrRight, right);
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
_pushColumn(boolean, left, operatorOrRight, right) {
|
|
163
|
+
// Two-arg form (`whereColumn('a', 'b')`) means equality; three-arg carries
|
|
164
|
+
// the operator in the middle.
|
|
165
|
+
const operator = (right === undefined ? '=' : operatorOrRight);
|
|
166
|
+
const rightCol = right === undefined ? operatorOrRight : right;
|
|
167
|
+
this._conditions.push({ kind: 'column', boolean, left, operator, right: rightCol });
|
|
168
|
+
}
|
|
169
|
+
// ── date-component predicates (whereDate / whereTime / whereDay / …) ──
|
|
170
|
+
whereDate(column, operatorOrValue, value) {
|
|
171
|
+
this._pushDatePart('AND', 'date', column, operatorOrValue, value);
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
orWhereDate(column, operatorOrValue, value) {
|
|
175
|
+
this._pushDatePart('OR', 'date', column, operatorOrValue, value);
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
whereTime(column, operatorOrValue, value) {
|
|
179
|
+
this._pushDatePart('AND', 'time', column, operatorOrValue, value);
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
orWhereTime(column, operatorOrValue, value) {
|
|
183
|
+
this._pushDatePart('OR', 'time', column, operatorOrValue, value);
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
whereDay(column, operatorOrValue, value) {
|
|
187
|
+
this._pushDatePart('AND', 'day', column, operatorOrValue, value);
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
orWhereDay(column, operatorOrValue, value) {
|
|
191
|
+
this._pushDatePart('OR', 'day', column, operatorOrValue, value);
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
whereMonth(column, operatorOrValue, value) {
|
|
195
|
+
this._pushDatePart('AND', 'month', column, operatorOrValue, value);
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
orWhereMonth(column, operatorOrValue, value) {
|
|
199
|
+
this._pushDatePart('OR', 'month', column, operatorOrValue, value);
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
whereYear(column, operatorOrValue, value) {
|
|
203
|
+
this._pushDatePart('AND', 'year', column, operatorOrValue, value);
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
orWhereYear(column, operatorOrValue, value) {
|
|
207
|
+
this._pushDatePart('OR', 'year', column, operatorOrValue, value);
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
_pushDatePart(boolean, part, column, operatorOrValue, value) {
|
|
211
|
+
// Two-arg form (`whereDate('createdAt', '2026-01-01')`) means equality;
|
|
212
|
+
// three-arg carries the operator in the middle (Laravel semantics).
|
|
213
|
+
const operator = (value === undefined ? '=' : operatorOrValue);
|
|
214
|
+
const rawValue = value === undefined ? operatorOrValue : value;
|
|
215
|
+
this._conditions.push({
|
|
216
|
+
kind: 'date', boolean, part, column,
|
|
217
|
+
operator,
|
|
218
|
+
value: normalizeDatePartValue(part, rawValue),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// ── JSON predicates (whereJsonContains / whereJsonLength) ──
|
|
222
|
+
/** `whereJsonContains('meta->tags', 'php')` — JSON containment at an arrow
|
|
223
|
+
* path (or the whole column when no `->`). `value` may be a scalar or an
|
|
224
|
+
* array (every element contained). pg `@>`, mysql `JSON_CONTAINS`, sqlite
|
|
225
|
+
* emulated via `json_each` EXISTS (scalars only there). */
|
|
226
|
+
whereJsonContains(column, value) {
|
|
227
|
+
this._pushJsonContains('AND', column, value, false);
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
/** OR-rooted {@link whereJsonContains}. */
|
|
231
|
+
orWhereJsonContains(column, value) {
|
|
232
|
+
this._pushJsonContains('OR', column, value, false);
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
/** Negated {@link whereJsonContains} — `NOT (…)` around the containment. */
|
|
236
|
+
whereJsonDoesntContain(column, value) {
|
|
237
|
+
this._pushJsonContains('AND', column, value, true);
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
240
|
+
/** OR-rooted {@link whereJsonDoesntContain}. */
|
|
241
|
+
orWhereJsonDoesntContain(column, value) {
|
|
242
|
+
this._pushJsonContains('OR', column, value, true);
|
|
243
|
+
return this;
|
|
244
|
+
}
|
|
245
|
+
/** `whereJsonLength('meta->tags', '>', 2)` — compare a JSON array's length.
|
|
246
|
+
* Two-arg form (`(column, n)`) is equality. sqlite/pg `json(b)_array_length`,
|
|
247
|
+
* mysql `JSON_LENGTH`. */
|
|
248
|
+
whereJsonLength(column, operatorOrValue, value) {
|
|
249
|
+
this._pushJsonLength('AND', column, operatorOrValue, value);
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
/** OR-rooted {@link whereJsonLength}. */
|
|
253
|
+
orWhereJsonLength(column, operatorOrValue, value) {
|
|
254
|
+
this._pushJsonLength('OR', column, operatorOrValue, value);
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
_pushJsonContains(boolean, column, value, negated) {
|
|
258
|
+
const target = this._jsonTarget(column);
|
|
259
|
+
this._conditions.push({ kind: 'jsonContains', boolean, ...target, value, negated });
|
|
260
|
+
}
|
|
261
|
+
_pushJsonLength(boolean, column, operatorOrValue, value) {
|
|
262
|
+
// Two-arg form (`whereJsonLength('meta->tags', 2)`) means equality;
|
|
263
|
+
// three-arg carries the operator in the middle (Laravel semantics).
|
|
264
|
+
const operator = (value === undefined ? '=' : operatorOrValue);
|
|
265
|
+
const count = value === undefined ? operatorOrValue : value;
|
|
266
|
+
if (!Number.isInteger(count)) {
|
|
267
|
+
throw new Error(`[RudderJS ORM native] whereJsonLength expects an integer length, got ${String(count)}.`);
|
|
268
|
+
}
|
|
269
|
+
const target = this._jsonTarget(column);
|
|
270
|
+
this._conditions.push({ kind: 'jsonLength', boolean, ...target, operator, value: count });
|
|
271
|
+
}
|
|
272
|
+
/** Column-or-arrow-path → `{ column, segments }` ( `[]` segments = whole column). */
|
|
273
|
+
_jsonTarget(column) {
|
|
274
|
+
return column.includes('->') ? parseJsonPath(column) : { column, segments: [] };
|
|
275
|
+
}
|
|
276
|
+
whereGroup(fn) {
|
|
277
|
+
this._addGroup('AND', fn);
|
|
278
|
+
return this;
|
|
279
|
+
}
|
|
280
|
+
orWhereGroup(fn) {
|
|
281
|
+
this._addGroup('OR', fn);
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
/** Negated group — `NOT (…)` around the callback's conditions (Laravel's
|
|
285
|
+
* `whereNot`). An empty callback is a no-op, same as `whereGroup`. */
|
|
286
|
+
whereNot(fn) {
|
|
287
|
+
this._addGroup('AND', fn, true);
|
|
288
|
+
return this;
|
|
289
|
+
}
|
|
290
|
+
/** OR-rooted {@link whereNot} — `… OR NOT (…)`. */
|
|
291
|
+
orWhereNot(fn) {
|
|
292
|
+
this._addGroup('OR', fn, true);
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
295
|
+
_addGroup(boolean, fn, negated = false) {
|
|
296
|
+
const sub = new NativeQueryBuilder(this.executor, this.dialect, this.table, this.primaryKey)
|
|
297
|
+
._markSubBuilder();
|
|
298
|
+
fn(sub);
|
|
299
|
+
if (sub._conditions.length === 0)
|
|
300
|
+
return; // empty group is a no-op
|
|
301
|
+
this._conditions.push(negated
|
|
302
|
+
? { kind: 'group', boolean, children: sub._conditions, negated: true }
|
|
303
|
+
: { kind: 'group', boolean, children: sub._conditions });
|
|
304
|
+
}
|
|
305
|
+
orderBy(column, direction = 'ASC') {
|
|
306
|
+
if (column instanceof Expression) {
|
|
307
|
+
this._orders.push({ kind: 'raw', raw: { sql: String(column.getValue()), bindings: [] } });
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
this._orders.push({ column, direction });
|
|
311
|
+
}
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
// ── raw-SQL escape hatch ─────────────────────────────────
|
|
315
|
+
selectRaw(sql, bindings = []) {
|
|
316
|
+
this._rawSelects.push({ sql, bindings });
|
|
317
|
+
return this;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Add a typed window-function projection:
|
|
321
|
+
* `selectWindow('rowNumber', { as: 'rn', partitionBy: 'userId', orderBy: { column: 'createdAt', direction: 'desc' } })`
|
|
322
|
+
* → `ROW_NUMBER() OVER (PARTITION BY "userId" ORDER BY "createdAt" DESC) AS "rn"`.
|
|
323
|
+
*
|
|
324
|
+
* ADDITIVE — appends to the projection (`SELECT *, … AS "rn"` by default), so
|
|
325
|
+
* rows still hydrate as full models with the alias as an extra attribute;
|
|
326
|
+
* `selectRaw`'s REPLACE semantics don't apply. Functions: `rowNumber` /
|
|
327
|
+
* `rank` / `denseRank` / `percentRank` / `cumeDist` (zero-arg ranking set —
|
|
328
|
+
* identical syntax on SQLite ≥3.25 / Postgres / MySQL 8). For aggregates
|
|
329
|
+
* OVER, lag/lead, or frame clauses, use `selectRaw`. SQL forbids window
|
|
330
|
+
* results in WHERE — filter via a CTE/subquery instead.
|
|
331
|
+
*/
|
|
332
|
+
selectWindow(fn, opts) {
|
|
333
|
+
// Runtime gates (JS callers bypass the TS union): the function name and
|
|
334
|
+
// each direction are SPLICED into SQL, never bound.
|
|
335
|
+
if (!isWindowFunction(fn)) {
|
|
336
|
+
throw new Error(`[RudderJS ORM native] selectWindow(): unknown window function '${String(fn)}'.`);
|
|
337
|
+
}
|
|
338
|
+
if (typeof opts?.as !== 'string' || opts.as.length === 0) {
|
|
339
|
+
throw new Error(`[RudderJS ORM native] selectWindow() requires a non-empty 'as' alias.`);
|
|
340
|
+
}
|
|
341
|
+
const partitionBy = opts.partitionBy === undefined
|
|
342
|
+
? []
|
|
343
|
+
: (typeof opts.partitionBy === 'string' ? [opts.partitionBy] : [...opts.partitionBy]);
|
|
344
|
+
const orderInputs = opts.orderBy === undefined
|
|
345
|
+
? []
|
|
346
|
+
: (Array.isArray(opts.orderBy) ? opts.orderBy : [opts.orderBy]);
|
|
347
|
+
const orderBy = orderInputs.map((o) => {
|
|
348
|
+
const entry = typeof o === 'string' ? { column: o, direction: 'asc' } : { column: o.column, direction: o.direction ?? 'asc' };
|
|
349
|
+
if (entry.direction !== 'asc' && entry.direction !== 'desc') {
|
|
350
|
+
throw new Error(`[RudderJS ORM native] selectWindow(): order direction must be 'asc' or 'desc', got '${String(entry.direction)}'.`);
|
|
351
|
+
}
|
|
352
|
+
return entry;
|
|
353
|
+
});
|
|
354
|
+
this._windows.push({ fn, as: opts.as, partitionBy, orderBy });
|
|
355
|
+
return this;
|
|
356
|
+
}
|
|
357
|
+
whereRaw(sql, bindings = []) {
|
|
358
|
+
this._conditions.push({ kind: 'raw', boolean: 'AND', raw: { sql, bindings } });
|
|
359
|
+
return this;
|
|
360
|
+
}
|
|
361
|
+
orWhereRaw(sql, bindings = []) {
|
|
362
|
+
this._conditions.push({ kind: 'raw', boolean: 'OR', raw: { sql, bindings } });
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
orderByRaw(sql, bindings = []) {
|
|
366
|
+
this._orders.push({ kind: 'raw', raw: { sql, bindings } });
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
369
|
+
// ── projection + joins ───────────────────────────────────
|
|
370
|
+
/** Structured projection — `select('users.id', 'posts.title')`. Each column is
|
|
371
|
+
* identifier-quoted (qualified `table.col` supported) and REPLACES the default
|
|
372
|
+
* `*`. Accumulates with `selectRaw` (structured first, then raw). */
|
|
373
|
+
select(...columns) {
|
|
374
|
+
this._selects.push(...columns);
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
/** `SELECT DISTINCT` — de-duplicate the projected rows. */
|
|
378
|
+
distinct() {
|
|
379
|
+
this._distinct = true;
|
|
380
|
+
return this;
|
|
381
|
+
}
|
|
382
|
+
/** `INNER JOIN`. Simple form `join('posts', 'posts.userId', '=', 'users.id')`
|
|
383
|
+
* (the operator is optional and defaults to `=`); callback form
|
|
384
|
+
* `join('posts', (j) => j.on(...).where(...))` for compound ON clauses. */
|
|
385
|
+
join(table, first, operator, second) {
|
|
386
|
+
return this._addJoin('inner', table, first, operator, second);
|
|
387
|
+
}
|
|
388
|
+
/** `LEFT JOIN` — same call forms as {@link join}. */
|
|
389
|
+
leftJoin(table, first, operator, second) {
|
|
390
|
+
return this._addJoin('left', table, first, operator, second);
|
|
391
|
+
}
|
|
392
|
+
/** `RIGHT JOIN` — same call forms as {@link join}. (SQLite 3.39+; native on pg/mysql.) */
|
|
393
|
+
rightJoin(table, first, operator, second) {
|
|
394
|
+
return this._addJoin('right', table, first, operator, second);
|
|
395
|
+
}
|
|
396
|
+
/** `CROSS JOIN` — Cartesian product, no ON clause. */
|
|
397
|
+
crossJoin(table) {
|
|
398
|
+
this._joins.push({ type: 'cross', table, conditions: [] });
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
_addJoin(type, table, first, operator, second) {
|
|
402
|
+
const conditions = [];
|
|
403
|
+
if (typeof first === 'function') {
|
|
404
|
+
first(new NativeJoinClause(conditions));
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
// Two-arg ON (`join(t, 'a', 'b')`) is equality; three-arg carries the operator.
|
|
408
|
+
const op = (second === undefined ? '=' : operator);
|
|
409
|
+
const right = second === undefined ? operator : second;
|
|
410
|
+
conditions.push({ kind: 'on', boolean: 'AND', left: first, operator: op, right });
|
|
411
|
+
}
|
|
412
|
+
this._joins.push({ type, table, conditions });
|
|
413
|
+
return this;
|
|
414
|
+
}
|
|
415
|
+
// ── grouping ─────────────────────────────────────────────
|
|
416
|
+
/** `GROUP BY col [, …]` — columns identifier-quoted (qualified `table.col` ok). */
|
|
417
|
+
groupBy(...columns) {
|
|
418
|
+
this._groupBy.push(...columns);
|
|
419
|
+
return this;
|
|
420
|
+
}
|
|
421
|
+
/** `HAVING col <op> value` — filter on grouped rows / a SELECT alias. Two-arg
|
|
422
|
+
* form is equality; the value binds. For an aggregate use {@link havingRaw}. */
|
|
423
|
+
having(column, operatorOrValue, value) {
|
|
424
|
+
return this._pushHaving('AND', column, operatorOrValue, value);
|
|
425
|
+
}
|
|
426
|
+
/** OR-rooted {@link having}. */
|
|
427
|
+
orHaving(column, operatorOrValue, value) {
|
|
428
|
+
return this._pushHaving('OR', column, operatorOrValue, value);
|
|
429
|
+
}
|
|
430
|
+
/** `HAVING <raw>` — the portable way to filter on an aggregate, e.g.
|
|
431
|
+
* `havingRaw('COUNT(*) > ?', [3])`. `?` placeholders bind positionally. */
|
|
432
|
+
havingRaw(sql, bindings = []) {
|
|
433
|
+
this._having.push({ kind: 'raw', boolean: 'AND', raw: { sql, bindings } });
|
|
434
|
+
return this;
|
|
435
|
+
}
|
|
436
|
+
/** OR-rooted {@link havingRaw}. */
|
|
437
|
+
orHavingRaw(sql, bindings = []) {
|
|
438
|
+
this._having.push({ kind: 'raw', boolean: 'OR', raw: { sql, bindings } });
|
|
439
|
+
return this;
|
|
440
|
+
}
|
|
441
|
+
_pushHaving(boolean, column, operatorOrValue, value) {
|
|
442
|
+
const operator = (value === undefined ? '=' : operatorOrValue);
|
|
443
|
+
const val = value === undefined ? operatorOrValue : value;
|
|
444
|
+
this._having.push({ kind: 'clause', boolean, clause: { column, operator, value: val } });
|
|
445
|
+
return this;
|
|
446
|
+
}
|
|
447
|
+
// ── unions ───────────────────────────────────────────────
|
|
448
|
+
/** `… UNION …` — append another query as a UNION member (duplicate rows
|
|
449
|
+
* removed). The combined result takes THIS query's ORDER BY / LIMIT / OFFSET;
|
|
450
|
+
* the member's own are ignored. `other` is another native query (`Model.query()`). */
|
|
451
|
+
union(other) {
|
|
452
|
+
return this._addUnion(other, false);
|
|
453
|
+
}
|
|
454
|
+
/** `… UNION ALL …` — like {@link union} but keeps duplicate rows. */
|
|
455
|
+
unionAll(other) {
|
|
456
|
+
return this._addUnion(other, true);
|
|
457
|
+
}
|
|
458
|
+
_addUnion(other, all) {
|
|
459
|
+
// `other` is usually the HydratingQueryBuilder Proxy wrapping a
|
|
460
|
+
// NativeQueryBuilder — unwrap it via the global symbol the proxy answers.
|
|
461
|
+
const target = other[QB_TARGET] ?? other;
|
|
462
|
+
if (!(target instanceof NativeQueryBuilder)) {
|
|
463
|
+
throw new Error('[RudderJS ORM native] union()/unionAll() requires another native query builder — pass a Model.query() of a native-engine model.');
|
|
464
|
+
}
|
|
465
|
+
this._unions.push({ all, state: target._state() });
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
// ── common table expressions ─────────────────────────────
|
|
469
|
+
/**
|
|
470
|
+
* `WITH name AS (…)` — prepend a common table expression the main query can
|
|
471
|
+
* reference (typically via `join('name', …)` — the FROM stays the model's
|
|
472
|
+
* table). `query` is another native query (`Model.query()` chain) or a raw
|
|
473
|
+
* SQL string with `?` placeholders + `opts.bindings`. CTE bindings precede
|
|
474
|
+
* the main query's (SQL text order). Read-side only (`get`/`first`/`find`/
|
|
475
|
+
* `count`/`paginate`); a builder-backed body keeps its own UNION members but
|
|
476
|
+
* drops ORDER BY / LIMIT (same rule as `union()`).
|
|
477
|
+
*/
|
|
478
|
+
withExpression(name, query, opts = {}) {
|
|
479
|
+
return this._addCte(name, query, opts, false);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* `WITH RECURSIVE name [(cols)] AS (…)` — like {@link withExpression} for a
|
|
483
|
+
* self-referencing body. Recursive bodies are usually a raw SQL string (the
|
|
484
|
+
* body references the CTE's own name, which a table-rooted builder can't
|
|
485
|
+
* express): `withRecursiveExpression('tree', 'SELECT … UNION ALL SELECT …
|
|
486
|
+
* FROM t JOIN tree …', { bindings: [rootId], columns: ['id'] })`.
|
|
487
|
+
*/
|
|
488
|
+
withRecursiveExpression(name, query, opts = {}) {
|
|
489
|
+
return this._addCte(name, query, opts, true);
|
|
490
|
+
}
|
|
491
|
+
_addCte(name, query, opts, recursive) {
|
|
492
|
+
const body = this._subqueryBody('withExpression', query, opts.bindings);
|
|
493
|
+
this._ctes.push({ name, recursive, body, ...(opts.columns !== undefined ? { columns: opts.columns } : {}) });
|
|
494
|
+
return this;
|
|
495
|
+
}
|
|
496
|
+
// ── EXISTS subqueries ────────────────────────────────────
|
|
497
|
+
/**
|
|
498
|
+
* `WHERE EXISTS (…)` — an arbitrary EXISTS subquery. `query` is another
|
|
499
|
+
* native query (`Model.query()` chain — correlate to the outer table via
|
|
500
|
+
* qualified `whereColumn('orders.userId', 'users.id')` refs) or a raw SQL
|
|
501
|
+
* string with `?` placeholders + `bindings`. For relation-shaped existence
|
|
502
|
+
* checks prefer `whereHas` — this is the escape hatch for subqueries no
|
|
503
|
+
* declared relation describes.
|
|
504
|
+
*/
|
|
505
|
+
whereExists(query, bindings) {
|
|
506
|
+
return this._addExists('AND', false, query, bindings);
|
|
507
|
+
}
|
|
508
|
+
/** `WHERE NOT EXISTS (…)` — negated {@link whereExists}. */
|
|
509
|
+
whereNotExists(query, bindings) {
|
|
510
|
+
return this._addExists('AND', true, query, bindings);
|
|
511
|
+
}
|
|
512
|
+
/** OR-rooted {@link whereExists}. */
|
|
513
|
+
orWhereExists(query, bindings) {
|
|
514
|
+
return this._addExists('OR', false, query, bindings);
|
|
515
|
+
}
|
|
516
|
+
/** OR-rooted {@link whereNotExists}. */
|
|
517
|
+
orWhereNotExists(query, bindings) {
|
|
518
|
+
return this._addExists('OR', true, query, bindings);
|
|
519
|
+
}
|
|
520
|
+
_addExists(boolean, negated, query, bindings) {
|
|
521
|
+
this._conditions.push({ kind: 'exists', boolean, negated, body: this._subqueryBody('whereExists', query, bindings) });
|
|
522
|
+
return this;
|
|
523
|
+
}
|
|
524
|
+
/** @internal — resolve a builder-or-raw subquery argument to a `SubqueryBody`
|
|
525
|
+
* (shared by `whereExists` and `insertUsing`). Raw strings carry their own
|
|
526
|
+
* `?` bindings; builders are proxy-unwrapped and must be native. */
|
|
527
|
+
_subqueryBody(method, query, bindings) {
|
|
528
|
+
if (typeof query === 'string') {
|
|
529
|
+
return { kind: 'raw', raw: { sql: query, bindings: bindings ?? [] } };
|
|
530
|
+
}
|
|
531
|
+
if (bindings !== undefined) {
|
|
532
|
+
throw new Error(`[RudderJS ORM native] ${method}() bindings are only valid with a raw-SQL body — a query-builder body carries its own.`);
|
|
533
|
+
}
|
|
534
|
+
const target = query[QB_TARGET] ?? query;
|
|
535
|
+
if (!(target instanceof NativeQueryBuilder)) {
|
|
536
|
+
throw new Error(`[RudderJS ORM native] ${method}() requires a native query builder or a raw SQL string body.`);
|
|
537
|
+
}
|
|
538
|
+
return { kind: 'state', state: target._state() };
|
|
539
|
+
}
|
|
540
|
+
limit(n) { this._limitN = n; return this; }
|
|
541
|
+
offset(n) { this._offsetN = n; return this; }
|
|
542
|
+
withTrashed() { this._withTrashed = true; return this; }
|
|
543
|
+
onlyTrashed() { this._onlyTrashed = true; return this; }
|
|
544
|
+
/** Pessimistic `FOR UPDATE` row lock (no-op on SQLite — see {@link Dialect.lockSql}).
|
|
545
|
+
* Only meaningful inside a `transaction()`; the powering primitive for the
|
|
546
|
+
* native database queue's atomic job reservation. `opts.skipLocked` skips
|
|
547
|
+
* already-locked rows (`SKIP LOCKED`), `opts.noWait` errors instead of
|
|
548
|
+
* blocking (`NOWAIT`) — mutually exclusive, both throw. */
|
|
549
|
+
lockForUpdate(opts) {
|
|
550
|
+
this._lock = 'update';
|
|
551
|
+
this._lockOpts = this._validateLockOpts('lockForUpdate', opts);
|
|
552
|
+
return this;
|
|
553
|
+
}
|
|
554
|
+
/** Shared `FOR SHARE` row lock (no-op on SQLite). Same options as
|
|
555
|
+
* {@link lockForUpdate}. */
|
|
556
|
+
sharedLock(opts) {
|
|
557
|
+
this._lock = 'shared';
|
|
558
|
+
this._lockOpts = this._validateLockOpts('sharedLock', opts);
|
|
559
|
+
return this;
|
|
560
|
+
}
|
|
561
|
+
/** `skipLocked` skips conflicting rows; `noWait` errors on them — asking for
|
|
562
|
+
* both is a contradiction, so it throws here at the call site (every
|
|
563
|
+
* dialect), not at compile/execute time. */
|
|
564
|
+
_validateLockOpts(method, opts) {
|
|
565
|
+
if (!opts)
|
|
566
|
+
return null;
|
|
567
|
+
if (opts.skipLocked && opts.noWait) {
|
|
568
|
+
throw new Error(`[RudderJS ORM native] ${method}() options skipLocked and noWait are mutually ` +
|
|
569
|
+
'exclusive — skip conflicting rows OR fail fast on them, not both. Pass at most one.');
|
|
570
|
+
}
|
|
571
|
+
return opts;
|
|
572
|
+
}
|
|
573
|
+
// ── read terminals ───────────────────────────────────────
|
|
574
|
+
/**
|
|
575
|
+
* Coerce `withExists` aggregate aliases from SQLite's integer `1`/`0` to a JS
|
|
576
|
+
* boolean. SQLite has no boolean type, so the `(COUNT(*) > 0)` subselect comes
|
|
577
|
+
* back as a number — the Model contract (and the other adapters over Postgres,
|
|
578
|
+
* which returns a real boolean) expect `true`/`false`. Only `exists` requests
|
|
579
|
+
* are touched; count/sum/min/max/avg stay numeric. No-op when no aggregates.
|
|
580
|
+
*/
|
|
581
|
+
_coerceAggregates(rows) {
|
|
582
|
+
const existsAliases = this._aggregates.filter(a => a.fn === 'exists').map(a => a.alias);
|
|
583
|
+
if (existsAliases.length === 0)
|
|
584
|
+
return rows;
|
|
585
|
+
for (const row of rows) {
|
|
586
|
+
for (const alias of existsAliases) {
|
|
587
|
+
if (alias in row)
|
|
588
|
+
row[alias] = Number(row[alias]) > 0;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return rows;
|
|
592
|
+
}
|
|
593
|
+
async first() {
|
|
594
|
+
this._assertNotSubBuilder();
|
|
595
|
+
const { sql, bindings } = compileSelect(this._state(), this.dialect, { limit: 1 });
|
|
596
|
+
const rows = this._coerceAggregates(await this._readExecutor().execute(sql, bindings));
|
|
597
|
+
return rows[0] ?? null;
|
|
598
|
+
}
|
|
599
|
+
async find(id) {
|
|
600
|
+
this._assertNotSubBuilder();
|
|
601
|
+
const { sql, bindings } = compileSelect(this._state(), this.dialect, { limit: 1, extraConditions: this._pkCondition(id) });
|
|
602
|
+
const rows = this._coerceAggregates(await this._readExecutor().execute(sql, bindings));
|
|
603
|
+
return rows[0] ?? null;
|
|
604
|
+
}
|
|
605
|
+
async get() {
|
|
606
|
+
this._assertNotSubBuilder();
|
|
607
|
+
const { sql, bindings } = compileSelect(this._state(), this.dialect);
|
|
608
|
+
const rows = this._coerceAggregates(await this._readExecutor().execute(sql, bindings));
|
|
609
|
+
return rows;
|
|
610
|
+
}
|
|
611
|
+
async all() {
|
|
612
|
+
return this.get();
|
|
613
|
+
}
|
|
614
|
+
async count() {
|
|
615
|
+
this._assertNotSubBuilder();
|
|
616
|
+
const { sql, bindings } = compileCount(this._state(), this.dialect);
|
|
617
|
+
const rows = await this._readExecutor().execute(sql, bindings);
|
|
618
|
+
return Number(rows[0]?.['count'] ?? 0);
|
|
619
|
+
}
|
|
620
|
+
async paginate(page = 1, perPage = 15) {
|
|
621
|
+
this._assertNotSubBuilder();
|
|
622
|
+
const safePage = page < 1 ? 1 : Math.floor(page);
|
|
623
|
+
const safePerPage = perPage < 1 ? 15 : Math.floor(perPage);
|
|
624
|
+
const total = await this.count();
|
|
625
|
+
const pageState = {
|
|
626
|
+
...this._state(),
|
|
627
|
+
limitN: safePerPage,
|
|
628
|
+
offsetN: (safePage - 1) * safePerPage,
|
|
629
|
+
};
|
|
630
|
+
const { sql, bindings } = compileSelect(pageState, this.dialect);
|
|
631
|
+
const rows = this._coerceAggregates(await this._readExecutor().execute(sql, bindings));
|
|
632
|
+
const lastPage = Math.max(1, Math.ceil(total / safePerPage));
|
|
633
|
+
return {
|
|
634
|
+
data: rows,
|
|
635
|
+
total,
|
|
636
|
+
perPage: safePerPage,
|
|
637
|
+
currentPage: safePage,
|
|
638
|
+
lastPage,
|
|
639
|
+
from: total === 0 ? 0 : (safePage - 1) * safePerPage + 1,
|
|
640
|
+
to: Math.min(safePage * safePerPage, total),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
// ── write path (Phase 2) ─────────────────────────────────
|
|
644
|
+
with(...relations) {
|
|
645
|
+
// Eager loading isn't expressible at THIS level — the adapter receives
|
|
646
|
+
// relation NAMES only, with no join shape to compile from. The Model layer
|
|
647
|
+
// never forwards here: the adapter advertises `eagerLoadStrategy:
|
|
648
|
+
// 'model-layer'`, so direct relations resolve via batched WHERE-IN
|
|
649
|
+
// (direct-eager-load.ts) and polymorphic ones via their own loader. The
|
|
650
|
+
// only remaining callers are `withWhereHas`'s constrained-eager fallback
|
|
651
|
+
// and direct adapter-QB use, where this IS a no-op — warn once per
|
|
652
|
+
// relation in dev so it isn't mistaken for working; production stays silent.
|
|
653
|
+
const isProd = typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'production';
|
|
654
|
+
if (!isProd) {
|
|
655
|
+
for (const rel of relations) {
|
|
656
|
+
if (_warnedWith.has(rel))
|
|
657
|
+
continue;
|
|
658
|
+
_warnedWith.add(rel);
|
|
659
|
+
console.warn(`[RudderJS ORM native] adapter-level with("${rel}") is a no-op — the row is returned ` +
|
|
660
|
+
`without "${rel}" populated. Constrained eager-load (withWhereHas) isn't supported on ` +
|
|
661
|
+
`the native engine: chain .whereHas(...) for the filter plus .with("${rel}") for the ` +
|
|
662
|
+
`(unconstrained) load, or use instance.related("${rel}").`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return this;
|
|
666
|
+
}
|
|
667
|
+
withPivot(..._columns) { return this; }
|
|
668
|
+
/** @internal — the primary-key match used by by-id terminals. */
|
|
669
|
+
_pkCondition(id) {
|
|
670
|
+
return [{ kind: 'clause', boolean: 'AND', clause: { column: this.primaryKey, operator: '=', value: id } }];
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* @internal — state for a by-id write (`update`/`delete`/`restore`/
|
|
674
|
+
* `forceDelete`/`increment`). The accumulated `where()` predicate and
|
|
675
|
+
* soft-delete scoping are dropped — these target a single row by primary key
|
|
676
|
+
* (the PK match is passed as an `extraCondition`). Matches the orm-drizzle
|
|
677
|
+
* adapter, whose by-id writes also ignore chained wheres.
|
|
678
|
+
*/
|
|
679
|
+
_idState() {
|
|
680
|
+
return { ...this._state(), conditions: [], softDelete: 'with', lock: null };
|
|
681
|
+
}
|
|
682
|
+
// ── no-RETURNING write path (MySQL) ──────────────────────
|
|
683
|
+
//
|
|
684
|
+
// SQLite/Postgres read written rows back via `RETURNING *`. MySQL has none, so
|
|
685
|
+
// the write terminals below branch on `dialect.supportsReturning`: they run the
|
|
686
|
+
// bare INSERT/UPDATE/DELETE and read the result from the driver's metadata
|
|
687
|
+
// ({@link AffectingExecutor}) — `insertId` for `create`, `affectedRows` for the
|
|
688
|
+
// bulk terminals — then re-SELECT by primary key for terminals that must return
|
|
689
|
+
// the row. SQLite/Postgres keep their exact existing RETURNING path untouched.
|
|
690
|
+
/** @internal — run a write and read its `insertId` / `affectedRows`. Throws if
|
|
691
|
+
* the active driver can't report write metadata (an internal invariant: every
|
|
692
|
+
* no-RETURNING dialect driver implements `AffectingExecutor`). */
|
|
693
|
+
async _affecting(sql, bindings) {
|
|
694
|
+
const ex = this.executor;
|
|
695
|
+
if (typeof ex.affectingExecute !== 'function') {
|
|
696
|
+
throw new Error('[RudderJS ORM native] The active driver cannot run writes without RETURNING ' +
|
|
697
|
+
'(no affectingExecute). Every non-RETURNING dialect driver must implement AffectingExecutor.');
|
|
698
|
+
}
|
|
699
|
+
return ex.affectingExecute(sql, bindings);
|
|
700
|
+
}
|
|
701
|
+
/** @internal — re-SELECT a row by primary key after a no-RETURNING write. Runs
|
|
702
|
+
* on `this.executor`, so inside a transaction it stays on the txn connection. */
|
|
703
|
+
async _reselect(id) {
|
|
704
|
+
const { sql, bindings } = compileSelect(this._idState(), this.dialect, { limit: 1, extraConditions: this._pkCondition(id) });
|
|
705
|
+
const rows = await this.executor.execute(sql, bindings);
|
|
706
|
+
return rows[0] ?? null;
|
|
707
|
+
}
|
|
708
|
+
async create(data) {
|
|
709
|
+
this._assertNotSubBuilder();
|
|
710
|
+
if (this.dialect.supportsReturning) {
|
|
711
|
+
const { sql, bindings } = compileInsert(this._state(), this.dialect, [data], { returning: true });
|
|
712
|
+
const rows = await this.executor.execute(sql, bindings);
|
|
713
|
+
if (!rows[0])
|
|
714
|
+
throw new Error('[RudderJS ORM native] create() returned no rows.');
|
|
715
|
+
return rows[0];
|
|
716
|
+
}
|
|
717
|
+
// No RETURNING (MySQL): INSERT, then re-SELECT by primary key — same as the
|
|
718
|
+
// `update()` path — so the returned row is the REAL stored row (DB-applied
|
|
719
|
+
// defaults, driver type mapping), byte-consistent with the RETURNING
|
|
720
|
+
// dialects. The PK is the auto-increment `insertId`, or the caller-supplied
|
|
721
|
+
// key (uuid/ulid models stamp it before insert).
|
|
722
|
+
const { sql, bindings } = compileInsert(this._state(), this.dialect, [data], { returning: false });
|
|
723
|
+
const { insertId } = await this._affecting(sql, bindings);
|
|
724
|
+
const suppliedKey = data[this.primaryKey];
|
|
725
|
+
const id = insertId ?? suppliedKey;
|
|
726
|
+
if (id === undefined || id === null) {
|
|
727
|
+
// No way to identify the row (no auto-increment, no supplied key) —
|
|
728
|
+
// synthesize from the input as the best effort.
|
|
729
|
+
return { ...data };
|
|
730
|
+
}
|
|
731
|
+
const row = await this._reselect(id);
|
|
732
|
+
if (!row)
|
|
733
|
+
throw new Error('[RudderJS ORM native] create() could not re-select the inserted row.');
|
|
734
|
+
return row;
|
|
735
|
+
}
|
|
736
|
+
async update(id, data) {
|
|
737
|
+
this._assertNotSubBuilder();
|
|
738
|
+
if (this.dialect.supportsReturning) {
|
|
739
|
+
const { sql, bindings } = compileUpdate(this._idState(), this.dialect, data, { extraConditions: this._pkCondition(id), returning: true });
|
|
740
|
+
const rows = await this.executor.execute(sql, bindings);
|
|
741
|
+
if (!rows[0])
|
|
742
|
+
throw new Error('[RudderJS ORM native] update() returned no rows.');
|
|
743
|
+
return rows[0];
|
|
744
|
+
}
|
|
745
|
+
// No RETURNING (MySQL): UPDATE, then re-SELECT the row by primary key.
|
|
746
|
+
const { sql, bindings } = compileUpdate(this._idState(), this.dialect, data, { extraConditions: this._pkCondition(id), returning: false });
|
|
747
|
+
await this.executor.execute(sql, bindings);
|
|
748
|
+
const row = await this._reselect(id);
|
|
749
|
+
if (!row)
|
|
750
|
+
throw new Error('[RudderJS ORM native] update() target row not found.');
|
|
751
|
+
return row;
|
|
752
|
+
}
|
|
753
|
+
async updateAll(data) {
|
|
754
|
+
this._assertNotSubBuilder();
|
|
755
|
+
if (this.dialect.supportsReturning) {
|
|
756
|
+
const { sql, bindings } = compileUpdate(this._state(), this.dialect, data, { returning: true });
|
|
757
|
+
const rows = await this.executor.execute(sql, bindings);
|
|
758
|
+
return rows.length;
|
|
759
|
+
}
|
|
760
|
+
const { sql, bindings } = compileUpdate(this._state(), this.dialect, data, { returning: false });
|
|
761
|
+
const { affectedRows } = await this._affecting(sql, bindings);
|
|
762
|
+
return affectedRows;
|
|
763
|
+
}
|
|
764
|
+
async delete(id) {
|
|
765
|
+
this._assertNotSubBuilder();
|
|
766
|
+
if (this._softDeletes) {
|
|
767
|
+
// Soft delete: stamp deletedAt instead of removing the row. ISO string —
|
|
768
|
+
// the read path filters on `deletedAt IS [NOT] NULL` regardless of format,
|
|
769
|
+
// and better-sqlite3 can't bind a Date directly.
|
|
770
|
+
const { sql, bindings } = compileUpdate(this._idState(), this.dialect, { deletedAt: new Date().toISOString() }, { extraConditions: this._pkCondition(id) });
|
|
771
|
+
await this.executor.execute(sql, bindings);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const { sql, bindings } = compileDelete(this._idState(), this.dialect, { extraConditions: this._pkCondition(id) });
|
|
775
|
+
await this.executor.execute(sql, bindings);
|
|
776
|
+
}
|
|
777
|
+
async deleteAll() {
|
|
778
|
+
this._assertNotSubBuilder();
|
|
779
|
+
// Uses the full current predicate INCLUDING soft-delete scope (call
|
|
780
|
+
// withTrashed() first to bulk-delete trashed rows too) — matches orm-drizzle.
|
|
781
|
+
if (this.dialect.supportsReturning) {
|
|
782
|
+
const { sql, bindings } = compileDelete(this._state(), this.dialect, { returning: true });
|
|
783
|
+
const rows = await this.executor.execute(sql, bindings);
|
|
784
|
+
return rows.length;
|
|
785
|
+
}
|
|
786
|
+
const { sql, bindings } = compileDelete(this._state(), this.dialect, { returning: false });
|
|
787
|
+
const { affectedRows } = await this._affecting(sql, bindings);
|
|
788
|
+
return affectedRows;
|
|
789
|
+
}
|
|
790
|
+
async insertMany(rows) {
|
|
791
|
+
this._assertNotSubBuilder();
|
|
792
|
+
if (rows.length === 0)
|
|
793
|
+
return;
|
|
794
|
+
const { sql, bindings } = compileInsert(this._state(), this.dialect, rows, { returning: false });
|
|
795
|
+
await this.executor.execute(sql, bindings);
|
|
796
|
+
}
|
|
797
|
+
async upsert(rows, uniqueBy, update) {
|
|
798
|
+
this._assertNotSubBuilder();
|
|
799
|
+
if (rows.length === 0)
|
|
800
|
+
return 0;
|
|
801
|
+
const upsert = { uniqueBy, update };
|
|
802
|
+
// SQLite/Postgres: one statement with RETURNING — affected = rows returned.
|
|
803
|
+
if (this.dialect.supportsReturning) {
|
|
804
|
+
const { sql, bindings } = compileInsert(this._state(), this.dialect, rows, { returning: true, upsert });
|
|
805
|
+
const out = await this.executor.execute(sql, bindings);
|
|
806
|
+
return out.length;
|
|
807
|
+
}
|
|
808
|
+
// MySQL: no RETURNING — read affectedRows off the driver metadata. (MySQL
|
|
809
|
+
// counts 1 per inserted row and 2 per row updated via ON DUPLICATE KEY, so
|
|
810
|
+
// this is rows-touched, not rows-distinct — a documented MySQL quirk.)
|
|
811
|
+
const { sql, bindings } = compileInsert(this._state(), this.dialect, rows, { returning: false, upsert });
|
|
812
|
+
const { affectedRows } = await this._affecting(sql, bindings);
|
|
813
|
+
return affectedRows;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* `INSERT INTO table (cols) SELECT …` — insert rows produced by a subquery
|
|
817
|
+
* (another native query or a raw SQL string + bindings; same body forms as
|
|
818
|
+
* {@link whereExists}). The column list is required and maps the subquery's
|
|
819
|
+
* projection positionally. Returns the inserted-row count. Bulk data-plane
|
|
820
|
+
* write: no observer events, no fillable/guarded filtering, no key
|
|
821
|
+
* generation — like `insertMany`/`upsert`.
|
|
822
|
+
*/
|
|
823
|
+
async insertUsing(columns, query, bindings) {
|
|
824
|
+
this._assertNotSubBuilder();
|
|
825
|
+
const body = this._subqueryBody('insertUsing', query, bindings);
|
|
826
|
+
// SQLite/Postgres: RETURNING * — inserted = rows returned.
|
|
827
|
+
if (this.dialect.supportsReturning) {
|
|
828
|
+
const { sql, bindings: binds } = compileInsertUsing(this._state(), this.dialect, columns, body, { returning: true });
|
|
829
|
+
const out = await this.executor.execute(sql, binds);
|
|
830
|
+
return out.length;
|
|
831
|
+
}
|
|
832
|
+
// MySQL: no RETURNING — read affectedRows off the driver metadata.
|
|
833
|
+
const { sql, bindings: binds } = compileInsertUsing(this._state(), this.dialect, columns, body, { returning: false });
|
|
834
|
+
const { affectedRows } = await this._affecting(sql, binds);
|
|
835
|
+
return affectedRows;
|
|
836
|
+
}
|
|
837
|
+
async restore(id) {
|
|
838
|
+
this._assertNotSubBuilder();
|
|
839
|
+
if (this.dialect.supportsReturning) {
|
|
840
|
+
const { sql, bindings } = compileUpdate(this._idState(), this.dialect, { deletedAt: null }, { extraConditions: this._pkCondition(id), returning: true });
|
|
841
|
+
const rows = await this.executor.execute(sql, bindings);
|
|
842
|
+
return rows[0];
|
|
843
|
+
}
|
|
844
|
+
// No RETURNING (MySQL): clear deletedAt, then re-SELECT the restored row.
|
|
845
|
+
const { sql, bindings } = compileUpdate(this._idState(), this.dialect, { deletedAt: null }, { extraConditions: this._pkCondition(id), returning: false });
|
|
846
|
+
await this.executor.execute(sql, bindings);
|
|
847
|
+
return (await this._reselect(id));
|
|
848
|
+
}
|
|
849
|
+
async forceDelete(id) {
|
|
850
|
+
this._assertNotSubBuilder();
|
|
851
|
+
const { sql, bindings } = compileDelete(this._idState(), this.dialect, { extraConditions: this._pkCondition(id) });
|
|
852
|
+
await this.executor.execute(sql, bindings);
|
|
853
|
+
}
|
|
854
|
+
increment(id, column, amount = 1, extra = {}) {
|
|
855
|
+
return this._delta(id, column, amount, extra);
|
|
856
|
+
}
|
|
857
|
+
decrement(id, column, amount = 1, extra = {}) {
|
|
858
|
+
return this._delta(id, column, -amount, extra);
|
|
859
|
+
}
|
|
860
|
+
/** @internal — shared increment/decrement path. `delta` is signed. Atomic
|
|
861
|
+
* `SET col = col + ?` at the DB; NO observer events fire (pure data-plane,
|
|
862
|
+
* matching the ORM's documented increment/decrement semantics). */
|
|
863
|
+
async _delta(id, column, delta, extra) {
|
|
864
|
+
this._assertNotSubBuilder();
|
|
865
|
+
if (this.dialect.supportsReturning) {
|
|
866
|
+
const { sql, bindings } = compileIncrement(this._idState(), this.dialect, column, delta, extra, { extraConditions: this._pkCondition(id), returning: true });
|
|
867
|
+
const rows = await this.executor.execute(sql, bindings);
|
|
868
|
+
if (!rows[0])
|
|
869
|
+
throw new Error('[RudderJS ORM native] increment/decrement target row not found.');
|
|
870
|
+
return rows[0];
|
|
871
|
+
}
|
|
872
|
+
// No RETURNING (MySQL): atomic UPDATE, then re-SELECT the updated row.
|
|
873
|
+
const { sql, bindings } = compileIncrement(this._idState(), this.dialect, column, delta, extra, { extraConditions: this._pkCondition(id), returning: false });
|
|
874
|
+
await this.executor.execute(sql, bindings);
|
|
875
|
+
const row = await this._reselect(id);
|
|
876
|
+
if (!row)
|
|
877
|
+
throw new Error('[RudderJS ORM native] increment/decrement target row not found.');
|
|
878
|
+
return row;
|
|
879
|
+
}
|
|
880
|
+
// ── relations + aggregates (Phase 3) ─────────────────────
|
|
881
|
+
/**
|
|
882
|
+
* Accumulate a relation-existence predicate (`whereHas` / `whereDoesntHave`).
|
|
883
|
+
* Compiled to a correlated `EXISTS` / `NOT EXISTS` subquery AND-merged into
|
|
884
|
+
* the WHERE at terminal time. Composes with flat wheres, soft deletes, and
|
|
885
|
+
* other relation predicates.
|
|
886
|
+
*/
|
|
887
|
+
whereRelationExists(predicate) {
|
|
888
|
+
this._relationExists.push(predicate);
|
|
889
|
+
return this;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Accumulate aggregate eager-load requests (`withCount`/`withSum`/etc.). Each
|
|
893
|
+
* becomes a correlated `(subselect) AS alias` column in the SELECT list, so
|
|
894
|
+
* the value is stamped on every returned row under `alias` (the Model
|
|
895
|
+
* hydration layer copies it onto the instance).
|
|
896
|
+
*/
|
|
897
|
+
withAggregate(requests) {
|
|
898
|
+
this._aggregates.push(...requests);
|
|
899
|
+
return this;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Single-scalar aggregate terminal — `SELECT fn(col) FROM table WHERE …`.
|
|
903
|
+
* Powers `instance.loadSum`/`loadMin`/etc. Returns `0` for count, `0` for sum
|
|
904
|
+
* on an empty set, `null` for min/max/avg on an empty set, and a boolean for
|
|
905
|
+
* `exists`. `column` is required for sum/min/max/avg.
|
|
906
|
+
*/
|
|
907
|
+
async _aggregate(fn, column) {
|
|
908
|
+
this._assertNotSubBuilder();
|
|
909
|
+
const { sql, bindings } = compileScalarAggregate(this._state(), this.dialect, fn, column);
|
|
910
|
+
const rows = await this._readExecutor().execute(sql, bindings);
|
|
911
|
+
const raw = rows[0]?.['value'];
|
|
912
|
+
if (fn === 'count')
|
|
913
|
+
return Number(raw ?? 0);
|
|
914
|
+
if (fn === 'exists')
|
|
915
|
+
return Number(raw ?? 0) > 0;
|
|
916
|
+
if (raw === null || raw === undefined)
|
|
917
|
+
return fn === 'sum' ? 0 : null;
|
|
918
|
+
return Number(raw);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* The sub-builder passed to the callback form of `join(...)`. Pushes
|
|
923
|
+
* {@link JoinCondition}s into the array the `NativeQueryBuilder` holds for that
|
|
924
|
+
* join — `on`/`orOn` are column-vs-column (nothing binds), `where`/`orWhere`
|
|
925
|
+
* are column-vs-value (the value binds at compile time).
|
|
926
|
+
*/
|
|
927
|
+
export class NativeJoinClause {
|
|
928
|
+
conditions;
|
|
929
|
+
constructor(conditions) {
|
|
930
|
+
this.conditions = conditions;
|
|
931
|
+
}
|
|
932
|
+
on(left, operatorOrRight, right) {
|
|
933
|
+
return this._pushOn('AND', left, operatorOrRight, right);
|
|
934
|
+
}
|
|
935
|
+
orOn(left, operatorOrRight, right) {
|
|
936
|
+
return this._pushOn('OR', left, operatorOrRight, right);
|
|
937
|
+
}
|
|
938
|
+
where(column, operatorOrValue, value) {
|
|
939
|
+
return this._pushWhere('AND', column, operatorOrValue, value);
|
|
940
|
+
}
|
|
941
|
+
orWhere(column, operatorOrValue, value) {
|
|
942
|
+
return this._pushWhere('OR', column, operatorOrValue, value);
|
|
943
|
+
}
|
|
944
|
+
_pushOn(boolean, left, operatorOrRight, right) {
|
|
945
|
+
const operator = (right === undefined ? '=' : operatorOrRight);
|
|
946
|
+
const rightCol = right === undefined ? operatorOrRight : right;
|
|
947
|
+
this.conditions.push({ kind: 'on', boolean, left, operator, right: rightCol });
|
|
948
|
+
return this;
|
|
949
|
+
}
|
|
950
|
+
_pushWhere(boolean, column, operatorOrValue, value) {
|
|
951
|
+
const operator = (value === undefined ? '=' : operatorOrValue);
|
|
952
|
+
const val = value === undefined ? operatorOrValue : value;
|
|
953
|
+
this.conditions.push({ kind: 'where', boolean, clause: { column, operator, value: val } });
|
|
954
|
+
return this;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Normalize a date-helper comparison value for binding (Laravel accepts a
|
|
959
|
+
* `DateTimeInterface` everywhere; the JS analogue is `Date`):
|
|
960
|
+
*
|
|
961
|
+
* - `Date` → the matching component, **UTC-based** (`'YYYY-MM-DD'` for `date`,
|
|
962
|
+
* `'HH:MM:SS'` for `time`, integers for `day`/`month`/`year`) — consistent
|
|
963
|
+
* with the ORM storing ISO-8601/UTC timestamps.
|
|
964
|
+
* - numeric strings on `day`/`month`/`year` → `Number`, so a `'05'` compares
|
|
965
|
+
* against the dialect's INTEGER extraction (SQLite never equates TEXT with
|
|
966
|
+
* INTEGER; pg/mysql would have to coerce).
|
|
967
|
+
* - everything else passes through and binds as-is.
|
|
968
|
+
*/
|
|
969
|
+
function normalizeDatePartValue(part, value) {
|
|
970
|
+
if (value instanceof Date) {
|
|
971
|
+
switch (part) {
|
|
972
|
+
case 'date': return value.toISOString().slice(0, 10);
|
|
973
|
+
case 'time': return value.toISOString().slice(11, 19);
|
|
974
|
+
case 'day': return value.getUTCDate();
|
|
975
|
+
case 'month': return value.getUTCMonth() + 1;
|
|
976
|
+
case 'year': return value.getUTCFullYear();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if ((part === 'day' || part === 'month' || part === 'year') && typeof value === 'string' && /^\d+$/.test(value)) {
|
|
980
|
+
return Number(value);
|
|
981
|
+
}
|
|
982
|
+
return value;
|
|
983
|
+
}
|
|
984
|
+
//# sourceMappingURL=query-builder.js.map
|