@bunyad/orm 0.1.0-alpha.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.
@@ -0,0 +1,3170 @@
1
+ import { DatabaseManager as Db, CursorPaginator, LengthAwarePaginator, Paginator, dateTimeForStorage, hasQueryListeners, resolvePaginatorPage, wrapSqlName, } from "@bunyad/database";
2
+ import { Collection } from "@bunyad/common";
3
+ import { OrmCollection } from "./orm-collection.js";
4
+ import { bootIfNotBooted, fireModelEvent, hasModelEventListeners, } from "./model-events.js";
5
+ import { getGlobalScopes, hasGlobalScopes, } from "./scopes.js";
6
+ import { resolveModelBuilder } from "./decorators.js";
7
+ import { deletedAtColumn, normalizeWithRelations, nowForConnection, parseRelationAlias, singular, usesSoftDeletes, } from "./model-helpers.js";
8
+ import { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, MorphMany, MorphOne, MorphTo, MorphToMany, morphTypeFor, resolveRelation, } from "./relations.js";
9
+ import { eagerLoadAggregates, eagerLoadModels } from "./eager.js";
10
+ import {} from "./casts.js";
11
+ import { Model, ModelNotFoundException, attributesNeedModelCasts, filterFillable, resolveConnection, } from "./model.js";
12
+ /**
13
+ * Local-scope Proxy exposes this so callers can reach the real `ModelQuery`
14
+ * (private fields are not accessible through a Proxy).
15
+ */
16
+ export const MODEL_QUERY_TARGET = Symbol.for("bunyad.orm.ModelQuery.target");
17
+ /** Unwrap a local-scope Proxy to the underlying `ModelQuery` instance. */
18
+ export function unwrapModelQuery(query) {
19
+ const target = query[MODEL_QUERY_TARGET];
20
+ return target ?? query;
21
+ }
22
+ /** Shared empty array — copy-on-write before mutate (ModelQuery ctor hot path). */
23
+ const MQ_EMPTY = Object.freeze([]);
24
+ const MQ_EMPTY_OBJ = Object.freeze({});
25
+ /** Copy-on-write: return a mutable array (clone shared MQ_EMPTY). */
26
+ function mqMut(arr) {
27
+ return arr === MQ_EMPTY ? [] : arr;
28
+ }
29
+ function mqMutObj(obj) {
30
+ return obj === MQ_EMPTY_OBJ ? {} : obj;
31
+ }
32
+ /** Compiled simple-SELECT SQL keyed by constraint fingerprint (per model). */
33
+ const simpleSelectSqlCache = new WeakMap();
34
+ /** Compiled parent+EXISTS SELECT SQL (whereHas fast path). */
35
+ const hasExistsSelectSqlCache = new WeakMap();
36
+ const SIMPLE_WHERE_OPS = new Set([
37
+ "=",
38
+ "!=",
39
+ "<>",
40
+ "<",
41
+ "<=",
42
+ ">",
43
+ ">=",
44
+ "like",
45
+ "LIKE",
46
+ "not like",
47
+ "NOT LIKE",
48
+ "__null__",
49
+ "__notnull__",
50
+ ]);
51
+ function wheresAreSimple(wheres) {
52
+ for (const where of wheres) {
53
+ if (!SIMPLE_WHERE_OPS.has(where.op))
54
+ return false;
55
+ }
56
+ return true;
57
+ }
58
+ /** `id`, `name`, `categories.id`, or `*`. Expressions stay on the full builder. */
59
+ function plainColumnList(columns) {
60
+ if (columns.length === 0)
61
+ return "*";
62
+ for (const column of columns) {
63
+ if (column !== "*" &&
64
+ !/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(column)) {
65
+ return null;
66
+ }
67
+ }
68
+ return columns.join(", ");
69
+ }
70
+ export class ModelQuery {
71
+ model;
72
+ #wheres = MQ_EMPTY;
73
+ #whereIns = MQ_EMPTY;
74
+ #whereNulls = MQ_EMPTY;
75
+ #whereBetweens = MQ_EMPTY;
76
+ #whereColumns = MQ_EMPTY;
77
+ #whereRaws = MQ_EMPTY;
78
+ #whereLikes = MQ_EMPTY;
79
+ #dateWheres = MQ_EMPTY;
80
+ #orders = MQ_EMPTY;
81
+ #selectColumns = null;
82
+ #addSelectColumns = MQ_EMPTY;
83
+ #selectRaws = MQ_EMPTY;
84
+ #distinct = false;
85
+ #limitValue;
86
+ #offsetValue;
87
+ #groupByColumns = MQ_EMPTY;
88
+ #havings = MQ_EMPTY;
89
+ #havingRaws = MQ_EMPTY;
90
+ #lock = false;
91
+ #hasConstraints = MQ_EMPTY;
92
+ #withCounts = MQ_EMPTY;
93
+ #withAggregates = MQ_EMPTY;
94
+ #morphHasConstraints = MQ_EMPTY;
95
+ #builderExtras = MQ_EMPTY;
96
+ #nestedGroups = MQ_EMPTY;
97
+ #joins = MQ_EMPTY;
98
+ #withTrashed;
99
+ #onlyTrashed;
100
+ /**
101
+ * True when this query is only applied inside a WHERE nested group
102
+ * (`where(callback)`). BelongsTo whereHas must not expand to JOIN.
103
+ */
104
+ #nestedWhereGroup;
105
+ /** Pending attrs from `withAttributes` — merged into create + optional wheres. */
106
+ #pendingAttributes = MQ_EMPTY_OBJ;
107
+ #eagerLoad;
108
+ #casts = MQ_EMPTY_OBJ;
109
+ /** True once `withCasts` adds any query-time cast (avoid per-row key scan). */
110
+ #hasQueryCasts = false;
111
+ /**
112
+ * False once a constraint needs the full QueryBuilder
113
+ * (joins, nested wheres, raws, has/whereRelation, …).
114
+ */
115
+ #simple = true;
116
+ /** Cached `retrieved` listener presence (hydrate hot path). */
117
+ #hasRetrievedListeners = false;
118
+ #withoutGlobalScopes;
119
+ #connection;
120
+ /** When true, `get()` returns plain row objects instead of models. */
121
+ #asRows = false;
122
+ constructor(model, options = {}) {
123
+ this.model = model;
124
+ bootIfNotBooted(model);
125
+ this.#hasRetrievedListeners = hasModelEventListeners(model, "retrieved");
126
+ this.#withTrashed = options.withTrashed ?? false;
127
+ this.#onlyTrashed = options.onlyTrashed ?? false;
128
+ this.#nestedWhereGroup = options.nestedWhereGroup ?? false;
129
+ this.#eagerLoad = options.eagerLoad ?? MQ_EMPTY;
130
+ this.#connection = options.connection;
131
+ if (options.withoutGlobalScopes === true) {
132
+ this.#withoutGlobalScopes = true;
133
+ }
134
+ else if (Array.isArray(options.withoutGlobalScopes)) {
135
+ this.#withoutGlobalScopes = new Set(options.withoutGlobalScopes);
136
+ }
137
+ }
138
+ /** Query builder bound to this query's connection (or the model default). */
139
+ #baseTable() {
140
+ if (this.#connection !== undefined) {
141
+ return new Db(resolveConnection(this.#connection)).table(this.model.table);
142
+ }
143
+ return this.model.db().table(this.model.table);
144
+ }
145
+ /** `get()` returns plain row objects. Models are not built. */
146
+ rows() {
147
+ this.#asRows = true;
148
+ return this;
149
+ }
150
+ /** Laravel `withoutGlobalScopes`. */
151
+ withoutGlobalScopes(names) {
152
+ if (!names)
153
+ this.#withoutGlobalScopes = true;
154
+ else {
155
+ const set = this.#withoutGlobalScopes === true || this.#withoutGlobalScopes == null
156
+ ? new Set()
157
+ : this.#withoutGlobalScopes;
158
+ for (const name of names)
159
+ set.add(name);
160
+ this.#withoutGlobalScopes = set;
161
+ }
162
+ return this;
163
+ }
164
+ /** Laravel `withoutGlobalScope`. */
165
+ withoutGlobalScope(name) {
166
+ return this.withoutGlobalScopes([name]);
167
+ }
168
+ /** Laravel `withTrashed()`. */
169
+ withTrashed() {
170
+ this.#withTrashed = true;
171
+ this.#onlyTrashed = false;
172
+ return this;
173
+ }
174
+ /** Laravel `onlyTrashed()`. */
175
+ onlyTrashed() {
176
+ this.#onlyTrashed = true;
177
+ this.#withTrashed = false;
178
+ return this;
179
+ }
180
+ /** Laravel `withoutTrashed()`. */
181
+ withoutTrashed() {
182
+ this.#withTrashed = false;
183
+ this.#onlyTrashed = false;
184
+ return this;
185
+ }
186
+ /**
187
+ * Laravel `Builder::withAttributes` — pending attrs for `create()`, and
188
+ * matching `where` conditions when `asConditions` is true (default).
189
+ */
190
+ withAttributes(attributes, valueOrAsConditions, asConditions = true) {
191
+ let attrs;
192
+ let applyAsConditions = asConditions;
193
+ if (typeof attributes === "string") {
194
+ attrs = { [attributes]: valueOrAsConditions };
195
+ }
196
+ else {
197
+ attrs = { ...attributes };
198
+ // `withAttributes({ hidden: 1 }, false)` — second arg is asConditions
199
+ if (typeof valueOrAsConditions === "boolean") {
200
+ applyAsConditions = valueOrAsConditions;
201
+ }
202
+ }
203
+ this.#pendingAttributes = mqMutObj(this.#pendingAttributes);
204
+ Object.assign(this.#pendingAttributes, attrs);
205
+ if (applyAsConditions) {
206
+ for (const [column, v] of Object.entries(attrs)) {
207
+ this.where(column, v);
208
+ }
209
+ }
210
+ return this;
211
+ }
212
+ /** Laravel `withCasts` — merge query-time casts for hydration. */
213
+ withCasts(casts) {
214
+ this.#casts = mqMutObj(this.#casts);
215
+ Object.assign(this.#casts, casts);
216
+ for (const _ in casts) {
217
+ this.#hasQueryCasts = true;
218
+ break;
219
+ }
220
+ return this;
221
+ }
222
+ /** Laravel `has`. */
223
+ has(relation) {
224
+ return this.whereHas(relation);
225
+ }
226
+ /** Laravel `doesntHave`. */
227
+ doesntHave(relation) {
228
+ return this.whereDoesntHave(relation);
229
+ }
230
+ /** Laravel `whereHas`. */
231
+ whereHas(relation, callback) {
232
+ this.#simple = false;
233
+ // BelongsTo AND: expand to INNER JOIN once (same SQL as join()), avoid re-resolve per get().
234
+ // Nested where groups only emit WHERE SQL — JOIN expand would reference related columns
235
+ // without a FROM entry ("missing FROM-clause entry for table …").
236
+ const meta = resolveRelation(this.model, relation);
237
+ if (meta?.kind === "belongsTo" && !this.#nestedWhereGroup) {
238
+ this.#expandBelongsToHasJoin(meta, callback, relation);
239
+ return this;
240
+ }
241
+ return this.#pushHasConstraint(relation, false, false, callback, meta);
242
+ }
243
+ /** Laravel `orWhereHas`. */
244
+ orWhereHas(relation, callback) {
245
+ this.#simple = false;
246
+ return this.#pushHasConstraint(relation, false, true, callback);
247
+ }
248
+ /** Laravel `whereDoesntHave`. */
249
+ whereDoesntHave(relation, callback) {
250
+ this.#simple = false;
251
+ return this.#pushHasConstraint(relation, true, false, callback);
252
+ }
253
+ /** Laravel `orWhereDoesntHave`. */
254
+ orWhereDoesntHave(relation, callback) {
255
+ this.#simple = false;
256
+ return this.#pushHasConstraint(relation, true, true, callback);
257
+ }
258
+ /**
259
+ * Laravel `whereRelation($relation, $column, $operator = null, $value = null)`.
260
+ * Convenience wrapper around `whereHas` with a column constraint.
261
+ */
262
+ whereRelation(relation, column, opOrValue, value) {
263
+ this.#simple = false;
264
+ const meta = resolveRelation(this.model, relation);
265
+ // BelongsTo: join + qualified where — same build path as ORM join(), no related ModelQuery.
266
+ // Nested where groups cannot emit JOINs — fall through to has-constraint IN path.
267
+ if (meta?.kind === "belongsTo" && !this.#nestedWhereGroup) {
268
+ const alias = this.#expandBelongsToHasJoin(meta, undefined, relation);
269
+ const col = column.includes(".") ? column : `${alias}.${column}`;
270
+ if (value === undefined)
271
+ this.where(col, opOrValue);
272
+ else
273
+ this.where(col, String(opOrValue), value);
274
+ return this;
275
+ }
276
+ // HasMany / BelongsToMany: bake a single related where into EXISTS (no ModelQuery).
277
+ if (meta &&
278
+ (meta.kind === "has" ||
279
+ meta.kind === "belongsToMany" ||
280
+ meta.kind === "morphToMany")) {
281
+ const op = value === undefined ? "=" : String(opOrValue);
282
+ const val = value === undefined ? opOrValue : value;
283
+ return this.#pushHasConstraintWithRelatedApply(relation, false, false, meta, (sub) => {
284
+ sub.where(column, op, val);
285
+ });
286
+ }
287
+ return this.whereHas(relation, (q) => {
288
+ if (value === undefined)
289
+ q.where(column, opOrValue);
290
+ else
291
+ q.where(column, String(opOrValue), value);
292
+ });
293
+ }
294
+ /** Laravel `orWhereRelation`. */
295
+ orWhereRelation(relation, column, opOrValue, value) {
296
+ this.#simple = false;
297
+ const meta = resolveRelation(this.model, relation);
298
+ if (meta &&
299
+ (meta.kind === "has" ||
300
+ meta.kind === "belongsToMany" ||
301
+ meta.kind === "morphToMany")) {
302
+ const op = value === undefined ? "=" : String(opOrValue);
303
+ const val = value === undefined ? opOrValue : value;
304
+ return this.#pushHasConstraintWithRelatedApply(relation, false, true, meta, (sub) => {
305
+ sub.where(column, op, val);
306
+ });
307
+ }
308
+ return this.orWhereHas(relation, (q) => {
309
+ if (value === undefined)
310
+ q.where(column, opOrValue);
311
+ else
312
+ q.where(column, String(opOrValue), value);
313
+ });
314
+ }
315
+ /** Laravel `orHas`. */
316
+ orHas(relation) {
317
+ return this.orWhereHas(relation);
318
+ }
319
+ /** Laravel `orDoesntHave`. */
320
+ orDoesntHave(relation) {
321
+ return this.orWhereDoesntHave(relation);
322
+ }
323
+ /**
324
+ * Laravel `whereBelongsTo($model, $relationshipName = null)`.
325
+ * Constrains by the foreign key of a belongsTo relation.
326
+ */
327
+ whereBelongsTo(related, relationName) {
328
+ return this.#belongsToConstraint(related, relationName, false);
329
+ }
330
+ /** Laravel `orWhereBelongsTo`. */
331
+ orWhereBelongsTo(related, relationName) {
332
+ return this.#belongsToConstraint(related, relationName, true);
333
+ }
334
+ #belongsToConstraint(related, relationName, or) {
335
+ const RelatedCtor = related.constructor;
336
+ const rel = relationName ?? singular(RelatedCtor.table);
337
+ const meta = resolveRelation(this.model, rel);
338
+ const pkName = meta?.kind === "belongsTo" ? meta.ownerKey : RelatedCtor.primaryKey;
339
+ const fk = meta?.kind === "belongsTo"
340
+ ? meta.foreignKey
341
+ : `${singular(RelatedCtor.table)}_id`;
342
+ const pk = related[pkName];
343
+ if (or)
344
+ this.orWhere(fk, pk);
345
+ else
346
+ this.where(fk, pk);
347
+ return this;
348
+ }
349
+ /**
350
+ * Laravel `whereMorphedTo($relation, $model)`.
351
+ */
352
+ whereMorphedTo(relation, model) {
353
+ return this.#morphToConstraint(relation, model, false, false);
354
+ }
355
+ orWhereMorphedTo(relation, model) {
356
+ return this.#morphToConstraint(relation, model, false, true);
357
+ }
358
+ whereNotMorphedTo(relation, model) {
359
+ return this.#morphToConstraint(relation, model, true, false);
360
+ }
361
+ orWhereNotMorphedTo(relation, model) {
362
+ return this.#morphToConstraint(relation, model, true, true);
363
+ }
364
+ #morphToConstraint(relation, model, not, or) {
365
+ const instance = new this.model();
366
+ let rel;
367
+ try {
368
+ rel = instance.related(relation);
369
+ }
370
+ catch {
371
+ throw new Error(`Unknown morphTo relation [${relation}].`);
372
+ }
373
+ if (!(rel instanceof MorphTo)) {
374
+ throw new Error(`[${relation}] is not a morphTo relation.`);
375
+ }
376
+ const Related = model.constructor;
377
+ const type = morphTypeFor(Related);
378
+ const key = rel.getOwnerKeyName() ?? Related.primaryKey;
379
+ const id = model[key];
380
+ const typeCol = rel.getTypeColumn();
381
+ const idCol = rel.getIdColumn();
382
+ if (not) {
383
+ // NOT (type = X AND id = Y) → type <> X OR id <> Y
384
+ if (or) {
385
+ // orWhereNot — approximate with orWhereNested not
386
+ this.orWhere(typeCol, "!=", type);
387
+ return this;
388
+ }
389
+ return this.whereNot((q) => {
390
+ q.where(typeCol, type).where(idCol, id);
391
+ });
392
+ }
393
+ if (or) {
394
+ this.orWhere(typeCol, type);
395
+ this.where(idCol, id);
396
+ return this;
397
+ }
398
+ this.where(typeCol, type);
399
+ this.where(idCol, id);
400
+ return this;
401
+ }
402
+ /**
403
+ * Laravel `whereMorphRelation($relation, $types, $column, …)` — simplified:
404
+ * `whereMorphRelation('commentable', TypeModel, column, op?, value?)`.
405
+ */
406
+ whereMorphRelation(relation, type, column, opOrValue, value) {
407
+ return this.whereHasMorph(relation, type, (q) => {
408
+ if (value === undefined)
409
+ q.where(column, opOrValue);
410
+ else
411
+ q.where(column, String(opOrValue), value);
412
+ });
413
+ }
414
+ orWhereMorphRelation(relation, type, column, opOrValue, value) {
415
+ return this.orWhereHasMorph(relation, type, (q) => {
416
+ if (value === undefined)
417
+ q.where(column, opOrValue);
418
+ else
419
+ q.where(column, String(opOrValue), value);
420
+ });
421
+ }
422
+ /**
423
+ * Laravel `whereHasMorph($relation, $types, $callback?)`.
424
+ * `$types` may be a single model class or an array.
425
+ */
426
+ whereHasMorph(relation, types, callback) {
427
+ return this.#hasMorphConstraint(relation, types, false, false, callback);
428
+ }
429
+ orWhereHasMorph(relation, types, callback) {
430
+ return this.#hasMorphConstraint(relation, types, false, true, callback);
431
+ }
432
+ whereDoesntHaveMorph(relation, types, callback) {
433
+ return this.#hasMorphConstraint(relation, types, true, false, callback);
434
+ }
435
+ orWhereDoesntHaveMorph(relation, types, callback) {
436
+ return this.#hasMorphConstraint(relation, types, true, true, callback);
437
+ }
438
+ hasMorph(relation, types) {
439
+ return this.whereHasMorph(relation, types);
440
+ }
441
+ doesntHaveMorph(relation, types) {
442
+ return this.whereDoesntHaveMorph(relation, types);
443
+ }
444
+ #hasMorphConstraint(relation, types, not, or, callback) {
445
+ const list = Array.isArray(types) ? types : [types];
446
+ const instance = new this.model();
447
+ let rel;
448
+ try {
449
+ rel = instance.related(relation);
450
+ }
451
+ catch {
452
+ throw new Error(`Unknown morphTo relation [${relation}].`);
453
+ }
454
+ if (!(rel instanceof MorphTo)) {
455
+ throw new Error(`[${relation}] is not a morphTo relation.`);
456
+ }
457
+ this.#simple = false;
458
+ (this.#morphHasConstraints = mqMut(this.#morphHasConstraints)).push({
459
+ typeColumn: rel.getTypeColumn(),
460
+ idColumn: rel.getIdColumn(),
461
+ types: list,
462
+ not,
463
+ or,
464
+ callback,
465
+ });
466
+ return this;
467
+ }
468
+ /**
469
+ * Laravel `withWhereHas($relation, $callback?)` — whereHas + eager with same constraint.
470
+ */
471
+ withWhereHas(relation, callback) {
472
+ this.whereHas(relation, callback);
473
+ this.with(relation);
474
+ return this;
475
+ }
476
+ /** Laravel `whereAny` (forwarded to query builder). */
477
+ whereAny(columns, opOrValue, value) {
478
+ this.#simple = false;
479
+ (this.#builderExtras = mqMut(this.#builderExtras)).push((q) => q.whereAny(columns, opOrValue, value));
480
+ return this;
481
+ }
482
+ orWhereAny(columns, opOrValue, value) {
483
+ this.#simple = false;
484
+ (this.#builderExtras = mqMut(this.#builderExtras)).push((q) => q.orWhereAny(columns, opOrValue, value));
485
+ return this;
486
+ }
487
+ whereAll(columns, opOrValue, value) {
488
+ this.#simple = false;
489
+ (this.#builderExtras = mqMut(this.#builderExtras)).push((q) => q.whereAll(columns, opOrValue, value));
490
+ return this;
491
+ }
492
+ orWhereAll(columns, opOrValue, value) {
493
+ this.#simple = false;
494
+ (this.#builderExtras = mqMut(this.#builderExtras)).push((q) => q.orWhereAll(columns, opOrValue, value));
495
+ return this;
496
+ }
497
+ whereNone(columns, opOrValue, value) {
498
+ this.#simple = false;
499
+ (this.#builderExtras = mqMut(this.#builderExtras)).push((q) => q.whereNone(columns, opOrValue, value));
500
+ return this;
501
+ }
502
+ orWhereNone(columns, opOrValue, value) {
503
+ this.#simple = false;
504
+ (this.#builderExtras = mqMut(this.#builderExtras)).push((q) => q.orWhereNone(columns, opOrValue, value));
505
+ return this;
506
+ }
507
+ /** Laravel `whereKey($id)` / `whereKey([$ids])`. */
508
+ whereKey(id) {
509
+ const key = this.model.primaryKey;
510
+ if (Array.isArray(id))
511
+ return this.whereIn(key, id);
512
+ return this.where(key, id);
513
+ }
514
+ /** Laravel `whereKeyNot`. */
515
+ whereKeyNot(id) {
516
+ const key = this.model.primaryKey;
517
+ if (Array.isArray(id))
518
+ return this.whereNotIn(key, id);
519
+ return this.where(key, "!=", id);
520
+ }
521
+ /** Laravel `whereUuid($column, $uuid)`. */
522
+ whereUuid(column, value) {
523
+ return this.where(column, value);
524
+ }
525
+ /** Laravel `whereUlid($column, $ulid)`. */
526
+ whereUlid(column, value) {
527
+ return this.where(column, value);
528
+ }
529
+ /** Laravel `whereNot(closure)` nested group. */
530
+ whereNot(callback) {
531
+ this.#simple = false;
532
+ (this.#nestedGroups = mqMut(this.#nestedGroups)).push({ boolean: "and", not: true, callback });
533
+ return this;
534
+ }
535
+ orWhereNot(callback) {
536
+ this.#simple = false;
537
+ (this.#nestedGroups = mqMut(this.#nestedGroups)).push({ boolean: "or", not: true, callback });
538
+ return this;
539
+ }
540
+ firstWhere(column, opOrValue, value) {
541
+ if (value === undefined)
542
+ this.where(column, opOrValue);
543
+ else
544
+ this.where(column, String(opOrValue), value);
545
+ return this.first();
546
+ }
547
+ /** Laravel `findMany`. */
548
+ findMany(ids) {
549
+ return this.whereIn(this.model.primaryKey, ids).#fetchModels();
550
+ }
551
+ /** Laravel `findOr($id, $callback)`. */
552
+ async findOr(id, callback) {
553
+ const model = await this.where(this.model.primaryKey, id).first();
554
+ if (model)
555
+ return model;
556
+ return callback();
557
+ }
558
+ /** Laravel `findOrNew`. */
559
+ async findOrNew(id) {
560
+ const model = await this.where(this.model.primaryKey, id).first();
561
+ if (model)
562
+ return model;
563
+ const fresh = new this.model();
564
+ fresh[this.model.primaryKey] = id;
565
+ return fresh;
566
+ }
567
+ /** Laravel `firstOr($callback)`. */
568
+ async firstOr(callback) {
569
+ const model = await this.first();
570
+ if (model)
571
+ return model;
572
+ return callback();
573
+ }
574
+ /** Laravel `valueOrFail`. */
575
+ async valueOrFail(column) {
576
+ const model = await this.firstOrFail();
577
+ return model[column];
578
+ }
579
+ /** Laravel `soleValue`. */
580
+ async soleValue(column) {
581
+ const model = await this.sole();
582
+ return model[column];
583
+ }
584
+ /** `withCount` — `'posts'`, `'posts as post_total'`, a string list, or `{ products: { as: 'productsCount' } }`. */
585
+ withCount(...relations) {
586
+ for (const arg of relations) {
587
+ if (Array.isArray(arg)) {
588
+ this.withCount(...arg);
589
+ continue;
590
+ }
591
+ if (typeof arg === "string") {
592
+ const parsed = parseRelationAlias(arg);
593
+ this.#simple = false;
594
+ (this.#withAggregates = mqMut(this.#withAggregates)).push({
595
+ relation: parsed.relation,
596
+ alias: parsed.alias ?? `${parsed.relation}_count`,
597
+ fn: "count",
598
+ });
599
+ (this.#withCounts = mqMut(this.#withCounts)).push(arg);
600
+ continue;
601
+ }
602
+ for (const [relation, value] of Object.entries(arg)) {
603
+ let alias = `${relation}_count`;
604
+ if (value === true) {
605
+ // default alias
606
+ }
607
+ else if (typeof value === "string") {
608
+ alias = value;
609
+ }
610
+ else if (typeof value === "function") {
611
+ // constrained count — store as aggregate without callback support yet (count all)
612
+ }
613
+ else if (value && typeof value === "object" && "as" in value && value.as) {
614
+ alias = value.as;
615
+ }
616
+ this.#simple = false;
617
+ (this.#withAggregates = mqMut(this.#withAggregates)).push({ relation, alias, fn: "count" });
618
+ (this.#withCounts = mqMut(this.#withCounts)).push(relation);
619
+ }
620
+ }
621
+ return this;
622
+ }
623
+ /** Laravel `withSum($relation, $column)`. */
624
+ withSum(relation, column) {
625
+ const parsed = parseRelationAlias(relation);
626
+ const base = parsed.relation;
627
+ this.#simple = false;
628
+ (this.#withAggregates = mqMut(this.#withAggregates)).push({
629
+ relation: base,
630
+ alias: parsed.alias ?? `${base}_sum_${column}`,
631
+ fn: "sum",
632
+ column,
633
+ });
634
+ return this;
635
+ }
636
+ withAvg(relation, column) {
637
+ const parsed = parseRelationAlias(relation);
638
+ const base = parsed.relation;
639
+ this.#simple = false;
640
+ (this.#withAggregates = mqMut(this.#withAggregates)).push({
641
+ relation: base,
642
+ alias: parsed.alias ?? `${base}_avg_${column}`,
643
+ fn: "avg",
644
+ column,
645
+ });
646
+ return this;
647
+ }
648
+ withMin(relation, column) {
649
+ const parsed = parseRelationAlias(relation);
650
+ const base = parsed.relation;
651
+ this.#simple = false;
652
+ (this.#withAggregates = mqMut(this.#withAggregates)).push({
653
+ relation: base,
654
+ alias: parsed.alias ?? `${base}_min_${column}`,
655
+ fn: "min",
656
+ column,
657
+ });
658
+ return this;
659
+ }
660
+ withMax(relation, column) {
661
+ const parsed = parseRelationAlias(relation);
662
+ const base = parsed.relation;
663
+ this.#simple = false;
664
+ (this.#withAggregates = mqMut(this.#withAggregates)).push({
665
+ relation: base,
666
+ alias: parsed.alias ?? `${base}_max_${column}`,
667
+ fn: "max",
668
+ column,
669
+ });
670
+ return this;
671
+ }
672
+ /** Laravel `withExists($relation)`. */
673
+ withExists(...relations) {
674
+ for (const raw of relations.flat()) {
675
+ const parsed = parseRelationAlias(raw);
676
+ this.#simple = false;
677
+ (this.#withAggregates = mqMut(this.#withAggregates)).push({
678
+ relation: parsed.relation,
679
+ alias: parsed.alias ?? `${parsed.relation}_exists`,
680
+ fn: "exists",
681
+ });
682
+ }
683
+ return this;
684
+ }
685
+ /** Laravel `withAggregate($relation, $column, $function)`. */
686
+ withAggregate(relation, column, fn) {
687
+ const parsed = parseRelationAlias(relation);
688
+ const base = parsed.relation;
689
+ this.#simple = false;
690
+ (this.#withAggregates = mqMut(this.#withAggregates)).push({
691
+ relation: base,
692
+ alias: parsed.alias ?? `${base}_${fn}_${column}`,
693
+ fn,
694
+ column: fn === "count" ? undefined : column,
695
+ });
696
+ return this;
697
+ }
698
+ /** Laravel Eloquent `join` (hydrated models still from the base table). */
699
+ join(table, first, op, second) {
700
+ this.#simple = false;
701
+ (this.#joins = mqMut(this.#joins)).push({ type: "inner", table, first, op, second });
702
+ return this;
703
+ }
704
+ /** Laravel `leftJoin`. */
705
+ leftJoin(table, first, op, second) {
706
+ this.#simple = false;
707
+ (this.#joins = mqMut(this.#joins)).push({ type: "left", table, first, op, second });
708
+ return this;
709
+ }
710
+ /** Laravel `rightJoin`. */
711
+ rightJoin(table, first, op, second) {
712
+ this.#simple = false;
713
+ (this.#joins = mqMut(this.#joins)).push({ type: "right", table, first, op, second });
714
+ return this;
715
+ }
716
+ where(columnOrCallback, opOrValue, value) {
717
+ if (typeof columnOrCallback === "function") {
718
+ this.#simple = false;
719
+ (this.#nestedGroups = mqMut(this.#nestedGroups)).push({
720
+ boolean: "and",
721
+ not: false,
722
+ callback: columnOrCallback,
723
+ });
724
+ return this;
725
+ }
726
+ if (value === undefined) {
727
+ (this.#wheres = mqMut(this.#wheres)).push({
728
+ column: columnOrCallback,
729
+ op: "=",
730
+ value: opOrValue,
731
+ boolean: "and",
732
+ });
733
+ }
734
+ else {
735
+ const op = String(opOrValue);
736
+ if (op !== "=" &&
737
+ op !== "!=" &&
738
+ op !== "<>" &&
739
+ op !== "<" &&
740
+ op !== "<=" &&
741
+ op !== ">" &&
742
+ op !== ">=" &&
743
+ op !== "like" &&
744
+ op !== "LIKE" &&
745
+ op !== "not like" &&
746
+ op !== "NOT LIKE") {
747
+ this.#simple = false;
748
+ }
749
+ (this.#wheres = mqMut(this.#wheres)).push({
750
+ column: columnOrCallback,
751
+ op,
752
+ value,
753
+ boolean: "and",
754
+ });
755
+ }
756
+ return this;
757
+ }
758
+ orWhere(columnOrCallback, opOrValue, value) {
759
+ if (typeof columnOrCallback === "function") {
760
+ this.#simple = false;
761
+ (this.#nestedGroups = mqMut(this.#nestedGroups)).push({
762
+ boolean: "or",
763
+ not: false,
764
+ callback: columnOrCallback,
765
+ });
766
+ return this;
767
+ }
768
+ if (value === undefined) {
769
+ (this.#wheres = mqMut(this.#wheres)).push({
770
+ column: columnOrCallback,
771
+ op: "=",
772
+ value: opOrValue,
773
+ boolean: "or",
774
+ });
775
+ }
776
+ else {
777
+ const op = String(opOrValue);
778
+ if (op !== "=" &&
779
+ op !== "!=" &&
780
+ op !== "<>" &&
781
+ op !== "<" &&
782
+ op !== "<=" &&
783
+ op !== ">" &&
784
+ op !== ">=" &&
785
+ op !== "like" &&
786
+ op !== "LIKE" &&
787
+ op !== "not like" &&
788
+ op !== "NOT LIKE") {
789
+ this.#simple = false;
790
+ }
791
+ (this.#wheres = mqMut(this.#wheres)).push({
792
+ column: columnOrCallback,
793
+ op,
794
+ value,
795
+ boolean: "or",
796
+ });
797
+ }
798
+ return this;
799
+ }
800
+ whereIn(column, values) {
801
+ this.#simple = false;
802
+ (this.#whereIns = mqMut(this.#whereIns)).push({ column, values, not: false, boolean: "and" });
803
+ return this;
804
+ }
805
+ orWhereIn(column, values) {
806
+ this.#simple = false;
807
+ (this.#whereIns = mqMut(this.#whereIns)).push({ column, values, not: false, boolean: "or" });
808
+ return this;
809
+ }
810
+ whereNotIn(column, values) {
811
+ this.#simple = false;
812
+ (this.#whereIns = mqMut(this.#whereIns)).push({ column, values, not: true, boolean: "and" });
813
+ return this;
814
+ }
815
+ orWhereNotIn(column, values) {
816
+ this.#simple = false;
817
+ (this.#whereIns = mqMut(this.#whereIns)).push({ column, values, not: true, boolean: "or" });
818
+ return this;
819
+ }
820
+ whereNull(column) {
821
+ (this.#wheres = mqMut(this.#wheres)).push({
822
+ column,
823
+ op: "__null__",
824
+ value: null,
825
+ boolean: "and",
826
+ });
827
+ return this;
828
+ }
829
+ orWhereNull(column) {
830
+ (this.#wheres = mqMut(this.#wheres)).push({
831
+ column,
832
+ op: "__null__",
833
+ value: null,
834
+ boolean: "or",
835
+ });
836
+ return this;
837
+ }
838
+ whereNotNull(column) {
839
+ (this.#wheres = mqMut(this.#wheres)).push({
840
+ column,
841
+ op: "__notnull__",
842
+ value: null,
843
+ boolean: "and",
844
+ });
845
+ return this;
846
+ }
847
+ orWhereNotNull(column) {
848
+ (this.#wheres = mqMut(this.#wheres)).push({
849
+ column,
850
+ op: "__notnull__",
851
+ value: null,
852
+ boolean: "or",
853
+ });
854
+ return this;
855
+ }
856
+ whereBetween(column, values) {
857
+ this.#simple = false;
858
+ (this.#whereBetweens = mqMut(this.#whereBetweens)).push({
859
+ column,
860
+ values,
861
+ not: false,
862
+ boolean: "and",
863
+ });
864
+ return this;
865
+ }
866
+ orWhereBetween(column, values) {
867
+ this.#simple = false;
868
+ (this.#whereBetweens = mqMut(this.#whereBetweens)).push({
869
+ column,
870
+ values,
871
+ not: false,
872
+ boolean: "or",
873
+ });
874
+ return this;
875
+ }
876
+ whereNotBetween(column, values) {
877
+ this.#simple = false;
878
+ (this.#whereBetweens = mqMut(this.#whereBetweens)).push({
879
+ column,
880
+ values,
881
+ not: true,
882
+ boolean: "and",
883
+ });
884
+ return this;
885
+ }
886
+ orWhereNotBetween(column, values) {
887
+ this.#simple = false;
888
+ (this.#whereBetweens = mqMut(this.#whereBetweens)).push({
889
+ column,
890
+ values,
891
+ not: true,
892
+ boolean: "or",
893
+ });
894
+ return this;
895
+ }
896
+ whereColumn(first, opOrSecond, second) {
897
+ if (second === undefined) {
898
+ this.#simple = false;
899
+ (this.#whereColumns = mqMut(this.#whereColumns)).push({
900
+ first,
901
+ op: "=",
902
+ second: opOrSecond,
903
+ boolean: "and",
904
+ });
905
+ }
906
+ else {
907
+ this.#simple = false;
908
+ (this.#whereColumns = mqMut(this.#whereColumns)).push({
909
+ first,
910
+ op: opOrSecond,
911
+ second,
912
+ boolean: "and",
913
+ });
914
+ }
915
+ return this;
916
+ }
917
+ orWhereColumn(first, opOrSecond, second) {
918
+ if (second === undefined) {
919
+ this.#simple = false;
920
+ (this.#whereColumns = mqMut(this.#whereColumns)).push({
921
+ first,
922
+ op: "=",
923
+ second: opOrSecond,
924
+ boolean: "or",
925
+ });
926
+ }
927
+ else {
928
+ this.#simple = false;
929
+ (this.#whereColumns = mqMut(this.#whereColumns)).push({
930
+ first,
931
+ op: opOrSecond,
932
+ second,
933
+ boolean: "or",
934
+ });
935
+ }
936
+ return this;
937
+ }
938
+ whereRaw(sql, bindings = []) {
939
+ this.#simple = false;
940
+ (this.#whereRaws = mqMut(this.#whereRaws)).push({ sql, bindings, boolean: "and" });
941
+ return this;
942
+ }
943
+ orWhereRaw(sql, bindings = []) {
944
+ this.#simple = false;
945
+ (this.#whereRaws = mqMut(this.#whereRaws)).push({ sql, bindings, boolean: "or" });
946
+ return this;
947
+ }
948
+ whereLike(column, value, caseSensitive = false) {
949
+ this.#simple = false;
950
+ (this.#whereLikes = mqMut(this.#whereLikes)).push({
951
+ column,
952
+ value,
953
+ not: false,
954
+ caseSensitive,
955
+ boolean: "and",
956
+ });
957
+ return this;
958
+ }
959
+ orWhereLike(column, value, caseSensitive = false) {
960
+ this.#simple = false;
961
+ (this.#whereLikes = mqMut(this.#whereLikes)).push({
962
+ column,
963
+ value,
964
+ not: false,
965
+ caseSensitive,
966
+ boolean: "or",
967
+ });
968
+ return this;
969
+ }
970
+ whereNotLike(column, value, caseSensitive = false) {
971
+ this.#simple = false;
972
+ (this.#whereLikes = mqMut(this.#whereLikes)).push({
973
+ column,
974
+ value,
975
+ not: true,
976
+ caseSensitive,
977
+ boolean: "and",
978
+ });
979
+ return this;
980
+ }
981
+ orWhereNotLike(column, value, caseSensitive = false) {
982
+ this.#simple = false;
983
+ (this.#whereLikes = mqMut(this.#whereLikes)).push({
984
+ column,
985
+ value,
986
+ not: true,
987
+ caseSensitive,
988
+ boolean: "or",
989
+ });
990
+ return this;
991
+ }
992
+ whereDate(column, opOrValue, value) {
993
+ const op = value === undefined ? "=" : String(opOrValue);
994
+ const raw = (value === undefined ? opOrValue : value);
995
+ this.#simple = false;
996
+ (this.#dateWheres = mqMut(this.#dateWheres)).push({ column, op, value: raw, kind: "date" });
997
+ return this;
998
+ }
999
+ whereYear(column, value) {
1000
+ this.#simple = false;
1001
+ (this.#dateWheres = mqMut(this.#dateWheres)).push({
1002
+ column,
1003
+ op: "=",
1004
+ value: String(value),
1005
+ kind: "year",
1006
+ });
1007
+ return this;
1008
+ }
1009
+ whereMonth(column, value) {
1010
+ this.#simple = false;
1011
+ (this.#dateWheres = mqMut(this.#dateWheres)).push({
1012
+ column,
1013
+ op: "=",
1014
+ value: String(value),
1015
+ kind: "month",
1016
+ });
1017
+ return this;
1018
+ }
1019
+ whereDay(column, value) {
1020
+ this.#simple = false;
1021
+ (this.#dateWheres = mqMut(this.#dateWheres)).push({
1022
+ column,
1023
+ op: "=",
1024
+ value: String(value),
1025
+ kind: "day",
1026
+ });
1027
+ return this;
1028
+ }
1029
+ select(...columns) {
1030
+ this.#simple = false;
1031
+ this.#selectColumns = columns.flat();
1032
+ return this;
1033
+ }
1034
+ addSelect(...columns) {
1035
+ this.#simple = false;
1036
+ (this.#addSelectColumns = mqMut(this.#addSelectColumns)).push(...columns.flat());
1037
+ return this;
1038
+ }
1039
+ selectRaw(expression, bindings = []) {
1040
+ this.#simple = false;
1041
+ (this.#selectRaws = mqMut(this.#selectRaws)).push({ expression, bindings });
1042
+ return this;
1043
+ }
1044
+ distinct(value = true) {
1045
+ if (value)
1046
+ this.#simple = false;
1047
+ this.#distinct = value;
1048
+ return this;
1049
+ }
1050
+ orderBy(column, direction = "asc") {
1051
+ (this.#orders = mqMut(this.#orders)).push({ column, direction });
1052
+ return this;
1053
+ }
1054
+ orderByDesc(column) {
1055
+ return this.orderBy(column, "desc");
1056
+ }
1057
+ orderByRaw(sql) {
1058
+ this.#simple = false;
1059
+ (this.#orders = mqMut(this.#orders)).push({ raw: sql });
1060
+ return this;
1061
+ }
1062
+ /** Karobar / dialect alias for `orderByRaw`. */
1063
+ orderBySql(sql) {
1064
+ return this.orderByRaw(sql);
1065
+ }
1066
+ reorder(column, direction = "asc") {
1067
+ this.#orders = [];
1068
+ if (column !== undefined)
1069
+ this.orderBy(column, direction);
1070
+ return this;
1071
+ }
1072
+ inRandomOrder() {
1073
+ this.#simple = false;
1074
+ (this.#orders = mqMut(this.#orders)).push({ raw: "__inRandomOrder__" });
1075
+ return this;
1076
+ }
1077
+ latest(column = "created_at") {
1078
+ return this.orderBy(column, "desc");
1079
+ }
1080
+ oldest(column = "created_at") {
1081
+ return this.orderBy(column, "asc");
1082
+ }
1083
+ groupBy(...columns) {
1084
+ this.#simple = false;
1085
+ (this.#groupByColumns = mqMut(this.#groupByColumns)).push(...columns.flat());
1086
+ return this;
1087
+ }
1088
+ having(column, op, value) {
1089
+ this.#simple = false;
1090
+ (this.#havings = mqMut(this.#havings)).push({ column, op, value });
1091
+ return this;
1092
+ }
1093
+ havingRaw(sql, bindings = []) {
1094
+ this.#simple = false;
1095
+ (this.#havingRaws = mqMut(this.#havingRaws)).push({ sql, bindings });
1096
+ return this;
1097
+ }
1098
+ limit(value) {
1099
+ this.#limitValue = value;
1100
+ return this;
1101
+ }
1102
+ take(value) {
1103
+ return this.limit(value);
1104
+ }
1105
+ offset(value) {
1106
+ this.#offsetValue = value;
1107
+ return this;
1108
+ }
1109
+ skip(value) {
1110
+ return this.offset(value);
1111
+ }
1112
+ forPage(page, perPage = 15) {
1113
+ const p = Math.max(1, Math.floor(page) || 1);
1114
+ const size = Math.max(1, Math.floor(perPage) || 15);
1115
+ return this.limit(size).offset((p - 1) * size);
1116
+ }
1117
+ lockForUpdate() {
1118
+ this.#simple = false;
1119
+ this.#lock = "update";
1120
+ return this;
1121
+ }
1122
+ sharedLock() {
1123
+ this.#simple = false;
1124
+ this.#lock = "share";
1125
+ return this;
1126
+ }
1127
+ when(condition, callback, defaultCallback) {
1128
+ if (condition)
1129
+ callback(this);
1130
+ else if (defaultCallback)
1131
+ defaultCallback(this);
1132
+ return this;
1133
+ }
1134
+ /**
1135
+ * OR `LIKE %term%` across columns (Karobar list `search`).
1136
+ * Empty/whitespace term is a no-op.
1137
+ */
1138
+ search(columns, term) {
1139
+ const needle = String(term ?? "").trim();
1140
+ if (!needle || columns.length === 0)
1141
+ return this;
1142
+ const pattern = `%${needle}%`;
1143
+ return this.where((q) => {
1144
+ for (let i = 0; i < columns.length; i++) {
1145
+ const col = columns[i];
1146
+ if (i === 0)
1147
+ q.where(col, "like", pattern);
1148
+ else
1149
+ q.orWhere(col, "like", pattern);
1150
+ }
1151
+ });
1152
+ }
1153
+ unless(condition, callback, defaultCallback) {
1154
+ return this.when(!condition, callback, defaultCallback);
1155
+ }
1156
+ tap(callback) {
1157
+ callback(this);
1158
+ return this;
1159
+ }
1160
+ clone() {
1161
+ const copy = new ModelQuery(this.model, {
1162
+ withTrashed: this.#withTrashed,
1163
+ onlyTrashed: this.#onlyTrashed,
1164
+ eagerLoad: this.#eagerLoad.map((e) => ({ ...e })),
1165
+ withoutGlobalScopes: this.#withoutGlobalScopes === true
1166
+ ? true
1167
+ : this.#withoutGlobalScopes
1168
+ ? [...this.#withoutGlobalScopes]
1169
+ : undefined,
1170
+ connection: this.#connection,
1171
+ });
1172
+ copy.#wheres = this.#wheres.map((w) => ({ ...w }));
1173
+ copy.#pendingAttributes = { ...this.#pendingAttributes };
1174
+ copy.#whereIns = this.#whereIns.map((w) => ({
1175
+ ...w,
1176
+ values: [...w.values],
1177
+ }));
1178
+ copy.#whereNulls = this.#whereNulls.map((w) => ({ ...w }));
1179
+ copy.#whereBetweens = this.#whereBetweens.map((w) => ({
1180
+ ...w,
1181
+ values: [...w.values],
1182
+ }));
1183
+ copy.#whereColumns = this.#whereColumns.map((w) => ({ ...w }));
1184
+ copy.#whereRaws = this.#whereRaws.map((w) => ({
1185
+ ...w,
1186
+ bindings: [...w.bindings],
1187
+ }));
1188
+ copy.#whereLikes = this.#whereLikes.map((w) => ({ ...w }));
1189
+ copy.#dateWheres = this.#dateWheres.map((w) => ({ ...w }));
1190
+ copy.#orders = this.#orders.map((o) => ({ ...o }));
1191
+ copy.#selectColumns = this.#selectColumns
1192
+ ? [...this.#selectColumns]
1193
+ : null;
1194
+ copy.#addSelectColumns = [...this.#addSelectColumns];
1195
+ copy.#selectRaws = this.#selectRaws.map((s) => ({
1196
+ ...s,
1197
+ bindings: [...s.bindings],
1198
+ }));
1199
+ copy.#distinct = this.#distinct;
1200
+ copy.#limitValue = this.#limitValue;
1201
+ copy.#offsetValue = this.#offsetValue;
1202
+ copy.#groupByColumns = [...this.#groupByColumns];
1203
+ copy.#havings = this.#havings.map((h) => ({ ...h }));
1204
+ copy.#havingRaws = this.#havingRaws.map((h) => ({
1205
+ ...h,
1206
+ bindings: [...h.bindings],
1207
+ }));
1208
+ copy.#lock = this.#lock;
1209
+ copy.#hasConstraints = this.#hasConstraints.map((c) => ({
1210
+ ...c,
1211
+ existsBindings: c.existsBindings ? [...c.existsBindings] : undefined,
1212
+ }));
1213
+ copy.#withCounts = [...this.#withCounts];
1214
+ copy.#withAggregates = this.#withAggregates.map((a) => ({ ...a }));
1215
+ copy.#morphHasConstraints = this.#morphHasConstraints.map((m) => ({
1216
+ ...m,
1217
+ types: [...m.types],
1218
+ }));
1219
+ copy.#builderExtras = [...this.#builderExtras];
1220
+ copy.#nestedGroups = this.#nestedGroups.map((g) => ({ ...g }));
1221
+ copy.#joins = this.#joins.map((j) => ({ ...j }));
1222
+ copy.#nestedWhereGroup = this.#nestedWhereGroup;
1223
+ copy.#casts = { ...this.#casts };
1224
+ copy.#hasQueryCasts = this.#hasQueryCasts;
1225
+ copy.#simple = this.#simple;
1226
+ copy.#hasRetrievedListeners = this.#hasRetrievedListeners;
1227
+ copy.#asRows = this.#asRows;
1228
+ return copy;
1229
+ }
1230
+ with(...relations) {
1231
+ (this.#eagerLoad = mqMut(this.#eagerLoad)).push(...normalizeWithRelations(relations));
1232
+ return this;
1233
+ }
1234
+ /** Underlying query builder (fresh compile each call). */
1235
+ toBase() {
1236
+ return this.#buildQuery();
1237
+ }
1238
+ /** Compiled SQL (bindings via `getBindings()`). */
1239
+ toSql() {
1240
+ return this.#buildQuery().toSql();
1241
+ }
1242
+ /** SQL with bindings interpolated for debugging. */
1243
+ toRawSql() {
1244
+ return this.#buildQuery().toRawSql();
1245
+ }
1246
+ getBindings() {
1247
+ return this.#buildQuery().getBindings();
1248
+ }
1249
+ dump() {
1250
+ console.log(this.toRawSql());
1251
+ return this;
1252
+ }
1253
+ dd() {
1254
+ console.log(this.toRawSql());
1255
+ process.exit(1);
1256
+ }
1257
+ #applySoftDeletes(q) {
1258
+ if (!usesSoftDeletes(this.model))
1259
+ return q;
1260
+ const col = this.#qualifyParentColumn(deletedAtColumn(this.model));
1261
+ if (this.#onlyTrashed)
1262
+ return q.whereNotNull(col);
1263
+ if (!this.#withTrashed)
1264
+ return q.whereNull(col);
1265
+ return q;
1266
+ }
1267
+ /**
1268
+ * True when this query can compile without a full QueryBuilder
1269
+ * (basic wheres + order + limit/offset, optional plain column list).
1270
+ * Global scopes that only add basic wheres stay on this path.
1271
+ */
1272
+ #simplePlan() {
1273
+ if (this.#exoticConstraints())
1274
+ return undefined;
1275
+ let columns = "*";
1276
+ if (!this.#simple) {
1277
+ // `select('id', 'name')` is the usual reason a flat query leaves this path.
1278
+ if (this.#selectColumns == null)
1279
+ return undefined;
1280
+ const plain = plainColumnList(this.#selectColumns);
1281
+ if (plain == null || !wheresAreSimple(this.#wheres))
1282
+ return undefined;
1283
+ columns = plain;
1284
+ }
1285
+ const scopeWheres = this.#scopeWheres();
1286
+ if (scopeWheres == null)
1287
+ return undefined;
1288
+ return { columns, scopeWheres };
1289
+ }
1290
+ /** Joins, raws, groups, and other shapes the simple compiler does not emit. */
1291
+ #exoticConstraints() {
1292
+ return (this.#joins.length > 0 ||
1293
+ this.#morphHasConstraints.length > 0 ||
1294
+ this.#nestedGroups.length > 0 ||
1295
+ this.#builderExtras.length > 0 ||
1296
+ this.#withCounts.length > 0 ||
1297
+ this.#withAggregates.length > 0 ||
1298
+ this.#addSelectColumns.length > 0 ||
1299
+ this.#selectRaws.length > 0 ||
1300
+ this.#distinct ||
1301
+ this.#groupByColumns.length > 0 ||
1302
+ this.#havings.length > 0 ||
1303
+ this.#havingRaws.length > 0 ||
1304
+ this.#lock !== false ||
1305
+ this.#whereIns.length > 0 ||
1306
+ this.#whereNulls.length > 0 ||
1307
+ this.#whereBetweens.length > 0 ||
1308
+ this.#whereColumns.length > 0 ||
1309
+ this.#whereRaws.length > 0 ||
1310
+ this.#whereLikes.length > 0 ||
1311
+ this.#dateWheres.length > 0 ||
1312
+ this.#hasConstraints.length > 0);
1313
+ }
1314
+ /**
1315
+ * Basic wheres added by global scopes, or null when a scope needs the full builder.
1316
+ * An empty list means every scope was a no-op (for example no current tenant).
1317
+ */
1318
+ #scopeWheres() {
1319
+ if (this.#withoutGlobalScopes === true)
1320
+ return [];
1321
+ if (!hasGlobalScopes(this.model))
1322
+ return [];
1323
+ const wheres = [];
1324
+ for (const [name, scope] of getGlobalScopes(this.model)) {
1325
+ if (this.#withoutGlobalScopes?.has(name))
1326
+ continue;
1327
+ const temp = new ModelQuery(this.model, {
1328
+ withoutGlobalScopes: true,
1329
+ withTrashed: this.#withTrashed,
1330
+ onlyTrashed: this.#onlyTrashed,
1331
+ });
1332
+ scope(temp);
1333
+ if (!temp.#simple || temp.#exoticConstraints() || !wheresAreSimple(temp.#wheres)) {
1334
+ return null;
1335
+ }
1336
+ for (const where of temp.#wheres)
1337
+ wheres.push(where);
1338
+ }
1339
+ return wheres;
1340
+ }
1341
+ /** Compile a simple SELECT matching QueryBuilder's default shape. */
1342
+ #compileSimpleSelect(conn = this.#queryConnection(), plan = { columns: "*", scopeWheres: [] }) {
1343
+ const dialect = conn.dialect;
1344
+ const params = [];
1345
+ // Fingerprint (no values) — reuse quoted SQL for repeated list shapes.
1346
+ let key = this.#withTrashed ? "T" : this.#onlyTrashed ? "O" : "N";
1347
+ key += `\u001e${this.model.table}\u001e${plan.columns}`;
1348
+ for (const w of plan.scopeWheres) {
1349
+ key += `\u001eG\u001e${w.boolean}\u001e${w.column}\u001e${w.op}`;
1350
+ if (w.op !== "__null__" && w.op !== "__notnull__")
1351
+ params.push(w.value);
1352
+ }
1353
+ for (const w of this.#wheres) {
1354
+ key += `\u001e${w.boolean}\u001e${w.column}\u001e${w.op}`;
1355
+ if (w.op !== "__null__" && w.op !== "__notnull__")
1356
+ params.push(w.value);
1357
+ }
1358
+ for (const o of this.#orders) {
1359
+ if ("raw" in o)
1360
+ key += `\u001eR\u001e${o.raw}`;
1361
+ else
1362
+ key += `\u001eO\u001e${o.column}\u001e${o.direction}`;
1363
+ }
1364
+ if (this.#limitValue !== undefined)
1365
+ key += `\u001eL\u001e${this.#limitValue}`;
1366
+ if (this.#offsetValue !== undefined)
1367
+ key += `\u001eF\u001e${this.#offsetValue}`;
1368
+ if (usesSoftDeletes(this.model))
1369
+ key += "\u001eS";
1370
+ let byModel = simpleSelectSqlCache.get(this.model);
1371
+ if (!byModel) {
1372
+ byModel = new Map();
1373
+ simpleSelectSqlCache.set(this.model, byModel);
1374
+ }
1375
+ let sql = byModel.get(key);
1376
+ if (sql !== undefined)
1377
+ return { sql, params };
1378
+ const wrap = (name) => wrapSqlName(dialect, name);
1379
+ const whereParts = [];
1380
+ const pushBool = (boolean) => {
1381
+ if (whereParts.length === 0)
1382
+ return "";
1383
+ return boolean === "or" ? " OR " : " AND ";
1384
+ };
1385
+ if (usesSoftDeletes(this.model)) {
1386
+ const col = wrap(deletedAtColumn(this.model));
1387
+ if (this.#onlyTrashed) {
1388
+ whereParts.push(`${col} IS NOT NULL`);
1389
+ }
1390
+ else if (!this.#withTrashed) {
1391
+ whereParts.push(`${col} IS NULL`);
1392
+ }
1393
+ }
1394
+ const appendWhere = (w) => {
1395
+ const bool = pushBool(w.boolean);
1396
+ const col = wrap(w.column);
1397
+ if (w.op === "__null__") {
1398
+ whereParts.push(`${bool}${col} IS NULL`);
1399
+ return;
1400
+ }
1401
+ if (w.op === "__notnull__") {
1402
+ whereParts.push(`${bool}${col} IS NOT NULL`);
1403
+ return;
1404
+ }
1405
+ whereParts.push(`${bool}${col} ${w.op} ?`);
1406
+ };
1407
+ // Scope predicates first, then the query's own wheres (matches builder order).
1408
+ for (const w of plan.scopeWheres)
1409
+ appendWhere(w);
1410
+ for (const w of this.#wheres)
1411
+ appendWhere(w);
1412
+ sql = `SELECT ${plan.columns} FROM ${this.model.table}`;
1413
+ if (whereParts.length > 0) {
1414
+ sql += ` WHERE ${whereParts.join("")}`;
1415
+ }
1416
+ if (this.#orders.length > 0) {
1417
+ sql += ` ORDER BY ${this.#orders
1418
+ .map((o) => {
1419
+ if ("raw" in o)
1420
+ return o.raw;
1421
+ return `${wrap(o.column)} ${o.direction.toUpperCase()}`;
1422
+ })
1423
+ .join(", ")}`;
1424
+ }
1425
+ if (this.#limitValue !== undefined) {
1426
+ sql += ` LIMIT ${this.#limitValue}`;
1427
+ }
1428
+ if (this.#offsetValue !== undefined) {
1429
+ sql += ` OFFSET ${this.#offsetValue}`;
1430
+ }
1431
+ byModel.set(key, sql);
1432
+ return { sql, params };
1433
+ }
1434
+ /**
1435
+ * Fetch rows without allocating a QueryBuilder when the shape is simple
1436
+ * or a call-time-compiled whereHas EXISTS (SQLite hot path).
1437
+ * Returns undefined to fall back to the full builder path.
1438
+ */
1439
+ #trySimpleRows() {
1440
+ const conn = this.#queryConnection();
1441
+ let compiled;
1442
+ const plan = this.#simplePlan();
1443
+ if (plan) {
1444
+ compiled = this.#compileSimpleSelect(conn, plan);
1445
+ }
1446
+ else if (this.#canHasExistsSelect()) {
1447
+ compiled = this.#compileHasExistsSelect(conn);
1448
+ }
1449
+ else {
1450
+ return undefined;
1451
+ }
1452
+ const { sql, params } = compiled;
1453
+ if (conn.allSync) {
1454
+ return conn.allSync(sql, params);
1455
+ }
1456
+ if (conn.driver === "postgres" &&
1457
+ params.length === 0 &&
1458
+ !hasQueryListeners()) {
1459
+ const raw = conn.raw;
1460
+ if (raw && typeof raw.unsafe === "function") {
1461
+ return raw.unsafe(sql);
1462
+ }
1463
+ }
1464
+ return conn.all(sql, params);
1465
+ }
1466
+ #trySimpleFirst() {
1467
+ const plan = this.#simplePlan();
1468
+ const useHas = !plan && this.#canHasExistsSelect();
1469
+ if (!plan && !useHas)
1470
+ return undefined;
1471
+ // Match Builder.first(): LIMIT 1 (preserve explicit smaller limit).
1472
+ const prev = this.#limitValue;
1473
+ if (prev === undefined || prev > 1)
1474
+ this.#limitValue = 1;
1475
+ try {
1476
+ const conn = this.#queryConnection();
1477
+ const { sql, params } = plan
1478
+ ? this.#compileSimpleSelect(conn, plan)
1479
+ : this.#compileHasExistsSelect(conn);
1480
+ if (conn.getSync) {
1481
+ return conn.getSync(sql, params) ?? null;
1482
+ }
1483
+ if (conn.allSync) {
1484
+ const rows = conn.allSync(sql, params);
1485
+ return rows[0] ?? null;
1486
+ }
1487
+ return conn.all(sql, params).then((rows) => rows[0] ?? null);
1488
+ }
1489
+ finally {
1490
+ this.#limitValue = prev;
1491
+ }
1492
+ }
1493
+ /**
1494
+ * whereHas/whereDoesntHave with call-time EXISTS SQL + flat parent wheres only.
1495
+ * Skips QueryBuilder on SQLite get/getSync (same idea as #canSimpleSelect).
1496
+ */
1497
+ #canHasExistsSelect() {
1498
+ if (this.#hasConstraints.length === 0)
1499
+ return false;
1500
+ for (const c of this.#hasConstraints) {
1501
+ if (c.existsSql === undefined)
1502
+ return false;
1503
+ }
1504
+ if (hasGlobalScopes(this.model) && this.#withoutGlobalScopes !== true) {
1505
+ return false;
1506
+ }
1507
+ if (this.#joins.length > 0)
1508
+ return false;
1509
+ if (this.#morphHasConstraints.length > 0)
1510
+ return false;
1511
+ if (this.#nestedGroups.length > 0)
1512
+ return false;
1513
+ if (this.#builderExtras.length > 0)
1514
+ return false;
1515
+ if (this.#withCounts.length > 0 || this.#withAggregates.length > 0)
1516
+ return false;
1517
+ if (this.#selectColumns || this.#addSelectColumns.length > 0)
1518
+ return false;
1519
+ if (this.#selectRaws.length > 0 || this.#distinct)
1520
+ return false;
1521
+ if (this.#groupByColumns.length > 0)
1522
+ return false;
1523
+ if (this.#havings.length > 0 || this.#havingRaws.length > 0)
1524
+ return false;
1525
+ if (this.#lock)
1526
+ return false;
1527
+ if (this.#whereIns.length > 0 || this.#whereNulls.length > 0)
1528
+ return false;
1529
+ if (this.#whereBetweens.length > 0 || this.#whereColumns.length > 0)
1530
+ return false;
1531
+ if (this.#whereRaws.length > 0 || this.#whereLikes.length > 0)
1532
+ return false;
1533
+ if (this.#dateWheres.length > 0)
1534
+ return false;
1535
+ return true;
1536
+ }
1537
+ /** Compile SELECT … WHERE [parent] EXISTS (cached subquery) [LIMIT]. */
1538
+ #compileHasExistsSelect(conn = this.#queryConnection()) {
1539
+ const dialect = conn.dialect;
1540
+ const params = [];
1541
+ let key = this.#withTrashed ? "T" : this.#onlyTrashed ? "O" : "N";
1542
+ key += `\u001e${this.model.table}`;
1543
+ for (const w of this.#wheres) {
1544
+ key += `\u001e${w.boolean}\u001e${w.column}\u001e${w.op}`;
1545
+ if (w.op !== "__null__" && w.op !== "__notnull__")
1546
+ params.push(w.value);
1547
+ }
1548
+ for (const c of this.#hasConstraints) {
1549
+ key += `\u001eH\u001e${c.not ? "1" : "0"}\u001e${c.or ? "1" : "0"}\u001e${c.inColumn ?? ""}\u001e${c.existsSql}`;
1550
+ if (c.existsBindings?.length)
1551
+ params.push(...c.existsBindings);
1552
+ }
1553
+ for (const o of this.#orders) {
1554
+ if ("raw" in o)
1555
+ key += `\u001eR\u001e${o.raw}`;
1556
+ else
1557
+ key += `\u001eO\u001e${o.column}\u001e${o.direction}`;
1558
+ }
1559
+ if (this.#limitValue !== undefined)
1560
+ key += `\u001eL\u001e${this.#limitValue}`;
1561
+ if (this.#offsetValue !== undefined)
1562
+ key += `\u001eF\u001e${this.#offsetValue}`;
1563
+ if (usesSoftDeletes(this.model))
1564
+ key += "\u001eS";
1565
+ let byModel = hasExistsSelectSqlCache.get(this.model);
1566
+ if (!byModel) {
1567
+ byModel = new Map();
1568
+ hasExistsSelectSqlCache.set(this.model, byModel);
1569
+ }
1570
+ let sql = byModel.get(key);
1571
+ if (sql !== undefined)
1572
+ return { sql, params };
1573
+ const wrap = (name) => wrapSqlName(dialect, name);
1574
+ const whereParts = [];
1575
+ const pushBool = (boolean) => {
1576
+ if (whereParts.length === 0)
1577
+ return "";
1578
+ return boolean === "or" ? " OR " : " AND ";
1579
+ };
1580
+ if (usesSoftDeletes(this.model)) {
1581
+ const col = wrap(deletedAtColumn(this.model));
1582
+ if (this.#onlyTrashed) {
1583
+ whereParts.push(`${col} IS NOT NULL`);
1584
+ }
1585
+ else if (!this.#withTrashed) {
1586
+ whereParts.push(`${col} IS NULL`);
1587
+ }
1588
+ }
1589
+ for (const w of this.#wheres) {
1590
+ const bool = pushBool(w.boolean);
1591
+ const col = wrap(w.column);
1592
+ if (w.op === "__null__") {
1593
+ whereParts.push(`${bool}${col} IS NULL`);
1594
+ continue;
1595
+ }
1596
+ if (w.op === "__notnull__") {
1597
+ whereParts.push(`${bool}${col} IS NOT NULL`);
1598
+ continue;
1599
+ }
1600
+ whereParts.push(`${bool}${col} ${w.op} ?`);
1601
+ }
1602
+ for (const c of this.#hasConstraints) {
1603
+ const bool = pushBool(c.or ? "or" : "and");
1604
+ if (c.inColumn) {
1605
+ whereParts.push(`${bool}${c.inColumn} ${c.not ? "NOT IN" : "IN"} (${c.existsSql})`);
1606
+ }
1607
+ else {
1608
+ whereParts.push(`${bool}${c.not ? "NOT EXISTS" : "EXISTS"} (${c.existsSql})`);
1609
+ }
1610
+ }
1611
+ sql = `SELECT * FROM ${this.model.table}`;
1612
+ if (whereParts.length > 0) {
1613
+ sql += ` WHERE ${whereParts.join("")}`;
1614
+ }
1615
+ if (this.#orders.length > 0) {
1616
+ sql += ` ORDER BY ${this.#orders
1617
+ .map((o) => {
1618
+ if ("raw" in o)
1619
+ return o.raw;
1620
+ return `${wrap(o.column)} ${o.direction.toUpperCase()}`;
1621
+ })
1622
+ .join(", ")}`;
1623
+ }
1624
+ if (this.#limitValue !== undefined) {
1625
+ sql += ` LIMIT ${this.#limitValue}`;
1626
+ }
1627
+ if (this.#offsetValue !== undefined) {
1628
+ sql += ` OFFSET ${this.#offsetValue}`;
1629
+ }
1630
+ byModel.set(key, sql);
1631
+ return { sql, params };
1632
+ }
1633
+ #buildQuery() {
1634
+ let q = this.#baseTable();
1635
+ q = this.#applySoftDeletes(q);
1636
+ q = this.#applyGlobalScopesTo(q);
1637
+ this.#qualifyParentConstraintsForJoins();
1638
+ q = this.applyConstraintsTo(q);
1639
+ if (this.#joins.length > 0) {
1640
+ if (!this.#selectColumns && this.#addSelectColumns.length === 0) {
1641
+ q = q.select(`${this.model.table}.*`);
1642
+ }
1643
+ for (const j of this.#joins) {
1644
+ if (j.type === "left") {
1645
+ q = q.leftJoin(j.table, j.first, j.op, j.second);
1646
+ }
1647
+ else if (j.type === "right") {
1648
+ q = q.rightJoin(j.table, j.first, j.op, j.second);
1649
+ }
1650
+ else {
1651
+ q = q.join(j.table, j.first, j.op, j.second);
1652
+ }
1653
+ }
1654
+ }
1655
+ if (this.#selectColumns) {
1656
+ q = q.select(...this.#selectColumns);
1657
+ }
1658
+ if (this.#addSelectColumns.length > 0) {
1659
+ q = q.addSelect(...this.#addSelectColumns);
1660
+ }
1661
+ for (const raw of this.#selectRaws) {
1662
+ q = q.selectRaw(raw.expression, raw.bindings);
1663
+ }
1664
+ if (this.#distinct)
1665
+ q = q.distinct();
1666
+ for (const o of this.#orders) {
1667
+ if ("raw" in o) {
1668
+ q =
1669
+ o.raw === "__inRandomOrder__"
1670
+ ? q.inRandomOrder()
1671
+ : q.orderByRaw(o.raw);
1672
+ }
1673
+ else
1674
+ q = q.orderBy(o.column, o.direction);
1675
+ }
1676
+ if (this.#groupByColumns.length > 0) {
1677
+ q = q.groupBy(...this.#groupByColumns);
1678
+ }
1679
+ for (const h of this.#havings) {
1680
+ q = q.having(h.column, h.op, h.value);
1681
+ }
1682
+ for (const h of this.#havingRaws) {
1683
+ q = q.havingRaw(h.sql, h.bindings);
1684
+ }
1685
+ for (const extra of this.#builderExtras) {
1686
+ extra(q);
1687
+ }
1688
+ const aggregates = this.#withAggregates.length > 0
1689
+ ? this.#withAggregates
1690
+ : this.#withCounts.map((relation) => ({
1691
+ relation,
1692
+ alias: `${relation}_count`,
1693
+ fn: "count",
1694
+ column: undefined,
1695
+ }));
1696
+ if (aggregates.length > 0) {
1697
+ q = q.select(`${this.model.table}.*`);
1698
+ for (const agg of aggregates) {
1699
+ const meta = resolveRelation(this.model, agg.relation);
1700
+ if (!meta)
1701
+ continue;
1702
+ const expr = this.#aggregateSubquery(meta, agg.fn, agg.column);
1703
+ if (!expr)
1704
+ continue;
1705
+ const alias = wrapSqlName(this.model.getConnection().dialect, agg.alias);
1706
+ q = q.selectRaw(`(${expr}) as ${alias}`);
1707
+ }
1708
+ }
1709
+ if (this.#limitValue !== undefined)
1710
+ q = q.limit(this.#limitValue);
1711
+ if (this.#offsetValue !== undefined)
1712
+ q = q.offset(this.#offsetValue);
1713
+ if (this.#lock === "update")
1714
+ q = q.lockForUpdate();
1715
+ if (this.#lock === "share")
1716
+ q = q.sharedLock();
1717
+ return q;
1718
+ }
1719
+ #applyGlobalScopesTo(q) {
1720
+ if (this.#withoutGlobalScopes === true)
1721
+ return q;
1722
+ if (!hasGlobalScopes(this.model))
1723
+ return q;
1724
+ for (const [name, scope] of getGlobalScopes(this.model)) {
1725
+ if (this.#withoutGlobalScopes?.has(name))
1726
+ continue;
1727
+ const temp = new ModelQuery(this.model, {
1728
+ withoutGlobalScopes: true,
1729
+ withTrashed: this.#withTrashed,
1730
+ onlyTrashed: this.#onlyTrashed,
1731
+ });
1732
+ scope(temp);
1733
+ // Joined related tables often share column names (e.g. tenant_id).
1734
+ if (this.#joins.length > 0) {
1735
+ temp.qualifyColumns(this.model.table);
1736
+ }
1737
+ q = temp.applyConstraintsTo(q);
1738
+ }
1739
+ return q;
1740
+ }
1741
+ /** Parent-table prefix when BelongsTo whereHas (or other joins) are present. */
1742
+ #qualifyParentColumn(column) {
1743
+ if (this.#joins.length === 0 || column.includes("."))
1744
+ return column;
1745
+ return `${this.model.table}.${column}`;
1746
+ }
1747
+ /**
1748
+ * Qualify unqualified parent wheres before JOIN so shared columns
1749
+ * (tenant_id, …) are not ambiguous on Postgres.
1750
+ */
1751
+ #qualifyParentConstraintsForJoins() {
1752
+ if (this.#joins.length === 0)
1753
+ return;
1754
+ this.qualifyColumns(this.model.table);
1755
+ }
1756
+ /**
1757
+ * Prefix unqualified columns with `table.` (BelongsTo whereHas → JOIN path).
1758
+ */
1759
+ qualifyColumns(table) {
1760
+ const qual = (column) => column.includes(".") ? column : `${table}.${column}`;
1761
+ for (const w of this.#wheres)
1762
+ w.column = qual(w.column);
1763
+ for (const w of this.#whereIns)
1764
+ w.column = qual(w.column);
1765
+ for (const w of this.#whereNulls)
1766
+ w.column = qual(w.column);
1767
+ for (const w of this.#whereBetweens)
1768
+ w.column = qual(w.column);
1769
+ for (const w of this.#whereLikes)
1770
+ w.column = qual(w.column);
1771
+ for (const w of this.#dateWheres)
1772
+ w.column = qual(w.column);
1773
+ for (const w of this.#whereColumns) {
1774
+ w.first = qual(w.first);
1775
+ w.second = qual(w.second);
1776
+ }
1777
+ return this;
1778
+ }
1779
+ /** Apply this query's wheres onto a QueryBuilder (exists subqueries). */
1780
+ applyConstraintsTo(q) {
1781
+ for (const w of this.#wheres) {
1782
+ if (w.op === "__null__") {
1783
+ q =
1784
+ w.boolean === "or" ? q.orWhereNull(w.column) : q.whereNull(w.column);
1785
+ continue;
1786
+ }
1787
+ if (w.op === "__notnull__") {
1788
+ q =
1789
+ w.boolean === "or"
1790
+ ? q.orWhereNotNull(w.column)
1791
+ : q.whereNotNull(w.column);
1792
+ continue;
1793
+ }
1794
+ q =
1795
+ w.boolean === "or"
1796
+ ? q.orWhere(w.column, w.op, w.value)
1797
+ : q.where(w.column, w.op, w.value);
1798
+ }
1799
+ for (const w of this.#whereIns) {
1800
+ if (w.boolean === "or") {
1801
+ q = w.not
1802
+ ? q.orWhereNotIn(w.column, w.values)
1803
+ : q.orWhereIn(w.column, w.values);
1804
+ }
1805
+ else {
1806
+ q = w.not
1807
+ ? q.whereNotIn(w.column, w.values)
1808
+ : q.whereIn(w.column, w.values);
1809
+ }
1810
+ }
1811
+ for (const w of this.#whereNulls) {
1812
+ if (w.boolean === "or") {
1813
+ q = w.not ? q.orWhereNotNull(w.column) : q.orWhereNull(w.column);
1814
+ }
1815
+ else {
1816
+ q = w.not ? q.whereNotNull(w.column) : q.whereNull(w.column);
1817
+ }
1818
+ }
1819
+ for (const w of this.#whereBetweens) {
1820
+ if (w.boolean === "or") {
1821
+ q = w.not
1822
+ ? q.orWhereNotBetween(w.column, w.values)
1823
+ : q.orWhereBetween(w.column, w.values);
1824
+ }
1825
+ else {
1826
+ q = w.not
1827
+ ? q.whereNotBetween(w.column, w.values)
1828
+ : q.whereBetween(w.column, w.values);
1829
+ }
1830
+ }
1831
+ for (const w of this.#whereColumns) {
1832
+ q =
1833
+ w.boolean === "or"
1834
+ ? q.orWhereColumn(w.first, w.op, w.second)
1835
+ : q.whereColumn(w.first, w.op, w.second);
1836
+ }
1837
+ for (const w of this.#whereRaws) {
1838
+ q =
1839
+ w.boolean === "or"
1840
+ ? q.orWhereRaw(w.sql, w.bindings)
1841
+ : q.whereRaw(w.sql, w.bindings);
1842
+ }
1843
+ for (const w of this.#whereLikes) {
1844
+ if (w.boolean === "or") {
1845
+ q = w.not
1846
+ ? q.orWhereNotLike(w.column, w.value, w.caseSensitive)
1847
+ : q.orWhereLike(w.column, w.value, w.caseSensitive);
1848
+ }
1849
+ else {
1850
+ q = w.not
1851
+ ? q.whereNotLike(w.column, w.value, w.caseSensitive)
1852
+ : q.whereLike(w.column, w.value, w.caseSensitive);
1853
+ }
1854
+ }
1855
+ for (const w of this.#dateWheres) {
1856
+ if (w.kind === "date")
1857
+ q = q.whereDate(w.column, w.op, w.value);
1858
+ else if (w.kind === "year")
1859
+ q = q.whereYear(w.column, w.op, Number(w.value));
1860
+ else if (w.kind === "month")
1861
+ q = q.whereMonth(w.column, w.op, Number(w.value));
1862
+ else
1863
+ q = q.whereDay(w.column, w.op, Number(w.value));
1864
+ }
1865
+ for (const constraint of this.#hasConstraints) {
1866
+ q = this.#applyHasConstraint(q, constraint);
1867
+ }
1868
+ for (const morph of this.#morphHasConstraints) {
1869
+ q = this.#applyMorphHasConstraint(q, morph);
1870
+ }
1871
+ for (const group of this.#nestedGroups) {
1872
+ const run = (subQ) => {
1873
+ const nested = this.model.newQuery({
1874
+ withoutGlobalScopes: true,
1875
+ withTrashed: this.#withTrashed,
1876
+ onlyTrashed: this.#onlyTrashed,
1877
+ nestedWhereGroup: true,
1878
+ });
1879
+ group.callback(nested);
1880
+ nested.applyConstraintsTo(subQ);
1881
+ };
1882
+ if (group.not) {
1883
+ q = group.boolean === "or" ? q.orWhereNot(run) : q.whereNot(run);
1884
+ }
1885
+ else {
1886
+ q =
1887
+ group.boolean === "or" ? q.orWhereNested(run) : q.whereNested(run);
1888
+ }
1889
+ }
1890
+ return q;
1891
+ }
1892
+ /**
1893
+ * HasMany / BelongsToMany / MorphToMany whereHas → correlated EXISTS.
1894
+ * Resolve meta + run related callback once at call time (same freeze as BelongsTo join expand).
1895
+ * Mergeable flat callbacks become a lightweight apply (no ModelQuery retained).
1896
+ */
1897
+ #pushHasConstraint(relation, not, or, callback, metaHint) {
1898
+ const meta = metaHint === undefined ? resolveRelation(this.model, relation) : metaHint;
1899
+ if (!meta) {
1900
+ (this.#hasConstraints = mqMut(this.#hasConstraints)).push({
1901
+ relation,
1902
+ not,
1903
+ or,
1904
+ callback,
1905
+ });
1906
+ return this;
1907
+ }
1908
+ if (meta.kind === "has" ||
1909
+ meta.kind === "belongsToMany" ||
1910
+ meta.kind === "morphToMany") {
1911
+ let applyRelated;
1912
+ let relatedQuery;
1913
+ if (callback) {
1914
+ const rq = meta.related.newQuery({
1915
+ withoutGlobalScopes: true,
1916
+ });
1917
+ callback(rq);
1918
+ if (this.#relatedConstraintsAreMergeable(rq)) {
1919
+ applyRelated = this.#bindFlatRelatedApply(rq);
1920
+ }
1921
+ else {
1922
+ relatedQuery = rq;
1923
+ applyRelated = (sub) => {
1924
+ rq.applyConstraintsTo(sub);
1925
+ };
1926
+ }
1927
+ }
1928
+ this.#pushHasConstraintWithRelatedApply(relation, not, or, meta, applyRelated);
1929
+ // Attach relatedQuery on the constraint we just pushed (belongsTo or/not reuse).
1930
+ if (relatedQuery) {
1931
+ const last = this.#hasConstraints[this.#hasConstraints.length - 1];
1932
+ last.relatedQuery = relatedQuery;
1933
+ }
1934
+ return this;
1935
+ }
1936
+ // belongsTo or/not / unsupported: cache meta (+ optional relatedQuery) for #applyHasConstraint.
1937
+ let relatedQuery;
1938
+ if (callback) {
1939
+ relatedQuery = meta.related.newQuery({
1940
+ withoutGlobalScopes: true,
1941
+ });
1942
+ callback(relatedQuery);
1943
+ }
1944
+ (this.#hasConstraints = mqMut(this.#hasConstraints)).push({
1945
+ relation,
1946
+ not,
1947
+ or,
1948
+ meta,
1949
+ relatedQuery,
1950
+ });
1951
+ return this;
1952
+ }
1953
+ /**
1954
+ * HasMany / BelongsToMany / MorphToMany existence constraint.
1955
+ * Call-time bake: HasMany → EXISTS (SELECT 1 … LIMIT 1); BelongsToMany →
1956
+ * parent.pk IN (SELECT pivot.fk …) when not tenant-correlated (SQLite avoids
1957
+ * correlated SCAN). Morph tenant correlation keeps EXISTS.
1958
+ */
1959
+ #pushHasConstraintWithRelatedApply(relation, not, or, meta, applyRelated) {
1960
+ const parentTable = this.model.table;
1961
+ let applyExists;
1962
+ let inColumn;
1963
+ if (meta.kind === "has") {
1964
+ const relatedTable = meta.related.table;
1965
+ const foreignKey = meta.foreignKey;
1966
+ const localKey = meta.localKey;
1967
+ const selfAlias = relatedTable === parentTable ? `${relatedTable}_has` : null;
1968
+ applyExists = (sub) => {
1969
+ if (selfAlias) {
1970
+ sub.from(`${relatedTable} as ${selfAlias}`);
1971
+ sub.whereColumn(`${selfAlias}.${foreignKey}`, `${parentTable}.${localKey}`);
1972
+ }
1973
+ else {
1974
+ sub.from(relatedTable);
1975
+ sub.whereColumn(`${relatedTable}.${foreignKey}`, `${parentTable}.${localKey}`);
1976
+ }
1977
+ applyRelated?.(sub);
1978
+ };
1979
+ }
1980
+ else if (meta.kind === "belongsToMany" || meta.kind === "morphToMany") {
1981
+ const relatedTable = meta.related.table;
1982
+ const relatedPk = meta.related.primaryKey;
1983
+ const pivotTable = meta.pivotTable;
1984
+ const relatedPivotKey = meta.relatedPivotKey;
1985
+ const foreignPivotKey = meta.foreignPivotKey;
1986
+ const localKey = meta.localKey;
1987
+ const morphTypeColumn = meta.kind === "morphToMany" ? meta.morphTypeColumn : null;
1988
+ const morphTypes = meta.kind === "morphToMany" ? meta.morphTypes : null;
1989
+ const pivotTenantKey = meta.kind === "morphToMany" ? meta.pivotTenantKey : null;
1990
+ const parentTenantKey = meta.kind === "morphToMany" ? meta.parentTenantKey : null;
1991
+ // Tenant correlation must stay EXISTS; otherwise use IN (same rows, much
1992
+ // faster on SQLite without pivot indexes).
1993
+ const tenantCorrelated = Boolean(pivotTenantKey && parentTenantKey);
1994
+ if (!tenantCorrelated) {
1995
+ inColumn = `${parentTable}.${localKey}`;
1996
+ applyExists = (sub) => {
1997
+ sub.from(relatedTable);
1998
+ sub.join(pivotTable, `${pivotTable}.${relatedPivotKey}`, "=", `${relatedTable}.${relatedPk}`);
1999
+ if (morphTypeColumn && morphTypes) {
2000
+ sub.whereIn(`${pivotTable}.${morphTypeColumn}`, morphTypes);
2001
+ }
2002
+ applyRelated?.(sub);
2003
+ sub.select(`${pivotTable}.${foreignPivotKey}`);
2004
+ };
2005
+ }
2006
+ else {
2007
+ applyExists = (sub) => {
2008
+ sub.from(relatedTable);
2009
+ sub.join(pivotTable, `${pivotTable}.${relatedPivotKey}`, "=", `${relatedTable}.${relatedPk}`);
2010
+ sub.whereColumn(`${pivotTable}.${foreignPivotKey}`, `${parentTable}.${localKey}`);
2011
+ if (morphTypeColumn && morphTypes) {
2012
+ sub.whereIn(`${pivotTable}.${morphTypeColumn}`, morphTypes);
2013
+ sub.whereColumn(`${pivotTable}.${pivotTenantKey}`, `${parentTable}.${parentTenantKey}`);
2014
+ }
2015
+ applyRelated?.(sub);
2016
+ };
2017
+ }
2018
+ }
2019
+ let existsSql;
2020
+ let existsBindings;
2021
+ if (applyExists) {
2022
+ const sub = this.#baseTable();
2023
+ applyExists(sub);
2024
+ // SQLite: correlated EXISTS without LIMIT 1 scans the full related table
2025
+ // per parent row (~10–50× slower). Match withExists aggregate shape.
2026
+ if (!inColumn) {
2027
+ sub.select("1");
2028
+ sub.limit(1);
2029
+ }
2030
+ existsSql = sub.toSql();
2031
+ existsBindings = sub.getBindings();
2032
+ }
2033
+ (this.#hasConstraints = mqMut(this.#hasConstraints)).push({
2034
+ relation,
2035
+ not,
2036
+ or,
2037
+ meta,
2038
+ applyExists,
2039
+ existsSql,
2040
+ existsBindings,
2041
+ inColumn,
2042
+ });
2043
+ return this;
2044
+ }
2045
+ /**
2046
+ * Snapshot flat related wheres into a closure (no ModelQuery on the hot apply path).
2047
+ */
2048
+ #bindFlatRelatedApply(from) {
2049
+ from = unwrapModelQuery(from);
2050
+ const wheres = from.#wheres.length ? from.#wheres.slice() : null;
2051
+ const whereIns = from.#whereIns.length ? from.#whereIns.slice() : null;
2052
+ const whereNulls = from.#whereNulls.length ? from.#whereNulls.slice() : null;
2053
+ const whereBetweens = from.#whereBetweens.length
2054
+ ? from.#whereBetweens.slice()
2055
+ : null;
2056
+ const whereColumns = from.#whereColumns.length
2057
+ ? from.#whereColumns.slice()
2058
+ : null;
2059
+ const whereRaws = from.#whereRaws.length ? from.#whereRaws.slice() : null;
2060
+ const whereLikes = from.#whereLikes.length ? from.#whereLikes.slice() : null;
2061
+ const dateWheres = from.#dateWheres.length ? from.#dateWheres.slice() : null;
2062
+ if (!wheres &&
2063
+ !whereIns &&
2064
+ !whereNulls &&
2065
+ !whereBetweens &&
2066
+ !whereColumns &&
2067
+ !whereRaws &&
2068
+ !whereLikes &&
2069
+ !dateWheres) {
2070
+ return () => { };
2071
+ }
2072
+ return (q) => {
2073
+ if (wheres) {
2074
+ for (const w of wheres) {
2075
+ if (w.op === "__null__") {
2076
+ q =
2077
+ w.boolean === "or" ? q.orWhereNull(w.column) : q.whereNull(w.column);
2078
+ continue;
2079
+ }
2080
+ if (w.op === "__notnull__") {
2081
+ q =
2082
+ w.boolean === "or"
2083
+ ? q.orWhereNotNull(w.column)
2084
+ : q.whereNotNull(w.column);
2085
+ continue;
2086
+ }
2087
+ q =
2088
+ w.boolean === "or"
2089
+ ? q.orWhere(w.column, w.op, w.value)
2090
+ : q.where(w.column, w.op, w.value);
2091
+ }
2092
+ }
2093
+ if (whereIns) {
2094
+ for (const w of whereIns) {
2095
+ if (w.boolean === "or") {
2096
+ q = w.not
2097
+ ? q.orWhereNotIn(w.column, w.values)
2098
+ : q.orWhereIn(w.column, w.values);
2099
+ }
2100
+ else {
2101
+ q = w.not
2102
+ ? q.whereNotIn(w.column, w.values)
2103
+ : q.whereIn(w.column, w.values);
2104
+ }
2105
+ }
2106
+ }
2107
+ if (whereNulls) {
2108
+ for (const w of whereNulls) {
2109
+ if (w.boolean === "or") {
2110
+ q = w.not ? q.orWhereNotNull(w.column) : q.orWhereNull(w.column);
2111
+ }
2112
+ else {
2113
+ q = w.not ? q.whereNotNull(w.column) : q.whereNull(w.column);
2114
+ }
2115
+ }
2116
+ }
2117
+ if (whereBetweens) {
2118
+ for (const w of whereBetweens) {
2119
+ if (w.boolean === "or") {
2120
+ q = w.not
2121
+ ? q.orWhereNotBetween(w.column, w.values)
2122
+ : q.orWhereBetween(w.column, w.values);
2123
+ }
2124
+ else {
2125
+ q = w.not
2126
+ ? q.whereNotBetween(w.column, w.values)
2127
+ : q.whereBetween(w.column, w.values);
2128
+ }
2129
+ }
2130
+ }
2131
+ if (whereColumns) {
2132
+ for (const w of whereColumns) {
2133
+ q =
2134
+ w.boolean === "or"
2135
+ ? q.orWhereColumn(w.first, w.op, w.second)
2136
+ : q.whereColumn(w.first, w.op, w.second);
2137
+ }
2138
+ }
2139
+ if (whereRaws) {
2140
+ for (const w of whereRaws) {
2141
+ q =
2142
+ w.boolean === "or"
2143
+ ? q.orWhereRaw(w.sql, w.bindings)
2144
+ : q.whereRaw(w.sql, w.bindings);
2145
+ }
2146
+ }
2147
+ if (whereLikes) {
2148
+ for (const w of whereLikes) {
2149
+ if (w.boolean === "or") {
2150
+ q = w.not
2151
+ ? q.orWhereNotLike(w.column, w.value, w.caseSensitive)
2152
+ : q.orWhereLike(w.column, w.value, w.caseSensitive);
2153
+ }
2154
+ else {
2155
+ q = w.not
2156
+ ? q.whereNotLike(w.column, w.value, w.caseSensitive)
2157
+ : q.whereLike(w.column, w.value, w.caseSensitive);
2158
+ }
2159
+ }
2160
+ }
2161
+ if (dateWheres) {
2162
+ for (const w of dateWheres) {
2163
+ if (w.kind === "date")
2164
+ q = q.whereDate(w.column, w.op, w.value);
2165
+ else if (w.kind === "year")
2166
+ q = q.whereYear(w.column, w.op, Number(w.value));
2167
+ else if (w.kind === "month")
2168
+ q = q.whereMonth(w.column, w.op, Number(w.value));
2169
+ else
2170
+ q = q.whereDay(w.column, w.op, Number(w.value));
2171
+ }
2172
+ }
2173
+ };
2174
+ }
2175
+ /**
2176
+ * BelongsTo whereHas/whereRelation → INNER JOIN on related PK (no parent duplication).
2177
+ * Expands at call time so #buildQuery matches plain join() (no per-get resolve/newQuery).
2178
+ * When the related table is already joined (two BelongsTo on the same table),
2179
+ * uses `table as table_relation` so Postgres does not raise 42712.
2180
+ * Returns related table alias used in the join.
2181
+ */
2182
+ #expandBelongsToHasJoin(meta, callback, relation = "") {
2183
+ const parentTable = this.model.table;
2184
+ const relatedTable = meta.related.table;
2185
+ const relatedAlias = this.#uniqueBelongsToJoinAlias(relatedTable, relation);
2186
+ const joinTable = relatedAlias === relatedTable
2187
+ ? relatedTable
2188
+ : `${relatedTable} as ${relatedAlias}`;
2189
+ (this.#joins = mqMut(this.#joins)).push({
2190
+ type: "inner",
2191
+ table: joinTable,
2192
+ first: `${parentTable}.${meta.foreignKey}`,
2193
+ op: "=",
2194
+ second: `${relatedAlias}.${meta.ownerKey}`,
2195
+ });
2196
+ if (usesSoftDeletes(meta.related)) {
2197
+ const delCol = deletedAtColumn(meta.related);
2198
+ (this.#wheres = mqMut(this.#wheres)).push({
2199
+ column: `${relatedAlias}.${delCol}`,
2200
+ op: "__null__",
2201
+ value: null,
2202
+ boolean: "and",
2203
+ });
2204
+ }
2205
+ if (callback) {
2206
+ const relatedQuery = meta.related.newQuery({
2207
+ withoutGlobalScopes: true,
2208
+ });
2209
+ callback(relatedQuery);
2210
+ relatedQuery.qualifyColumns(relatedAlias);
2211
+ if (this.#relatedConstraintsAreMergeable(relatedQuery)) {
2212
+ this.#mergeRelatedConstraints(relatedQuery);
2213
+ }
2214
+ else {
2215
+ // Nested has / groups must keep related model identity → apply on QB each build.
2216
+ (this.#builderExtras = mqMut(this.#builderExtras)).push((q) => {
2217
+ relatedQuery.applyConstraintsTo(q);
2218
+ });
2219
+ }
2220
+ }
2221
+ return relatedAlias;
2222
+ }
2223
+ /** Alias already used by a join table (`accounts` or `accounts as foo`). */
2224
+ #joinedNames() {
2225
+ const names = new Set();
2226
+ for (const join of this.#joins) {
2227
+ const parts = join.table.split(/\s+as\s+/i);
2228
+ names.add(parts[0].trim());
2229
+ names.add(parts[parts.length - 1].trim());
2230
+ }
2231
+ return names;
2232
+ }
2233
+ #uniqueBelongsToJoinAlias(relatedTable, relation) {
2234
+ const taken = this.#joinedNames();
2235
+ const parentTable = this.model.table;
2236
+ if (relatedTable === parentTable) {
2237
+ let alias = `${relatedTable}_bt`;
2238
+ let n = 2;
2239
+ while (taken.has(alias)) {
2240
+ alias = `${relatedTable}_bt${n++}`;
2241
+ }
2242
+ return alias;
2243
+ }
2244
+ if (!taken.has(relatedTable)) {
2245
+ return relatedTable;
2246
+ }
2247
+ const fromRelation = relation
2248
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
2249
+ .replace(/[^\w]+/g, "_")
2250
+ .toLowerCase();
2251
+ const base = fromRelation
2252
+ ? `${relatedTable}_${fromRelation}`
2253
+ : `${relatedTable}_bt`;
2254
+ let alias = base;
2255
+ let n = 2;
2256
+ while (taken.has(alias)) {
2257
+ alias = `${base}_${n++}`;
2258
+ }
2259
+ return alias;
2260
+ }
2261
+ /** True when related callback only added flat wheres (safe to merge onto parent). */
2262
+ #relatedConstraintsAreMergeable(from) {
2263
+ from = unwrapModelQuery(from);
2264
+ return (from.#hasConstraints.length === 0 &&
2265
+ from.#morphHasConstraints.length === 0 &&
2266
+ from.#nestedGroups.length === 0 &&
2267
+ from.#joins.length === 0 &&
2268
+ from.#builderExtras.length === 0 &&
2269
+ from.#withCounts.length === 0 &&
2270
+ from.#withAggregates.length === 0);
2271
+ }
2272
+ /** Copy flat constraints from a related whereHas callback onto this query. */
2273
+ #mergeRelatedConstraints(from) {
2274
+ from = unwrapModelQuery(from);
2275
+ if (from.#wheres.length > 0) {
2276
+ (this.#wheres = mqMut(this.#wheres)).push(...from.#wheres);
2277
+ }
2278
+ if (from.#whereIns.length > 0) {
2279
+ (this.#whereIns = mqMut(this.#whereIns)).push(...from.#whereIns);
2280
+ }
2281
+ if (from.#whereNulls.length > 0) {
2282
+ (this.#whereNulls = mqMut(this.#whereNulls)).push(...from.#whereNulls);
2283
+ }
2284
+ if (from.#whereBetweens.length > 0) {
2285
+ (this.#whereBetweens = mqMut(this.#whereBetweens)).push(...from.#whereBetweens);
2286
+ }
2287
+ if (from.#whereColumns.length > 0) {
2288
+ (this.#whereColumns = mqMut(this.#whereColumns)).push(...from.#whereColumns);
2289
+ }
2290
+ if (from.#whereRaws.length > 0) {
2291
+ (this.#whereRaws = mqMut(this.#whereRaws)).push(...from.#whereRaws);
2292
+ }
2293
+ if (from.#whereLikes.length > 0) {
2294
+ (this.#whereLikes = mqMut(this.#whereLikes)).push(...from.#whereLikes);
2295
+ }
2296
+ if (from.#dateWheres.length > 0) {
2297
+ (this.#dateWheres = mqMut(this.#dateWheres)).push(...from.#dateWheres);
2298
+ }
2299
+ }
2300
+ #applyHasConstraint(q, constraint) {
2301
+ // Fast path: call-time compiled EXISTS / IN (HasMany / BelongsToMany / MorphToMany).
2302
+ if (constraint.existsSql !== undefined) {
2303
+ const bindings = constraint.existsBindings ?? [];
2304
+ const sql = constraint.inColumn
2305
+ ? constraint.not
2306
+ ? `${constraint.inColumn} not in (${constraint.existsSql})`
2307
+ : `${constraint.inColumn} in (${constraint.existsSql})`
2308
+ : constraint.not
2309
+ ? `not exists (${constraint.existsSql})`
2310
+ : `exists (${constraint.existsSql})`;
2311
+ return constraint.or
2312
+ ? q.orWhereRaw(sql, bindings)
2313
+ : q.whereRaw(sql, bindings);
2314
+ }
2315
+ if (constraint.applyExists) {
2316
+ if (constraint.or) {
2317
+ return constraint.not
2318
+ ? q.orWhereNotExists(constraint.applyExists)
2319
+ : q.orWhereExists(constraint.applyExists);
2320
+ }
2321
+ return constraint.not
2322
+ ? q.whereNotExists(constraint.applyExists)
2323
+ : q.whereExists(constraint.applyExists);
2324
+ }
2325
+ const meta = constraint.meta ?? resolveRelation(this.model, constraint.relation);
2326
+ if (!meta) {
2327
+ throw new Error(`whereHas supports HasMany/HasOne/BelongsTo/BelongsToMany; [${constraint.relation}] is not supported.`);
2328
+ }
2329
+ // BelongsTo: INNER JOIN matches hand-written join and beats IN (SELECT) on SQLite.
2330
+ // Safe: join is on related PK so parents cannot duplicate. not/or/nested keep IN subquery
2331
+ // (nested where builders only serialize WHERE — JOINs would be dropped).
2332
+ if (meta.kind === "belongsTo") {
2333
+ const parentTable = this.model.table;
2334
+ const relatedTable = meta.related.table;
2335
+ const joinTable = relatedTable === parentTable
2336
+ ? `${relatedTable} as ${relatedTable}_bt`
2337
+ : relatedTable;
2338
+ const relatedAlias = relatedTable === parentTable ? `${relatedTable}_bt` : relatedTable;
2339
+ if (!constraint.not && !constraint.or && !this.#nestedWhereGroup) {
2340
+ if (!this.#selectColumns && this.#addSelectColumns.length === 0) {
2341
+ q = q.select(`${parentTable}.*`);
2342
+ }
2343
+ q = q.join(joinTable, `${parentTable}.${meta.foreignKey}`, "=", `${relatedAlias}.${meta.ownerKey}`);
2344
+ if (usesSoftDeletes(meta.related)) {
2345
+ const delCol = deletedAtColumn(meta.related);
2346
+ q = q.whereNull(`${relatedAlias}.${delCol}`);
2347
+ }
2348
+ if (constraint.relatedQuery) {
2349
+ constraint.relatedQuery.qualifyColumns(relatedAlias);
2350
+ constraint.relatedQuery.applyConstraintsTo(q);
2351
+ }
2352
+ else if (constraint.callback) {
2353
+ const relatedQuery = meta.related.newQuery({
2354
+ withoutGlobalScopes: true,
2355
+ });
2356
+ constraint.callback(relatedQuery);
2357
+ relatedQuery.qualifyColumns(relatedAlias);
2358
+ relatedQuery.applyConstraintsTo(q);
2359
+ }
2360
+ return q;
2361
+ }
2362
+ // or / not / nested where: IN (SELECT ownerKey ...) — reuse call-time relatedQuery when present.
2363
+ let relatedQuery = constraint.relatedQuery;
2364
+ if (!relatedQuery) {
2365
+ relatedQuery = meta.related.newQuery({
2366
+ withoutGlobalScopes: true,
2367
+ });
2368
+ if (constraint.callback) {
2369
+ constraint.callback(relatedQuery);
2370
+ }
2371
+ }
2372
+ // Select owner key for the IN subquery (clone-safe: mutate a dedicated select).
2373
+ const sub = relatedQuery.clone();
2374
+ sub.select(meta.ownerKey);
2375
+ const subSql = sub.toSql();
2376
+ const bindings = sub.getBindings();
2377
+ const col = `${parentTable}.${meta.foreignKey}`;
2378
+ const sql = constraint.not
2379
+ ? `${col} not in (${subSql})`
2380
+ : `${col} in (${subSql})`;
2381
+ return constraint.or
2382
+ ? q.orWhereRaw(sql, bindings)
2383
+ : q.whereRaw(sql, bindings);
2384
+ }
2385
+ // Fallback (unknown kind / legacy constraint without applyExists).
2386
+ const apply = (sub) => {
2387
+ if (meta.kind === "has") {
2388
+ const relatedTable = meta.related.table;
2389
+ const parentTable = this.model.table;
2390
+ if (relatedTable === parentTable) {
2391
+ const alias = `${relatedTable}_has`;
2392
+ sub.from(`${relatedTable} as ${alias}`);
2393
+ sub.whereColumn(`${alias}.${meta.foreignKey}`, `${parentTable}.${meta.localKey}`);
2394
+ }
2395
+ else {
2396
+ sub.from(relatedTable);
2397
+ sub.whereColumn(`${relatedTable}.${meta.foreignKey}`, `${parentTable}.${meta.localKey}`);
2398
+ }
2399
+ }
2400
+ else if (meta.kind === "belongsToMany" ||
2401
+ meta.kind === "morphToMany") {
2402
+ sub.from(meta.related.table);
2403
+ sub.join(meta.pivotTable, `${meta.pivotTable}.${meta.relatedPivotKey}`, "=", `${meta.related.table}.${meta.related.primaryKey}`);
2404
+ sub.whereColumn(`${meta.pivotTable}.${meta.foreignPivotKey}`, `${this.model.table}.${meta.localKey}`);
2405
+ if (meta.kind === "morphToMany") {
2406
+ sub.whereIn(`${meta.pivotTable}.${meta.morphTypeColumn}`, meta.morphTypes);
2407
+ if (meta.pivotTenantKey && meta.parentTenantKey) {
2408
+ sub.whereColumn(`${meta.pivotTable}.${meta.pivotTenantKey}`, `${this.model.table}.${meta.parentTenantKey}`);
2409
+ }
2410
+ }
2411
+ }
2412
+ else {
2413
+ throw new Error(`whereHas supports HasMany/HasOne/BelongsTo/BelongsToMany; [${constraint.relation}] is not supported.`);
2414
+ }
2415
+ if (constraint.relatedQuery) {
2416
+ constraint.relatedQuery.applyConstraintsTo(sub);
2417
+ }
2418
+ else if (constraint.callback) {
2419
+ const relatedQuery = meta.related.newQuery({
2420
+ withoutGlobalScopes: true,
2421
+ });
2422
+ constraint.callback(relatedQuery);
2423
+ relatedQuery.applyConstraintsTo(sub);
2424
+ }
2425
+ };
2426
+ if (constraint.or) {
2427
+ return constraint.not
2428
+ ? q.orWhereNotExists(apply)
2429
+ : q.orWhereExists(apply);
2430
+ }
2431
+ return constraint.not ? q.whereNotExists(apply) : q.whereExists(apply);
2432
+ }
2433
+ #applyMorphHasConstraint(q, constraint) {
2434
+ const apply = (sub) => {
2435
+ // EXISTS related row matching morph type+id; OR across types.
2436
+ sub.whereNested((inner) => {
2437
+ for (let i = 0; i < constraint.types.length; i++) {
2438
+ const Related = constraint.types[i];
2439
+ const type = morphTypeFor(Related);
2440
+ const branch = (b) => {
2441
+ b.where(`${this.model.table}.${constraint.typeColumn}`, type);
2442
+ b.whereExists((ex) => {
2443
+ ex.from(Related.table);
2444
+ ex.whereColumn(`${Related.table}.${Related.primaryKey}`, `${this.model.table}.${constraint.idColumn}`);
2445
+ if (constraint.callback) {
2446
+ const rq = Related.newQuery({ withoutGlobalScopes: true });
2447
+ constraint.callback(rq);
2448
+ rq.applyConstraintsTo(ex);
2449
+ }
2450
+ });
2451
+ };
2452
+ if (i === 0)
2453
+ inner.whereNested(branch);
2454
+ else
2455
+ inner.orWhereNested(branch);
2456
+ }
2457
+ });
2458
+ };
2459
+ // Morph existence is on the parent row's type/id pointing at related — use whereExists
2460
+ // that correlates type + related table.
2461
+ const morphExists = (sub) => {
2462
+ sub.from(this.model.table);
2463
+ // Re-bind: for each type, related.id = parent.idCol AND parent.typeCol = type
2464
+ // Simpler: OR of (type=? AND EXISTS related)
2465
+ sub.whereRaw("1=1");
2466
+ };
2467
+ void morphExists;
2468
+ // Correct approach: whereExists per type combined
2469
+ const combined = (sub) => {
2470
+ for (let i = 0; i < constraint.types.length; i++) {
2471
+ const Related = constraint.types[i];
2472
+ const type = morphTypeFor(Related);
2473
+ const one = (ex) => {
2474
+ ex.from(Related.table);
2475
+ ex.whereColumn(`${Related.table}.${Related.primaryKey}`, `${this.model.table}.${constraint.idColumn}`);
2476
+ ex.where(`${this.model.table}.${constraint.typeColumn}`, type);
2477
+ if (constraint.callback) {
2478
+ const rq = Related.newQuery({ withoutGlobalScopes: true });
2479
+ constraint.callback(rq);
2480
+ rq.applyConstraintsTo(ex);
2481
+ }
2482
+ };
2483
+ if (i === 0) {
2484
+ if (constraint.types.length === 1) {
2485
+ one(sub);
2486
+ }
2487
+ else {
2488
+ sub.whereExists(one);
2489
+ }
2490
+ }
2491
+ else {
2492
+ sub.orWhereExists(one);
2493
+ }
2494
+ }
2495
+ if (constraint.types.length === 1) {
2496
+ // already applied directly onto sub when length===1 — but whereExists wrapper needed
2497
+ }
2498
+ };
2499
+ // Always wrap in whereExists for single type too
2500
+ const wrap = (sub) => {
2501
+ if (constraint.types.length === 1) {
2502
+ const Related = constraint.types[0];
2503
+ const type = morphTypeFor(Related);
2504
+ sub.from(Related.table);
2505
+ sub.whereColumn(`${Related.table}.${Related.primaryKey}`, `${this.model.table}.${constraint.idColumn}`);
2506
+ // type filter on parent — use whereRaw correlated
2507
+ sub.whereRaw(`${this.model.table}.${constraint.typeColumn} = ?`, [type]);
2508
+ if (constraint.callback) {
2509
+ const rq = Related.newQuery({ withoutGlobalScopes: true });
2510
+ constraint.callback(rq);
2511
+ rq.applyConstraintsTo(sub);
2512
+ }
2513
+ return;
2514
+ }
2515
+ combined(sub);
2516
+ };
2517
+ void apply;
2518
+ if (constraint.or) {
2519
+ return constraint.not
2520
+ ? q.orWhereNotExists(wrap)
2521
+ : q.orWhereExists(wrap);
2522
+ }
2523
+ return constraint.not ? q.whereNotExists(wrap) : q.whereExists(wrap);
2524
+ }
2525
+ #aggregateSubquery(meta, fn, column) {
2526
+ const agg = fn === "exists"
2527
+ ? "1"
2528
+ : fn === "count"
2529
+ ? "COUNT(*)"
2530
+ : `${fn.toUpperCase()}(${column ?? "*"})`;
2531
+ if (meta.kind === "has") {
2532
+ const relatedTable = meta.related.table;
2533
+ const parentTable = this.model.table;
2534
+ if (relatedTable === parentTable) {
2535
+ const alias = `${relatedTable}_has`;
2536
+ const where = `${alias}.${meta.foreignKey} = ${parentTable}.${meta.localKey}`;
2537
+ if (fn === "exists") {
2538
+ return `SELECT 1 FROM ${relatedTable} as ${alias} WHERE ${where} LIMIT 1`;
2539
+ }
2540
+ return `SELECT ${agg} FROM ${relatedTable} as ${alias} WHERE ${where}`;
2541
+ }
2542
+ const where = `${relatedTable}.${meta.foreignKey} = ${parentTable}.${meta.localKey}`;
2543
+ if (fn === "exists") {
2544
+ return `SELECT 1 FROM ${relatedTable} WHERE ${where} LIMIT 1`;
2545
+ }
2546
+ return `SELECT ${agg} FROM ${relatedTable} WHERE ${where}`;
2547
+ }
2548
+ if (meta.kind === "belongsTo") {
2549
+ const where = `${meta.related.table}.${meta.ownerKey} = ${this.model.table}.${meta.foreignKey}`;
2550
+ if (fn === "exists") {
2551
+ return `SELECT 1 FROM ${meta.related.table} WHERE ${where} LIMIT 1`;
2552
+ }
2553
+ return `SELECT ${agg} FROM ${meta.related.table} WHERE ${where}`;
2554
+ }
2555
+ if (meta.kind !== "belongsToMany" && meta.kind !== "morphToMany") {
2556
+ return null;
2557
+ }
2558
+ const join = `INNER JOIN ${meta.pivotTable} ON ${meta.pivotTable}.${meta.relatedPivotKey} = ${meta.related.table}.${meta.related.primaryKey}`;
2559
+ const where = `${meta.pivotTable}.${meta.foreignPivotKey} = ${this.model.table}.${meta.localKey}`;
2560
+ if (fn === "exists") {
2561
+ return `SELECT 1 FROM ${meta.related.table} ${join} WHERE ${where} LIMIT 1`;
2562
+ }
2563
+ return `SELECT ${agg} FROM ${meta.related.table} ${join} WHERE ${where}`;
2564
+ }
2565
+ first() {
2566
+ const fast = this.#trySimpleFirst();
2567
+ if (fast !== undefined) {
2568
+ if (fast instanceof Promise) {
2569
+ return fast.then((row) => this.#hydrateFirst(row));
2570
+ }
2571
+ return this.#hydrateFirst(fast);
2572
+ }
2573
+ const rowOrPromise = this.#buildQuery().first();
2574
+ if (rowOrPromise instanceof Promise) {
2575
+ return rowOrPromise.then((row) => this.#hydrateFirst(row));
2576
+ }
2577
+ return this.#hydrateFirst(rowOrPromise);
2578
+ }
2579
+ #hydrateFirst(row) {
2580
+ if (!row)
2581
+ return null;
2582
+ const model = this.#makeModel(row);
2583
+ const retrieved = this.#hasRetrievedListeners;
2584
+ const eager = this.#eagerLoad.length > 0;
2585
+ if (!retrieved && !eager)
2586
+ return model;
2587
+ return (async () => {
2588
+ if (retrieved)
2589
+ await fireModelEvent(model, "retrieved");
2590
+ if (eager)
2591
+ await this.#loadEager([model]);
2592
+ return model;
2593
+ })();
2594
+ }
2595
+ /** Laravel `firstOrFail`. */
2596
+ async firstOrFail() {
2597
+ const model = await this.first();
2598
+ if (!model)
2599
+ throw new ModelNotFoundException();
2600
+ return model;
2601
+ }
2602
+ /**
2603
+ * Fetch matching models as an {@link OrmCollection}.
2604
+ * After {@link rows}, returns plain row objects and does not build models.
2605
+ * For synchronous SQLite reads, use {@link getSync}.
2606
+ */
2607
+ get() {
2608
+ if (this.#asRows) {
2609
+ return this.#collectRows();
2610
+ }
2611
+ return this.#fetchModels();
2612
+ }
2613
+ #fetchModels() {
2614
+ const fast = this.#trySimpleRows();
2615
+ if (fast !== undefined) {
2616
+ if (fast instanceof Promise) {
2617
+ return fast.then((rows) => this.#hydrateGet(rows));
2618
+ }
2619
+ return this.#hydrateGet(fast);
2620
+ }
2621
+ const rowsOrPromise = this.#buildQuery().getRows();
2622
+ if (rowsOrPromise instanceof Promise) {
2623
+ return rowsOrPromise.then((rows) => this.#hydrateGet(rows));
2624
+ }
2625
+ return this.#hydrateGet(rowsOrPromise);
2626
+ }
2627
+ #collectRows() {
2628
+ const fast = this.#trySimpleRows();
2629
+ if (fast !== undefined) {
2630
+ if (fast instanceof Promise) {
2631
+ return fast.then((rows) => this.#materializeRows(rows));
2632
+ }
2633
+ return this.#materializeRows(fast);
2634
+ }
2635
+ const rowsOrPromise = this.#buildQuery().getRows();
2636
+ if (rowsOrPromise instanceof Promise) {
2637
+ return rowsOrPromise.then((rows) => this.#materializeRows(rows));
2638
+ }
2639
+ return this.#materializeRows(rowsOrPromise);
2640
+ }
2641
+ #hydrateGet(rows) {
2642
+ const modelsOrPromise = this.#hydrateRows(rows);
2643
+ if (modelsOrPromise instanceof Promise) {
2644
+ return modelsOrPromise.then(async (models) => {
2645
+ if (this.#eagerLoad.length > 0) {
2646
+ await eagerLoadModels(models, this.#eagerLoad);
2647
+ }
2648
+ return new OrmCollection(models, { owned: true });
2649
+ });
2650
+ }
2651
+ if (this.#eagerLoad.length > 0) {
2652
+ const models = modelsOrPromise;
2653
+ const eager = eagerLoadModels(models, this.#eagerLoad);
2654
+ if (eager instanceof Promise) {
2655
+ return eager.then(() => new OrmCollection(models, { owned: true }));
2656
+ }
2657
+ return new OrmCollection(models, { owned: true });
2658
+ }
2659
+ return new OrmCollection(modelsOrPromise, { owned: true });
2660
+ }
2661
+ /** Row objects for {@link rows}. Falls back to model JSON when the row shortcut cannot apply. */
2662
+ #materializeRows(rows) {
2663
+ if (Array.isArray(rows)) {
2664
+ const prepared = this.#prepareJsonRows(rows);
2665
+ if (prepared)
2666
+ return prepared;
2667
+ }
2668
+ const modelsOrPromise = this.#hydrateRows(rows);
2669
+ const toRows = (models) => {
2670
+ if (this.#eagerLoad.length === 0) {
2671
+ return models.map((model) => model.toArray());
2672
+ }
2673
+ const eager = eagerLoadModels(models, this.#eagerLoad);
2674
+ const map = () => models.map((model) => model.toArray());
2675
+ if (eager instanceof Promise)
2676
+ return eager.then(map);
2677
+ return map();
2678
+ };
2679
+ if (modelsOrPromise instanceof Promise) {
2680
+ return modelsOrPromise.then(toRows);
2681
+ }
2682
+ return toRows(modelsOrPromise);
2683
+ }
2684
+ /**
2685
+ * Plain row objects for JSON when every value matches model JSON
2686
+ * (no casts, hidden, or appends) and each eager belongsTo is null
2687
+ * because its foreign key was not selected or is null.
2688
+ * Returns null to keep the model hydrate path.
2689
+ */
2690
+ #prepareJsonRows(rows) {
2691
+ if (this.#hasRetrievedListeners ||
2692
+ this.#hasQueryCasts ||
2693
+ this.#connection !== undefined) {
2694
+ return null;
2695
+ }
2696
+ const ctor = this.model;
2697
+ const hidden = ctor.hidden;
2698
+ const appends = ctor.appends;
2699
+ if ((hidden != null && hidden.length > 0) ||
2700
+ (appends != null && appends.length > 0)) {
2701
+ return null;
2702
+ }
2703
+ if (rows.length > 0 && attributesNeedModelCasts(ctor, rows[0])) {
2704
+ return null;
2705
+ }
2706
+ const stamps = [];
2707
+ for (const entry of this.#eagerLoad) {
2708
+ const relation = entry.name;
2709
+ if (entry.constraint ||
2710
+ typeof relation !== "string" ||
2711
+ relation.includes(".") ||
2712
+ relation.includes(" ")) {
2713
+ return null;
2714
+ }
2715
+ const meta = resolveRelation(ctor, relation);
2716
+ if (!meta || meta.kind !== "belongsTo")
2717
+ return null;
2718
+ const foreignKey = meta.foreignKey;
2719
+ for (const row of rows) {
2720
+ if (row[foreignKey] != null)
2721
+ return null;
2722
+ }
2723
+ stamps.push(relation);
2724
+ }
2725
+ const idKey = ctor.primaryKey;
2726
+ const numericId = ctor.incrementing !== false && ctor.keyType !== "string";
2727
+ const list = rows.constructor === Array
2728
+ ? rows
2729
+ : Array.prototype.slice.call(rows);
2730
+ for (let i = 0; i < list.length; i++) {
2731
+ const row = list[i];
2732
+ if (numericId) {
2733
+ const value = row[idKey];
2734
+ if (typeof value === "string" || typeof value === "bigint") {
2735
+ const n = Number(value);
2736
+ if (Number.isSafeInteger(n))
2737
+ row[idKey] = n;
2738
+ }
2739
+ else if (typeof value === "number" && Number.isFinite(value)) {
2740
+ row[idKey] = Math.trunc(value);
2741
+ }
2742
+ }
2743
+ for (let s = 0; s < stamps.length; s++)
2744
+ row[stamps[s]] = null;
2745
+ }
2746
+ return list;
2747
+ }
2748
+ #queryConnection() {
2749
+ if (this.#connection !== undefined) {
2750
+ return resolveConnection(this.#connection);
2751
+ }
2752
+ return this.model.getConnection();
2753
+ }
2754
+ #assertSyncReadable(method) {
2755
+ const conn = this.#queryConnection();
2756
+ if (!conn.allSync || !conn.getSync) {
2757
+ throw new Error(`${method}() requires SQLite (Connection.allSync / getSync). Use async get()/first() on Postgres.`);
2758
+ }
2759
+ if (this.#eagerLoad.length > 0) {
2760
+ throw new Error(`${method}() does not support with() — use async get()/first() for eager loads.`);
2761
+ }
2762
+ if (this.#hasRetrievedListeners) {
2763
+ throw new Error(`${method}() does not support retrieved listeners — use async get()/first().`);
2764
+ }
2765
+ }
2766
+ /**
2767
+ * SQLite-only synchronous `get()`.
2768
+ * No `with()` / retrieved events — for desktop hot paths that cannot await.
2769
+ */
2770
+ getSync() {
2771
+ this.#assertSyncReadable("getSync");
2772
+ const fast = this.#trySimpleRows();
2773
+ const rows = fast !== undefined ? fast : this.#buildQuery().getRows();
2774
+ if (rows instanceof Promise) {
2775
+ throw new Error("getSync() expected synchronous rows from SQLite");
2776
+ }
2777
+ if (this.#asRows) {
2778
+ const plain = this.#materializeRows(rows);
2779
+ if (plain instanceof Promise) {
2780
+ throw new Error("getSync() expected synchronous rows from SQLite");
2781
+ }
2782
+ return plain;
2783
+ }
2784
+ const models = this.#hydrateRows(rows);
2785
+ if (models instanceof Promise) {
2786
+ throw new Error("getSync() cannot hydrate with retrieved listeners");
2787
+ }
2788
+ return new OrmCollection(models, { owned: true });
2789
+ }
2790
+ /** SQLite-only synchronous `first()`. */
2791
+ firstSync() {
2792
+ this.#assertSyncReadable("firstSync");
2793
+ const fast = this.#trySimpleFirst();
2794
+ const row = fast !== undefined ? fast : this.#buildQuery().first();
2795
+ if (row instanceof Promise) {
2796
+ throw new Error("firstSync() expected synchronous row from SQLite");
2797
+ }
2798
+ if (!row)
2799
+ return null;
2800
+ return this.#makeModel(row);
2801
+ }
2802
+ /** SQLite-only synchronous `find($id)`. */
2803
+ findSync(id) {
2804
+ return this.clone().where(this.model.primaryKey, id).firstSync();
2805
+ }
2806
+ async #loadEager(models) {
2807
+ await eagerLoadModels(models, this.#eagerLoad);
2808
+ }
2809
+ #makeModel(row) {
2810
+ let model;
2811
+ if (!this.#hasQueryCasts) {
2812
+ model = this.model.newFromBuilder(row);
2813
+ }
2814
+ else {
2815
+ const ctor = this.model;
2816
+ const original = ctor.casts;
2817
+ ctor.casts = { ...ctor.getCasts(), ...this.#casts };
2818
+ try {
2819
+ model = ctor.newFromBuilder(row);
2820
+ }
2821
+ finally {
2822
+ ctor.casts = original;
2823
+ }
2824
+ }
2825
+ if (this.#connection !== undefined) {
2826
+ model.setConnection(this.#connection);
2827
+ }
2828
+ return model;
2829
+ }
2830
+ #hydrateRows(rows) {
2831
+ if (this.#hasRetrievedListeners) {
2832
+ return (async () => {
2833
+ const models = [];
2834
+ for (const row of rows) {
2835
+ const model = this.#makeModel(row);
2836
+ await fireModelEvent(model, "retrieved");
2837
+ models.push(model);
2838
+ }
2839
+ return models;
2840
+ })();
2841
+ }
2842
+ // Fast path: known array length from SQLite sync results
2843
+ if (Array.isArray(rows)) {
2844
+ const models = new Array(rows.length);
2845
+ // Plain list hydrate: skip #makeModel branch when no query casts / conn override.
2846
+ if (!this.#hasQueryCasts && this.#connection === undefined) {
2847
+ const Ctor = this.model;
2848
+ for (let i = 0; i < rows.length; i++) {
2849
+ models[i] = Ctor.newFromBuilder(rows[i]);
2850
+ }
2851
+ }
2852
+ else {
2853
+ for (let i = 0; i < rows.length; i++) {
2854
+ models[i] = this.#makeModel(rows[i]);
2855
+ }
2856
+ }
2857
+ return models;
2858
+ }
2859
+ const models = [];
2860
+ for (const row of rows) {
2861
+ models.push(this.#makeModel(row));
2862
+ }
2863
+ return models;
2864
+ }
2865
+ /** Laravel `Builder::create` — insert on this query's connection. */
2866
+ async create(attributes) {
2867
+ const merged = { ...this.#pendingAttributes, ...attributes };
2868
+ const useAttrs = filterFillable(this.model, merged);
2869
+ const model = new this.model();
2870
+ if (this.#connection !== undefined) {
2871
+ model.setConnection(this.#connection);
2872
+ }
2873
+ Object.assign(model, useAttrs);
2874
+ await model.save();
2875
+ return model;
2876
+ }
2877
+ async paginate(perPage = 15, page, options = {}) {
2878
+ const total = await this.#countQuery().count();
2879
+ const currentPage = resolvePaginatorPage(page, options.pageName);
2880
+ const size = Math.max(1, Math.floor(Number(perPage)) || 15);
2881
+ const offset = (currentPage - 1) * size;
2882
+ const rows = await this.#buildQuery().limit(size).offset(offset).getRows();
2883
+ const items = await this.#hydrateRows(rows);
2884
+ if (this.#eagerLoad.length > 0) {
2885
+ await eagerLoadModels(items, this.#eagerLoad);
2886
+ }
2887
+ return new LengthAwarePaginator(items, total, size, currentPage, options);
2888
+ }
2889
+ /** No total count; extra row is used to detect a next page. */
2890
+ async simplePaginate(perPage = 15, page, options = {}) {
2891
+ const currentPage = resolvePaginatorPage(page, options.pageName);
2892
+ const size = Math.max(1, Math.floor(Number(perPage)) || 15);
2893
+ const offset = (currentPage - 1) * size;
2894
+ const rows = await this.#buildQuery()
2895
+ .limit(size + 1)
2896
+ .offset(offset)
2897
+ .getRows();
2898
+ const items = await this.#hydrateRows(rows);
2899
+ if (this.#eagerLoad.length > 0) {
2900
+ await eagerLoadModels(items, this.#eagerLoad);
2901
+ }
2902
+ return new Paginator(items, size, currentPage, options);
2903
+ }
2904
+ /** Laravel `chunk($count, $callback)`. */
2905
+ async chunk(count, callback) {
2906
+ let page = 1;
2907
+ for (;;) {
2908
+ const offset = (page - 1) * count;
2909
+ const rows = await this.#buildQuery()
2910
+ .limit(count)
2911
+ .offset(offset)
2912
+ .getRows();
2913
+ if (rows.length === 0)
2914
+ break;
2915
+ const models = await this.#hydrateRows(rows);
2916
+ if (this.#eagerLoad.length > 0) {
2917
+ await eagerLoadModels(models, this.#eagerLoad);
2918
+ }
2919
+ await callback(new OrmCollection(models, { owned: true }));
2920
+ if (rows.length < count)
2921
+ break;
2922
+ page += 1;
2923
+ }
2924
+ }
2925
+ /** Laravel `lazy($chunkSize)`. */
2926
+ async *lazy(chunkSize = 1000) {
2927
+ let page = 1;
2928
+ const fireRetrieved = hasModelEventListeners(this.model, "retrieved");
2929
+ for (;;) {
2930
+ const offset = (page - 1) * chunkSize;
2931
+ const rows = await this.#buildQuery()
2932
+ .limit(chunkSize)
2933
+ .offset(offset)
2934
+ .getRows();
2935
+ if (rows.length === 0)
2936
+ break;
2937
+ for (const row of rows) {
2938
+ const model = this.#makeModel(row);
2939
+ if (fireRetrieved) {
2940
+ await fireModelEvent(model, "retrieved");
2941
+ }
2942
+ if (this.#eagerLoad.length > 0) {
2943
+ await this.#loadEager([model]);
2944
+ }
2945
+ yield model;
2946
+ }
2947
+ if (rows.length < chunkSize)
2948
+ break;
2949
+ page += 1;
2950
+ }
2951
+ }
2952
+ /** Laravel `chunkById`. */
2953
+ async chunkById(count, callback, column) {
2954
+ return this.#chunkById(count, callback, column ?? this.model.primaryKey, "asc");
2955
+ }
2956
+ async chunkByIdDesc(count, callback, column) {
2957
+ return this.#chunkById(count, callback, column ?? this.model.primaryKey, "desc");
2958
+ }
2959
+ async #chunkById(count, callback, column, direction) {
2960
+ const size = Math.max(1, Math.floor(count));
2961
+ let lastId = null;
2962
+ for (;;) {
2963
+ const q = this.clone().reorder(column, direction).limit(size);
2964
+ if (lastId !== null) {
2965
+ q.where(column, direction === "asc" ? ">" : "<", lastId);
2966
+ }
2967
+ const models = await q.#fetchModels();
2968
+ if (models.isEmpty())
2969
+ return true;
2970
+ const result = await callback(models);
2971
+ if (result === false)
2972
+ return false;
2973
+ lastId = models.last()?.[column];
2974
+ if (models.count() < size)
2975
+ return true;
2976
+ }
2977
+ }
2978
+ /** Laravel `lazyById`. */
2979
+ async *lazyById(chunkSize = 1000, column) {
2980
+ yield* this.#lazyById(chunkSize, column ?? this.model.primaryKey, "asc");
2981
+ }
2982
+ async *lazyByIdDesc(chunkSize = 1000, column) {
2983
+ yield* this.#lazyById(chunkSize, column ?? this.model.primaryKey, "desc");
2984
+ }
2985
+ async *#lazyById(chunkSize, column, direction) {
2986
+ const size = Math.max(1, Math.floor(chunkSize));
2987
+ let lastId = null;
2988
+ for (;;) {
2989
+ const q = this.clone().reorder(column, direction).limit(size);
2990
+ if (lastId !== null) {
2991
+ q.where(column, direction === "asc" ? ">" : "<", lastId);
2992
+ }
2993
+ const models = await q.#fetchModels();
2994
+ if (models.isEmpty())
2995
+ return;
2996
+ for (const model of models) {
2997
+ yield model;
2998
+ }
2999
+ lastId = models.last()?.[column];
3000
+ if (models.count() < size)
3001
+ return;
3002
+ }
3003
+ }
3004
+ async increment(column, amount = 1) {
3005
+ await this.#buildQuery().increment(column, amount);
3006
+ }
3007
+ async decrement(column, amount = 1) {
3008
+ await this.#buildQuery().decrement(column, amount);
3009
+ }
3010
+ /** Laravel `cursorPaginate`. */
3011
+ async cursorPaginate(perPage = 15, cursor = null, options = {}) {
3012
+ const page = await this.#buildQuery().cursorPaginate(perPage, cursor, options);
3013
+ const items = await this.#hydrateRows(page.items);
3014
+ if (this.#eagerLoad.length > 0) {
3015
+ await eagerLoadModels(items, this.#eagerLoad);
3016
+ }
3017
+ return new CursorPaginator(items, page.perPage, page.options);
3018
+ }
3019
+ /** Laravel `Builder::count($columns = '*')` — honors `distinct()` for non-`*`. */
3020
+ async count(column = "*") {
3021
+ let countQ = this.#countQuery();
3022
+ if (this.#distinct) {
3023
+ countQ = countQ.distinct();
3024
+ }
3025
+ return countQ.count(column);
3026
+ }
3027
+ #countQuery() {
3028
+ let countQ = this.#baseTable();
3029
+ countQ = this.#applySoftDeletes(countQ);
3030
+ countQ = this.#applyGlobalScopesTo(countQ);
3031
+ // BelongsTo whereHas expands to JOIN + qualified related wheres on the parent.
3032
+ // Count must include those joins (and builderExtras) or Postgres fails with
3033
+ // "missing FROM-clause entry for table …".
3034
+ this.#qualifyParentConstraintsForJoins();
3035
+ countQ = this.applyConstraintsTo(countQ);
3036
+ if (this.#joins.length > 0) {
3037
+ for (const j of this.#joins) {
3038
+ if (j.type === "left") {
3039
+ countQ = countQ.leftJoin(j.table, j.first, j.op, j.second);
3040
+ }
3041
+ else if (j.type === "right") {
3042
+ countQ = countQ.rightJoin(j.table, j.first, j.op, j.second);
3043
+ }
3044
+ else {
3045
+ countQ = countQ.join(j.table, j.first, j.op, j.second);
3046
+ }
3047
+ }
3048
+ }
3049
+ for (const extra of this.#builderExtras) {
3050
+ extra(countQ);
3051
+ }
3052
+ return countQ;
3053
+ }
3054
+ async exists() {
3055
+ return (await this.#countQuery().first()) != null;
3056
+ }
3057
+ async doesntExist() {
3058
+ return !(await this.exists());
3059
+ }
3060
+ /** Find by primary key within this query. */
3061
+ async find(id) {
3062
+ return this.clone()
3063
+ .where(this.model.primaryKey, id)
3064
+ .first();
3065
+ }
3066
+ async findOrFail(id) {
3067
+ const model = await this.find(id);
3068
+ if (!model)
3069
+ throw new ModelNotFoundException();
3070
+ return model;
3071
+ }
3072
+ /** Exactly one matching model, or throw. */
3073
+ async sole() {
3074
+ const rows = await this.clone().limit(2).#fetchModels();
3075
+ if (rows.isEmpty())
3076
+ throw new ModelNotFoundException();
3077
+ if (rows.length > 1) {
3078
+ throw new ModelNotFoundException("Query returned more than one model.");
3079
+ }
3080
+ return rows.first();
3081
+ }
3082
+ async value(column) {
3083
+ const model = await this.clone().select(column).first();
3084
+ if (!model)
3085
+ return null;
3086
+ return model[column] ?? null;
3087
+ }
3088
+ async pluck(column, key) {
3089
+ const cols = key !== undefined && key !== column ? [column, key] : [column];
3090
+ const models = await this.clone().select(...cols).#fetchModels();
3091
+ if (key === undefined)
3092
+ return models.pluck(column);
3093
+ return models.pluck(column, key);
3094
+ }
3095
+ async sum(column) {
3096
+ return this.#buildQuery().sum(column);
3097
+ }
3098
+ async avg(column) {
3099
+ return this.#buildQuery().avg(column);
3100
+ }
3101
+ async min(column) {
3102
+ return this.#buildQuery().min(column);
3103
+ }
3104
+ async max(column) {
3105
+ return this.#buildQuery().max(column);
3106
+ }
3107
+ /**
3108
+ * Bulk insert. When the model uses timestamps, stamps `created_at`/`updated_at`
3109
+ * (row values win). Does not fire creating/created events.
3110
+ */
3111
+ async insert(values) {
3112
+ const rows = Array.isArray(values) ? values : [values];
3113
+ if (rows.length === 0)
3114
+ return true;
3115
+ let finalRows = rows;
3116
+ if (this.model.timestamps !== false) {
3117
+ const now = nowForConnection(resolveConnection(this.#connection));
3118
+ finalRows = rows.map((row) => ({
3119
+ created_at: now,
3120
+ updated_at: now,
3121
+ ...row,
3122
+ }));
3123
+ }
3124
+ return this.#baseTable().insert(finalRows);
3125
+ }
3126
+ /**
3127
+ * Laravel `Builder::upsert` — insert or update on unique conflict.
3128
+ * When the model uses timestamps, stamps `created_at`/`updated_at` like Eloquent
3129
+ * (row values win; `updated_at` is always included in the update column list).
3130
+ */
3131
+ async upsert(values, uniqueBy, update = null) {
3132
+ const rows = Array.isArray(values) ? values : [values];
3133
+ if (rows.length === 0)
3134
+ return 0;
3135
+ let finalRows = rows;
3136
+ let finalUpdate = update;
3137
+ if (this.model.timestamps !== false) {
3138
+ const now = nowForConnection(resolveConnection(this.#connection));
3139
+ finalRows = rows.map((row) => ({
3140
+ created_at: now,
3141
+ updated_at: now,
3142
+ ...row,
3143
+ }));
3144
+ if (finalUpdate == null) {
3145
+ const columns = Object.keys(finalRows[0]);
3146
+ const uniqueCols = Array.isArray(uniqueBy) ? uniqueBy : [uniqueBy];
3147
+ finalUpdate = columns.filter((c) => !uniqueCols.includes(c));
3148
+ }
3149
+ else if (!finalUpdate.includes("updated_at")) {
3150
+ finalUpdate = [...finalUpdate, "updated_at"];
3151
+ }
3152
+ }
3153
+ return this.#baseTable().upsert(finalRows, uniqueBy, finalUpdate);
3154
+ }
3155
+ async update(values) {
3156
+ return this.#buildQuery().update(values);
3157
+ }
3158
+ async delete() {
3159
+ if (usesSoftDeletes(this.model) && !this.#withTrashed && !this.#onlyTrashed) {
3160
+ const col = deletedAtColumn(this.model);
3161
+ return this.#buildQuery().update({
3162
+ [col]: nowForConnection(resolveConnection(this.#connection)),
3163
+ });
3164
+ }
3165
+ return this.#buildQuery().delete();
3166
+ }
3167
+ async forceDelete() {
3168
+ return this.#buildQuery().delete();
3169
+ }
3170
+ }