@bunnykit/orm 0.1.20 → 0.1.21

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.
@@ -28,12 +28,12 @@ export class ConnectionManager {
28
28
  }
29
29
  const now = Date.now();
30
30
  const idleTimeout = poolConfig.idleTimeout || 30000;
31
+ // Reuse a healthy idle connection
31
32
  while (pool.length > 0) {
32
33
  const idx = pool.findIndex((c) => !c.inUse && (now - c.lastUsed) < idleTimeout);
33
34
  if (idx === -1)
34
35
  break;
35
36
  const pooled = pool[idx];
36
- pool.splice(idx, 1);
37
37
  try {
38
38
  await pooled.connection.query("SELECT 1");
39
39
  pooled.inUse = true;
@@ -41,6 +41,14 @@ export class ConnectionManager {
41
41
  }
42
42
  catch {
43
43
  await pooled.connection.close().catch(() => null);
44
+ pool.splice(idx, 1);
45
+ }
46
+ }
47
+ // Clean up expired idle connections
48
+ for (let i = pool.length - 1; i >= 0; i--) {
49
+ if (!pool[i].inUse && (now - pool[i].lastUsed) >= idleTimeout) {
50
+ await pool[i].connection.close().catch(() => null);
51
+ pool.splice(i, 1);
44
52
  }
45
53
  }
46
54
  if (pool.length < (poolConfig.maxConnections || 10)) {
@@ -203,6 +211,14 @@ export class ConnectionManager {
203
211
  this.connections.clear();
204
212
  this.pools.clear();
205
213
  this.tenantCache.clear();
214
+ // Reject all pending waiters
215
+ for (const [name, poolWaiters] of this.waiters) {
216
+ for (const waiter of poolWaiters) {
217
+ clearTimeout(waiter.timer);
218
+ waiter.reject(new Error(`Connection pool "${name}" is closing`));
219
+ }
220
+ }
221
+ this.waiters.clear();
206
222
  for (const connection of connections) {
207
223
  await connection.close();
208
224
  }
@@ -23,6 +23,15 @@ export class BelongsToMany {
23
23
  this.relatedPivotKey = relatedPivotKey || `${snakeCase(related.name)}_id`;
24
24
  this.builder = related.on(parent.getConnection());
25
25
  this.addConstraints();
26
+ // Wrap getResults with lazy-loading guard
27
+ const originalGetResults = this.getResults.bind(this);
28
+ this.getResults = async () => {
29
+ if (this.parent.constructor.preventLazyLoading) {
30
+ throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
31
+ `Eager load the relation using with().`);
32
+ }
33
+ return await originalGetResults();
34
+ };
26
35
  }
27
36
  addConstraints() {
28
37
  const relatedTable = this.related.getTable();
@@ -5,4 +5,5 @@ export declare class IdentityMap {
5
5
  static get(table: string, key: string | number): Model | undefined;
6
6
  static set(table: string, key: string | number, model: Model): void;
7
7
  static clear(): void;
8
+ static delete(table: string, key: string | number): void;
8
9
  }
@@ -25,4 +25,10 @@ export class IdentityMap {
25
25
  return;
26
26
  map.clear();
27
27
  }
28
+ static delete(table, key) {
29
+ const map = this.current();
30
+ if (!map)
31
+ return;
32
+ map.delete(`${table}:${String(key)}`);
33
+ }
28
34
  }
@@ -332,6 +332,29 @@ export class Model {
332
332
  }
333
333
  return Reflect.set(target, prop, value, receiver);
334
334
  },
335
+ has(target, prop) {
336
+ if (typeof prop === "string" && prop in target.$attributes)
337
+ return true;
338
+ return Reflect.has(target, prop);
339
+ },
340
+ getOwnPropertyDescriptor(target, prop) {
341
+ if (typeof prop === "string" && prop in target.$attributes) {
342
+ return {
343
+ enumerable: true,
344
+ configurable: true,
345
+ value: target.getAttribute(prop),
346
+ };
347
+ }
348
+ return Reflect.getOwnPropertyDescriptor(target, prop);
349
+ },
350
+ ownKeys(target) {
351
+ const keys = new Set(Reflect.ownKeys(target));
352
+ for (const key of Object.keys(target.$attributes)) {
353
+ if (!key.startsWith("$"))
354
+ keys.add(key);
355
+ }
356
+ return Array.from(keys);
357
+ },
335
358
  });
336
359
  }
337
360
  defineAttributeProperty(key) {
@@ -345,7 +368,17 @@ export class Model {
345
368
  });
346
369
  }
347
370
  syncAttributeProperties() {
348
- for (const key of Object.keys(this.$attributes)) {
371
+ const currentKeys = new Set(Object.keys(this.$attributes));
372
+ // Remove stale attribute properties
373
+ for (const key of Reflect.ownKeys(this)) {
374
+ if (key.startsWith("$") || typeof key !== "string")
375
+ continue;
376
+ const desc = Object.getOwnPropertyDescriptor(this, key);
377
+ if (desc && desc.get && desc.configurable && !currentKeys.has(key)) {
378
+ delete this[key];
379
+ }
380
+ }
381
+ for (const key of currentKeys) {
349
382
  this.defineAttributeProperty(key);
350
383
  }
351
384
  }
@@ -918,6 +951,10 @@ export class Model {
918
951
  .delete();
919
952
  this.$exists = false;
920
953
  }
954
+ const identityMap = IdentityMap.current();
955
+ if (identityMap) {
956
+ IdentityMap.delete(constructor.getTable(), pk);
957
+ }
921
958
  await ObserverRegistry.dispatch("deleted", this);
922
959
  return true;
923
960
  }
@@ -948,6 +985,10 @@ export class Model {
948
985
  .where(constructor.primaryKey, pk)
949
986
  .delete();
950
987
  this.$exists = false;
988
+ const identityMap = IdentityMap.current();
989
+ if (identityMap) {
990
+ IdentityMap.delete(constructor.getTable(), pk);
991
+ }
951
992
  return true;
952
993
  }
953
994
  async refresh() {
@@ -955,11 +996,20 @@ export class Model {
955
996
  const pk = this.getAttribute(constructor.primaryKey);
956
997
  if (!pk)
957
998
  return this;
999
+ // Bypass identity map to fetch fresh data
1000
+ const identityMap = IdentityMap.current();
1001
+ if (identityMap) {
1002
+ IdentityMap.delete(constructor.getTable(), pk);
1003
+ }
958
1004
  const result = await constructor.find(pk);
959
1005
  if (result) {
960
1006
  this.$attributes = result.$attributes;
961
1007
  this.$original = { ...result.$attributes };
962
1008
  this.syncAttributeProperties();
1009
+ // Ensure this instance is the canonical one in the identity map
1010
+ if (identityMap) {
1011
+ IdentityMap.set(constructor.getTable(), pk, this);
1012
+ }
963
1013
  }
964
1014
  return this;
965
1015
  }
@@ -13,6 +13,15 @@ export class MorphTo {
13
13
  this.typeColumn = `${name}_type`;
14
14
  this.idColumn = `${name}_id`;
15
15
  this.typeMap = typeMap;
16
+ // Wrap getResults with lazy-loading guard
17
+ const originalGetResults = this.getResults.bind(this);
18
+ this.getResults = async () => {
19
+ if (this.parent.constructor.preventLazyLoading) {
20
+ throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
21
+ `Eager load the relation using with().`);
22
+ }
23
+ return await originalGetResults();
24
+ };
16
25
  }
17
26
  async getResults() {
18
27
  const type = this.parent.getAttribute(this.typeColumn);
@@ -99,6 +108,15 @@ export class MorphOne {
99
108
  this.builder = related.on(parent.getConnection());
100
109
  this.builder.where(this.typeColumn, this.getMorphType());
101
110
  this.builder.where(this.idColumn, this.parent.getAttribute(this.localKey));
111
+ // Wrap getResults with lazy-loading guard
112
+ const originalGetResults = this.getResults.bind(this);
113
+ this.getResults = async () => {
114
+ if (this.parent.constructor.preventLazyLoading) {
115
+ throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
116
+ `Eager load the relation using with().`);
117
+ }
118
+ return await originalGetResults();
119
+ };
102
120
  }
103
121
  getMorphType() {
104
122
  return this.parent.constructor.morphName || this.parent.constructor.name;
@@ -168,6 +186,15 @@ export class MorphMany {
168
186
  this.builder = related.on(parent.getConnection());
169
187
  this.builder.where(this.typeColumn, this.getMorphType());
170
188
  this.builder.where(this.idColumn, this.parent.getAttribute(this.localKey));
189
+ // Wrap getResults with lazy-loading guard
190
+ const originalGetResults = this.getResults.bind(this);
191
+ this.getResults = async () => {
192
+ if (this.parent.constructor.preventLazyLoading) {
193
+ throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
194
+ `Eager load the relation using with().`);
195
+ }
196
+ return await originalGetResults();
197
+ };
171
198
  }
172
199
  getMorphType() {
173
200
  return this.parent.constructor.morphName || this.parent.constructor.name;
@@ -244,6 +271,15 @@ export class MorphToMany {
244
271
  this.relatedPivotKey = relatedPivotKey || `${snakeCase(related.name)}_id`;
245
272
  this.builder = related.on(parent.getConnection());
246
273
  this.addConstraints();
274
+ // Wrap getResults with lazy-loading guard
275
+ const originalGetResults = this.getResults.bind(this);
276
+ this.getResults = async () => {
277
+ if (this.parent.constructor.preventLazyLoading) {
278
+ throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
279
+ `Eager load the relation using with().`);
280
+ }
281
+ return await originalGetResults();
282
+ };
247
283
  }
248
284
  addConstraints() {
249
285
  const relatedTable = this.related.getTable();
@@ -182,10 +182,7 @@ export class Builder {
182
182
  return this.whereRaw(sql, "or", scope);
183
183
  }
184
184
  whereJsonContains(column, value, boolean = "and", not = false) {
185
- let sql = this.grammar.compileJsonContains(this.grammar.wrap(column), value);
186
- if (not)
187
- sql = `NOT (${sql})`;
188
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
185
+ this.wheres.push({ type: "json_contains", column, value, boolean, scope: undefined, not });
189
186
  return this;
190
187
  }
191
188
  whereJsonLength(column, operator = "=", value, boolean = "and", not = false) {
@@ -193,41 +190,31 @@ export class Builder {
193
190
  value = operator;
194
191
  operator = "=";
195
192
  }
196
- let sql = this.grammar.compileJsonLength(this.grammar.wrap(column), String(operator), value);
197
- if (not)
198
- sql = `NOT (${sql})`;
199
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
193
+ this.wheres.push({ type: "json_length", column, operator: String(operator), value, boolean, scope: undefined, not });
200
194
  return this;
201
195
  }
202
196
  whereLike(column, value, boolean = "and", not = false) {
203
- const sql = this.grammar.compileLike(this.grammar.wrap(column), value, not);
204
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
197
+ this.wheres.push({ type: "like", column, value, boolean, scope: undefined, not });
205
198
  return this;
206
199
  }
207
200
  whereNotLike(column, value) {
208
201
  return this.whereLike(column, value, "and", true);
209
202
  }
210
203
  whereRegexp(column, value, boolean = "and", not = false) {
211
- const sql = this.grammar.compileRegexp(this.grammar.wrap(column), value, not);
212
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
204
+ this.wheres.push({ type: "regexp", column, value, boolean, scope: undefined, not });
213
205
  return this;
214
206
  }
215
207
  whereFullText(columns, value, boolean = "and", not = false) {
216
208
  const cols = Array.isArray(columns) ? columns : [columns];
217
- let sql = this.grammar.compileFullText(cols.map((c) => this.grammar.wrap(c)), value);
218
- if (not)
219
- sql = `NOT (${sql})`;
220
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
209
+ this.wheres.push({ type: "fulltext", column: "", columns: cols, value, boolean, scope: undefined, not });
221
210
  return this;
222
211
  }
223
212
  whereAll(columns, operator, value, boolean = "and") {
224
- const sql = columns.map((c) => `${this.grammar.wrap(c)} ${operator} ${this.grammar.escape(value)}`).join(" AND ");
225
- this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
213
+ this.wheres.push({ type: "all", column: "", columns: columns, operator, value, boolean, scope: undefined });
226
214
  return this;
227
215
  }
228
216
  whereAny(columns, operator, value, boolean = "and") {
229
- const sql = columns.map((c) => `${this.grammar.wrap(c)} ${operator} ${this.grammar.escape(value)}`).join(" OR ");
230
- this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
217
+ this.wheres.push({ type: "any", column: "", columns: columns, operator, value, boolean, scope: undefined });
231
218
  return this;
232
219
  }
233
220
  orderBy(column, direction = "asc") {
@@ -459,6 +446,7 @@ export class Builder {
459
446
  cloned.fromRaw = this.fromRaw;
460
447
  cloned.updateJoins = [...this.updateJoins];
461
448
  cloned.bindings = [...this.bindings];
449
+ cloned.parameterize = this.parameterize;
462
450
  return cloned;
463
451
  }
464
452
  wrapColumn(value) {
@@ -496,6 +484,53 @@ export class Builder {
496
484
  else if (where.type === "raw") {
497
485
  return `${prefix} ${where.column}`;
498
486
  }
487
+ else if (where.type === "like") {
488
+ const sql = this.grammar.compileLike(this.grammar.wrap(where.column), where.value, !!where.not, this.parameterize ? (v) => this.addBinding(v) : undefined);
489
+ return `${prefix} ${sql}`;
490
+ }
491
+ else if (where.type === "regexp") {
492
+ const sql = this.grammar.compileRegexp(this.grammar.wrap(where.column), where.value, !!where.not, this.parameterize ? (v) => this.addBinding(v) : undefined);
493
+ return `${prefix} ${sql}`;
494
+ }
495
+ else if (where.type === "fulltext") {
496
+ const cols = (where.columns || []).map((c) => this.grammar.wrap(c));
497
+ let sql = this.grammar.compileFullText(cols, where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
498
+ if (where.not)
499
+ sql = `NOT (${sql})`;
500
+ return `${prefix} ${sql}`;
501
+ }
502
+ else if (where.type === "json_contains") {
503
+ let sql = this.grammar.compileJsonContains(this.grammar.wrap(where.column), where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
504
+ if (where.not)
505
+ sql = `NOT (${sql})`;
506
+ return `${prefix} ${sql}`;
507
+ }
508
+ else if (where.type === "json_length") {
509
+ let sql = this.grammar.compileJsonLength(this.grammar.wrap(where.column), where.operator || "=", where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
510
+ if (where.not)
511
+ sql = `NOT (${sql})`;
512
+ return `${prefix} ${sql}`;
513
+ }
514
+ else if (where.type === "date") {
515
+ const sql = this.grammar.compileDateWhere(where.dateType || "date", this.grammar.wrap(where.column), where.operator || "=", where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
516
+ return `${prefix} ${sql}`;
517
+ }
518
+ else if (where.type === "all") {
519
+ const cols = (where.columns || []).map((c) => this.grammar.wrap(c));
520
+ const inner = cols.map((c) => {
521
+ const val = this.parameterize ? this.addBinding(where.value) : this.grammar.escape(where.value);
522
+ return `${c} ${where.operator} ${val}`;
523
+ }).join(" AND ");
524
+ return `${prefix} (${inner})`;
525
+ }
526
+ else if (where.type === "any") {
527
+ const cols = (where.columns || []).map((c) => this.grammar.wrap(c));
528
+ const inner = cols.map((c) => {
529
+ const val = this.parameterize ? this.addBinding(where.value) : this.grammar.escape(where.value);
530
+ return `${c} ${where.operator} ${val}`;
531
+ }).join(" OR ");
532
+ return `${prefix} (${inner})`;
533
+ }
499
534
  else if (where.type === "column") {
500
535
  return `${prefix} ${this.grammar.wrap(where.column)} ${where.operator} ${this.grammar.wrap(where.value)}`;
501
536
  }
@@ -740,16 +775,34 @@ export class Builder {
740
775
  async *cursor(chunkSize = 1000) {
741
776
  const model = this.model;
742
777
  const primaryKey = model ? model.primaryKey || "id" : "id";
778
+ // Cursor pagination is incompatible with random ordering
779
+ if (this.randomOrderFlag) {
780
+ throw new Error("cursor() does not support inRandomOrder(). Use lazy() instead.");
781
+ }
743
782
  const orderColumn = this.orders[0]?.column || primaryKey;
744
783
  const orderDirection = this.orders[0]?.direction || "asc";
784
+ // Use unqualified column name for property access on model instances
785
+ const accessColumn = orderColumn.includes(".") ? orderColumn.split(".")[1] : orderColumn;
745
786
  let lastValue = undefined;
746
787
  while (true) {
747
788
  const builder = this.clone();
748
- builder.orders = [{ column: orderColumn, direction: orderDirection }];
789
+ // Preserve multi-column ORDER BY, appending PK tie-breaker if not present
790
+ builder.orders = this.orders.length > 0 ? [...this.orders] : [{ column: orderColumn, direction: orderDirection }];
791
+ const hasPkOrder = builder.orders.some((o) => o.column === primaryKey);
792
+ if (!hasPkOrder) {
793
+ builder.orders.push({ column: primaryKey, direction: orderDirection });
794
+ }
749
795
  builder.offsetValue = undefined;
750
796
  builder.limitValue = chunkSize;
751
797
  if (lastValue !== undefined) {
752
798
  const op = orderDirection === "asc" ? ">" : "<";
799
+ // Parenthesize existing wheres when appending cursor condition to preserve OR precedence
800
+ if (builder.wheres.length > 0) {
801
+ const hasOr = builder.wheres.some((w) => w.boolean === "or");
802
+ if (hasOr) {
803
+ builder.wheres = [{ type: "raw", column: `(${builder.compileWheres().replace(/^WHERE /, "")})`, boolean: "and", scope: undefined }];
804
+ }
805
+ }
753
806
  builder.wheres.push({
754
807
  type: "basic",
755
808
  column: orderColumn,
@@ -769,7 +822,7 @@ export class Builder {
769
822
  break;
770
823
  const lastItem = items[items.length - 1];
771
824
  lastValue = lastItem && typeof lastItem === "object"
772
- ? lastItem[orderColumn]
825
+ ? lastItem[accessColumn]
773
826
  : undefined;
774
827
  }
775
828
  }
@@ -958,8 +1011,7 @@ export class Builder {
958
1011
  value = operator;
959
1012
  operator = "=";
960
1013
  }
961
- const sql = this.grammar.compileDateWhere(type, this.grammar.wrap(column), operator, value);
962
- this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
1014
+ this.wheres.push({ type: "date", column: column, operator, value, boolean, scope: undefined, dateType: type });
963
1015
  return this;
964
1016
  }
965
1017
  getModelRelation(relationName) {
@@ -6,15 +6,15 @@ export declare abstract class Grammar {
6
6
  abstract compileRandomOrder(): string;
7
7
  compileOffset(offset: number, _limit?: number): string;
8
8
  compileLock(lockMode?: string): string;
9
- abstract compileDateWhere(type: string, column: string, operator: string, value: any): string;
9
+ abstract compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
10
10
  abstract compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
11
11
  abstract compileUpsert(table: string, columns: string[], values: string[], uniqueBy: string[], updateColumns: string[]): string;
12
12
  compileUpdate(table: string, sets: string[], wheres: string, joins?: string[]): string;
13
13
  compileDelete(table: string, wheres: string, joins?: string[], limit?: number): string;
14
- abstract compileJsonContains(column: string, value: any): string;
15
- abstract compileJsonLength(column: string, operator: string, value: any): string;
16
- compileLike(column: string, value: string, not: boolean): string;
17
- abstract compileRegexp(column: string, value: string, not: boolean): string;
18
- abstract compileFullText(columns: string[], value: string): string;
14
+ abstract compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
15
+ abstract compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
16
+ compileLike(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
17
+ abstract compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
18
+ abstract compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
19
19
  abstract compileExplain(sql: string): string;
20
20
  }
@@ -40,8 +40,8 @@ export class Grammar {
40
40
  sql += ` LIMIT ${limit}`;
41
41
  return sql.trim();
42
42
  }
43
- compileLike(column, value, not) {
43
+ compileLike(column, value, not, binding) {
44
44
  const op = not ? "NOT LIKE" : "LIKE";
45
- return `${column} ${op} ${this.escape(value)}`;
45
+ return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
46
46
  }
47
47
  }
@@ -3,12 +3,12 @@ export declare class MySqlGrammar extends Grammar {
3
3
  wrap(value: string): string;
4
4
  placeholder(_index: number): string;
5
5
  compileRandomOrder(): string;
6
- compileDateWhere(type: string, column: string, operator: string, value: any): string;
6
+ compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
7
7
  compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
8
8
  compileUpsert(table: string, columns: string[], values: string[], _uniqueBy: string[], updateColumns: string[]): string;
9
- compileJsonContains(column: string, value: any): string;
10
- compileJsonLength(column: string, operator: string, value: any): string;
11
- compileRegexp(column: string, value: string, not: boolean): string;
12
- compileFullText(columns: string[], value: string): string;
9
+ compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
10
+ compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
11
+ compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
12
+ compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
13
13
  compileExplain(sql: string): string;
14
14
  }
@@ -18,20 +18,21 @@ export class MySqlGrammar extends Grammar {
18
18
  compileRandomOrder() {
19
19
  return "ORDER BY RAND()";
20
20
  }
21
- compileDateWhere(type, column, operator, value) {
21
+ compileDateWhere(type, column, operator, value, binding) {
22
+ const val = binding ? binding(value) : this.escape(value);
22
23
  switch (type) {
23
24
  case "date":
24
- return `DATE(${column}) ${operator} ${this.escape(value)}`;
25
+ return `DATE(${column}) ${operator} ${val}`;
25
26
  case "day":
26
- return `DAY(${column}) ${operator} ${this.escape(value)}`;
27
+ return `DAY(${column}) ${operator} ${val}`;
27
28
  case "month":
28
- return `MONTH(${column}) ${operator} ${this.escape(value)}`;
29
+ return `MONTH(${column}) ${operator} ${val}`;
29
30
  case "year":
30
- return `YEAR(${column}) ${operator} ${this.escape(value)}`;
31
+ return `YEAR(${column}) ${operator} ${val}`;
31
32
  case "time":
32
- return `TIME(${column}) ${operator} ${this.escape(value)}`;
33
+ return `TIME(${column}) ${operator} ${val}`;
33
34
  default:
34
- return `${column} ${operator} ${this.escape(value)}`;
35
+ return `${column} ${operator} ${val}`;
35
36
  }
36
37
  }
37
38
  compileInsertOrIgnore(table, columns, values) {
@@ -43,18 +44,18 @@ export class MySqlGrammar extends Grammar {
43
44
  .join(", ");
44
45
  return `INSERT INTO ${table} (${columns.map((c) => this.wrap(c)).join(", ")}) VALUES ${values.join(", ")} ON DUPLICATE KEY UPDATE ${updateCols}`;
45
46
  }
46
- compileJsonContains(column, value) {
47
- return `JSON_CONTAINS(${column}, ${this.escape(JSON.stringify(value))})`;
47
+ compileJsonContains(column, value, binding) {
48
+ return `JSON_CONTAINS(${column}, ${binding ? binding(JSON.stringify(value)) : this.escape(JSON.stringify(value))})`;
48
49
  }
49
- compileJsonLength(column, operator, value) {
50
- return `JSON_LENGTH(${column}) ${operator} ${this.escape(value)}`;
50
+ compileJsonLength(column, operator, value, binding) {
51
+ return `JSON_LENGTH(${column}) ${operator} ${binding ? binding(value) : this.escape(value)}`;
51
52
  }
52
- compileRegexp(column, value, not) {
53
+ compileRegexp(column, value, not, binding) {
53
54
  const op = not ? "NOT REGEXP" : "REGEXP";
54
- return `${column} ${op} ${this.escape(value)}`;
55
+ return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
55
56
  }
56
- compileFullText(columns, value) {
57
- return `MATCH (${columns.join(", ")}) AGAINST (${this.escape(value)})`;
57
+ compileFullText(columns, value, binding) {
58
+ return `MATCH (${columns.join(", ")}) AGAINST (${binding ? binding(value) : this.escape(value)})`;
58
59
  }
59
60
  compileExplain(sql) {
60
61
  return `EXPLAIN ${sql}`;
@@ -3,12 +3,12 @@ export declare class PostgresGrammar extends Grammar {
3
3
  wrap(value: string): string;
4
4
  placeholder(index: number): string;
5
5
  compileRandomOrder(): string;
6
- compileDateWhere(type: string, column: string, operator: string, value: any): string;
6
+ compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
7
7
  compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
8
8
  compileUpsert(table: string, columns: string[], values: string[], uniqueBy: string[], updateColumns: string[]): string;
9
- compileJsonContains(column: string, value: any): string;
10
- compileJsonLength(column: string, operator: string, value: any): string;
11
- compileRegexp(column: string, value: string, not: boolean): string;
12
- compileFullText(columns: string[], value: string): string;
9
+ compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
10
+ compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
11
+ compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
12
+ compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
13
13
  compileExplain(sql: string): string;
14
14
  }
@@ -18,20 +18,21 @@ export class PostgresGrammar extends Grammar {
18
18
  compileRandomOrder() {
19
19
  return "ORDER BY RANDOM()";
20
20
  }
21
- compileDateWhere(type, column, operator, value) {
21
+ compileDateWhere(type, column, operator, value, binding) {
22
+ const val = binding ? binding(value) : this.escape(value);
22
23
  switch (type) {
23
24
  case "date":
24
- return `(${column})::date ${operator} ${this.escape(value)}`;
25
+ return `(${column})::date ${operator} ${val}`;
25
26
  case "day":
26
- return `EXTRACT(DAY FROM ${column}) ${operator} ${this.escape(value)}`;
27
+ return `EXTRACT(DAY FROM ${column}) ${operator} ${val}`;
27
28
  case "month":
28
- return `EXTRACT(MONTH FROM ${column}) ${operator} ${this.escape(value)}`;
29
+ return `EXTRACT(MONTH FROM ${column}) ${operator} ${val}`;
29
30
  case "year":
30
- return `EXTRACT(YEAR FROM ${column}) ${operator} ${this.escape(value)}`;
31
+ return `EXTRACT(YEAR FROM ${column}) ${operator} ${val}`;
31
32
  case "time":
32
- return `(${column})::time ${operator} ${this.escape(value)}`;
33
+ return `(${column})::time ${operator} ${val}`;
33
34
  default:
34
- return `${column} ${operator} ${this.escape(value)}`;
35
+ return `${column} ${operator} ${val}`;
35
36
  }
36
37
  }
37
38
  compileInsertOrIgnore(table, columns, values) {
@@ -43,21 +44,21 @@ export class PostgresGrammar extends Grammar {
43
44
  .join(", ");
44
45
  return `INSERT INTO ${table} (${columns.map((c) => this.wrap(c)).join(", ")}) VALUES ${values.join(", ")} ON CONFLICT (${uniqueBy.map((c) => this.wrap(c)).join(", ")}) DO UPDATE SET ${updateCols}`;
45
46
  }
46
- compileJsonContains(column, value) {
47
- return `${column} @> ${this.escape(JSON.stringify([value]))}`;
47
+ compileJsonContains(column, value, binding) {
48
+ return `${column} @> ${binding ? binding(JSON.stringify([value])) : this.escape(JSON.stringify([value]))}`;
48
49
  }
49
- compileJsonLength(column, operator, value) {
50
- return `jsonb_array_length(${column}) ${operator} ${this.escape(value)}`;
50
+ compileJsonLength(column, operator, value, binding) {
51
+ return `jsonb_array_length(${column}) ${operator} ${binding ? binding(value) : this.escape(value)}`;
51
52
  }
52
- compileRegexp(column, value, not) {
53
+ compileRegexp(column, value, not, binding) {
53
54
  const op = not ? "!~" : "~";
54
- return `${column} ${op} ${this.escape(value)}`;
55
+ return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
55
56
  }
56
- compileFullText(columns, value) {
57
+ compileFullText(columns, value, binding) {
57
58
  const cols = columns.length > 1
58
59
  ? `concat_ws(' ', ${columns.join(", ")})`
59
60
  : columns[0];
60
- return `to_tsvector('english', ${cols}) @@ plainto_tsquery('english', ${this.escape(value)})`;
61
+ return `to_tsvector('english', ${cols}) @@ plainto_tsquery('english', ${binding ? binding(value) : this.escape(value)})`;
61
62
  }
62
63
  compileExplain(sql) {
63
64
  return `EXPLAIN (FORMAT JSON) ${sql}`;
@@ -4,12 +4,12 @@ export declare class SQLiteGrammar extends Grammar {
4
4
  placeholder(_index: number): string;
5
5
  compileRandomOrder(): string;
6
6
  compileOffset(offset: number, limit?: number): string;
7
- compileDateWhere(type: string, column: string, operator: string, value: any): string;
7
+ compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
8
8
  compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
9
9
  compileUpsert(table: string, columns: string[], values: string[], uniqueBy: string[], updateColumns: string[]): string;
10
- compileJsonContains(column: string, value: any): string;
11
- compileJsonLength(column: string, operator: string, value: any): string;
12
- compileRegexp(column: string, value: string, not: boolean): string;
13
- compileFullText(columns: string[], value: string): string;
10
+ compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
11
+ compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
12
+ compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
13
+ compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
14
14
  compileExplain(sql: string): string;
15
15
  }
@@ -22,20 +22,21 @@ export class SQLiteGrammar extends Grammar {
22
22
  const limitSql = limit === undefined ? "LIMIT -1 " : "";
23
23
  return `${limitSql}OFFSET ${offset}`;
24
24
  }
25
- compileDateWhere(type, column, operator, value) {
25
+ compileDateWhere(type, column, operator, value, binding) {
26
+ const val = binding ? binding(value) : this.escape(value);
26
27
  switch (type) {
27
28
  case "date":
28
- return `date(${column}) ${operator} ${this.escape(value)}`;
29
+ return `date(${column}) ${operator} ${val}`;
29
30
  case "day":
30
- return `CAST(strftime('%d', ${column}) AS INTEGER) ${operator} ${this.escape(value)}`;
31
+ return `CAST(strftime('%d', ${column}) AS INTEGER) ${operator} ${val}`;
31
32
  case "month":
32
- return `CAST(strftime('%m', ${column}) AS INTEGER) ${operator} ${this.escape(value)}`;
33
+ return `CAST(strftime('%m', ${column}) AS INTEGER) ${operator} ${val}`;
33
34
  case "year":
34
- return `CAST(strftime('%Y', ${column}) AS INTEGER) ${operator} ${this.escape(value)}`;
35
+ return `CAST(strftime('%Y', ${column}) AS INTEGER) ${operator} ${val}`;
35
36
  case "time":
36
- return `time(${column}) ${operator} ${this.escape(value)}`;
37
+ return `time(${column}) ${operator} ${val}`;
37
38
  default:
38
- return `${column} ${operator} ${this.escape(value)}`;
39
+ return `${column} ${operator} ${val}`;
39
40
  }
40
41
  }
41
42
  compileInsertOrIgnore(table, columns, values) {
@@ -47,18 +48,18 @@ export class SQLiteGrammar extends Grammar {
47
48
  .join(", ");
48
49
  return `INSERT INTO ${table} (${columns.map((c) => this.wrap(c)).join(", ")}) VALUES ${values.join(", ")} ON CONFLICT(${uniqueBy.map((c) => this.wrap(c)).join(", ")}) DO UPDATE SET ${updateCols}`;
49
50
  }
50
- compileJsonContains(column, value) {
51
- return `${column} IN (SELECT value FROM json_each(${column})) AND ${this.escape(value)} IN (SELECT value FROM json_each(${column}))`;
51
+ compileJsonContains(column, value, binding) {
52
+ return `${column} IN (SELECT value FROM json_each(${column})) AND ${binding ? binding(value) : this.escape(value)} IN (SELECT value FROM json_each(${column}))`;
52
53
  }
53
- compileJsonLength(column, operator, value) {
54
- return `(SELECT COUNT(*) FROM json_each(${column})) ${operator} ${this.escape(value)}`;
54
+ compileJsonLength(column, operator, value, binding) {
55
+ return `(SELECT COUNT(*) FROM json_each(${column})) ${operator} ${binding ? binding(value) : this.escape(value)}`;
55
56
  }
56
- compileRegexp(column, value, not) {
57
+ compileRegexp(column, value, not, binding) {
57
58
  const op = not ? "NOT REGEXP" : "REGEXP";
58
- return `${column} ${op} ${this.escape(value)}`;
59
+ return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
59
60
  }
60
- compileFullText(columns, value) {
61
- return columns.map((c) => `${this.wrap(c)} LIKE ${this.escape(`%${value}%`)}`).join(" OR ");
61
+ compileFullText(columns, value, binding) {
62
+ return columns.map((c) => `${this.wrap(c)} LIKE ${binding ? binding(`%${value}%`) : this.escape(`%${value}%`)}`).join(" OR ");
62
63
  }
63
64
  compileExplain(sql) {
64
65
  return `EXPLAIN QUERY PLAN ${sql}`;
@@ -29,12 +29,15 @@ export interface ForeignKeyDefinition {
29
29
  onUpdate?: string;
30
30
  }
31
31
  export interface WhereClause {
32
- type: "basic" | "in" | "null" | "raw" | "between" | "column" | "exists";
32
+ type: "basic" | "in" | "null" | "raw" | "between" | "column" | "exists" | "like" | "regexp" | "fulltext" | "json_contains" | "json_length" | "date" | "all" | "any";
33
33
  column: string;
34
+ columns?: string[];
34
35
  operator?: string;
35
36
  value?: any;
36
37
  boolean: "and" | "or";
37
38
  scope?: string;
39
+ not?: boolean;
40
+ dateType?: string;
38
41
  }
39
42
  export interface OrderClause {
40
43
  column: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "An Eloquent-inspired ORM for Bun's native SQL client supporting SQLite, MySQL, and PostgreSQL.",
5
5
  "license": "MIT",
6
6
  "packageManager": "bun@1.3.12",