@bunnykit/orm 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +420 -81
  2. package/dist/bin/bunny.js +133 -19
  3. package/dist/src/config/BunnyConfig.d.ts +28 -0
  4. package/dist/src/config/BunnyConfig.js +14 -0
  5. package/dist/src/connection/Connection.d.ts +20 -1
  6. package/dist/src/connection/Connection.js +80 -2
  7. package/dist/src/connection/ConnectionManager.d.ts +40 -0
  8. package/dist/src/connection/ConnectionManager.js +104 -0
  9. package/dist/src/connection/TenantContext.d.ts +15 -0
  10. package/dist/src/connection/TenantContext.js +22 -0
  11. package/dist/src/index.d.ts +7 -0
  12. package/dist/src/index.js +4 -0
  13. package/dist/src/model/BelongsToMany.js +9 -6
  14. package/dist/src/model/Model.d.ts +41 -0
  15. package/dist/src/model/Model.js +217 -18
  16. package/dist/src/model/ModelNotFoundError.d.ts +5 -0
  17. package/dist/src/model/ModelNotFoundError.js +13 -0
  18. package/dist/src/model/MorphRelations.js +10 -10
  19. package/dist/src/query/Builder.d.ts +85 -7
  20. package/dist/src/query/Builder.js +489 -68
  21. package/dist/src/query/grammars/Grammar.d.ts +19 -0
  22. package/dist/src/query/grammars/Grammar.js +47 -0
  23. package/dist/src/query/grammars/MySqlGrammar.d.ts +13 -0
  24. package/dist/src/query/grammars/MySqlGrammar.js +59 -0
  25. package/dist/src/query/grammars/PostgresGrammar.d.ts +13 -0
  26. package/dist/src/query/grammars/PostgresGrammar.js +62 -0
  27. package/dist/src/query/grammars/SQLiteGrammar.d.ts +14 -0
  28. package/dist/src/query/grammars/SQLiteGrammar.js +63 -0
  29. package/dist/src/schema/Schema.js +44 -26
  30. package/dist/src/typegen/TypeGenerator.js +4 -2
  31. package/dist/src/types/index.d.ts +10 -0
  32. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { ModelNotFoundError } from "../model/ModelNotFoundError.js";
1
2
  export class Builder {
2
3
  connection;
3
4
  tableName;
@@ -12,10 +13,18 @@ export class Builder {
12
13
  distinctFlag = false;
13
14
  model;
14
15
  eagerLoads = [];
16
+ randomOrderFlag = false;
17
+ lockMode;
18
+ unions = [];
19
+ fromRaw;
20
+ updateJoins = [];
15
21
  constructor(connection, table) {
16
22
  this.connection = connection;
17
23
  this.tableName = table;
18
24
  }
25
+ get grammar() {
26
+ return this.connection.getGrammar();
27
+ }
19
28
  setModel(model) {
20
29
  this.model = model;
21
30
  return this;
@@ -33,6 +42,9 @@ export class Builder {
33
42
  return this;
34
43
  }
35
44
  where(column, operator, value, boolean = "and", scope) {
45
+ if (typeof column === "function") {
46
+ return this.whereNested(column, boolean);
47
+ }
36
48
  if (typeof column === "object" && column !== null) {
37
49
  for (const [key, val] of Object.entries(column)) {
38
50
  this.where(key, "=", val, boolean, scope);
@@ -46,9 +58,30 @@ export class Builder {
46
58
  this.wheres.push({ type: "basic", column, operator, value, boolean, scope });
47
59
  return this;
48
60
  }
61
+ whereNested(callback, boolean = "and") {
62
+ const nested = new Builder(this.connection, this.tableName);
63
+ callback(nested);
64
+ if (nested.wheres.length > 0) {
65
+ const sql = this.compileNestedWheres(nested);
66
+ this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
67
+ }
68
+ return this;
69
+ }
49
70
  orWhere(column, operator, value) {
50
71
  return this.where(column, operator, value, "or");
51
72
  }
73
+ whereNot(column, value, boolean = "and") {
74
+ if (typeof column === "object" && column !== null) {
75
+ for (const [key, val] of Object.entries(column)) {
76
+ this.whereNot(key, val, boolean);
77
+ }
78
+ return this;
79
+ }
80
+ return this.where(column, "!=", value, boolean);
81
+ }
82
+ orWhereNot(column, value) {
83
+ return this.whereNot(column, value, "or");
84
+ }
52
85
  whereIn(column, values, boolean = "and", scope) {
53
86
  this.wheres.push({ type: "in", column, value: values, boolean, scope });
54
87
  return this;
@@ -73,6 +106,36 @@ export class Builder {
73
106
  this.wheres.push({ type: "between", column, value: values, boolean, operator: "NOT BETWEEN", scope });
74
107
  return this;
75
108
  }
109
+ whereDate(column, operator, value, boolean = "and") {
110
+ return this.addDateWhere("date", column, operator, value, boolean);
111
+ }
112
+ orWhereDate(column, operator, value) {
113
+ return this.whereDate(column, operator, value, "or");
114
+ }
115
+ whereDay(column, operator, value, boolean = "and") {
116
+ return this.addDateWhere("day", column, operator, value, boolean);
117
+ }
118
+ orWhereDay(column, operator, value) {
119
+ return this.whereDay(column, operator, value, "or");
120
+ }
121
+ whereMonth(column, operator, value, boolean = "and") {
122
+ return this.addDateWhere("month", column, operator, value, boolean);
123
+ }
124
+ orWhereMonth(column, operator, value) {
125
+ return this.whereMonth(column, operator, value, "or");
126
+ }
127
+ whereYear(column, operator, value, boolean = "and") {
128
+ return this.addDateWhere("year", column, operator, value, boolean);
129
+ }
130
+ orWhereYear(column, operator, value) {
131
+ return this.whereYear(column, operator, value, "or");
132
+ }
133
+ whereTime(column, operator, value, boolean = "and") {
134
+ return this.addDateWhere("time", column, operator, value, boolean);
135
+ }
136
+ orWhereTime(column, operator, value) {
137
+ return this.whereTime(column, operator, value, "or");
138
+ }
76
139
  whereRaw(sql, boolean = "and", scope) {
77
140
  this.wheres.push({ type: "raw", column: sql, boolean, scope });
78
141
  return this;
@@ -85,18 +148,129 @@ export class Builder {
85
148
  this.wheres.push({ type: "exists", column: sql, boolean, operator: not ? "NOT EXISTS" : "EXISTS" });
86
149
  return this;
87
150
  }
151
+ orWhereNull(column, scope) {
152
+ return this.whereNull(column, "or", scope);
153
+ }
154
+ orWhereNotNull(column, scope) {
155
+ return this.whereNotNull(column, "or", scope);
156
+ }
157
+ orWhereBetween(column, values, scope) {
158
+ return this.whereBetween(column, values, "or", scope);
159
+ }
160
+ orWhereNotBetween(column, values, scope) {
161
+ return this.whereNotBetween(column, values, "or", scope);
162
+ }
163
+ orWhereIn(column, values, scope) {
164
+ return this.whereIn(column, values, "or", scope);
165
+ }
166
+ orWhereNotIn(column, values, scope) {
167
+ return this.whereNotIn(column, values, "or", scope);
168
+ }
169
+ orWhereExists(sql) {
170
+ return this.whereExists(sql, "or");
171
+ }
172
+ orWhereNotExists(sql) {
173
+ return this.whereExists(sql, "or", true);
174
+ }
175
+ orWhereColumn(first, operator, second) {
176
+ return this.whereColumn(first, operator, second, "or");
177
+ }
178
+ orWhereRaw(sql, scope) {
179
+ return this.whereRaw(sql, "or", scope);
180
+ }
181
+ whereJsonContains(column, value, boolean = "and", not = false) {
182
+ let sql = this.grammar.compileJsonContains(this.grammar.wrap(column), value);
183
+ if (not)
184
+ sql = `NOT (${sql})`;
185
+ this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
186
+ return this;
187
+ }
188
+ whereJsonLength(column, operator = "=", value, boolean = "and", not = false) {
189
+ if (value === undefined) {
190
+ value = operator;
191
+ operator = "=";
192
+ }
193
+ let sql = this.grammar.compileJsonLength(this.grammar.wrap(column), String(operator), value);
194
+ if (not)
195
+ sql = `NOT (${sql})`;
196
+ this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
197
+ return this;
198
+ }
199
+ whereLike(column, value, boolean = "and", not = false) {
200
+ const sql = this.grammar.compileLike(this.grammar.wrap(column), value, not);
201
+ this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
202
+ return this;
203
+ }
204
+ whereNotLike(column, value) {
205
+ return this.whereLike(column, value, "and", true);
206
+ }
207
+ whereRegexp(column, value, boolean = "and", not = false) {
208
+ const sql = this.grammar.compileRegexp(this.grammar.wrap(column), value, not);
209
+ this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
210
+ return this;
211
+ }
212
+ whereFullText(columns, value, boolean = "and", not = false) {
213
+ const cols = Array.isArray(columns) ? columns : [columns];
214
+ let sql = this.grammar.compileFullText(cols.map((c) => this.grammar.wrap(c)), value);
215
+ if (not)
216
+ sql = `NOT (${sql})`;
217
+ this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
218
+ return this;
219
+ }
220
+ whereAll(columns, operator, value, boolean = "and") {
221
+ const sql = columns.map((c) => `${this.grammar.wrap(c)} ${operator} ${this.grammar.escape(value)}`).join(" AND ");
222
+ this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
223
+ return this;
224
+ }
225
+ whereAny(columns, operator, value, boolean = "and") {
226
+ const sql = columns.map((c) => `${this.grammar.wrap(c)} ${operator} ${this.grammar.escape(value)}`).join(" OR ");
227
+ this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
228
+ return this;
229
+ }
88
230
  orderBy(column, direction = "asc") {
89
231
  this.orders.push({ column, direction });
90
232
  return this;
91
233
  }
234
+ latest(column = "created_at") {
235
+ return this.orderBy(column, "desc");
236
+ }
237
+ oldest(column = "created_at") {
238
+ return this.orderBy(column, "asc");
239
+ }
240
+ inRandomOrder() {
241
+ this.randomOrderFlag = true;
242
+ return this;
243
+ }
244
+ orderByDesc(column) {
245
+ return this.orderBy(column, "desc");
246
+ }
247
+ reorder(column, direction = "asc") {
248
+ this.orders = [];
249
+ this.randomOrderFlag = false;
250
+ if (column) {
251
+ this.orderBy(column, direction);
252
+ }
253
+ return this;
254
+ }
92
255
  groupBy(...columns) {
93
256
  this.groups.push(...columns);
94
257
  return this;
95
258
  }
96
259
  having(column, operator, value) {
97
- this.havings.push(`${this.wrap(column)} ${operator} ${this.escape(value)}`);
260
+ this.havings.push({ sql: `${this.grammar.wrap(column)} ${operator} ${this.grammar.escape(value)}`, boolean: "and" });
261
+ return this;
262
+ }
263
+ orHaving(column, operator, value) {
264
+ this.havings.push({ sql: `${this.grammar.wrap(column)} ${operator} ${this.grammar.escape(value)}`, boolean: "or" });
265
+ return this;
266
+ }
267
+ havingRaw(sql, boolean = "and") {
268
+ this.havings.push({ sql, boolean });
98
269
  return this;
99
270
  }
271
+ orHavingRaw(sql) {
272
+ return this.havingRaw(sql, "or");
273
+ }
100
274
  limit(count) {
101
275
  this.limitValue = count;
102
276
  return this;
@@ -109,7 +283,7 @@ export class Builder {
109
283
  return this.offset((page - 1) * perPage).limit(perPage);
110
284
  }
111
285
  join(table, first, operator, second, type = "INNER") {
112
- const joinSql = `${type} JOIN ${this.wrap(table)} ON ${this.wrap(first)} ${operator} ${this.wrap(second)}`;
286
+ const joinSql = `${type} JOIN ${this.grammar.wrap(table)} ON ${this.grammar.wrap(first)} ${operator} ${this.grammar.wrap(second)}`;
113
287
  this.joins.push(joinSql);
114
288
  return this;
115
289
  }
@@ -119,6 +293,18 @@ export class Builder {
119
293
  rightJoin(table, first, operator, second) {
120
294
  return this.join(table, first, operator, second, "RIGHT");
121
295
  }
296
+ crossJoin(table) {
297
+ this.joins.push(`CROSS JOIN ${this.grammar.wrap(table)}`);
298
+ return this;
299
+ }
300
+ union(query, all = false) {
301
+ const sql = typeof query === "string" ? query : query.toSql();
302
+ this.unions.push({ query: sql, all });
303
+ return this;
304
+ }
305
+ unionAll(query) {
306
+ return this.union(query, true);
307
+ }
122
308
  with(...relations) {
123
309
  this.eagerLoads.push(...relations);
124
310
  return this;
@@ -154,6 +340,24 @@ export class Builder {
154
340
  const result = scope.call(this.model, this, ...args);
155
341
  return (result || this);
156
342
  }
343
+ when(condition, callback, defaultCallback) {
344
+ if (condition) {
345
+ const result = callback(this);
346
+ return (result || this);
347
+ }
348
+ else if (defaultCallback) {
349
+ const result = defaultCallback(this);
350
+ return (result || this);
351
+ }
352
+ return this;
353
+ }
354
+ unless(condition, callback, defaultCallback) {
355
+ return this.when(!condition, callback, defaultCallback);
356
+ }
357
+ tap(callback) {
358
+ const result = callback(this);
359
+ return (result || this);
360
+ }
157
361
  has(relationName, operator = ">=", count = 1, callback) {
158
362
  if (typeof operator === "function") {
159
363
  callback = operator;
@@ -167,7 +371,7 @@ export class Builder {
167
371
  if ((operator === "<" || operator === "=") && count <= 0) {
168
372
  return this.whereExists(relation.getRelationExistenceSql(this, callback), "and", true);
169
373
  }
170
- return this.whereRaw(`(${relation.getRelationCountSql(this, callback)}) ${operator} ${this.escape(count)}`);
374
+ return this.whereRaw(`(${relation.getRelationCountSql(this, callback)}) ${operator} ${this.grammar.escape(count)}`);
171
375
  }
172
376
  orHas(relationName, operator = ">=", count = 1, callback) {
173
377
  if (typeof operator === "function") {
@@ -182,7 +386,7 @@ export class Builder {
182
386
  if ((operator === "<" || operator === "=") && count <= 0) {
183
387
  return this.whereExists(relation.getRelationExistenceSql(this, callback), "or", true);
184
388
  }
185
- return this.whereRaw(`(${relation.getRelationCountSql(this, callback)}) ${operator} ${this.escape(count)}`, "or");
389
+ return this.whereRaw(`(${relation.getRelationCountSql(this, callback)}) ${operator} ${this.grammar.escape(count)}`, "or");
186
390
  }
187
391
  whereHas(relationName, callback, operator = ">=", count = 1) {
188
392
  return this.has(relationName, operator, count, callback);
@@ -220,6 +424,19 @@ export class Builder {
220
424
  this.columns.push(...columns);
221
425
  return this;
222
426
  }
427
+ selectRaw(sql) {
428
+ this.columns.push(sql);
429
+ return this;
430
+ }
431
+ fromSub(query, as) {
432
+ const sql = typeof query === "string" ? query : query.toSql();
433
+ this.fromRaw = `(${sql}) AS ${this.grammar.wrap(as)}`;
434
+ return this;
435
+ }
436
+ updateFrom(table, first, operator, second) {
437
+ this.updateJoins.push(`INNER JOIN ${this.grammar.wrap(table)} ON ${this.grammar.wrap(first)} ${operator} ${this.grammar.wrap(second)}`);
438
+ return this;
439
+ }
223
440
  clone() {
224
441
  const cloned = new Builder(this.connection, this.tableName);
225
442
  cloned.columns = [...this.columns];
@@ -233,89 +450,85 @@ export class Builder {
233
450
  cloned.distinctFlag = this.distinctFlag;
234
451
  cloned.model = this.model;
235
452
  cloned.eagerLoads = [...this.eagerLoads];
453
+ cloned.randomOrderFlag = this.randomOrderFlag;
454
+ cloned.lockMode = this.lockMode;
455
+ cloned.unions = [...this.unions];
456
+ cloned.fromRaw = this.fromRaw;
457
+ cloned.updateJoins = [...this.updateJoins];
236
458
  return cloned;
237
459
  }
238
460
  wrapColumn(value) {
239
- return this.wrap(value);
461
+ return this.grammar.wrap(value);
240
462
  }
241
463
  escapeValue(value) {
242
- return this.escape(value);
464
+ return this.grammar.escape(value);
243
465
  }
244
- wrap(value) {
245
- if (value.includes(" as ")) {
246
- const [column, alias] = value.split(/\s+as\s+/i);
247
- return `${this.wrap(column)} AS ${this.wrapValue(alias)}`;
466
+ compileWhereClause(where, prefix) {
467
+ if (where.type === "basic") {
468
+ return `${prefix} ${this.grammar.wrap(where.column)} ${where.operator} ${this.grammar.escape(where.value)}`;
248
469
  }
249
- if (value.includes(".")) {
250
- return value.split(".").map((v) => this.wrapValue(v)).join(".");
470
+ else if (where.type === "in") {
471
+ const op = where.operator === "NOT IN" ? "NOT IN" : "IN";
472
+ return `${prefix} ${this.grammar.wrap(where.column)} ${op} (${where.value.map((v) => this.grammar.escape(v)).join(", ")})`;
251
473
  }
252
- return this.wrapValue(value);
253
- }
254
- wrapValue(value) {
255
- const driver = this.connection.getDriverName();
256
- const char = driver === "mysql" ? "`" : '"';
257
- if (value === "*")
258
- return value;
259
- return `${char}${value}${char}`;
260
- }
261
- escape(value) {
262
- if (value === null)
263
- return "NULL";
264
- if (typeof value === "boolean")
265
- return value ? "1" : "0";
266
- if (typeof value === "number")
267
- return String(value);
268
- if (typeof value === "string" && value.toUpperCase().includes("CURRENT_TIMESTAMP"))
269
- return value;
270
- return `'${String(value).replace(/'/g, "''")}'`;
474
+ else if (where.type === "null") {
475
+ const op = where.operator === "NOT NULL" ? "IS NOT NULL" : "IS NULL";
476
+ return `${prefix} ${this.grammar.wrap(where.column)} ${op}`;
477
+ }
478
+ else if (where.type === "between") {
479
+ const op = where.operator === "NOT BETWEEN" ? "NOT BETWEEN" : "BETWEEN";
480
+ return `${prefix} ${this.grammar.wrap(where.column)} ${op} ${this.grammar.escape(where.value[0])} AND ${this.grammar.escape(where.value[1])}`;
481
+ }
482
+ else if (where.type === "raw") {
483
+ return `${prefix} ${where.column}`;
484
+ }
485
+ else if (where.type === "column") {
486
+ return `${prefix} ${this.grammar.wrap(where.column)} ${where.operator} ${this.grammar.wrap(where.value)}`;
487
+ }
488
+ else if (where.type === "exists") {
489
+ return `${prefix} ${where.operator} (${where.column})`;
490
+ }
491
+ return "";
271
492
  }
272
493
  compileWheres() {
273
494
  if (this.wheres.length === 0)
274
495
  return "";
275
496
  const clauses = this.wheres.map((where, index) => {
276
497
  const prefix = index === 0 ? "WHERE" : where.boolean.toUpperCase();
277
- if (where.type === "basic") {
278
- return `${prefix} ${this.wrap(where.column)} ${where.operator} ${this.escape(where.value)}`;
279
- }
280
- else if (where.type === "in") {
281
- const op = where.operator === "NOT IN" ? "NOT IN" : "IN";
282
- return `${prefix} ${this.wrap(where.column)} ${op} (${where.value.map((v) => this.escape(v)).join(", ")})`;
283
- }
284
- else if (where.type === "null") {
285
- const op = where.operator === "NOT NULL" ? "IS NOT NULL" : "IS NULL";
286
- return `${prefix} ${this.wrap(where.column)} ${op}`;
287
- }
288
- else if (where.type === "between") {
289
- const op = where.operator === "NOT BETWEEN" ? "NOT BETWEEN" : "BETWEEN";
290
- return `${prefix} ${this.wrap(where.column)} ${op} ${this.escape(where.value[0])} AND ${this.escape(where.value[1])}`;
291
- }
292
- else if (where.type === "raw") {
293
- return `${prefix} ${where.column}`;
294
- }
295
- else if (where.type === "column") {
296
- return `${prefix} ${this.wrap(where.column)} ${where.operator} ${this.wrap(where.value)}`;
297
- }
298
- else if (where.type === "exists") {
299
- return `${prefix} ${where.operator} (${where.column})`;
300
- }
301
- return "";
498
+ return this.compileWhereClause(where, prefix);
302
499
  });
303
500
  return clauses.join(" ");
304
501
  }
502
+ compileNestedWheres(builder) {
503
+ if (builder.wheres.length === 0)
504
+ return "";
505
+ const clauses = builder.wheres.map((where, index) => {
506
+ const prefix = index === 0 ? "" : where.boolean.toUpperCase();
507
+ return this.compileWhereClause(where, prefix);
508
+ });
509
+ return clauses.join(" ").trim();
510
+ }
305
511
  compileOrders() {
512
+ if (this.randomOrderFlag) {
513
+ return this.grammar.compileRandomOrder();
514
+ }
306
515
  if (this.orders.length === 0)
307
516
  return "";
308
- return `ORDER BY ${this.orders.map((o) => `${this.wrap(o.column)} ${o.direction.toUpperCase()}`).join(", ")}`;
517
+ return `ORDER BY ${this.orders.map((o) => `${this.grammar.wrap(o.column)} ${o.direction.toUpperCase()}`).join(", ")}`;
309
518
  }
310
519
  compileGroups() {
311
520
  if (this.groups.length === 0)
312
521
  return "";
313
- return `GROUP BY ${this.groups.map((c) => this.wrap(c)).join(", ")}`;
522
+ return `GROUP BY ${this.groups.map((c) => this.grammar.wrap(c)).join(", ")}`;
314
523
  }
315
524
  compileHavings() {
316
525
  if (this.havings.length === 0)
317
526
  return "";
318
- return `HAVING ${this.havings.join(" AND ")}`;
527
+ const clauses = this.havings.map((h, index) => {
528
+ const prefix = index === 0 ? "" : h.boolean.toUpperCase() + " ";
529
+ return prefix + h.sql;
530
+ });
531
+ return `HAVING ${clauses.join(" ")}`;
319
532
  }
320
533
  compileLimit() {
321
534
  if (this.limitValue === undefined)
@@ -325,17 +538,18 @@ export class Builder {
325
538
  compileOffset() {
326
539
  if (this.offsetValue === undefined)
327
540
  return "";
328
- return `OFFSET ${this.offsetValue}`;
541
+ return this.grammar.compileOffset(this.offsetValue, this.limitValue);
329
542
  }
330
543
  compileColumns() {
331
- return this.columns.map((c) => (this.isRawColumn(c) ? c : this.wrap(c))).join(", ");
544
+ return this.columns.map((c) => (this.isRawColumn(c) ? c : this.grammar.wrap(c))).join(", ");
332
545
  }
333
546
  isRawColumn(column) {
334
547
  return column.includes("(") || /\s+as\s+/i.test(column) || /^[0-9]+$/.test(column);
335
548
  }
336
549
  toSql() {
337
550
  const distinct = this.distinctFlag ? "DISTINCT " : "";
338
- let sql = `SELECT ${distinct}${this.compileColumns()} FROM ${this.wrap(this.tableName)}`;
551
+ const from = this.fromRaw || this.grammar.wrap(this.tableName);
552
+ let sql = `SELECT ${distinct}${this.compileColumns()} FROM ${from}`;
339
553
  if (this.joins.length > 0)
340
554
  sql += " " + this.joins.join(" ");
341
555
  sql += " " + this.compileWheres();
@@ -344,6 +558,10 @@ export class Builder {
344
558
  sql += " " + this.compileOrders();
345
559
  sql += " " + this.compileLimit();
346
560
  sql += " " + this.compileOffset();
561
+ sql += this.grammar.compileLock(this.lockMode);
562
+ for (const union of this.unions) {
563
+ sql += ` UNION${union.all ? " ALL" : ""} ${union.query}`;
564
+ }
347
565
  return sql.replace(/\s+/g, " ").trim();
348
566
  }
349
567
  async get() {
@@ -354,6 +572,9 @@ export class Builder {
354
572
  const instance = new this.model(row);
355
573
  instance.$exists = true;
356
574
  instance.$original = { ...row };
575
+ if (typeof instance.setConnection === "function") {
576
+ instance.setConnection(this.connection);
577
+ }
357
578
  return instance;
358
579
  });
359
580
  if (this.eagerLoads.length > 0) {
@@ -370,6 +591,54 @@ export class Builder {
370
591
  async find(id, column = "id") {
371
592
  return this.where(column, id).first();
372
593
  }
594
+ async findOrFail(id, column = "id") {
595
+ const result = await this.find(id, column);
596
+ if (!result) {
597
+ throw new ModelNotFoundError(this.model?.name || "Model", id);
598
+ }
599
+ return result;
600
+ }
601
+ async firstOrFail() {
602
+ const result = await this.first();
603
+ if (!result) {
604
+ throw new ModelNotFoundError(this.model?.name || "Model");
605
+ }
606
+ return result;
607
+ }
608
+ async firstOrCreate(attributes = {}, values = {}) {
609
+ const found = await this.clone().where(attributes).first();
610
+ if (found)
611
+ return found;
612
+ if (!this.model) {
613
+ throw new Error("firstOrCreate requires a model to be set on the builder");
614
+ }
615
+ const instance = new this.model({ ...attributes, ...values });
616
+ if (typeof instance.setConnection === "function") {
617
+ instance.setConnection(this.connection);
618
+ }
619
+ await instance.save();
620
+ return instance;
621
+ }
622
+ async updateOrCreate(attributes, values = {}) {
623
+ const found = await this.clone().where(attributes).first();
624
+ if (found) {
625
+ const model = found;
626
+ if (typeof model.fill === "function") {
627
+ model.fill(values);
628
+ await model.save();
629
+ }
630
+ return found;
631
+ }
632
+ if (!this.model) {
633
+ throw new Error("updateOrCreate requires a model to be set on the builder");
634
+ }
635
+ const instance = new this.model({ ...attributes, ...values });
636
+ if (typeof instance.setConnection === "function") {
637
+ instance.setConnection(this.connection);
638
+ }
639
+ await instance.save();
640
+ return instance;
641
+ }
373
642
  async pluck(column) {
374
643
  const results = await this.select(column).get();
375
644
  return results.map((row) => row[column]);
@@ -409,32 +678,110 @@ export class Builder {
409
678
  to: total === 0 ? 0 : Math.min(page * perPage, total),
410
679
  };
411
680
  }
681
+ async chunk(count, callback) {
682
+ let page = 1;
683
+ while (true) {
684
+ const items = await this.clone().forPage(page, count).get();
685
+ if (items.length === 0)
686
+ break;
687
+ await callback(items);
688
+ if (items.length < count)
689
+ break;
690
+ page++;
691
+ }
692
+ }
693
+ async each(count, callback) {
694
+ await this.chunk(count, async (items) => {
695
+ for (const item of items) {
696
+ await callback(item);
697
+ }
698
+ });
699
+ }
700
+ async *cursor() {
701
+ let offset = 0;
702
+ while (true) {
703
+ const items = await this.clone().offset(offset).limit(1).get();
704
+ if (items.length === 0)
705
+ break;
706
+ yield items[0];
707
+ offset++;
708
+ }
709
+ }
710
+ async *lazy(count = 1000) {
711
+ let page = 1;
712
+ while (true) {
713
+ const items = await this.clone().forPage(page, count).get();
714
+ if (items.length === 0)
715
+ break;
716
+ for (const item of items) {
717
+ yield item;
718
+ }
719
+ if (items.length < count)
720
+ break;
721
+ page++;
722
+ }
723
+ }
412
724
  async insert(data) {
413
725
  const records = Array.isArray(data) ? data : [data];
414
726
  if (records.length === 0)
415
727
  return;
416
728
  const columns = Object.keys(records[0]);
417
729
  const values = records.map((record) => {
418
- return `(${columns.map((col) => this.escape(record[col])).join(", ")})`;
730
+ return `(${columns.map((col) => this.grammar.escape(record[col])).join(", ")})`;
419
731
  });
420
- const sql = `INSERT INTO ${this.wrap(this.tableName)} (${columns.map((c) => this.wrap(c)).join(", ")}) VALUES ${values.join(", ")}`;
732
+ const sql = `INSERT INTO ${this.grammar.wrap(this.tableName)} (${columns.map((c) => this.grammar.wrap(c)).join(", ")}) VALUES ${values.join(", ")}`;
421
733
  return await this.connection.run(sql);
422
734
  }
423
735
  async insertGetId(data, idColumn = "id") {
424
736
  const result = await this.insert(data);
425
737
  return result?.lastInsertRowid ?? result?.insertId ?? null;
426
738
  }
739
+ async insertOrIgnore(data) {
740
+ const records = Array.isArray(data) ? data : [data];
741
+ if (records.length === 0)
742
+ return;
743
+ const columns = Object.keys(records[0]);
744
+ const values = records.map((record) => {
745
+ return `(${columns.map((col) => this.grammar.escape(record[col])).join(", ")})`;
746
+ });
747
+ const sql = this.grammar.compileInsertOrIgnore(this.grammar.wrap(this.tableName), columns, values);
748
+ return await this.connection.run(sql);
749
+ }
750
+ async upsert(data, uniqueBy, updateColumns) {
751
+ const records = Array.isArray(data) ? data : [data];
752
+ if (records.length === 0)
753
+ return;
754
+ const columns = Object.keys(records[0]);
755
+ const values = records.map((record) => {
756
+ return `(${columns.map((col) => this.grammar.escape(record[col])).join(", ")})`;
757
+ });
758
+ const uniqueCols = Array.isArray(uniqueBy) ? uniqueBy : [uniqueBy];
759
+ const updateCols = updateColumns ?? columns.filter((c) => !uniqueCols.includes(c));
760
+ const sql = this.grammar.compileUpsert(this.grammar.wrap(this.tableName), columns, values, uniqueCols, updateCols);
761
+ return await this.connection.run(sql);
762
+ }
427
763
  async update(data) {
428
764
  const sets = Object.entries(data)
429
- .map(([key, value]) => `${this.wrap(key)} = ${this.escape(value)}`)
765
+ .map(([key, value]) => `${this.grammar.wrap(key)} = ${this.grammar.escape(value)}`)
430
766
  .join(", ");
431
- const sql = `UPDATE ${this.wrap(this.tableName)} SET ${sets} ${this.compileWheres()}`;
432
- return await this.connection.run(sql.trim());
767
+ const sql = this.grammar.compileUpdate(this.grammar.wrap(this.tableName), sets.split(", "), this.compileWheres(), this.updateJoins);
768
+ return await this.connection.run(sql);
433
769
  }
434
770
  async delete() {
435
- const sql = `DELETE FROM ${this.wrap(this.tableName)} ${this.compileWheres()}`;
771
+ const sql = this.grammar.compileDelete(this.grammar.wrap(this.tableName), this.compileWheres(), this.updateJoins, this.limitValue);
772
+ return await this.connection.run(sql);
773
+ }
774
+ async increment(column, amount = 1, extra = {}) {
775
+ const sets = [`${this.grammar.wrap(column)} = ${this.grammar.wrap(column)} + ${amount}`];
776
+ for (const [key, value] of Object.entries(extra)) {
777
+ sets.push(`${this.grammar.wrap(key)} = ${this.grammar.escape(value)}`);
778
+ }
779
+ const sql = `UPDATE ${this.grammar.wrap(this.tableName)} SET ${sets.join(", ")} ${this.compileWheres()}`;
436
780
  return await this.connection.run(sql.trim());
437
781
  }
782
+ async decrement(column, amount = 1, extra = {}) {
783
+ return this.increment(column, -amount, extra);
784
+ }
438
785
  async restore() {
439
786
  const model = this.model;
440
787
  if (!model?.softDeletes) {
@@ -446,6 +793,80 @@ export class Builder {
446
793
  const result = await this.select("1 as exists_check").limit(1).get();
447
794
  return result.length > 0;
448
795
  }
796
+ async doesntExist() {
797
+ return !(await this.exists());
798
+ }
799
+ async sole() {
800
+ const results = await this.limit(2).get();
801
+ if (results.length === 0) {
802
+ throw new ModelNotFoundError(this.model?.name || "Model");
803
+ }
804
+ if (results.length > 1) {
805
+ throw new Error("Multiple records found when only one was expected.");
806
+ }
807
+ return results[0];
808
+ }
809
+ async value(column) {
810
+ const result = await this.first();
811
+ return result ? result[column] : null;
812
+ }
813
+ dump() {
814
+ console.log(this.toSql());
815
+ return this;
816
+ }
817
+ dd() {
818
+ console.log(this.toSql());
819
+ throw new Error("dd() called — execution halted.");
820
+ }
821
+ async explain() {
822
+ const sql = this.grammar.compileExplain(this.toSql());
823
+ const results = await this.connection.query(sql);
824
+ return Array.from(results);
825
+ }
826
+ take(count) {
827
+ return this.limit(count);
828
+ }
829
+ skip(count) {
830
+ return this.offset(count);
831
+ }
832
+ lockForUpdate() {
833
+ const driver = this.connection.getDriverName();
834
+ if (driver !== "sqlite") {
835
+ this.lockMode = "FOR UPDATE";
836
+ }
837
+ return this;
838
+ }
839
+ sharedLock() {
840
+ const driver = this.connection.getDriverName();
841
+ if (driver === "mysql") {
842
+ this.lockMode = "LOCK IN SHARE MODE";
843
+ }
844
+ else if (driver === "postgres") {
845
+ this.lockMode = "FOR SHARE";
846
+ }
847
+ return this;
848
+ }
849
+ skipLocked() {
850
+ if (this.lockMode) {
851
+ this.lockMode += " SKIP LOCKED";
852
+ }
853
+ return this;
854
+ }
855
+ noWait() {
856
+ if (this.lockMode) {
857
+ this.lockMode += " NOWAIT";
858
+ }
859
+ return this;
860
+ }
861
+ addDateWhere(type, column, operator, value, boolean = "and") {
862
+ if (value === undefined) {
863
+ value = operator;
864
+ operator = "=";
865
+ }
866
+ const sql = this.grammar.compileDateWhere(type, this.grammar.wrap(column), operator, value);
867
+ this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
868
+ return this;
869
+ }
449
870
  getModelRelation(relationName) {
450
871
  if (!this.model) {
451
872
  throw new Error(`Cannot query relation "${relationName}" without a model`);