sqlite-zod-orm 3.25.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
@@ -5221,7 +5221,7 @@ function createQueryBuilder(ctx, entityName, initialCols) {
5221
5221
  const schema = ctx.schemas[entityName];
5222
5222
  const executor = (sql, params, raw) => {
5223
5223
  return ctx._m(`SQL: ${sql.slice(0, 60)}`, () => {
5224
- const rows = ctx.db.query(sql).all(...params);
5224
+ const rows = ctx._stmt(sql).all(...params);
5225
5225
  if (raw)
5226
5226
  return rows;
5227
5227
  return rows.map((row) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
@@ -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) {
@@ -5419,7 +5419,7 @@ function insert(ctx, entityName, data) {
5419
5419
  const sql = columns.length === 0 ? `INSERT INTO "${entityName}" DEFAULT VALUES` : `INSERT INTO "${entityName}" (${quotedCols.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
5420
5420
  let lastId = 0;
5421
5421
  ctx._m(`SQL: ${sql.slice(0, 40)}`, () => {
5422
- const result = ctx.db.query(sql).run(...Object.values(transformed));
5422
+ const result = ctx._stmt(sql).run(...Object.values(transformed));
5423
5423
  lastId = result.lastInsertRowid;
5424
5424
  });
5425
5425
  const newEntity = getById(ctx, entityName, lastId);
@@ -5448,7 +5448,7 @@ function update(ctx, entityName, id, data) {
5448
5448
  const setClause = Object.keys(transformed).map((key) => `"${key}" = ?`).join(", ");
5449
5449
  const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
5450
5450
  ctx._m(`SQL: UPDATE ${entityName} SET ...`, () => {
5451
- ctx.db.query(sql).run(...Object.values(transformed), id);
5451
+ ctx._stmt(sql).run(...Object.values(transformed), id);
5452
5452
  });
5453
5453
  const updated = getById(ctx, entityName, id);
5454
5454
  if (hooks?.afterUpdate && updated)
@@ -5466,7 +5466,7 @@ function updateWhere(ctx, entityName, data, conditions) {
5466
5466
  throw new Error("update().where() requires at least one condition");
5467
5467
  const setCols = Object.keys(transformed);
5468
5468
  const setClause = setCols.map((key) => `"${key}" = ?`).join(", ");
5469
- 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);
5470
5470
  return result.changes ?? 0;
5471
5471
  }
5472
5472
  function createUpdateBuilder(ctx, entityName, data) {
@@ -5507,7 +5507,7 @@ function deleteEntity(ctx, entityName, id) {
5507
5507
  if (result === false)
5508
5508
  return;
5509
5509
  }
5510
- ctx.db.query(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
5510
+ ctx._stmt(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
5511
5511
  if (hooks?.afterDelete)
5512
5512
  hooks.afterDelete(id);
5513
5513
  }
@@ -5518,11 +5518,11 @@ function deleteWhere(ctx, entityName, conditions) {
5518
5518
  if (ctx.softDeletes) {
5519
5519
  const now = new Date().toISOString();
5520
5520
  const sql2 = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
5521
- const result2 = ctx._m(`SQL: ${sql2.slice(0, 50)}`, () => 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
- const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx.db.query(sql).run(...values));
5525
+ const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx._stmt(sql).run(...values));
5526
5526
  return result.changes ?? 0;
5527
5527
  }
5528
5528
  function createDeleteBuilder(ctx, entityName) {
@@ -5561,7 +5561,7 @@ function insertMany(ctx, entityName, rows) {
5561
5561
  const columns = Object.keys(transformed);
5562
5562
  const quotedCols = columns.map((c) => `"${c}"`);
5563
5563
  const sql = columns.length === 0 ? `INSERT INTO "${entityName}" DEFAULT VALUES` : `INSERT INTO "${entityName}" (${quotedCols.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
5564
- const result = ctx.db.query(sql).run(...Object.values(transformed));
5564
+ const result = ctx._stmt(sql).run(...Object.values(transformed));
5565
5565
  ids2.push(result.lastInsertRowid);
5566
5566
  }
5567
5567
  return ids2;
@@ -5647,6 +5647,15 @@ class _Database {
5647
5647
  _pollTimer = null;
5648
5648
  _pollInterval;
5649
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
+ }
5650
5659
  _m(label, fn) {
5651
5660
  if (this._debug)
5652
5661
  return this._measure.measureSync.assert(label, fn);
@@ -5678,7 +5687,8 @@ class _Database {
5678
5687
  hooks: options.hooks ?? {},
5679
5688
  computed: options.computed ?? {},
5680
5689
  cascade: options.cascade ?? {},
5681
- _m: (label, fn) => this._m(label, fn)
5690
+ _m: (label, fn) => this._m(label, fn),
5691
+ _stmt: (sql) => this._stmt(sql)
5682
5692
  };
5683
5693
  this._m("Init tables", () => this.initializeTables());
5684
5694
  if (this._reactive)
@@ -5717,16 +5727,16 @@ class _Database {
5717
5727
  if (rel) {
5718
5728
  if (this._softDeletes) {
5719
5729
  const now = new Date().toISOString();
5720
- this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
5730
+ this._stmt(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
5721
5731
  } else {
5722
- this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
5732
+ this._stmt(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
5723
5733
  }
5724
5734
  }
5725
5735
  }
5726
5736
  }
5727
5737
  if (this._softDeletes) {
5728
5738
  const now = new Date().toISOString();
5729
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
5739
+ this._stmt(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
5730
5740
  if (hooks?.afterDelete)
5731
5741
  hooks.afterDelete(id);
5732
5742
  return;
@@ -5740,12 +5750,12 @@ class _Database {
5740
5750
  if (!this._softDeletes)
5741
5751
  throw new Error("restore() requires softDeletes: true");
5742
5752
  this._m(`${entityName}.restore(${id})`, () => {
5743
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
5753
+ this._stmt(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
5744
5754
  });
5745
5755
  },
5746
5756
  select: (...cols) => createQueryBuilder(this._ctx, entityName, cols),
5747
5757
  count: () => this._m(`${entityName}.count`, () => {
5748
- const row = this.db.query(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ""}`).get();
5758
+ const row = this._stmt(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ""}`).get();
5749
5759
  return row?.count ?? 0;
5750
5760
  }),
5751
5761
  on: (event, callback) => {
@@ -5861,11 +5871,11 @@ class _Database {
5861
5871
  }
5862
5872
  }
5863
5873
  _processChanges() {
5864
- 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();
5865
5875
  const maxId = head?.m ?? 0;
5866
5876
  if (maxId <= this._changeWatermark)
5867
5877
  return;
5868
- 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);
5869
5879
  for (const change of changes) {
5870
5880
  const listeners = this._listeners.filter((l) => l.table === change.tbl && l.event === change.op);
5871
5881
  if (listeners.length > 0) {
@@ -5889,22 +5899,23 @@ class _Database {
5889
5899
  }
5890
5900
  this._changeWatermark = change.id;
5891
5901
  }
5892
- this.db.query('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
5902
+ this._stmt('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
5893
5903
  }
5894
5904
  transaction(callback) {
5895
5905
  return this._m("transaction", () => this.db.transaction(callback)());
5896
5906
  }
5897
5907
  close() {
5898
5908
  this._stopPolling();
5909
+ this._stmtCache.clear();
5899
5910
  this.db.close();
5900
5911
  }
5901
5912
  query(callback) {
5902
5913
  return this._m("query(proxy)", () => executeProxyQuery(this.schemas, callback, (sql, params) => {
5903
- return this.db.query(sql).all(...params);
5914
+ return this._stmt(sql).all(...params);
5904
5915
  }));
5905
5916
  }
5906
5917
  raw(sql, ...params) {
5907
- return this._m(`raw: ${sql.slice(0, 60)}`, () => this.db.query(sql).all(...params));
5918
+ return this._m(`raw: ${sql.slice(0, 60)}`, () => this._stmt(sql).all(...params));
5908
5919
  }
5909
5920
  exec(sql, ...params) {
5910
5921
  this._m(`exec: ${sql.slice(0, 60)}`, () => this.db.run(sql, ...params));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.25.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
@@ -46,4 +46,10 @@ export interface DatabaseContext {
46
46
  * When debug is off, executes fn directly with zero overhead.
47
47
  */
48
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']>;
49
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
  );
@@ -68,7 +68,7 @@ export function insert<T extends Record<string, any>>(ctx: DatabaseContext, enti
68
68
 
69
69
  let lastId = 0;
70
70
  ctx._m(`SQL: ${sql.slice(0, 40)}`, () => {
71
- const result = ctx.db.query(sql).run(...Object.values(transformed));
71
+ const result = ctx._stmt(sql).run(...Object.values(transformed));
72
72
  lastId = result.lastInsertRowid as number;
73
73
  });
74
74
  const newEntity = getById(ctx, entityName, lastId);
@@ -103,7 +103,7 @@ export function update<T extends Record<string, any>>(ctx: DatabaseContext, enti
103
103
  const setClause = Object.keys(transformed).map(key => `"${key}" = ?`).join(', ');
104
104
  const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
105
105
  ctx._m(`SQL: UPDATE ${entityName} SET ...`, () => {
106
- ctx.db.query(sql).run(...Object.values(transformed), id);
106
+ ctx._stmt(sql).run(...Object.values(transformed), id);
107
107
  });
108
108
 
109
109
  const updated = getById(ctx, entityName, id);
@@ -125,7 +125,7 @@ export function updateWhere(ctx: DatabaseContext, entityName: string, data: Reco
125
125
 
126
126
  const setCols = Object.keys(transformed);
127
127
  const setClause = setCols.map(key => `"${key}" = ?`).join(', ');
128
- const result = ctx.db.query(`UPDATE "${entityName}" SET ${setClause} ${clause}`).run(
128
+ const result = ctx._stmt(`UPDATE "${entityName}" SET ${setClause} ${clause}`).run(
129
129
  ...setCols.map(key => transformed[key]),
130
130
  ...whereValues
131
131
  );
@@ -181,7 +181,7 @@ export function deleteEntity(ctx: DatabaseContext, entityName: string, id: numbe
181
181
  if (result === false) return;
182
182
  }
183
183
 
184
- ctx.db.query(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
184
+ ctx._stmt(`DELETE FROM "${entityName}" WHERE id = ?`).run(id);
185
185
 
186
186
  // afterDelete hook
187
187
  if (hooks?.afterDelete) hooks.afterDelete(id);
@@ -196,12 +196,12 @@ export function deleteWhere(ctx: DatabaseContext, entityName: string, conditions
196
196
  // Soft delete: set deletedAt instead of removing rows
197
197
  const now = new Date().toISOString();
198
198
  const sql = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
199
- const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx.db.query(sql).run(now, ...values));
199
+ const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx._stmt(sql).run(now, ...values));
200
200
  return (result as any).changes ?? 0;
201
201
  }
202
202
 
203
203
  const sql = `DELETE FROM "${entityName}" ${clause}`;
204
- const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx.db.query(sql).run(...values));
204
+ const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx._stmt(sql).run(...values));
205
205
  return (result as any).changes ?? 0;
206
206
  }
207
207
 
@@ -247,7 +247,7 @@ export function insertMany<T extends Record<string, any>>(ctx: DatabaseContext,
247
247
  const sql = columns.length === 0
248
248
  ? `INSERT INTO "${entityName}" DEFAULT VALUES`
249
249
  : `INSERT INTO "${entityName}" (${quotedCols.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
250
- const result = ctx.db.query(sql).run(...Object.values(transformed));
250
+ const result = ctx._stmt(sql).run(...Object.values(transformed));
251
251
  ids.push(result.lastInsertRowid as number);
252
252
  }
253
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.
@@ -105,6 +118,7 @@ class _Database<Schemas extends SchemaMap> {
105
118
  computed: options.computed ?? {},
106
119
  cascade: options.cascade ?? {},
107
120
  _m: <T>(label: string, fn: () => T): T => this._m(label, fn),
121
+ _stmt: (sql: string) => this._stmt(sql),
108
122
  };
109
123
 
110
124
  this._m('Init tables', () => this.initializeTables());
@@ -146,9 +160,9 @@ class _Database<Schemas extends SchemaMap> {
146
160
  if (rel) {
147
161
  if (this._softDeletes) {
148
162
  const now = new Date().toISOString();
149
- this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
163
+ this._stmt(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
150
164
  } else {
151
- this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
165
+ this._stmt(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
152
166
  }
153
167
  }
154
168
  }
@@ -156,7 +170,7 @@ class _Database<Schemas extends SchemaMap> {
156
170
 
157
171
  if (this._softDeletes) {
158
172
  const now = new Date().toISOString();
159
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
173
+ this._stmt(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
160
174
  if (hooks?.afterDelete) hooks.afterDelete(id);
161
175
  return;
162
176
  }
@@ -168,12 +182,12 @@ class _Database<Schemas extends SchemaMap> {
168
182
  restore: ((id: number) => {
169
183
  if (!this._softDeletes) throw new Error('restore() requires softDeletes: true');
170
184
  this._m(`${entityName}.restore(${id})`, () => {
171
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
185
+ this._stmt(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
172
186
  });
173
187
  }) as any,
174
188
  select: (...cols: string[]) => createQueryBuilder(this._ctx, entityName, cols),
175
189
  count: () => this._m(`${entityName}.count`, () => {
176
- const row = this.db.query(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ''}`).get() as any;
190
+ const row = this._stmt(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ''}`).get() as any;
177
191
  return row?.count ?? 0;
178
192
  }),
179
193
  on: (event: ChangeEvent, callback: (row: any) => void | Promise<void>) => {
@@ -339,11 +353,11 @@ class _Database<Schemas extends SchemaMap> {
339
353
  */
340
354
  private _processChanges(): void {
341
355
  // Fast path: check if anything changed at all (single scalar, index-only)
342
- 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;
343
357
  const maxId: number = head?.m ?? 0;
344
358
  if (maxId <= this._changeWatermark) return;
345
359
 
346
- const changes = this.db.query(
360
+ const changes = this._stmt(
347
361
  'SELECT id, tbl, op, row_id FROM "_changes" WHERE id > ? ORDER BY id'
348
362
  ).all(this._changeWatermark) as { id: number; tbl: string; op: string; row_id: number }[];
349
363
 
@@ -374,7 +388,7 @@ class _Database<Schemas extends SchemaMap> {
374
388
  }
375
389
 
376
390
  // Clean up consumed changes
377
- this.db.query('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
391
+ this._stmt('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
378
392
  }
379
393
 
380
394
  // =========================================================================
@@ -385,9 +399,10 @@ class _Database<Schemas extends SchemaMap> {
385
399
  return this._m('transaction', () => this.db.transaction(callback)());
386
400
  }
387
401
 
388
- /** Close the database: stops polling and releases the SQLite handle. */
402
+ /** Close the database: stops polling, clears cache, and releases the SQLite handle. */
389
403
  public close(): void {
390
404
  this._stopPolling();
405
+ this._stmtCache.clear();
391
406
  this.db.close();
392
407
  }
393
408
 
@@ -403,7 +418,7 @@ class _Database<Schemas extends SchemaMap> {
403
418
  this.schemas,
404
419
  callback as any,
405
420
  (sql: string, params: any[]) => {
406
- return this.db.query(sql).all(...params) as T[];
421
+ return this._stmt(sql).all(...params) as T[];
407
422
  },
408
423
  ));
409
424
  }
@@ -414,7 +429,7 @@ class _Database<Schemas extends SchemaMap> {
414
429
 
415
430
  /** Execute a raw SQL query and return results. */
416
431
  public raw<T = any>(sql: string, ...params: any[]): T[] {
417
- return this._m(`raw: ${sql.slice(0, 60)}`, () => this.db.query(sql).all(...params) as T[]);
432
+ return this._m(`raw: ${sql.slice(0, 60)}`, () => this._stmt(sql).all(...params) as T[]);
418
433
  }
419
434
 
420
435
  /** Execute a raw SQL statement (INSERT/UPDATE/DELETE) without returning rows. */
package/src/query.ts CHANGED
@@ -40,7 +40,7 @@ export function createQueryBuilder(ctx: DatabaseContext, entityName: string, ini
40
40
 
41
41
  const executor = (sql: string, params: any[], raw: boolean): any[] => {
42
42
  return ctx._m(`SQL: ${sql.slice(0, 60)}`, () => {
43
- const rows = ctx.db.query(sql).all(...params);
43
+ const rows = ctx._stmt(sql).all(...params);
44
44
  if (raw) return rows;
45
45
  return rows.map((row: any) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
46
46
  });
@@ -103,7 +103,7 @@ export function createQueryBuilder(ctx: DatabaseContext, entityName: string, ini
103
103
  if (belongsTo) {
104
104
  const fk = belongsTo.foreignKey;
105
105
  const placeholders = parentIds.map(() => '?').join(', ');
106
- const childRows = ctx.db.query(
106
+ const childRows = ctx._stmt(
107
107
  `SELECT * FROM ${hasMany.to} WHERE ${fk} IN (${placeholders})`
108
108
  ).all(...parentIds) as any[];
109
109