@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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/casts.d.ts +50 -0
- package/dist/casts.js +309 -0
- package/dist/concerns.d.ts +64 -0
- package/dist/concerns.js +216 -0
- package/dist/decorators.d.ts +7 -0
- package/dist/decorators.js +12 -0
- package/dist/eager-fetch.d.ts +7 -0
- package/dist/eager-fetch.js +38 -0
- package/dist/eager.d.ts +24 -0
- package/dist/eager.js +854 -0
- package/dist/factory.d.ts +68 -0
- package/dist/factory.js +265 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/model-events.d.ts +43 -0
- package/dist/model-events.js +138 -0
- package/dist/model-helpers.d.ts +32 -0
- package/dist/model-helpers.js +99 -0
- package/dist/model-query.d.ts +312 -0
- package/dist/model-query.js +3170 -0
- package/dist/model-strictness.d.ts +35 -0
- package/dist/model-strictness.js +114 -0
- package/dist/model.d.ts +490 -0
- package/dist/model.js +1931 -0
- package/dist/orm-collection.d.ts +37 -0
- package/dist/orm-collection.js +150 -0
- package/dist/pivot.d.ts +9 -0
- package/dist/pivot.js +9 -0
- package/dist/relations.d.ts +335 -0
- package/dist/relations.js +1164 -0
- package/dist/scopes.d.ts +36 -0
- package/dist/scopes.js +128 -0
- package/package.json +49 -0
|
@@ -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
|
+
}
|