@bunnykit/orm 0.1.22 → 0.1.24

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.
@@ -9,6 +9,7 @@ export declare class Connection {
9
9
  private schema?;
10
10
  private ownsDriver;
11
11
  private transactionDepth;
12
+ private savepointId;
12
13
  constructor(config: ConnectionConfig, options?: {
13
14
  driver?: SQL;
14
15
  schema?: string;
@@ -10,6 +10,7 @@ export class Connection {
10
10
  schema;
11
11
  ownsDriver;
12
12
  transactionDepth = 0;
13
+ savepointId = 0;
13
14
  constructor(config, options = {}) {
14
15
  this.config = config;
15
16
  this.schema = options.schema || ("schema" in config ? config.schema : undefined);
@@ -83,16 +84,36 @@ export class Connection {
83
84
  return await this.driver.unsafe(sqlString, bindings);
84
85
  }
85
86
  async beginTransaction() {
86
- await this.driver.unsafe("BEGIN");
87
+ if (this.transactionDepth === 0) {
88
+ await this.driver.unsafe("BEGIN");
89
+ }
90
+ else {
91
+ await this.driver.unsafe(`SAVEPOINT bunny_trans_${++this.savepointId}`);
92
+ }
87
93
  this.transactionDepth++;
88
94
  }
89
95
  async commit() {
90
- await this.driver.unsafe("COMMIT");
91
- this.transactionDepth = Math.max(0, this.transactionDepth - 1);
96
+ if (this.transactionDepth <= 0)
97
+ return;
98
+ if (this.transactionDepth === 1) {
99
+ await this.driver.unsafe("COMMIT");
100
+ }
101
+ else {
102
+ await this.driver.unsafe(`RELEASE SAVEPOINT bunny_trans_${this.savepointId--}`);
103
+ }
104
+ this.transactionDepth--;
92
105
  }
93
106
  async rollback() {
94
- await this.driver.unsafe("ROLLBACK");
95
- this.transactionDepth = Math.max(0, this.transactionDepth - 1);
107
+ if (this.transactionDepth <= 0)
108
+ return;
109
+ if (this.transactionDepth === 1) {
110
+ await this.driver.unsafe("ROLLBACK");
111
+ }
112
+ else {
113
+ await this.driver.unsafe(`ROLLBACK TO SAVEPOINT bunny_trans_${this.savepointId}`);
114
+ await this.driver.unsafe(`RELEASE SAVEPOINT bunny_trans_${this.savepointId--}`);
115
+ }
116
+ this.transactionDepth--;
96
117
  }
97
118
  isInTransaction() {
98
119
  return this.transactionDepth > 0;
@@ -195,30 +195,30 @@ export class Migrator {
195
195
  const statements = [];
196
196
  for (const row of tables) {
197
197
  const table = row[key];
198
- const createRows = await this.connection.query(`SHOW CREATE TABLE ${table}`);
198
+ const createRows = await this.connection.query(`SHOW CREATE TABLE ${this.connection.getGrammar().wrap(table)}`);
199
199
  statements.push(`${createRows[0]["Create Table"]};`);
200
200
  }
201
201
  return statements.join("\n\n") + "\n";
202
202
  }
203
203
  const schema = this.connection.getSchema() || "public";
204
- const tables = await this.connection.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = '${schema}' AND table_type = 'BASE TABLE' ORDER BY table_name`);
204
+ const tables = await this.connection.query("SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE' ORDER BY table_name", [schema]);
205
205
  const statements = [];
206
206
  for (const tableRow of tables) {
207
207
  const table = tableRow.table_name;
208
208
  const columns = await this.connection.query(`SELECT column_name, data_type, is_nullable, column_default, character_maximum_length, numeric_precision, numeric_scale
209
209
  FROM information_schema.columns
210
- WHERE table_schema = '${schema}' AND table_name = '${table}'
211
- ORDER BY ordinal_position`);
210
+ WHERE table_schema = $1 AND table_name = $2
211
+ ORDER BY ordinal_position`, [schema, table]);
212
212
  const primaryKeys = await this.connection.query(`SELECT kcu.column_name
213
213
  FROM information_schema.table_constraints tc
214
214
  JOIN information_schema.key_column_usage kcu
215
215
  ON tc.constraint_name = kcu.constraint_name
216
216
  AND tc.table_schema = kcu.table_schema
217
217
  AND tc.table_name = kcu.table_name
218
- WHERE tc.table_schema = '${schema}'
219
- AND tc.table_name = '${table}'
218
+ WHERE tc.table_schema = $1
219
+ AND tc.table_name = $2
220
220
  AND tc.constraint_type = 'PRIMARY KEY'
221
- ORDER BY kcu.ordinal_position`);
221
+ ORDER BY kcu.ordinal_position`, [schema, table]);
222
222
  const pkColumns = primaryKeys.map((row) => row.column_name);
223
223
  const columnSql = columns.map((column) => {
224
224
  let type = String(column.data_type).toUpperCase();
@@ -228,7 +228,7 @@ export class Migrator {
228
228
  else if ((type === "NUMERIC" || type === "DECIMAL") && column.numeric_precision) {
229
229
  type = `${type}(${column.numeric_precision}${column.numeric_scale ? `, ${column.numeric_scale}` : ""})`;
230
230
  }
231
- let sql = ` "${column.column_name}" ${type}`;
231
+ let sql = ` ${this.connection.getGrammar().wrap(column.column_name)} ${type}`;
232
232
  if (column.is_nullable === "NO")
233
233
  sql += " NOT NULL";
234
234
  if (column.column_default !== null && column.column_default !== undefined)
@@ -236,9 +236,9 @@ export class Migrator {
236
236
  return sql;
237
237
  });
238
238
  if (pkColumns.length > 0) {
239
- columnSql.push(` PRIMARY KEY (${pkColumns.map((column) => `"${column}"`).join(", ")})`);
239
+ columnSql.push(` PRIMARY KEY (${pkColumns.map((column) => this.connection.getGrammar().wrap(column)).join(", ")})`);
240
240
  }
241
- statements.push(`CREATE TABLE "${schema}"."${table}" (\n${columnSql.join(",\n")}\n);`);
241
+ statements.push(`CREATE TABLE ${this.connection.getGrammar().wrap(`${schema}.${table}`)} (\n${columnSql.join(",\n")}\n);`);
242
242
  }
243
243
  return statements.join("\n\n") + "\n";
244
244
  }
@@ -5,7 +5,7 @@ import { BelongsToMany } from "./BelongsToMany.js";
5
5
  export type ModelConstructor<T extends Model = Model> = (new (...args: any[]) => T) & Omit<typeof Model, "prototype">;
6
6
  export type GlobalScope = (builder: Builder<any>, model: ModelConstructor) => void;
7
7
  export type LiteralUnion<T extends string> = T | (string & {});
8
- type BaseModelInstanceKey = "$attributes" | "$original" | "$exists" | "$relations" | "$casts" | "$connection" | "fill" | "setConnection" | "getConnection" | "isFillable" | "getAttribute" | "setAttribute" | "castAttribute" | "serializeCastAttribute" | "mergeCasts" | "getDirty" | "isDirty" | "save" | "updateTimestamps" | "touch" | "increment" | "decrement" | "load" | "delete" | "restore" | "forceDelete" | "refresh" | "toJSON" | "toString" | "freshTimestamp" | "setRelation" | "getRelation" | "hasMany" | "belongsTo" | "hasOne" | "hasManyThrough" | "hasOneThrough" | "belongsToMany" | "morphTo" | "morphOne" | "morphMany" | "morphToMany" | "morphedByMany";
8
+ type BaseModelInstanceKey = "$attributes" | "$original" | "$exists" | "$relations" | "$casts" | "$castCache" | "$connection" | "fill" | "setConnection" | "getConnection" | "isFillable" | "getAttribute" | "setAttribute" | "castAttribute" | "serializeCastAttribute" | "mergeCasts" | "getDirty" | "isDirty" | "save" | "updateTimestamps" | "touch" | "increment" | "decrement" | "load" | "delete" | "restore" | "forceDelete" | "refresh" | "toJSON" | "toString" | "freshTimestamp" | "setRelation" | "getRelation" | "hasMany" | "belongsTo" | "hasOne" | "hasManyThrough" | "hasOneThrough" | "belongsToMany" | "morphTo" | "morphOne" | "morphMany" | "morphToMany" | "morphedByMany";
9
9
  export type ModelInstanceAttributeKeys<T> = Extract<Exclude<keyof T, BaseModelInstanceKey>, string>;
10
10
  export type ModelAttributes<T> = T extends {
11
11
  $attributes: Record<string, any>;
@@ -111,10 +111,9 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
111
111
  $exists: boolean;
112
112
  $relations: Record<string, any>;
113
113
  $casts: Record<string, CastDefinition>;
114
+ $castCache: Record<string, any>;
114
115
  $connection?: Connection;
115
116
  constructor(attributes?: Partial<T>);
116
- private defineAttributeProperty;
117
- private syncAttributeProperties;
118
117
  static getTable(): string;
119
118
  static getConnection(): Connection;
120
119
  static setConnection(connection: Connection): void;
@@ -127,6 +126,7 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
127
126
  static applyGlobalScopes(builder: Builder<any>): void;
128
127
  static getQualifiedDeletedAtColumn(): string;
129
128
  static shouldAutoGeneratePrimaryKey(): Promise<boolean>;
129
+ static hydrate<M extends ModelConstructor>(this: M, row: Record<string, any>, connection?: Connection): InstanceType<M>;
130
130
  static create<M extends ModelConstructor>(this: M, attributes: ModelAttributeInput<InstanceType<M>>): Promise<InstanceType<M>>;
131
131
  static find<M extends ModelConstructor>(this: M, id: any): Promise<InstanceType<M> | null>;
132
132
  static findOrFail<M extends ModelConstructor>(this: M, id: any): Promise<InstanceType<M>>;
@@ -8,6 +8,44 @@ import { ModelNotFoundError } from "./ModelNotFoundError.js";
8
8
  import { ConnectionManager } from "../connection/ConnectionManager.js";
9
9
  import { TenantContext } from "../connection/TenantContext.js";
10
10
  import { IdentityMap } from "./IdentityMap.js";
11
+ const modelProxyHandler = {
12
+ get(target, prop, receiver) {
13
+ if (typeof prop === "string" && !(prop in target) && prop in target.$attributes) {
14
+ return target.getAttribute(prop);
15
+ }
16
+ return Reflect.get(target, prop, receiver);
17
+ },
18
+ set(target, prop, value, receiver) {
19
+ if (typeof prop === "string" && !prop.startsWith("$") && !(prop in target)) {
20
+ target.setAttribute(prop, value);
21
+ return true;
22
+ }
23
+ return Reflect.set(target, prop, value, receiver);
24
+ },
25
+ has(target, prop) {
26
+ if (typeof prop === "string" && prop in target.$attributes)
27
+ return true;
28
+ return Reflect.has(target, prop);
29
+ },
30
+ getOwnPropertyDescriptor(target, prop) {
31
+ if (typeof prop === "string" && prop in target.$attributes) {
32
+ return {
33
+ enumerable: true,
34
+ configurable: true,
35
+ value: target.getAttribute(prop),
36
+ };
37
+ }
38
+ return Reflect.getOwnPropertyDescriptor(target, prop);
39
+ },
40
+ ownKeys(target) {
41
+ const keys = new Set(Reflect.ownKeys(target));
42
+ for (const key of Object.keys(target.$attributes)) {
43
+ if (!key.startsWith("$"))
44
+ keys.add(key);
45
+ }
46
+ return Array.from(keys);
47
+ },
48
+ };
11
49
  const globalScopes = new WeakMap();
12
50
  function getGlobalScopes(model) {
13
51
  const scopes = new Map();
@@ -306,6 +344,7 @@ export class Model {
306
344
  $exists = false;
307
345
  $relations = {};
308
346
  $casts = {};
347
+ $castCache = {};
309
348
  $connection;
310
349
  constructor(attributes) {
311
350
  const defaults = this.constructor.attributes;
@@ -315,72 +354,7 @@ export class Model {
315
354
  if (attributes) {
316
355
  this.fill(attributes);
317
356
  }
318
- this.syncAttributeProperties();
319
- // Minimal Proxy fallback for dynamic property access on undefined keys.
320
- // Pre-defined attribute getters/setters bypass the Proxy entirely.
321
- return new Proxy(this, {
322
- get(target, prop, receiver) {
323
- if (typeof prop === "string" && !(prop in target) && prop in target.$attributes) {
324
- return target.getAttribute(prop);
325
- }
326
- return Reflect.get(target, prop, receiver);
327
- },
328
- set(target, prop, value, receiver) {
329
- if (typeof prop === "string" && !prop.startsWith("$") && !(prop in target)) {
330
- target.setAttribute(prop, value);
331
- return true;
332
- }
333
- return Reflect.set(target, prop, value, receiver);
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
- },
358
- });
359
- }
360
- defineAttributeProperty(key) {
361
- if (key in this)
362
- return;
363
- Object.defineProperty(this, key, {
364
- get: () => this.getAttribute(key),
365
- set: (value) => this.setAttribute(key, value),
366
- enumerable: true,
367
- configurable: true,
368
- });
369
- }
370
- syncAttributeProperties() {
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) {
382
- this.defineAttributeProperty(key);
383
- }
357
+ return new Proxy(this, modelProxyHandler);
384
358
  }
385
359
  static getTable() {
386
360
  return this.table || snakeCase(this.name) + "s";
@@ -459,6 +433,17 @@ export class Model {
459
433
  const numericTypes = new Set(["integer", "int", "bigint", "smallint", "tinyint", "real", "float", "double", "decimal", "numeric"]);
460
434
  return !numericTypes.has(type);
461
435
  }
436
+ static hydrate(row, connection) {
437
+ const instance = new this();
438
+ instance.$attributes = { ...instance.$attributes, ...row };
439
+ instance.$original = { ...row };
440
+ instance.$castCache = {};
441
+ instance.$exists = true;
442
+ if (connection) {
443
+ instance.setConnection(connection);
444
+ }
445
+ return instance;
446
+ }
462
447
  static async create(attributes) {
463
448
  const instance = new this();
464
449
  instance.fill(attributes);
@@ -701,12 +686,19 @@ export class Model {
701
686
  return true;
702
687
  }
703
688
  getAttribute(key) {
689
+ if (Object.prototype.hasOwnProperty.call(this.$castCache, key)) {
690
+ return this.$castCache[key];
691
+ }
704
692
  const value = this.$attributes[key];
705
- return this.castAttribute(key, value);
693
+ const casted = this.castAttribute(key, value);
694
+ if (this.getCastDefinition(key) && value !== null && value !== undefined) {
695
+ this.$castCache[key] = casted;
696
+ }
697
+ return casted;
706
698
  }
707
699
  setAttribute(key, value) {
708
700
  this.$attributes[key] = this.serializeCastAttribute(key, value);
709
- this.defineAttributeProperty(key);
701
+ delete this.$castCache[key];
710
702
  }
711
703
  castAttribute(key, value) {
712
704
  const cast = this.getCastDefinition(key);
@@ -785,6 +777,7 @@ export class Model {
785
777
  }
786
778
  mergeCasts(casts) {
787
779
  this.$casts = { ...this.$casts, ...casts };
780
+ this.$castCache = {};
788
781
  return this;
789
782
  }
790
783
  getCastDefinition(key) {
@@ -815,21 +808,23 @@ export class Model {
815
808
  async save() {
816
809
  const constructor = this.constructor;
817
810
  if (this.$exists) {
818
- await ObserverRegistry.dispatch("updating", this);
819
811
  await ObserverRegistry.dispatch("saving", this);
820
- if (constructor.timestamps) {
812
+ let dirty = this.getDirty();
813
+ if (Object.keys(dirty).length > 0 && constructor.timestamps) {
821
814
  this.$attributes["updated_at"] = this.freshTimestamp();
815
+ delete this.$castCache.updated_at;
816
+ dirty = this.getDirty();
822
817
  }
823
- const dirty = this.getDirty();
824
818
  if (Object.keys(dirty).length > 0) {
819
+ await ObserverRegistry.dispatch("updating", this);
825
820
  const pk = this.getAttribute(constructor.primaryKey);
826
821
  const connection = this.getConnection();
827
822
  await new Builder(connection, connection.qualifyTable(constructor.getTable()))
828
823
  .where(constructor.primaryKey, pk)
829
824
  .update(dirty);
825
+ await ObserverRegistry.dispatch("updated", this);
830
826
  }
831
827
  this.$original = { ...this.$attributes };
832
- await ObserverRegistry.dispatch("updated", this);
833
828
  await ObserverRegistry.dispatch("saved", this);
834
829
  }
835
830
  else {
@@ -839,6 +834,8 @@ export class Model {
839
834
  const now = this.freshTimestamp();
840
835
  this.$attributes["created_at"] = now;
841
836
  this.$attributes["updated_at"] = now;
837
+ delete this.$castCache.created_at;
838
+ delete this.$castCache.updated_at;
842
839
  }
843
840
  const primaryKey = constructor.primaryKey;
844
841
  const primaryKeyValue = this.getAttribute(primaryKey);
@@ -846,6 +843,7 @@ export class Model {
846
843
  if ((primaryKeyValue === null || primaryKeyValue === undefined || primaryKeyValue === "") && shouldGeneratePrimaryKey) {
847
844
  const generated = crypto.randomUUID();
848
845
  this.$attributes[primaryKey] = generated;
846
+ delete this.$castCache[primaryKey];
849
847
  }
850
848
  const connection = this.getConnection();
851
849
  if (shouldGeneratePrimaryKey || primaryKeyValue !== null && primaryKeyValue !== undefined && primaryKeyValue !== "") {
@@ -855,6 +853,7 @@ export class Model {
855
853
  const result = await new Builder(connection, connection.qualifyTable(constructor.getTable())).insertGetId(this.$attributes);
856
854
  if (result) {
857
855
  this.$attributes[constructor.primaryKey] = result;
856
+ delete this.$castCache[constructor.primaryKey];
858
857
  }
859
858
  }
860
859
  this.$exists = true;
@@ -862,7 +861,6 @@ export class Model {
862
861
  await ObserverRegistry.dispatch("created", this);
863
862
  await ObserverRegistry.dispatch("saved", this);
864
863
  }
865
- this.syncAttributeProperties();
866
864
  const identityMap = IdentityMap.current();
867
865
  if (identityMap) {
868
866
  const pk = this.getAttribute(constructor.primaryKey);
@@ -878,10 +876,11 @@ export class Model {
878
876
  return;
879
877
  const now = this.freshTimestamp();
880
878
  this.$attributes["updated_at"] = now;
879
+ delete this.$castCache.updated_at;
881
880
  if (!this.$exists) {
882
881
  this.$attributes["created_at"] = now;
882
+ delete this.$castCache.created_at;
883
883
  }
884
- this.syncAttributeProperties();
885
884
  }
886
885
  async touch() {
887
886
  if (!this.$exists)
@@ -896,8 +895,8 @@ export class Model {
896
895
  .where(constructor.primaryKey, pk)
897
896
  .update({ updated_at: now });
898
897
  this.$attributes["updated_at"] = now;
898
+ delete this.$castCache.updated_at;
899
899
  this.$original = { ...this.$attributes };
900
- this.syncAttributeProperties();
901
900
  return true;
902
901
  }
903
902
  async increment(column, amount = 1, extra = {}) {
@@ -913,11 +912,12 @@ export class Model {
913
912
  }
914
913
  await builder.increment(column, amount, extra);
915
914
  this.$attributes[column] = (this.$attributes[column] || 0) + amount;
915
+ delete this.$castCache[column];
916
916
  for (const [key, value] of Object.entries(extra)) {
917
917
  this.$attributes[key] = value;
918
+ delete this.$castCache[key];
918
919
  }
919
920
  this.$original = { ...this.$attributes };
920
- this.syncAttributeProperties();
921
921
  return this;
922
922
  }
923
923
  async decrement(column, amount = 1, extra = {}) {
@@ -930,10 +930,10 @@ export class Model {
930
930
  }
931
931
  async delete() {
932
932
  const constructor = this.constructor;
933
- await ObserverRegistry.dispatch("deleting", this);
934
933
  const pk = this.getAttribute(constructor.primaryKey);
935
934
  if (!pk)
936
935
  return false;
936
+ await ObserverRegistry.dispatch("deleting", this);
937
937
  if (constructor.softDeletes) {
938
938
  const deletedAt = this.freshTimestamp();
939
939
  const connection = this.getConnection();
@@ -941,8 +941,8 @@ export class Model {
941
941
  .where(constructor.primaryKey, pk)
942
942
  .update({ [constructor.deletedAtColumn]: deletedAt });
943
943
  this.$attributes[constructor.deletedAtColumn] = deletedAt;
944
+ delete this.$castCache[constructor.deletedAtColumn];
944
945
  this.$original = { ...this.$attributes };
945
- this.syncAttributeProperties();
946
946
  }
947
947
  else {
948
948
  const connection = this.getConnection();
@@ -970,9 +970,9 @@ export class Model {
970
970
  .where(constructor.primaryKey, pk)
971
971
  .update({ [constructor.deletedAtColumn]: null });
972
972
  this.$attributes[constructor.deletedAtColumn] = null;
973
+ delete this.$castCache[constructor.deletedAtColumn];
973
974
  this.$original = { ...this.$attributes };
974
975
  this.$exists = true;
975
- this.syncAttributeProperties();
976
976
  return true;
977
977
  }
978
978
  async forceDelete() {
@@ -1005,7 +1005,7 @@ export class Model {
1005
1005
  if (result) {
1006
1006
  this.$attributes = result.$attributes;
1007
1007
  this.$original = { ...result.$attributes };
1008
- this.syncAttributeProperties();
1008
+ this.$castCache = {};
1009
1009
  // Ensure this instance is the canonical one in the identity map
1010
1010
  if (identityMap) {
1011
1011
  IdentityMap.set(constructor.getTable(), pk, this);
@@ -32,8 +32,10 @@ export declare class Builder<T = Record<string, any>> {
32
32
  updateJoins: string[];
33
33
  bindings: any[];
34
34
  private parameterize;
35
+ private sqlCache?;
35
36
  constructor(connection: Connection, table: string);
36
37
  private get grammar();
38
+ private invalidateSqlCache;
37
39
  setModel(model: ModelConstructor): this;
38
40
  table(table: string): this;
39
41
  select(...columns: ModelColumn<T>[]): this;
@@ -168,6 +170,7 @@ export declare class Builder<T = Record<string, any>> {
168
170
  insertGetId(data: ModelAttributeInput<T>, idColumn?: ModelColumn<T>): Promise<any>;
169
171
  insertOrIgnore(data: ModelAttributeInput<T> | ModelAttributeInput<T>[]): Promise<any>;
170
172
  upsert(data: ModelAttributeInput<T> | ModelAttributeInput<T>[], uniqueBy: ModelColumn<T> | ModelColumn<T>[], updateColumns?: ModelColumn<T>[]): Promise<any>;
173
+ private getUniformColumns;
171
174
  update(data: ModelAttributeInput<T>): Promise<any>;
172
175
  delete(): Promise<any>;
173
176
  increment(column: ModelColumn<T>, amount?: number, extra?: ModelAttributeInput<T>): Promise<any>;
@@ -21,6 +21,7 @@ export class Builder {
21
21
  updateJoins = [];
22
22
  bindings = [];
23
23
  parameterize = false;
24
+ sqlCache;
24
25
  constructor(connection, table) {
25
26
  this.connection = connection;
26
27
  this.tableName = table;
@@ -28,19 +29,25 @@ export class Builder {
28
29
  get grammar() {
29
30
  return this.connection.getGrammar();
30
31
  }
32
+ invalidateSqlCache() {
33
+ this.sqlCache = undefined;
34
+ }
31
35
  setModel(model) {
32
36
  this.model = model;
33
37
  return this;
34
38
  }
35
39
  table(table) {
40
+ this.invalidateSqlCache();
36
41
  this.tableName = table;
37
42
  return this;
38
43
  }
39
44
  select(...columns) {
45
+ this.invalidateSqlCache();
40
46
  this.columns = columns;
41
47
  return this;
42
48
  }
43
49
  distinct() {
50
+ this.invalidateSqlCache();
44
51
  this.distinctFlag = true;
45
52
  return this;
46
53
  }
@@ -58,6 +65,7 @@ export class Builder {
58
65
  value = operator;
59
66
  operator = "=";
60
67
  }
68
+ this.invalidateSqlCache();
61
69
  this.wheres.push({ type: "basic", column, operator, value, boolean, scope });
62
70
  return this;
63
71
  }
@@ -65,6 +73,7 @@ export class Builder {
65
73
  const nested = new Builder(this.connection, this.tableName);
66
74
  callback(nested);
67
75
  if (nested.wheres.length > 0) {
76
+ this.invalidateSqlCache();
68
77
  this.wheres.push({ type: "nested", column: "", query: nested.wheres, boolean, scope: undefined });
69
78
  }
70
79
  return this;
@@ -85,26 +94,32 @@ export class Builder {
85
94
  return this.whereNot(column, value, "or");
86
95
  }
87
96
  whereIn(column, values, boolean = "and", scope) {
97
+ this.invalidateSqlCache();
88
98
  this.wheres.push({ type: "in", column, value: values, boolean, scope });
89
99
  return this;
90
100
  }
91
101
  whereNotIn(column, values, boolean = "and", scope) {
102
+ this.invalidateSqlCache();
92
103
  this.wheres.push({ type: "in", column, value: values, boolean, operator: "NOT IN", scope });
93
104
  return this;
94
105
  }
95
106
  whereNull(column, boolean = "and", scope) {
107
+ this.invalidateSqlCache();
96
108
  this.wheres.push({ type: "null", column, boolean, scope });
97
109
  return this;
98
110
  }
99
111
  whereNotNull(column, boolean = "and", scope) {
112
+ this.invalidateSqlCache();
100
113
  this.wheres.push({ type: "null", column, boolean, operator: "NOT NULL", scope });
101
114
  return this;
102
115
  }
103
116
  whereBetween(column, values, boolean = "and", scope) {
117
+ this.invalidateSqlCache();
104
118
  this.wheres.push({ type: "between", column, value: values, boolean, scope });
105
119
  return this;
106
120
  }
107
121
  whereNotBetween(column, values, boolean = "and", scope) {
122
+ this.invalidateSqlCache();
108
123
  this.wheres.push({ type: "between", column, value: values, boolean, operator: "NOT BETWEEN", scope });
109
124
  return this;
110
125
  }
@@ -139,14 +154,17 @@ export class Builder {
139
154
  return this.whereTime(column, operator, value, "or");
140
155
  }
141
156
  whereRaw(sql, boolean = "and", scope) {
157
+ this.invalidateSqlCache();
142
158
  this.wheres.push({ type: "raw", column: sql, boolean, scope });
143
159
  return this;
144
160
  }
145
161
  whereColumn(first, operator, second, boolean = "and") {
162
+ this.invalidateSqlCache();
146
163
  this.wheres.push({ type: "column", column: first, operator, value: second, boolean });
147
164
  return this;
148
165
  }
149
166
  whereExists(sql, boolean = "and", not = false) {
167
+ this.invalidateSqlCache();
150
168
  this.wheres.push({ type: "exists", column: sql, boolean, operator: not ? "NOT EXISTS" : "EXISTS" });
151
169
  return this;
152
170
  }
@@ -181,6 +199,7 @@ export class Builder {
181
199
  return this.whereRaw(sql, "or", scope);
182
200
  }
183
201
  whereJsonContains(column, value, boolean = "and", not = false) {
202
+ this.invalidateSqlCache();
184
203
  this.wheres.push({ type: "json_contains", column, value, boolean, scope: undefined, not });
185
204
  return this;
186
205
  }
@@ -189,10 +208,12 @@ export class Builder {
189
208
  value = operator;
190
209
  operator = "=";
191
210
  }
211
+ this.invalidateSqlCache();
192
212
  this.wheres.push({ type: "json_length", column, operator: String(operator), value, boolean, scope: undefined, not });
193
213
  return this;
194
214
  }
195
215
  whereLike(column, value, boolean = "and", not = false) {
216
+ this.invalidateSqlCache();
196
217
  this.wheres.push({ type: "like", column, value, boolean, scope: undefined, not });
197
218
  return this;
198
219
  }
@@ -200,23 +221,28 @@ export class Builder {
200
221
  return this.whereLike(column, value, "and", true);
201
222
  }
202
223
  whereRegexp(column, value, boolean = "and", not = false) {
224
+ this.invalidateSqlCache();
203
225
  this.wheres.push({ type: "regexp", column, value, boolean, scope: undefined, not });
204
226
  return this;
205
227
  }
206
228
  whereFullText(columns, value, boolean = "and", not = false) {
207
229
  const cols = Array.isArray(columns) ? columns : [columns];
230
+ this.invalidateSqlCache();
208
231
  this.wheres.push({ type: "fulltext", column: "", columns: cols, value, boolean, scope: undefined, not });
209
232
  return this;
210
233
  }
211
234
  whereAll(columns, operator, value, boolean = "and") {
235
+ this.invalidateSqlCache();
212
236
  this.wheres.push({ type: "all", column: "", columns: columns, operator, value, boolean, scope: undefined });
213
237
  return this;
214
238
  }
215
239
  whereAny(columns, operator, value, boolean = "and") {
240
+ this.invalidateSqlCache();
216
241
  this.wheres.push({ type: "any", column: "", columns: columns, operator, value, boolean, scope: undefined });
217
242
  return this;
218
243
  }
219
244
  orderBy(column, direction = "asc") {
245
+ this.invalidateSqlCache();
220
246
  this.orders.push({ column, direction });
221
247
  return this;
222
248
  }
@@ -227,6 +253,7 @@ export class Builder {
227
253
  return this.orderBy(column, "asc");
228
254
  }
229
255
  inRandomOrder() {
256
+ this.invalidateSqlCache();
230
257
  this.randomOrderFlag = true;
231
258
  return this;
232
259
  }
@@ -234,6 +261,7 @@ export class Builder {
234
261
  return this.orderBy(column, "desc");
235
262
  }
236
263
  reorder(column, direction = "asc") {
264
+ this.invalidateSqlCache();
237
265
  this.orders = [];
238
266
  this.randomOrderFlag = false;
239
267
  if (column) {
@@ -242,18 +270,22 @@ export class Builder {
242
270
  return this;
243
271
  }
244
272
  groupBy(...columns) {
273
+ this.invalidateSqlCache();
245
274
  this.groups.push(...columns);
246
275
  return this;
247
276
  }
248
277
  having(column, operator, value) {
278
+ this.invalidateSqlCache();
249
279
  this.havings.push({ column, operator, value, boolean: "and" });
250
280
  return this;
251
281
  }
252
282
  orHaving(column, operator, value) {
283
+ this.invalidateSqlCache();
253
284
  this.havings.push({ column, operator, value, boolean: "or" });
254
285
  return this;
255
286
  }
256
287
  havingRaw(sql, boolean = "and") {
288
+ this.invalidateSqlCache();
257
289
  this.havings.push({ sql, boolean });
258
290
  return this;
259
291
  }
@@ -261,10 +293,12 @@ export class Builder {
261
293
  return this.havingRaw(sql, "or");
262
294
  }
263
295
  limit(count) {
296
+ this.invalidateSqlCache();
264
297
  this.limitValue = count;
265
298
  return this;
266
299
  }
267
300
  offset(count) {
301
+ this.invalidateSqlCache();
268
302
  this.offsetValue = count;
269
303
  return this;
270
304
  }
@@ -273,6 +307,7 @@ export class Builder {
273
307
  }
274
308
  join(table, first, operator, second, type = "INNER") {
275
309
  const joinSql = `${type} JOIN ${this.grammar.wrap(table)} ON ${this.grammar.wrap(first)} ${operator} ${this.grammar.wrap(second)}`;
310
+ this.invalidateSqlCache();
276
311
  this.joins.push(joinSql);
277
312
  return this;
278
313
  }
@@ -283,11 +318,13 @@ export class Builder {
283
318
  return this.join(table, first, operator, second, "RIGHT");
284
319
  }
285
320
  crossJoin(table) {
321
+ this.invalidateSqlCache();
286
322
  this.joins.push(`CROSS JOIN ${this.grammar.wrap(table)}`);
287
323
  return this;
288
324
  }
289
325
  union(query, all = false) {
290
326
  const sql = typeof query === "string" ? query : query.toSql();
327
+ this.invalidateSqlCache();
291
328
  this.unions.push({ query: sql, all });
292
329
  return this;
293
330
  }
@@ -299,10 +336,12 @@ export class Builder {
299
336
  return this;
300
337
  }
301
338
  withoutGlobalScope(scope) {
339
+ this.invalidateSqlCache();
302
340
  this.wheres = this.wheres.filter((where) => where.scope !== scope);
303
341
  return this;
304
342
  }
305
343
  withoutGlobalScopes() {
344
+ this.invalidateSqlCache();
306
345
  this.wheres = this.wheres.filter((where) => !where.scope);
307
346
  return this;
308
347
  }
@@ -407,6 +446,7 @@ export class Builder {
407
446
  return this.withAggregate(relationName, column, "MAX", alias);
408
447
  }
409
448
  addSelect(...columns) {
449
+ this.invalidateSqlCache();
410
450
  if (this.columns.length === 1 && this.columns[0] === "*") {
411
451
  this.columns = [`${this.tableName}.*`];
412
452
  }
@@ -414,15 +454,18 @@ export class Builder {
414
454
  return this;
415
455
  }
416
456
  selectRaw(sql) {
457
+ this.invalidateSqlCache();
417
458
  this.columns.push(sql);
418
459
  return this;
419
460
  }
420
461
  fromSub(query, as) {
421
462
  const sql = typeof query === "string" ? query : query.toSql();
463
+ this.invalidateSqlCache();
422
464
  this.fromRaw = `(${sql}) AS ${this.grammar.wrap(as)}`;
423
465
  return this;
424
466
  }
425
467
  updateFrom(table, first, operator, second) {
468
+ this.invalidateSqlCache();
426
469
  this.updateJoins.push(`INNER JOIN ${this.grammar.wrap(table)} ON ${this.grammar.wrap(first)} ${operator} ${this.grammar.wrap(second)}`);
427
470
  return this;
428
471
  }
@@ -598,6 +641,8 @@ export class Builder {
598
641
  return column.includes("(") || /\s+as\s+/i.test(column) || /^[0-9]+$/.test(column);
599
642
  }
600
643
  toSql() {
644
+ if (!this.parameterize && this.sqlCache)
645
+ return this.sqlCache;
601
646
  const distinct = this.distinctFlag ? "DISTINCT " : "";
602
647
  const from = this.fromRaw || this.grammar.wrap(this.tableName);
603
648
  let sql = `SELECT ${distinct}${this.compileColumns()} FROM ${from}`;
@@ -613,7 +658,10 @@ export class Builder {
613
658
  for (const union of this.unions) {
614
659
  sql += ` UNION${union.all ? " ALL" : ""} ${union.query}`;
615
660
  }
616
- return sql.replace(/\s+/g, " ").trim();
661
+ const compiled = sql.replace(/\s+/g, " ").trim();
662
+ if (!this.parameterize)
663
+ this.sqlCache = compiled;
664
+ return compiled;
617
665
  }
618
666
  async get() {
619
667
  this.bindings = [];
@@ -636,12 +684,7 @@ export class Builder {
636
684
  }
637
685
  }
638
686
  }
639
- const instance = new this.model(row);
640
- instance.$exists = true;
641
- instance.$original = { ...row };
642
- if (typeof instance.setConnection === "function") {
643
- instance.setConnection(this.connection);
644
- }
687
+ const instance = this.model.hydrate(row, this.connection);
645
688
  if (identityMap) {
646
689
  const pk = row[primaryKey];
647
690
  if (pk !== null && pk !== undefined) {
@@ -717,10 +760,16 @@ export class Builder {
717
760
  return results.map((row) => row[column]);
718
761
  }
719
762
  async aggregate(sql, alias) {
720
- const model = this.model;
721
- this.model = undefined;
722
- const result = await this.select(`${sql} as ${alias}`).first();
723
- this.model = model;
763
+ const query = this.clone();
764
+ query.model = undefined;
765
+ query.columns = [`${sql} as ${alias}`];
766
+ query.orders = [];
767
+ query.limitValue = undefined;
768
+ query.offsetValue = undefined;
769
+ query.eagerLoads = [];
770
+ query.lockMode = undefined;
771
+ query.invalidateSqlCache();
772
+ const result = await query.first();
724
773
  return result ? result[alias] : null;
725
774
  }
726
775
  async count(column = "*") {
@@ -739,7 +788,12 @@ export class Builder {
739
788
  return await this.aggregate(`MAX(${column})`, "max");
740
789
  }
741
790
  async paginate(perPage = 15, page = 1) {
742
- const total = await this.clone().count();
791
+ const countQuery = this.clone();
792
+ countQuery.limitValue = undefined;
793
+ countQuery.offsetValue = undefined;
794
+ countQuery.orders = [];
795
+ countQuery.invalidateSqlCache();
796
+ const total = await countQuery.count();
743
797
  const data = await this.clone().forPage(page, perPage).get();
744
798
  return {
745
799
  data,
@@ -867,7 +921,7 @@ export class Builder {
867
921
  const records = Array.isArray(data) ? data : [data];
868
922
  if (records.length === 0)
869
923
  return;
870
- const columns = Object.keys(records[0]);
924
+ const columns = this.getUniformColumns(records);
871
925
  const bindings = [];
872
926
  const values = records.map((record) => {
873
927
  return `(${columns.map((col) => {
@@ -886,7 +940,7 @@ export class Builder {
886
940
  const records = Array.isArray(data) ? data : [data];
887
941
  if (records.length === 0)
888
942
  return;
889
- const columns = Object.keys(records[0]);
943
+ const columns = this.getUniformColumns(records);
890
944
  const bindings = [];
891
945
  const values = records.map((record) => {
892
946
  return `(${columns.map((col) => {
@@ -901,7 +955,7 @@ export class Builder {
901
955
  const records = Array.isArray(data) ? data : [data];
902
956
  if (records.length === 0)
903
957
  return;
904
- const columns = Object.keys(records[0]);
958
+ const columns = this.getUniformColumns(records);
905
959
  const bindings = [];
906
960
  const values = records.map((record) => {
907
961
  return `(${columns.map((col) => {
@@ -914,6 +968,17 @@ export class Builder {
914
968
  const sql = this.grammar.compileUpsert(this.grammar.wrap(this.tableName), columns, values, uniqueCols, updateCols);
915
969
  return await this.connection.run(sql, bindings);
916
970
  }
971
+ getUniformColumns(records) {
972
+ const columns = Object.keys(records[0]);
973
+ const signature = [...columns].sort().join("\0");
974
+ for (let i = 1; i < records.length; i++) {
975
+ const recordSignature = Object.keys(records[i]).sort().join("\0");
976
+ if (recordSignature !== signature) {
977
+ throw new Error("Bulk insert records must have the same columns.");
978
+ }
979
+ }
980
+ return columns;
981
+ }
917
982
  async update(data) {
918
983
  this.bindings = [];
919
984
  this.parameterize = true;
@@ -1003,6 +1068,7 @@ export class Builder {
1003
1068
  lockForUpdate() {
1004
1069
  const driver = this.connection.getDriverName();
1005
1070
  if (driver !== "sqlite") {
1071
+ this.invalidateSqlCache();
1006
1072
  this.lockMode = "FOR UPDATE";
1007
1073
  }
1008
1074
  return this;
@@ -1010,21 +1076,25 @@ export class Builder {
1010
1076
  sharedLock() {
1011
1077
  const driver = this.connection.getDriverName();
1012
1078
  if (driver === "mysql") {
1079
+ this.invalidateSqlCache();
1013
1080
  this.lockMode = "LOCK IN SHARE MODE";
1014
1081
  }
1015
1082
  else if (driver === "postgres") {
1083
+ this.invalidateSqlCache();
1016
1084
  this.lockMode = "FOR SHARE";
1017
1085
  }
1018
1086
  return this;
1019
1087
  }
1020
1088
  skipLocked() {
1021
1089
  if (this.lockMode) {
1090
+ this.invalidateSqlCache();
1022
1091
  this.lockMode += " SKIP LOCKED";
1023
1092
  }
1024
1093
  return this;
1025
1094
  }
1026
1095
  noWait() {
1027
1096
  if (this.lockMode) {
1097
+ this.invalidateSqlCache();
1028
1098
  this.lockMode += " NOWAIT";
1029
1099
  }
1030
1100
  return this;
@@ -1034,6 +1104,7 @@ export class Builder {
1034
1104
  value = operator;
1035
1105
  operator = "=";
1036
1106
  }
1107
+ this.invalidateSqlCache();
1037
1108
  this.wheres.push({ type: "date", column: column, operator, value, boolean, scope: undefined, dateType: type });
1038
1109
  return this;
1039
1110
  }
@@ -10,7 +10,7 @@ export class MySqlGrammar extends Grammar {
10
10
  }
11
11
  if (value === "*")
12
12
  return value;
13
- return `\`${value}\``;
13
+ return `\`${value.replaceAll("`", "``")}\``;
14
14
  }
15
15
  placeholder(_index) {
16
16
  return "?";
@@ -10,7 +10,7 @@ export class PostgresGrammar extends Grammar {
10
10
  }
11
11
  if (value === "*")
12
12
  return value;
13
- return `"${value}"`;
13
+ return `"${value.replaceAll('"', '""')}"`;
14
14
  }
15
15
  placeholder(index) {
16
16
  return `$${index}`;
@@ -10,7 +10,7 @@ export class SQLiteGrammar extends Grammar {
10
10
  }
11
11
  if (value === "*")
12
12
  return value;
13
- return `"${value}"`;
13
+ return `"${value.replaceAll('"', '""')}"`;
14
14
  }
15
15
  placeholder(_index) {
16
16
  return "?";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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",