sqlite-zod-orm 3.24.0 → 3.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5220,12 +5220,12 @@ function executeProxyQuery(schemas, callback, executor) {
5220
5220
  function createQueryBuilder(ctx, entityName, initialCols) {
5221
5221
  const schema = ctx.schemas[entityName];
5222
5222
  const executor = (sql, params, raw) => {
5223
- if (ctx.debug)
5224
- console.log("[satidb]", sql, params);
5225
- const rows = ctx.db.query(sql).all(...params);
5226
- if (raw)
5227
- return rows;
5228
- return rows.map((row) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
5223
+ return ctx._m(`SQL: ${sql.slice(0, 60)}`, () => {
5224
+ const rows = ctx._stmt(sql).all(...params);
5225
+ if (raw)
5226
+ return rows;
5227
+ return rows.map((row) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
5228
+ });
5229
5229
  };
5230
5230
  const singleExecutor = (sql, params, raw) => {
5231
5231
  const results = executor(sql, params, raw);
@@ -5269,7 +5269,7 @@ function createQueryBuilder(ctx, entityName, initialCols) {
5269
5269
  if (belongsTo2) {
5270
5270
  const fk = belongsTo2.foreignKey;
5271
5271
  const placeholders = parentIds.map(() => "?").join(", ");
5272
- const childRows = ctx.db.query(`SELECT * FROM ${hasMany.to} WHERE ${fk} IN (${placeholders})`).all(...parentIds);
5272
+ const childRows = ctx._stmt(`SELECT * FROM ${hasMany.to} WHERE ${fk} IN (${placeholders})`).all(...parentIds);
5273
5273
  const groups = new Map;
5274
5274
  const childSchema = ctx.schemas[hasMany.to];
5275
5275
  for (const rawRow of childRows) {
@@ -5381,21 +5381,21 @@ function buildWhereClause(conditions, tablePrefix) {
5381
5381
 
5382
5382
  // src/crud.ts
5383
5383
  function getById(ctx, entityName, id) {
5384
- const row = ctx.db.query(`SELECT * FROM "${entityName}" WHERE id = ?`).get(id);
5384
+ const row = ctx._stmt(`SELECT * FROM "${entityName}" WHERE id = ?`).get(id);
5385
5385
  if (!row)
5386
5386
  return null;
5387
5387
  return ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName]));
5388
5388
  }
5389
5389
  function getOne(ctx, entityName, conditions) {
5390
5390
  const { clause, values } = ctx.buildWhereClause(conditions);
5391
- const row = ctx.db.query(`SELECT * FROM "${entityName}" ${clause} LIMIT 1`).get(...values);
5391
+ const row = ctx._stmt(`SELECT * FROM "${entityName}" ${clause} LIMIT 1`).get(...values);
5392
5392
  if (!row)
5393
5393
  return null;
5394
5394
  return ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName]));
5395
5395
  }
5396
5396
  function findMany(ctx, entityName, conditions = {}) {
5397
5397
  const { clause, values } = ctx.buildWhereClause(conditions);
5398
- const rows = ctx.db.query(`SELECT * FROM "${entityName}" ${clause}`).all(...values);
5398
+ const rows = ctx._stmt(`SELECT * FROM "${entityName}" ${clause}`).all(...values);
5399
5399
  return rows.map((row) => ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName])));
5400
5400
  }
5401
5401
  function insert(ctx, entityName, data) {
@@ -5403,9 +5403,9 @@ function insert(ctx, entityName, data) {
5403
5403
  let inputData = { ...data };
5404
5404
  const hooks = ctx.hooks[entityName];
5405
5405
  if (hooks?.beforeInsert) {
5406
- const result2 = hooks.beforeInsert(inputData);
5407
- if (result2)
5408
- inputData = result2;
5406
+ const result = hooks.beforeInsert(inputData);
5407
+ if (result)
5408
+ inputData = result;
5409
5409
  }
5410
5410
  const validatedData = asZodObject(schema).passthrough().parse(inputData);
5411
5411
  const transformed = transformForStorage(validatedData);
@@ -5417,10 +5417,12 @@ function insert(ctx, entityName, data) {
5417
5417
  const columns = Object.keys(transformed);
5418
5418
  const quotedCols = columns.map((c) => `"${c}"`);
5419
5419
  const sql = columns.length === 0 ? `INSERT INTO "${entityName}" DEFAULT VALUES` : `INSERT INTO "${entityName}" (${quotedCols.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
5420
- if (ctx.debug)
5421
- console.log("[satidb]", sql, Object.values(transformed));
5422
- const result = ctx.db.query(sql).run(...Object.values(transformed));
5423
- const newEntity = getById(ctx, entityName, result.lastInsertRowid);
5420
+ let lastId = 0;
5421
+ ctx._m(`SQL: ${sql.slice(0, 40)}`, () => {
5422
+ const result = ctx._stmt(sql).run(...Object.values(transformed));
5423
+ lastId = result.lastInsertRowid;
5424
+ });
5425
+ const newEntity = getById(ctx, entityName, lastId);
5424
5426
  if (!newEntity)
5425
5427
  throw new Error("Failed to retrieve entity after insertion");
5426
5428
  if (hooks?.afterInsert)
@@ -5445,9 +5447,9 @@ function update(ctx, entityName, id, data) {
5445
5447
  }
5446
5448
  const setClause = Object.keys(transformed).map((key) => `"${key}" = ?`).join(", ");
5447
5449
  const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
5448
- if (ctx.debug)
5449
- console.log("[satidb]", sql, [...Object.values(transformed), id]);
5450
- ctx.db.query(sql).run(...Object.values(transformed), id);
5450
+ ctx._m(`SQL: UPDATE ${entityName} SET ...`, () => {
5451
+ ctx._stmt(sql).run(...Object.values(transformed), id);
5452
+ });
5451
5453
  const updated = getById(ctx, entityName, id);
5452
5454
  if (hooks?.afterUpdate && updated)
5453
5455
  hooks.afterUpdate(updated);
@@ -5464,7 +5466,7 @@ function updateWhere(ctx, entityName, data, conditions) {
5464
5466
  throw new Error("update().where() requires at least one condition");
5465
5467
  const setCols = Object.keys(transformed);
5466
5468
  const setClause = setCols.map((key) => `"${key}" = ?`).join(", ");
5467
- const result = ctx.db.query(`UPDATE "${entityName}" SET ${setClause} ${clause}`).run(...setCols.map((key) => transformed[key]), ...whereValues);
5469
+ const result = ctx._stmt(`UPDATE "${entityName}" SET ${setClause} ${clause}`).run(...setCols.map((key) => transformed[key]), ...whereValues);
5468
5470
  return result.changes ?? 0;
5469
5471
  }
5470
5472
  function createUpdateBuilder(ctx, entityName, data) {
@@ -5505,7 +5507,7 @@ function deleteEntity(ctx, entityName, id) {
5505
5507
  if (result === false)
5506
5508
  return;
5507
5509
  }
5508
- ctx.db.query(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
5510
+ ctx._stmt(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
5509
5511
  if (hooks?.afterDelete)
5510
5512
  hooks.afterDelete(id);
5511
5513
  }
@@ -5516,15 +5518,11 @@ function deleteWhere(ctx, entityName, conditions) {
5516
5518
  if (ctx.softDeletes) {
5517
5519
  const now = new Date().toISOString();
5518
5520
  const sql2 = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
5519
- if (ctx.debug)
5520
- console.log("[satidb]", sql2, [now, ...values]);
5521
- const result2 = ctx.db.query(sql2).run(now, ...values);
5521
+ const result2 = ctx._m(`SQL: ${sql2.slice(0, 50)}`, () => ctx._stmt(sql2).run(now, ...values));
5522
5522
  return result2.changes ?? 0;
5523
5523
  }
5524
5524
  const sql = `DELETE FROM "${entityName}" ${clause}`;
5525
- if (ctx.debug)
5526
- console.log("[satidb]", sql, values);
5527
- const result = ctx.db.query(sql).run(...values);
5525
+ const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx._stmt(sql).run(...values));
5528
5526
  return result.changes ?? 0;
5529
5527
  }
5530
5528
  function createDeleteBuilder(ctx, entityName) {
@@ -5563,7 +5561,7 @@ function insertMany(ctx, entityName, rows) {
5563
5561
  const columns = Object.keys(transformed);
5564
5562
  const quotedCols = columns.map((c) => `"${c}"`);
5565
5563
  const sql = columns.length === 0 ? `INSERT INTO "${entityName}" DEFAULT VALUES` : `INSERT INTO "${entityName}" (${quotedCols.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
5566
- const result = ctx.db.query(sql).run(...Object.values(transformed));
5564
+ const result = ctx._stmt(sql).run(...Object.values(transformed));
5567
5565
  ids2.push(result.lastInsertRowid);
5568
5566
  }
5569
5567
  return ids2;
@@ -5649,6 +5647,15 @@ class _Database {
5649
5647
  _pollTimer = null;
5650
5648
  _pollInterval;
5651
5649
  _measure;
5650
+ _stmtCache = new Map;
5651
+ _stmt(sql) {
5652
+ let stmt = this._stmtCache.get(sql);
5653
+ if (!stmt) {
5654
+ stmt = this.db.query(sql);
5655
+ this._stmtCache.set(sql, stmt);
5656
+ }
5657
+ return stmt;
5658
+ }
5652
5659
  _m(label, fn) {
5653
5660
  if (this._debug)
5654
5661
  return this._measure.measureSync.assert(label, fn);
@@ -5679,7 +5686,9 @@ class _Database {
5679
5686
  softDeletes: this._softDeletes,
5680
5687
  hooks: options.hooks ?? {},
5681
5688
  computed: options.computed ?? {},
5682
- cascade: options.cascade ?? {}
5689
+ cascade: options.cascade ?? {},
5690
+ _m: (label, fn) => this._m(label, fn),
5691
+ _stmt: (sql) => this._stmt(sql)
5683
5692
  };
5684
5693
  this._m("Init tables", () => this.initializeTables());
5685
5694
  if (this._reactive)
@@ -5696,55 +5705,59 @@ class _Database {
5696
5705
  insertMany: (rows) => this._m(`${entityName}.insertMany(${rows.length})`, () => insertMany(this._ctx, entityName, rows)),
5697
5706
  update: (idOrData, data) => {
5698
5707
  if (typeof idOrData === "number")
5699
- return update(this._ctx, entityName, idOrData, data);
5708
+ return this._m(`${entityName}.update(${idOrData})`, () => update(this._ctx, entityName, idOrData, data));
5700
5709
  return createUpdateBuilder(this._ctx, entityName, idOrData);
5701
5710
  },
5702
- upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
5703
- upsertMany: (rows, conditions) => upsertMany(this._ctx, entityName, rows, conditions),
5704
- findOrCreate: (conditions, defaults) => findOrCreate(this._ctx, entityName, conditions, defaults),
5711
+ upsert: (conditions, data) => this._m(`${entityName}.upsert`, () => upsert(this._ctx, entityName, data, conditions)),
5712
+ upsertMany: (rows, conditions) => this._m(`${entityName}.upsertMany(${rows.length})`, () => upsertMany(this._ctx, entityName, rows, conditions)),
5713
+ findOrCreate: (conditions, defaults) => this._m(`${entityName}.findOrCreate`, () => findOrCreate(this._ctx, entityName, conditions, defaults)),
5705
5714
  delete: (id) => {
5706
5715
  if (typeof id === "number") {
5707
- const hooks = this._ctx.hooks[entityName];
5708
- if (hooks?.beforeDelete) {
5709
- const result = hooks.beforeDelete(id);
5710
- if (result === false)
5711
- return;
5712
- }
5713
- const cascadeTargets = this._ctx.cascade[entityName];
5714
- if (cascadeTargets) {
5715
- for (const childTable of cascadeTargets) {
5716
- const rel = this._ctx.relationships.find((r) => r.type === "belongs-to" && r.from === childTable && r.to === entityName);
5717
- if (rel) {
5718
- if (this._softDeletes) {
5719
- const now = new Date().toISOString();
5720
- this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
5721
- } else {
5722
- this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
5716
+ return this._m(`${entityName}.delete(${id})`, () => {
5717
+ const hooks = this._ctx.hooks[entityName];
5718
+ if (hooks?.beforeDelete) {
5719
+ const result = hooks.beforeDelete(id);
5720
+ if (result === false)
5721
+ return;
5722
+ }
5723
+ const cascadeTargets = this._ctx.cascade[entityName];
5724
+ if (cascadeTargets) {
5725
+ for (const childTable of cascadeTargets) {
5726
+ const rel = this._ctx.relationships.find((r) => r.type === "belongs-to" && r.from === childTable && r.to === entityName);
5727
+ if (rel) {
5728
+ if (this._softDeletes) {
5729
+ const now = new Date().toISOString();
5730
+ this._stmt(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
5731
+ } else {
5732
+ this._stmt(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
5733
+ }
5723
5734
  }
5724
5735
  }
5725
5736
  }
5726
- }
5727
- if (this._softDeletes) {
5728
- const now = new Date().toISOString();
5729
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
5730
- if (hooks?.afterDelete)
5731
- hooks.afterDelete(id);
5732
- return;
5733
- }
5734
- return deleteEntity(this._ctx, entityName, id);
5737
+ if (this._softDeletes) {
5738
+ const now = new Date().toISOString();
5739
+ this._stmt(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
5740
+ if (hooks?.afterDelete)
5741
+ hooks.afterDelete(id);
5742
+ return;
5743
+ }
5744
+ return deleteEntity(this._ctx, entityName, id);
5745
+ });
5735
5746
  }
5736
5747
  return createDeleteBuilder(this._ctx, entityName);
5737
5748
  },
5738
5749
  restore: (id) => {
5739
5750
  if (!this._softDeletes)
5740
5751
  throw new Error("restore() requires softDeletes: true");
5741
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
5752
+ this._m(`${entityName}.restore(${id})`, () => {
5753
+ this._stmt(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
5754
+ });
5742
5755
  },
5743
5756
  select: (...cols) => createQueryBuilder(this._ctx, entityName, cols),
5744
- count: () => {
5745
- const row = this.db.query(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ""}`).get();
5757
+ count: () => this._m(`${entityName}.count`, () => {
5758
+ const row = this._stmt(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ""}`).get();
5746
5759
  return row?.count ?? 0;
5747
- },
5760
+ }),
5748
5761
  on: (event, callback) => {
5749
5762
  return this._registerListener(entityName, event, callback);
5750
5763
  },
@@ -5858,11 +5871,11 @@ class _Database {
5858
5871
  }
5859
5872
  }
5860
5873
  _processChanges() {
5861
- const head = this.db.query('SELECT MAX(id) as m FROM "_changes"').get();
5874
+ const head = this._stmt('SELECT MAX(id) as m FROM "_changes"').get();
5862
5875
  const maxId = head?.m ?? 0;
5863
5876
  if (maxId <= this._changeWatermark)
5864
5877
  return;
5865
- const changes = this.db.query('SELECT id, tbl, op, row_id FROM "_changes" WHERE id > ? ORDER BY id').all(this._changeWatermark);
5878
+ const changes = this._stmt('SELECT id, tbl, op, row_id FROM "_changes" WHERE id > ? ORDER BY id').all(this._changeWatermark);
5866
5879
  for (const change of changes) {
5867
5880
  const listeners = this._listeners.filter((l) => l.table === change.tbl && l.event === change.op);
5868
5881
  if (listeners.length > 0) {
@@ -5886,31 +5899,26 @@ class _Database {
5886
5899
  }
5887
5900
  this._changeWatermark = change.id;
5888
5901
  }
5889
- this.db.query('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
5902
+ this._stmt('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
5890
5903
  }
5891
5904
  transaction(callback) {
5892
- return this.db.transaction(callback)();
5905
+ return this._m("transaction", () => this.db.transaction(callback)());
5893
5906
  }
5894
5907
  close() {
5895
5908
  this._stopPolling();
5909
+ this._stmtCache.clear();
5896
5910
  this.db.close();
5897
5911
  }
5898
5912
  query(callback) {
5899
- return executeProxyQuery(this.schemas, callback, (sql, params) => {
5900
- if (this._debug)
5901
- console.log("[satidb]", sql, params);
5902
- return this.db.query(sql).all(...params);
5903
- });
5913
+ return this._m("query(proxy)", () => executeProxyQuery(this.schemas, callback, (sql, params) => {
5914
+ return this._stmt(sql).all(...params);
5915
+ }));
5904
5916
  }
5905
5917
  raw(sql, ...params) {
5906
- if (this._debug)
5907
- console.log("[satidb]", sql, params);
5908
- return this.db.query(sql).all(...params);
5918
+ return this._m(`raw: ${sql.slice(0, 60)}`, () => this._stmt(sql).all(...params));
5909
5919
  }
5910
5920
  exec(sql, ...params) {
5911
- if (this._debug)
5912
- console.log("[satidb]", sql, params);
5913
- this.db.run(sql, ...params);
5921
+ this._m(`exec: ${sql.slice(0, 60)}`, () => this.db.run(sql, ...params));
5914
5922
  }
5915
5923
  tables() {
5916
5924
  return Object.keys(this.schemas);
@@ -5919,70 +5927,76 @@ class _Database {
5919
5927
  return this.db.query(`PRAGMA table_info("${tableName}")`).all();
5920
5928
  }
5921
5929
  dump() {
5922
- const result = {};
5923
- for (const tableName of Object.keys(this.schemas)) {
5924
- result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
5925
- }
5926
- return result;
5930
+ return this._m("dump", () => {
5931
+ const result = {};
5932
+ for (const tableName of Object.keys(this.schemas)) {
5933
+ result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
5934
+ }
5935
+ return result;
5936
+ });
5927
5937
  }
5928
5938
  load(data, options) {
5929
- const txn = this.db.transaction(() => {
5930
- for (const [tableName, rows] of Object.entries(data)) {
5931
- if (!this.schemas[tableName])
5932
- continue;
5933
- if (!options?.append) {
5934
- this.db.run(`DELETE FROM "${tableName}"`);
5935
- }
5936
- for (const row of rows) {
5937
- const cols = Object.keys(row).filter((k) => k !== "id");
5938
- const placeholders = cols.map(() => "?").join(", ");
5939
- const values = cols.map((c) => {
5940
- const v = row[c];
5941
- if (v !== null && v !== undefined && typeof v === "object" && !(v instanceof Buffer)) {
5942
- return JSON.stringify(v);
5943
- }
5944
- return v;
5945
- });
5946
- this.db.query(`INSERT INTO "${tableName}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`).run(...values);
5939
+ this._m(`load(${Object.keys(data).join(",")})`, () => {
5940
+ const txn = this.db.transaction(() => {
5941
+ for (const [tableName, rows] of Object.entries(data)) {
5942
+ if (!this.schemas[tableName])
5943
+ continue;
5944
+ if (!options?.append) {
5945
+ this.db.run(`DELETE FROM "${tableName}"`);
5946
+ }
5947
+ for (const row of rows) {
5948
+ const cols = Object.keys(row).filter((k) => k !== "id");
5949
+ const placeholders = cols.map(() => "?").join(", ");
5950
+ const values = cols.map((c) => {
5951
+ const v = row[c];
5952
+ if (v !== null && v !== undefined && typeof v === "object" && !(v instanceof Buffer)) {
5953
+ return JSON.stringify(v);
5954
+ }
5955
+ return v;
5956
+ });
5957
+ this.db.query(`INSERT INTO "${tableName}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`).run(...values);
5958
+ }
5947
5959
  }
5948
- }
5960
+ });
5961
+ txn();
5949
5962
  });
5950
- txn();
5951
5963
  }
5952
5964
  seed(fixtures) {
5953
5965
  this.load(fixtures, { append: true });
5954
5966
  }
5955
5967
  diff() {
5956
- const result = {};
5957
- const systemCols = new Set(["id", "createdAt", "updatedAt", "deletedAt"]);
5958
- for (const [tableName, schema] of Object.entries(this.schemas)) {
5959
- const schemaFields = getStorableFields(schema);
5960
- const schemaColMap = new Map(schemaFields.map((f) => [f.name, zodTypeToSqlType(f.type)]));
5961
- const liveColumns = this.columns(tableName);
5962
- const liveColMap = new Map(liveColumns.map((c) => [c.name, c.type]));
5963
- const added = [];
5964
- const removed = [];
5965
- const typeChanged = [];
5966
- for (const [col, expectedType] of schemaColMap) {
5967
- if (!liveColMap.has(col)) {
5968
- added.push(col);
5969
- } else {
5970
- const actualType = liveColMap.get(col);
5971
- if (actualType !== expectedType) {
5972
- typeChanged.push({ column: col, expected: expectedType, actual: actualType });
5968
+ return this._m("diff", () => {
5969
+ const result = {};
5970
+ const systemCols = new Set(["id", "createdAt", "updatedAt", "deletedAt"]);
5971
+ for (const [tableName, schema] of Object.entries(this.schemas)) {
5972
+ const schemaFields = getStorableFields(schema);
5973
+ const schemaColMap = new Map(schemaFields.map((f) => [f.name, zodTypeToSqlType(f.type)]));
5974
+ const liveColumns = this.columns(tableName);
5975
+ const liveColMap = new Map(liveColumns.map((c) => [c.name, c.type]));
5976
+ const added = [];
5977
+ const removed = [];
5978
+ const typeChanged = [];
5979
+ for (const [col, expectedType] of schemaColMap) {
5980
+ if (!liveColMap.has(col)) {
5981
+ added.push(col);
5982
+ } else {
5983
+ const actualType = liveColMap.get(col);
5984
+ if (actualType !== expectedType) {
5985
+ typeChanged.push({ column: col, expected: expectedType, actual: actualType });
5986
+ }
5973
5987
  }
5974
5988
  }
5975
- }
5976
- for (const col of liveColMap.keys()) {
5977
- if (!systemCols.has(col) && !schemaColMap.has(col)) {
5978
- removed.push(col);
5989
+ for (const col of liveColMap.keys()) {
5990
+ if (!systemCols.has(col) && !schemaColMap.has(col)) {
5991
+ removed.push(col);
5992
+ }
5993
+ }
5994
+ if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
5995
+ result[tableName] = { added, removed, typeChanged };
5979
5996
  }
5980
5997
  }
5981
- if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
5982
- result[tableName] = { added, removed, typeChanged };
5983
- }
5984
- }
5985
- return result;
5998
+ return result;
5999
+ });
5986
6000
  }
5987
6001
  }
5988
6002
  var Database = _Database;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.24.0",
3
+ "version": "3.26.0",
4
4
  "description": "Type-safe SQLite ORM for Bun — Zod schemas, fluent queries, auto relationships, zero SQL",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/context.ts CHANGED
@@ -40,4 +40,16 @@ export interface DatabaseContext {
40
40
 
41
41
  /** Cascade delete config — parent table → list of child tables to auto-delete. */
42
42
  cascade: Record<string, string[]>;
43
+
44
+ /**
45
+ * Conditional measurement helper — wraps fn with measure-fn when debug is on.
46
+ * When debug is off, executes fn directly with zero overhead.
47
+ */
48
+ _m<T>(label: string, fn: () => T): T;
49
+
50
+ /**
51
+ * Get a cached prepared statement. Compiles SQL once, reuses on subsequent calls.
52
+ * Falls back to `db.query(sql)` if the statement was finalized.
53
+ */
54
+ _stmt(sql: string): ReturnType<SqliteDatabase['query']>;
43
55
  }
package/src/crud.ts CHANGED
@@ -14,21 +14,21 @@ import type { DatabaseContext } from './context';
14
14
  // ---------------------------------------------------------------------------
15
15
 
16
16
  export function getById(ctx: DatabaseContext, entityName: string, id: number): AugmentedEntity<any> | null {
17
- const row = ctx.db.query(`SELECT * FROM "${entityName}" WHERE id = ?`).get(id) as any;
17
+ const row = ctx._stmt(`SELECT * FROM "${entityName}" WHERE id = ?`).get(id) as any;
18
18
  if (!row) return null;
19
19
  return ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName]!));
20
20
  }
21
21
 
22
22
  export function getOne(ctx: DatabaseContext, entityName: string, conditions: Record<string, any>): AugmentedEntity<any> | null {
23
23
  const { clause, values } = ctx.buildWhereClause(conditions);
24
- const row = ctx.db.query(`SELECT * FROM "${entityName}" ${clause} LIMIT 1`).get(...values) as any;
24
+ const row = ctx._stmt(`SELECT * FROM "${entityName}" ${clause} LIMIT 1`).get(...values) as any;
25
25
  if (!row) return null;
26
26
  return ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName]!));
27
27
  }
28
28
 
29
29
  export function findMany(ctx: DatabaseContext, entityName: string, conditions: Record<string, any> = {}): AugmentedEntity<any>[] {
30
30
  const { clause, values } = ctx.buildWhereClause(conditions);
31
- const rows = ctx.db.query(`SELECT * FROM "${entityName}" ${clause}`).all(...values);
31
+ const rows = ctx._stmt(`SELECT * FROM "${entityName}" ${clause}`).all(...values);
32
32
  return rows.map((row: any) =>
33
33
  ctx.attachMethods(entityName, transformFromStorage(row, ctx.schemas[entityName]!))
34
34
  );
@@ -66,9 +66,12 @@ export function insert<T extends Record<string, any>>(ctx: DatabaseContext, enti
66
66
  ? `INSERT INTO "${entityName}" DEFAULT VALUES`
67
67
  : `INSERT INTO "${entityName}" (${quotedCols.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
68
68
 
69
- if (ctx.debug) console.log('[satidb]', sql, Object.values(transformed));
70
- const result = ctx.db.query(sql).run(...Object.values(transformed));
71
- const newEntity = getById(ctx, entityName, result.lastInsertRowid as number);
69
+ let lastId = 0;
70
+ ctx._m(`SQL: ${sql.slice(0, 40)}`, () => {
71
+ const result = ctx._stmt(sql).run(...Object.values(transformed));
72
+ lastId = result.lastInsertRowid as number;
73
+ });
74
+ const newEntity = getById(ctx, entityName, lastId);
72
75
  if (!newEntity) throw new Error('Failed to retrieve entity after insertion');
73
76
 
74
77
  // afterInsert hook
@@ -99,8 +102,9 @@ export function update<T extends Record<string, any>>(ctx: DatabaseContext, enti
99
102
 
100
103
  const setClause = Object.keys(transformed).map(key => `"${key}" = ?`).join(', ');
101
104
  const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
102
- if (ctx.debug) console.log('[satidb]', sql, [...Object.values(transformed), id]);
103
- ctx.db.query(sql).run(...Object.values(transformed), id);
105
+ ctx._m(`SQL: UPDATE ${entityName} SET ...`, () => {
106
+ ctx._stmt(sql).run(...Object.values(transformed), id);
107
+ });
104
108
 
105
109
  const updated = getById(ctx, entityName, id);
106
110
 
@@ -121,7 +125,7 @@ export function updateWhere(ctx: DatabaseContext, entityName: string, data: Reco
121
125
 
122
126
  const setCols = Object.keys(transformed);
123
127
  const setClause = setCols.map(key => `"${key}" = ?`).join(', ');
124
- const result = ctx.db.query(`UPDATE "${entityName}" SET ${setClause} ${clause}`).run(
128
+ const result = ctx._stmt(`UPDATE "${entityName}" SET ${setClause} ${clause}`).run(
125
129
  ...setCols.map(key => transformed[key]),
126
130
  ...whereValues
127
131
  );
@@ -177,7 +181,7 @@ export function deleteEntity(ctx: DatabaseContext, entityName: string, id: numbe
177
181
  if (result === false) return;
178
182
  }
179
183
 
180
- ctx.db.query(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
184
+ ctx._stmt(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
181
185
 
182
186
  // afterDelete hook
183
187
  if (hooks?.afterDelete) hooks.afterDelete(id);
@@ -192,14 +196,12 @@ export function deleteWhere(ctx: DatabaseContext, entityName: string, conditions
192
196
  // Soft delete: set deletedAt instead of removing rows
193
197
  const now = new Date().toISOString();
194
198
  const sql = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
195
- if (ctx.debug) console.log('[satidb]', sql, [now, ...values]);
196
- const result = ctx.db.query(sql).run(now, ...values);
199
+ const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx._stmt(sql).run(now, ...values));
197
200
  return (result as any).changes ?? 0;
198
201
  }
199
202
 
200
203
  const sql = `DELETE FROM "${entityName}" ${clause}`;
201
- if (ctx.debug) console.log('[satidb]', sql, values);
202
- const result = ctx.db.query(sql).run(...values);
204
+ const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx._stmt(sql).run(...values));
203
205
  return (result as any).changes ?? 0;
204
206
  }
205
207
 
@@ -245,7 +247,7 @@ export function insertMany<T extends Record<string, any>>(ctx: DatabaseContext,
245
247
  const sql = columns.length === 0
246
248
  ? `INSERT INTO "${entityName}" DEFAULT VALUES`
247
249
  : `INSERT INTO "${entityName}" (${quotedCols.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
248
- const result = ctx.db.query(sql).run(...Object.values(transformed));
250
+ const result = ctx._stmt(sql).run(...Object.values(transformed));
249
251
  ids.push(result.lastInsertRowid as number);
250
252
  }
251
253
  return ids;
package/src/database.ts CHANGED
@@ -67,6 +67,19 @@ class _Database<Schemas extends SchemaMap> {
67
67
  /** Scoped measure-fn instance for instrumentation. */
68
68
  private _measure: ReturnType<typeof createMeasure>;
69
69
 
70
+ /** Prepared statement cache — avoids re-compiling identical SQL. */
71
+ private _stmtCache = new Map<string, ReturnType<SqliteDatabase['query']>>();
72
+
73
+ /** Get or create a cached prepared statement. */
74
+ private _stmt(sql: string): ReturnType<SqliteDatabase['query']> {
75
+ let stmt = this._stmtCache.get(sql);
76
+ if (!stmt) {
77
+ stmt = this.db.query(sql);
78
+ this._stmtCache.set(sql, stmt);
79
+ }
80
+ return stmt;
81
+ }
82
+
70
83
  /**
71
84
  * Conditional measurement helper — wraps with measure-fn only when debug is on.
72
85
  * When debug is off, executes fn directly with zero overhead.
@@ -104,6 +117,8 @@ class _Database<Schemas extends SchemaMap> {
104
117
  hooks: options.hooks ?? {},
105
118
  computed: options.computed ?? {},
106
119
  cascade: options.cascade ?? {},
120
+ _m: <T>(label: string, fn: () => T): T => this._m(label, fn),
121
+ _stmt: (sql: string) => this._stmt(sql),
107
122
  };
108
123
 
109
124
  this._m('Init tables', () => this.initializeTables());
@@ -119,60 +134,62 @@ class _Database<Schemas extends SchemaMap> {
119
134
  insert: (data) => this._m(`${entityName}.insert`, () => insert(this._ctx, entityName, data)),
120
135
  insertMany: (rows: any[]) => this._m(`${entityName}.insertMany(${rows.length})`, () => insertMany(this._ctx, entityName, rows)),
121
136
  update: (idOrData: any, data?: any) => {
122
- if (typeof idOrData === 'number') return update(this._ctx, entityName, idOrData, data);
137
+ if (typeof idOrData === 'number') return this._m(`${entityName}.update(${idOrData})`, () => update(this._ctx, entityName, idOrData, data));
123
138
  return createUpdateBuilder(this._ctx, entityName, idOrData);
124
139
  },
125
- upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
126
- upsertMany: (rows: any[], conditions?: any) => upsertMany(this._ctx, entityName, rows, conditions),
127
- findOrCreate: (conditions: any, defaults?: any) => findOrCreate(this._ctx, entityName, conditions, defaults),
140
+ upsert: (conditions, data) => this._m(`${entityName}.upsert`, () => upsert(this._ctx, entityName, data, conditions)),
141
+ upsertMany: (rows: any[], conditions?: any) => this._m(`${entityName}.upsertMany(${rows.length})`, () => upsertMany(this._ctx, entityName, rows, conditions)),
142
+ findOrCreate: (conditions: any, defaults?: any) => this._m(`${entityName}.findOrCreate`, () => findOrCreate(this._ctx, entityName, conditions, defaults)),
128
143
  delete: ((id?: any) => {
129
144
  if (typeof id === 'number') {
130
- // beforeDelete hook return false to cancel
131
- const hooks = this._ctx.hooks[entityName];
132
- if (hooks?.beforeDelete) {
133
- const result = hooks.beforeDelete(id);
134
- if (result === false) return;
135
- }
145
+ return this._m(`${entityName}.delete(${id})`, () => {
146
+ // beforeDelete hook — return false to cancel
147
+ const hooks = this._ctx.hooks[entityName];
148
+ if (hooks?.beforeDelete) {
149
+ const result = hooks.beforeDelete(id);
150
+ if (result === false) return;
151
+ }
136
152
 
137
- // Cascade delete children first
138
- const cascadeTargets = this._ctx.cascade[entityName];
139
- if (cascadeTargets) {
140
- for (const childTable of cascadeTargets) {
141
- // Find FK from child → this parent via relationships
142
- const rel = this._ctx.relationships.find(
143
- r => r.type === 'belongs-to' && r.from === childTable && r.to === entityName
144
- );
145
- if (rel) {
146
- if (this._softDeletes) {
147
- const now = new Date().toISOString();
148
- this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
149
- } else {
150
- this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
153
+ // Cascade delete children first
154
+ const cascadeTargets = this._ctx.cascade[entityName];
155
+ if (cascadeTargets) {
156
+ for (const childTable of cascadeTargets) {
157
+ const rel = this._ctx.relationships.find(
158
+ r => r.type === 'belongs-to' && r.from === childTable && r.to === entityName
159
+ );
160
+ if (rel) {
161
+ if (this._softDeletes) {
162
+ const now = new Date().toISOString();
163
+ this._stmt(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
164
+ } else {
165
+ this._stmt(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
166
+ }
151
167
  }
152
168
  }
153
169
  }
154
- }
155
170
 
156
- if (this._softDeletes) {
157
- const now = new Date().toISOString();
158
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
159
- if (hooks?.afterDelete) hooks.afterDelete(id);
160
- return;
161
- }
162
- return deleteEntity(this._ctx, entityName, id);
171
+ if (this._softDeletes) {
172
+ const now = new Date().toISOString();
173
+ this._stmt(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
174
+ if (hooks?.afterDelete) hooks.afterDelete(id);
175
+ return;
176
+ }
177
+ return deleteEntity(this._ctx, entityName, id);
178
+ });
163
179
  }
164
180
  return createDeleteBuilder(this._ctx, entityName);
165
181
  }) as any,
166
182
  restore: ((id: number) => {
167
183
  if (!this._softDeletes) throw new Error('restore() requires softDeletes: true');
168
- // debug log replaced by measure-fn instrumentation
169
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
184
+ this._m(`${entityName}.restore(${id})`, () => {
185
+ this._stmt(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
186
+ });
170
187
  }) as any,
171
188
  select: (...cols: string[]) => createQueryBuilder(this._ctx, entityName, cols),
172
- count: () => {
173
- const row = this.db.query(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ''}`).get() as any;
189
+ count: () => this._m(`${entityName}.count`, () => {
190
+ const row = this._stmt(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ''}`).get() as any;
174
191
  return row?.count ?? 0;
175
- },
192
+ }),
176
193
  on: (event: ChangeEvent, callback: (row: any) => void | Promise<void>) => {
177
194
  return this._registerListener(entityName, event, callback);
178
195
  },
@@ -336,11 +353,11 @@ class _Database<Schemas extends SchemaMap> {
336
353
  */
337
354
  private _processChanges(): void {
338
355
  // Fast path: check if anything changed at all (single scalar, index-only)
339
- const head = this.db.query('SELECT MAX(id) as m FROM "_changes"').get() as any;
356
+ const head = this._stmt('SELECT MAX(id) as m FROM "_changes"').get() as any;
340
357
  const maxId: number = head?.m ?? 0;
341
358
  if (maxId <= this._changeWatermark) return;
342
359
 
343
- const changes = this.db.query(
360
+ const changes = this._stmt(
344
361
  'SELECT id, tbl, op, row_id FROM "_changes" WHERE id > ? ORDER BY id'
345
362
  ).all(this._changeWatermark) as { id: number; tbl: string; op: string; row_id: number }[];
346
363
 
@@ -371,7 +388,7 @@ class _Database<Schemas extends SchemaMap> {
371
388
  }
372
389
 
373
390
  // Clean up consumed changes
374
- this.db.query('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
391
+ this._stmt('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
375
392
  }
376
393
 
377
394
  // =========================================================================
@@ -379,12 +396,13 @@ class _Database<Schemas extends SchemaMap> {
379
396
  // =========================================================================
380
397
 
381
398
  public transaction<T>(callback: () => T): T {
382
- return this.db.transaction(callback)();
399
+ return this._m('transaction', () => this.db.transaction(callback)());
383
400
  }
384
401
 
385
- /** Close the database: stops polling and releases the SQLite handle. */
402
+ /** Close the database: stops polling, clears cache, and releases the SQLite handle. */
386
403
  public close(): void {
387
404
  this._stopPolling();
405
+ this._stmtCache.clear();
388
406
  this.db.close();
389
407
  }
390
408
 
@@ -396,14 +414,13 @@ class _Database<Schemas extends SchemaMap> {
396
414
  public query<T extends Record<string, any> = Record<string, any>>(
397
415
  callback: (ctx: { [K in keyof Schemas]: ProxyColumns<InferSchema<Schemas[K]>> }) => ProxyQueryResult
398
416
  ): T[] {
399
- return executeProxyQuery(
417
+ return this._m('query(proxy)', () => executeProxyQuery(
400
418
  this.schemas,
401
419
  callback as any,
402
420
  (sql: string, params: any[]) => {
403
- if (this._debug) console.log('[satidb]', sql, params);
404
- return this.db.query(sql).all(...params) as T[];
421
+ return this._stmt(sql).all(...params) as T[];
405
422
  },
406
- );
423
+ ));
407
424
  }
408
425
 
409
426
  // =========================================================================
@@ -412,14 +429,12 @@ class _Database<Schemas extends SchemaMap> {
412
429
 
413
430
  /** Execute a raw SQL query and return results. */
414
431
  public raw<T = any>(sql: string, ...params: any[]): T[] {
415
- if (this._debug) console.log('[satidb]', sql, params);
416
- return this.db.query(sql).all(...params) as T[];
432
+ return this._m(`raw: ${sql.slice(0, 60)}`, () => this._stmt(sql).all(...params) as T[]);
417
433
  }
418
434
 
419
435
  /** Execute a raw SQL statement (INSERT/UPDATE/DELETE) without returning rows. */
420
436
  public exec(sql: string, ...params: any[]): void {
421
- if (this._debug) console.log('[satidb]', sql, params);
422
- this.db.run(sql, ...params);
437
+ this._m(`exec: ${sql.slice(0, 60)}`, () => this.db.run(sql, ...params));
423
438
  }
424
439
 
425
440
  // =========================================================================
@@ -445,11 +460,13 @@ class _Database<Schemas extends SchemaMap> {
445
460
  * Each key is a table name, value is an array of raw row objects.
446
461
  */
447
462
  public dump(): Record<string, any[]> {
448
- const result: Record<string, any[]> = {};
449
- for (const tableName of Object.keys(this.schemas)) {
450
- result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
451
- }
452
- return result;
463
+ return this._m('dump', () => {
464
+ const result: Record<string, any[]> = {};
465
+ for (const tableName of Object.keys(this.schemas)) {
466
+ result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
467
+ }
468
+ return result;
469
+ });
453
470
  }
454
471
 
455
472
  /**
@@ -457,28 +474,29 @@ class _Database<Schemas extends SchemaMap> {
457
474
  * Use `{ append: true }` to insert without truncating.
458
475
  */
459
476
  public load(data: Record<string, any[]>, options?: { append?: boolean }): void {
460
- const txn = this.db.transaction(() => {
461
- for (const [tableName, rows] of Object.entries(data)) {
462
- if (!this.schemas[tableName]) continue;
463
- if (!options?.append) {
464
- this.db.run(`DELETE FROM "${tableName}"`);
465
- }
466
- for (const row of rows) {
467
- const cols = Object.keys(row).filter(k => k !== 'id');
468
- const placeholders = cols.map(() => '?').join(', ');
469
- const values = cols.map(c => {
470
- const v = row[c];
471
- // Auto-serialize objects/arrays
472
- if (v !== null && v !== undefined && typeof v === 'object' && !(v instanceof Buffer)) {
473
- return JSON.stringify(v);
474
- }
475
- return v;
476
- });
477
- this.db.query(`INSERT INTO "${tableName}" (${cols.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`).run(...values);
477
+ this._m(`load(${Object.keys(data).join(',')})`, () => {
478
+ const txn = this.db.transaction(() => {
479
+ for (const [tableName, rows] of Object.entries(data)) {
480
+ if (!this.schemas[tableName]) continue;
481
+ if (!options?.append) {
482
+ this.db.run(`DELETE FROM "${tableName}"`);
483
+ }
484
+ for (const row of rows) {
485
+ const cols = Object.keys(row).filter(k => k !== 'id');
486
+ const placeholders = cols.map(() => '?').join(', ');
487
+ const values = cols.map(c => {
488
+ const v = row[c];
489
+ if (v !== null && v !== undefined && typeof v === 'object' && !(v instanceof Buffer)) {
490
+ return JSON.stringify(v);
491
+ }
492
+ return v;
493
+ });
494
+ this.db.query(`INSERT INTO "${tableName}" (${cols.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`).run(...values);
495
+ }
478
496
  }
479
- }
497
+ });
498
+ txn();
480
499
  });
481
- txn();
482
500
  }
483
501
 
484
502
  /**
@@ -498,45 +516,45 @@ class _Database<Schemas extends SchemaMap> {
498
516
  * Returns a diff object per table: { added, removed, typeChanged }.
499
517
  */
500
518
  public diff(): Record<string, { added: string[]; removed: string[]; typeChanged: { column: string; expected: string; actual: string }[] }> {
501
- const result: Record<string, { added: string[]; removed: string[]; typeChanged: { column: string; expected: string; actual: string }[] }> = {};
502
- const systemCols = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt']);
503
-
504
- for (const [tableName, schema] of Object.entries(this.schemas)) {
505
- const schemaFields = getStorableFields(schema);
506
- const schemaColMap = new Map(schemaFields.map(f => [f.name, zodTypeToSqlType(f.type)]));
507
-
508
- const liveColumns = this.columns(tableName);
509
- const liveColMap = new Map(liveColumns.map(c => [c.name, c.type]));
510
-
511
- const added: string[] = [];
512
- const removed: string[] = [];
513
- const typeChanged: { column: string; expected: string; actual: string }[] = [];
514
-
515
- // Columns in schema but not in live DB
516
- for (const [col, expectedType] of schemaColMap) {
517
- if (!liveColMap.has(col)) {
518
- added.push(col);
519
- } else {
520
- const actualType = liveColMap.get(col)!;
521
- if (actualType !== expectedType) {
522
- typeChanged.push({ column: col, expected: expectedType, actual: actualType });
519
+ return this._m('diff', () => {
520
+ const result: Record<string, { added: string[]; removed: string[]; typeChanged: { column: string; expected: string; actual: string }[] }> = {};
521
+ const systemCols = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt']);
522
+
523
+ for (const [tableName, schema] of Object.entries(this.schemas)) {
524
+ const schemaFields = getStorableFields(schema);
525
+ const schemaColMap = new Map(schemaFields.map(f => [f.name, zodTypeToSqlType(f.type)]));
526
+
527
+ const liveColumns = this.columns(tableName);
528
+ const liveColMap = new Map(liveColumns.map(c => [c.name, c.type]));
529
+
530
+ const added: string[] = [];
531
+ const removed: string[] = [];
532
+ const typeChanged: { column: string; expected: string; actual: string }[] = [];
533
+
534
+ for (const [col, expectedType] of schemaColMap) {
535
+ if (!liveColMap.has(col)) {
536
+ added.push(col);
537
+ } else {
538
+ const actualType = liveColMap.get(col)!;
539
+ if (actualType !== expectedType) {
540
+ typeChanged.push({ column: col, expected: expectedType, actual: actualType });
541
+ }
523
542
  }
524
543
  }
525
- }
526
544
 
527
- // Columns in live DB but not in schema (excluding system columns)
528
- for (const col of liveColMap.keys()) {
529
- if (!systemCols.has(col) && !schemaColMap.has(col)) {
530
- removed.push(col);
545
+ for (const col of liveColMap.keys()) {
546
+ if (!systemCols.has(col) && !schemaColMap.has(col)) {
547
+ removed.push(col);
548
+ }
531
549
  }
532
- }
533
550
 
534
- if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
535
- result[tableName] = { added, removed, typeChanged };
551
+ if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
552
+ result[tableName] = { added, removed, typeChanged };
553
+ }
536
554
  }
537
- }
538
555
 
539
- return result;
556
+ return result;
557
+ });
540
558
  }
541
559
  }
542
560
 
package/src/query.ts CHANGED
@@ -39,10 +39,11 @@ export function createQueryBuilder(ctx: DatabaseContext, entityName: string, ini
39
39
  const schema = ctx.schemas[entityName]!;
40
40
 
41
41
  const executor = (sql: string, params: any[], raw: boolean): any[] => {
42
- if (ctx.debug) console.log('[satidb]', sql, params);
43
- const rows = ctx.db.query(sql).all(...params);
44
- if (raw) return rows;
45
- return rows.map((row: any) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
42
+ return ctx._m(`SQL: ${sql.slice(0, 60)}`, () => {
43
+ const rows = ctx._stmt(sql).all(...params);
44
+ if (raw) return rows;
45
+ return rows.map((row: any) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
46
+ });
46
47
  };
47
48
 
48
49
  const singleExecutor = (sql: string, params: any[], raw: boolean): any | null => {
@@ -102,7 +103,7 @@ export function createQueryBuilder(ctx: DatabaseContext, entityName: string, ini
102
103
  if (belongsTo) {
103
104
  const fk = belongsTo.foreignKey;
104
105
  const placeholders = parentIds.map(() => '?').join(', ');
105
- const childRows = ctx.db.query(
106
+ const childRows = ctx._stmt(
106
107
  `SELECT * FROM ${hasMany.to} WHERE ${fk} IN (${placeholders})`
107
108
  ).all(...parentIds) as any[];
108
109