@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.
Files changed (126) hide show
  1. package/README.md +70 -0
  2. package/dist/db.d.ts +21 -3
  3. package/dist/db.d.ts.map +1 -1
  4. package/dist/db.js +27 -5
  5. package/dist/db.js.map +1 -1
  6. package/dist/index.d.ts +14 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +23 -4
  9. package/dist/index.js.map +1 -1
  10. package/dist/native/adapter.d.ts +202 -0
  11. package/dist/native/adapter.d.ts.map +1 -0
  12. package/dist/native/adapter.js +440 -0
  13. package/dist/native/adapter.js.map +1 -0
  14. package/dist/native/compiler.d.ts +371 -0
  15. package/dist/native/compiler.d.ts.map +1 -0
  16. package/dist/native/compiler.js +978 -0
  17. package/dist/native/compiler.js.map +1 -0
  18. package/dist/native/dialect-mysql.d.ts +26 -0
  19. package/dist/native/dialect-mysql.d.ts.map +1 -0
  20. package/dist/native/dialect-mysql.js +188 -0
  21. package/dist/native/dialect-mysql.js.map +1 -0
  22. package/dist/native/dialect-pg.d.ts +26 -0
  23. package/dist/native/dialect-pg.d.ts.map +1 -0
  24. package/dist/native/dialect-pg.js +192 -0
  25. package/dist/native/dialect-pg.js.map +1 -0
  26. package/dist/native/dialect.d.ts +255 -0
  27. package/dist/native/dialect.d.ts.map +1 -0
  28. package/dist/native/dialect.js +237 -0
  29. package/dist/native/dialect.js.map +1 -0
  30. package/dist/native/driver.d.ts +37 -0
  31. package/dist/native/driver.d.ts.map +1 -0
  32. package/dist/native/driver.js +19 -0
  33. package/dist/native/driver.js.map +1 -0
  34. package/dist/native/drivers/better-sqlite3.d.ts +56 -0
  35. package/dist/native/drivers/better-sqlite3.d.ts.map +1 -0
  36. package/dist/native/drivers/better-sqlite3.js +171 -0
  37. package/dist/native/drivers/better-sqlite3.js.map +1 -0
  38. package/dist/native/drivers/mysql.d.ts +30 -0
  39. package/dist/native/drivers/mysql.d.ts.map +1 -0
  40. package/dist/native/drivers/mysql.js +176 -0
  41. package/dist/native/drivers/mysql.js.map +1 -0
  42. package/dist/native/drivers/postgres.d.ts +57 -0
  43. package/dist/native/drivers/postgres.d.ts.map +1 -0
  44. package/dist/native/drivers/postgres.js +155 -0
  45. package/dist/native/drivers/postgres.js.map +1 -0
  46. package/dist/native/errors.d.ts +43 -0
  47. package/dist/native/errors.d.ts.map +1 -0
  48. package/dist/native/errors.js +64 -0
  49. package/dist/native/errors.js.map +1 -0
  50. package/dist/native/index.d.ts +27 -0
  51. package/dist/native/index.d.ts.map +1 -0
  52. package/dist/native/index.js +55 -0
  53. package/dist/native/index.js.map +1 -0
  54. package/dist/native/isolation.d.ts +14 -0
  55. package/dist/native/isolation.d.ts.map +1 -0
  56. package/dist/native/isolation.js +37 -0
  57. package/dist/native/isolation.js.map +1 -0
  58. package/dist/native/query-builder.d.ts +303 -0
  59. package/dist/native/query-builder.d.ts.map +1 -0
  60. package/dist/native/query-builder.js +984 -0
  61. package/dist/native/query-builder.js.map +1 -0
  62. package/dist/native/replica-picker.d.ts +22 -0
  63. package/dist/native/replica-picker.d.ts.map +1 -0
  64. package/dist/native/replica-picker.js +65 -0
  65. package/dist/native/replica-picker.js.map +1 -0
  66. package/dist/native/schema/alter-blueprint.d.ts +37 -0
  67. package/dist/native/schema/alter-blueprint.d.ts.map +1 -0
  68. package/dist/native/schema/alter-blueprint.js +56 -0
  69. package/dist/native/schema/alter-blueprint.js.map +1 -0
  70. package/dist/native/schema/blueprint.d.ts +151 -0
  71. package/dist/native/schema/blueprint.d.ts.map +1 -0
  72. package/dist/native/schema/blueprint.js +286 -0
  73. package/dist/native/schema/blueprint.js.map +1 -0
  74. package/dist/native/schema/column.d.ts +168 -0
  75. package/dist/native/schema/column.d.ts.map +1 -0
  76. package/dist/native/schema/column.js +190 -0
  77. package/dist/native/schema/column.js.map +1 -0
  78. package/dist/native/schema/ddl-compiler.d.ts +34 -0
  79. package/dist/native/schema/ddl-compiler.d.ts.map +1 -0
  80. package/dist/native/schema/ddl-compiler.js +352 -0
  81. package/dist/native/schema/ddl-compiler.js.map +1 -0
  82. package/dist/native/schema/inspect.d.ts +67 -0
  83. package/dist/native/schema/inspect.d.ts.map +1 -0
  84. package/dist/native/schema/inspect.js +312 -0
  85. package/dist/native/schema/inspect.js.map +1 -0
  86. package/dist/native/schema/introspect.d.ts +34 -0
  87. package/dist/native/schema/introspect.d.ts.map +1 -0
  88. package/dist/native/schema/introspect.js +101 -0
  89. package/dist/native/schema/introspect.js.map +1 -0
  90. package/dist/native/schema/migration.d.ts +8 -0
  91. package/dist/native/schema/migration.d.ts.map +1 -0
  92. package/dist/native/schema/migration.js +19 -0
  93. package/dist/native/schema/migration.js.map +1 -0
  94. package/dist/native/schema/migrator.d.ts +144 -0
  95. package/dist/native/schema/migrator.d.ts.map +1 -0
  96. package/dist/native/schema/migrator.js +239 -0
  97. package/dist/native/schema/migrator.js.map +1 -0
  98. package/dist/native/schema/rebuild.d.ts +11 -0
  99. package/dist/native/schema/rebuild.d.ts.map +1 -0
  100. package/dist/native/schema/rebuild.js +92 -0
  101. package/dist/native/schema/rebuild.js.map +1 -0
  102. package/dist/native/schema/schema-builder.d.ts +46 -0
  103. package/dist/native/schema/schema-builder.d.ts.map +1 -0
  104. package/dist/native/schema/schema-builder.js +153 -0
  105. package/dist/native/schema/schema-builder.js.map +1 -0
  106. package/dist/native/schema/schema-facade.d.ts +63 -0
  107. package/dist/native/schema/schema-facade.d.ts.map +1 -0
  108. package/dist/native/schema/schema-facade.js +124 -0
  109. package/dist/native/schema/schema-facade.js.map +1 -0
  110. package/dist/native/schema/schema-types.d.ts +27 -0
  111. package/dist/native/schema/schema-types.d.ts.map +1 -0
  112. package/dist/native/schema/schema-types.js +52 -0
  113. package/dist/native/schema/schema-types.js.map +1 -0
  114. package/dist/native/schema/types-generator.d.ts +73 -0
  115. package/dist/native/schema/types-generator.d.ts.map +1 -0
  116. package/dist/native/schema/types-generator.js +181 -0
  117. package/dist/native/schema/types-generator.js.map +1 -0
  118. package/dist/registry-bridge.d.ts +24 -4
  119. package/dist/registry-bridge.d.ts.map +1 -1
  120. package/dist/registry-bridge.js +20 -0
  121. package/dist/registry-bridge.js.map +1 -1
  122. package/dist/sticky.d.ts +22 -0
  123. package/dist/sticky.d.ts.map +1 -0
  124. package/dist/sticky.js +61 -0
  125. package/dist/sticky.js.map +1 -0
  126. 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