sqlite-zod-orm 3.23.0 → 3.25.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
@@ -13,6 +13,389 @@ var __export = (target, all) => {
13
13
  // src/database.ts
14
14
  import { Database as SqliteDatabase } from "bun:sqlite";
15
15
 
16
+ // node_modules/measure-fn/index.ts
17
+ var toAlpha = (num) => {
18
+ let result = "";
19
+ let n = num;
20
+ do {
21
+ result = String.fromCharCode(97 + n % 26) + result;
22
+ n = Math.floor(n / 26) - 1;
23
+ } while (n >= 0);
24
+ return result;
25
+ };
26
+ var maxResultLen = 80;
27
+ var safeStringify = (value) => {
28
+ if (value === undefined)
29
+ return "";
30
+ if (value === null)
31
+ return "null";
32
+ if (typeof value === "number" || typeof value === "boolean")
33
+ return String(value);
34
+ if (typeof value === "function")
35
+ return `[Function: ${value.name || "anonymous"}]`;
36
+ if (typeof value === "symbol")
37
+ return value.toString();
38
+ if (typeof value === "string") {
39
+ const q = JSON.stringify(value);
40
+ return q.length > maxResultLen ? q.slice(0, maxResultLen - 1) + '\u2026"' : q;
41
+ }
42
+ try {
43
+ const seen = new WeakSet;
44
+ const str = JSON.stringify(value, (_key, val) => {
45
+ if (typeof val === "object" && val !== null) {
46
+ if (seen.has(val))
47
+ return "[Circular]";
48
+ seen.add(val);
49
+ }
50
+ if (typeof val === "function")
51
+ return `[Function: ${val.name || "anonymous"}]`;
52
+ if (typeof val === "bigint")
53
+ return `${val}n`;
54
+ return val;
55
+ });
56
+ return str.length > maxResultLen ? str.slice(0, maxResultLen) + "\u2026" : str;
57
+ } catch {
58
+ return String(value);
59
+ }
60
+ };
61
+ var formatDuration = (ms) => {
62
+ if (ms < 1000)
63
+ return `${ms.toFixed(2)}ms`;
64
+ if (ms < 60000)
65
+ return `${(ms / 1000).toFixed(1)}s`;
66
+ const mins = Math.floor(ms / 60000);
67
+ const secs = Math.round(ms % 60000 / 1000);
68
+ return `${mins}m ${secs}s`;
69
+ };
70
+ var timestamps = process.env.MEASURE_TIMESTAMPS === "1" || process.env.MEASURE_TIMESTAMPS === "true";
71
+ var ts = () => {
72
+ if (!timestamps)
73
+ return "";
74
+ const now = new Date;
75
+ const h = String(now.getHours()).padStart(2, "0");
76
+ const m = String(now.getMinutes()).padStart(2, "0");
77
+ const s = String(now.getSeconds()).padStart(2, "0");
78
+ const ms = String(now.getMilliseconds()).padStart(3, "0");
79
+ return `[${h}:${m}:${s}.${ms}] `;
80
+ };
81
+ var silent = process.env.MEASURE_SILENT === "1" || process.env.MEASURE_SILENT === "true";
82
+ var logger = null;
83
+ var buildActionLabel = (actionInternal) => {
84
+ return typeof actionInternal === "object" && actionInternal !== null && "label" in actionInternal ? String(actionInternal.label) : String(actionInternal);
85
+ };
86
+ var extractBudget = (actionInternal) => {
87
+ if (typeof actionInternal !== "object" || actionInternal === null)
88
+ return;
89
+ if ("budget" in actionInternal)
90
+ return Number(actionInternal.budget);
91
+ return;
92
+ };
93
+ var extractMeta = (actionInternal) => {
94
+ if (typeof actionInternal !== "object" || actionInternal === null)
95
+ return;
96
+ const details = { ...actionInternal };
97
+ if ("label" in details)
98
+ delete details.label;
99
+ if ("budget" in details)
100
+ delete details.budget;
101
+ if (Object.keys(details).length === 0)
102
+ return;
103
+ return details;
104
+ };
105
+ var formatMeta = (meta) => {
106
+ if (!meta)
107
+ return "";
108
+ const params = Object.entries(meta).map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(" ");
109
+ return ` (${params})`;
110
+ };
111
+ var emit = (event, prefix) => {
112
+ if (silent)
113
+ return;
114
+ if (logger) {
115
+ logger(event);
116
+ return;
117
+ }
118
+ defaultLogger(event, prefix);
119
+ };
120
+ var defaultLogger = (event, prefix) => {
121
+ const pfx = prefix ? `${prefix}:` : "";
122
+ const id = `[${pfx}${event.id}]`;
123
+ const t = ts();
124
+ switch (event.type) {
125
+ case "start":
126
+ console.log(`${t}${id} ... ${event.label}${formatMeta(event.meta)}`);
127
+ break;
128
+ case "success": {
129
+ const resultStr = event.result !== undefined ? safeStringify(event.result) : "";
130
+ const arrow = resultStr ? ` \u2192 ${resultStr}` : "";
131
+ const budgetWarn = event.budget && event.duration > event.budget ? ` \u26A0 OVER BUDGET (${formatDuration(event.budget)})` : "";
132
+ console.log(`${t}${id} \u2713 ${event.label} ${formatDuration(event.duration)}${arrow}${budgetWarn}`);
133
+ break;
134
+ }
135
+ case "error": {
136
+ const errorMsg = event.error instanceof Error ? event.error.message : String(event.error);
137
+ const budgetWarn = event.budget && event.duration > event.budget ? ` \u26A0 OVER BUDGET (${formatDuration(event.budget)})` : "";
138
+ console.log(`${t}${id} \u2717 ${event.label} ${formatDuration(event.duration)} (${errorMsg})${budgetWarn}`);
139
+ if (event.error instanceof Error) {
140
+ console.error(`${id}`, event.error.stack ?? event.error.message);
141
+ if (event.error.cause) {
142
+ console.error(`${id} Cause:`, event.error.cause);
143
+ }
144
+ } else {
145
+ console.error(`${id}`, event.error);
146
+ }
147
+ break;
148
+ }
149
+ case "annotation":
150
+ console.log(`${t}${id} = ${event.label}${formatMeta(event.meta)}`);
151
+ break;
152
+ }
153
+ };
154
+ var createNestedResolver = (isAsync, fullIdChain, childCounterRef, depth, resolver, prefix) => {
155
+ return (...args) => {
156
+ const label = args[0];
157
+ const fn = args[1];
158
+ if (typeof fn === "function") {
159
+ const childParentChain = [...fullIdChain, childCounterRef.value++];
160
+ return resolver(fn, label, childParentChain, depth + 1);
161
+ } else {
162
+ emit({
163
+ type: "annotation",
164
+ id: fullIdChain.join("-"),
165
+ label: buildActionLabel(label),
166
+ depth: depth + 1,
167
+ meta: extractMeta(label)
168
+ }, prefix);
169
+ return isAsync ? Promise.resolve(null) : null;
170
+ }
171
+ };
172
+ };
173
+ var globalRootCounter = 0;
174
+ var createMeasureImpl = (prefix, counterRef) => {
175
+ const counter = counterRef ?? { get value() {
176
+ return globalRootCounter;
177
+ }, set value(v) {
178
+ globalRootCounter = v;
179
+ } };
180
+ let _lastError = null;
181
+ const _measureInternal = async (fnInternal, actionInternal, parentIdChain, depth) => {
182
+ const start = performance.now();
183
+ const childCounterRef = { value: 0 };
184
+ const label = buildActionLabel(actionInternal);
185
+ const budget = extractBudget(actionInternal);
186
+ const currentId = toAlpha(parentIdChain.pop() ?? 0);
187
+ const fullIdChain = [...parentIdChain, currentId];
188
+ const idStr = fullIdChain.join("-");
189
+ emit({
190
+ type: "start",
191
+ id: idStr,
192
+ label,
193
+ depth,
194
+ meta: extractMeta(actionInternal)
195
+ }, prefix);
196
+ const measureForNextLevel = createNestedResolver(true, fullIdChain, childCounterRef, depth, _measureInternal, prefix);
197
+ try {
198
+ const result = await fnInternal(measureForNextLevel);
199
+ const duration = performance.now() - start;
200
+ emit({ type: "success", id: idStr, label, depth, duration, result, budget }, prefix);
201
+ return result;
202
+ } catch (error) {
203
+ const duration = performance.now() - start;
204
+ emit({ type: "error", id: idStr, label, depth, duration, error, budget }, prefix);
205
+ _lastError = error;
206
+ return null;
207
+ }
208
+ };
209
+ const _measureInternalSync = (fnInternal, actionInternal, parentIdChain, depth) => {
210
+ const start = performance.now();
211
+ const childCounterRef = { value: 0 };
212
+ const label = buildActionLabel(actionInternal);
213
+ const hasNested = fnInternal.length > 0;
214
+ const budget = extractBudget(actionInternal);
215
+ const currentId = toAlpha(parentIdChain.pop() ?? 0);
216
+ const fullIdChain = [...parentIdChain, currentId];
217
+ const idStr = fullIdChain.join("-");
218
+ if (hasNested) {
219
+ emit({
220
+ type: "start",
221
+ id: idStr,
222
+ label,
223
+ depth,
224
+ meta: extractMeta(actionInternal)
225
+ }, prefix);
226
+ }
227
+ const measureForNextLevel = createNestedResolver(false, fullIdChain, childCounterRef, depth, _measureInternalSync, prefix);
228
+ try {
229
+ const result = fnInternal(measureForNextLevel);
230
+ const duration = performance.now() - start;
231
+ emit({ type: "success", id: idStr, label, depth, duration, result, budget }, prefix);
232
+ return result;
233
+ } catch (error) {
234
+ const duration = performance.now() - start;
235
+ emit({ type: "error", id: idStr, label, depth, duration, error, budget }, prefix);
236
+ _lastError = error;
237
+ return null;
238
+ }
239
+ };
240
+ const measureFn = async (arg1, arg2) => {
241
+ if (typeof arg2 === "function") {
242
+ return _measureInternal(arg2, arg1, [counter.value++], 0);
243
+ } else {
244
+ const currentId = toAlpha(counter.value++);
245
+ emit({
246
+ type: "annotation",
247
+ id: currentId,
248
+ label: buildActionLabel(arg1),
249
+ depth: 0,
250
+ meta: extractMeta(arg1)
251
+ }, prefix);
252
+ return Promise.resolve(null);
253
+ }
254
+ };
255
+ measureFn.timed = async (arg1, arg2) => {
256
+ const start = performance.now();
257
+ const result = await measureFn(arg1, arg2);
258
+ const duration = performance.now() - start;
259
+ return { result, duration };
260
+ };
261
+ measureFn.retry = async (label, opts, fn) => {
262
+ const attempts = opts.attempts ?? 3;
263
+ const delay = opts.delay ?? 1000;
264
+ const backoff = opts.backoff ?? 1;
265
+ const lbl = buildActionLabel(label);
266
+ const budget = extractBudget(label);
267
+ for (let i = 0;i < attempts; i++) {
268
+ const attempt = i + 1;
269
+ const attemptLabel = `${lbl} [${attempt}/${attempts}]`;
270
+ const start = performance.now();
271
+ const currentId = toAlpha(counter.value++);
272
+ emit({
273
+ type: "start",
274
+ id: currentId,
275
+ label: attemptLabel,
276
+ depth: 0,
277
+ meta: extractMeta(label)
278
+ }, prefix);
279
+ try {
280
+ const result = await fn();
281
+ const duration = performance.now() - start;
282
+ emit({ type: "success", id: currentId, label: attemptLabel, depth: 0, duration, result, budget }, prefix);
283
+ return result;
284
+ } catch (error) {
285
+ const duration = performance.now() - start;
286
+ emit({ type: "error", id: currentId, label: attemptLabel, depth: 0, duration, error, budget }, prefix);
287
+ if (attempt < attempts) {
288
+ await new Promise((r) => setTimeout(r, delay * Math.pow(backoff, i)));
289
+ }
290
+ }
291
+ }
292
+ return null;
293
+ };
294
+ measureFn.assert = async (arg1, arg2) => {
295
+ const result = await measureFn(arg1, arg2);
296
+ if (result === null) {
297
+ const cause = _lastError;
298
+ _lastError = null;
299
+ throw new Error(`measure.assert: "${buildActionLabel(arg1)}" failed`, { cause });
300
+ }
301
+ return result;
302
+ };
303
+ measureFn.wrap = (label, fn) => {
304
+ return (...args) => measureFn(label, () => fn(...args));
305
+ };
306
+ measureFn.batch = async (label, items, fn, opts) => {
307
+ const lbl = buildActionLabel(label);
308
+ const total = items.length;
309
+ const every = opts?.every ?? Math.max(1, Math.ceil(total / 5));
310
+ const currentId = toAlpha(counter.value++);
311
+ const startTime = performance.now();
312
+ emit({
313
+ type: "start",
314
+ id: currentId,
315
+ label: `${lbl} (${total} items)`,
316
+ depth: 0,
317
+ meta: extractMeta(label)
318
+ }, prefix);
319
+ const results = [];
320
+ for (let i = 0;i < items.length; i++) {
321
+ try {
322
+ results.push(await fn(items[i], i));
323
+ } catch {
324
+ results.push(null);
325
+ }
326
+ if ((i + 1) % every === 0 && i + 1 < total) {
327
+ const elapsed = (performance.now() - startTime) / 1000;
328
+ const rate = ((i + 1) / elapsed).toFixed(0);
329
+ emit({
330
+ type: "annotation",
331
+ id: currentId,
332
+ label: `${i + 1}/${total} (${elapsed.toFixed(1)}s, ${rate}/s)`,
333
+ depth: 0
334
+ }, prefix);
335
+ }
336
+ }
337
+ const duration = performance.now() - startTime;
338
+ const budget = extractBudget(label);
339
+ emit({
340
+ type: "success",
341
+ id: currentId,
342
+ label: `${lbl} (${total} items)`,
343
+ depth: 0,
344
+ duration,
345
+ result: `${results.filter((r) => r !== null).length}/${total} ok`,
346
+ budget
347
+ }, prefix);
348
+ return results;
349
+ };
350
+ const measureSyncFn = (arg1, arg2) => {
351
+ if (typeof arg2 === "function") {
352
+ return _measureInternalSync(arg2, arg1, [counter.value++], 0);
353
+ } else {
354
+ const currentId = toAlpha(counter.value++);
355
+ emit({
356
+ type: "annotation",
357
+ id: currentId,
358
+ label: buildActionLabel(arg1),
359
+ depth: 0,
360
+ meta: extractMeta(arg1)
361
+ }, prefix);
362
+ return null;
363
+ }
364
+ };
365
+ measureSyncFn.timed = (arg1, arg2) => {
366
+ const start = performance.now();
367
+ const result = measureSyncFn(arg1, arg2);
368
+ const duration = performance.now() - start;
369
+ return { result, duration };
370
+ };
371
+ measureSyncFn.assert = (arg1, arg2) => {
372
+ const result = measureSyncFn(arg1, arg2);
373
+ if (result === null) {
374
+ const cause = _lastError;
375
+ _lastError = null;
376
+ throw new Error(`measureSync.assert: "${buildActionLabel(arg1)}" failed`, { cause });
377
+ }
378
+ return result;
379
+ };
380
+ measureSyncFn.wrap = (label, fn) => {
381
+ return (...args) => measureSyncFn(label, () => fn(...args));
382
+ };
383
+ return { measure: measureFn, measureSync: measureSyncFn };
384
+ };
385
+ var globalInstance = createMeasureImpl();
386
+ var measure = globalInstance.measure;
387
+ var measureSync = globalInstance.measureSync;
388
+ var createMeasure = (scopePrefix) => {
389
+ const scopeCounter = { value: 0 };
390
+ const scoped = createMeasureImpl(scopePrefix, scopeCounter);
391
+ return {
392
+ ...scoped,
393
+ resetCounter: () => {
394
+ scopeCounter.value = 0;
395
+ }
396
+ };
397
+ };
398
+
16
399
  // node_modules/zod/v3/external.js
17
400
  var exports_external = {};
18
401
  __export(exports_external, {
@@ -4837,12 +5220,12 @@ function executeProxyQuery(schemas, callback, executor) {
4837
5220
  function createQueryBuilder(ctx, entityName, initialCols) {
4838
5221
  const schema = ctx.schemas[entityName];
4839
5222
  const executor = (sql, params, raw) => {
4840
- if (ctx.debug)
4841
- console.log("[satidb]", sql, params);
4842
- const rows = ctx.db.query(sql).all(...params);
4843
- if (raw)
4844
- return rows;
4845
- return rows.map((row) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
5223
+ return ctx._m(`SQL: ${sql.slice(0, 60)}`, () => {
5224
+ const rows = ctx.db.query(sql).all(...params);
5225
+ if (raw)
5226
+ return rows;
5227
+ return rows.map((row) => ctx.attachMethods(entityName, transformFromStorage(row, schema)));
5228
+ });
4846
5229
  };
4847
5230
  const singleExecutor = (sql, params, raw) => {
4848
5231
  const results = executor(sql, params, raw);
@@ -5020,9 +5403,9 @@ function insert(ctx, entityName, data) {
5020
5403
  let inputData = { ...data };
5021
5404
  const hooks = ctx.hooks[entityName];
5022
5405
  if (hooks?.beforeInsert) {
5023
- const result2 = hooks.beforeInsert(inputData);
5024
- if (result2)
5025
- inputData = result2;
5406
+ const result = hooks.beforeInsert(inputData);
5407
+ if (result)
5408
+ inputData = result;
5026
5409
  }
5027
5410
  const validatedData = asZodObject(schema).passthrough().parse(inputData);
5028
5411
  const transformed = transformForStorage(validatedData);
@@ -5034,10 +5417,12 @@ function insert(ctx, entityName, data) {
5034
5417
  const columns = Object.keys(transformed);
5035
5418
  const quotedCols = columns.map((c) => `"${c}"`);
5036
5419
  const sql = columns.length === 0 ? `INSERT INTO "${entityName}" DEFAULT VALUES` : `INSERT INTO "${entityName}" (${quotedCols.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`;
5037
- if (ctx.debug)
5038
- console.log("[satidb]", sql, Object.values(transformed));
5039
- const result = ctx.db.query(sql).run(...Object.values(transformed));
5040
- const newEntity = getById(ctx, entityName, result.lastInsertRowid);
5420
+ let lastId = 0;
5421
+ ctx._m(`SQL: ${sql.slice(0, 40)}`, () => {
5422
+ const result = ctx.db.query(sql).run(...Object.values(transformed));
5423
+ lastId = result.lastInsertRowid;
5424
+ });
5425
+ const newEntity = getById(ctx, entityName, lastId);
5041
5426
  if (!newEntity)
5042
5427
  throw new Error("Failed to retrieve entity after insertion");
5043
5428
  if (hooks?.afterInsert)
@@ -5062,9 +5447,9 @@ function update(ctx, entityName, id, data) {
5062
5447
  }
5063
5448
  const setClause = Object.keys(transformed).map((key) => `"${key}" = ?`).join(", ");
5064
5449
  const sql = `UPDATE "${entityName}" SET ${setClause} WHERE id = ?`;
5065
- if (ctx.debug)
5066
- console.log("[satidb]", sql, [...Object.values(transformed), id]);
5067
- ctx.db.query(sql).run(...Object.values(transformed), id);
5450
+ ctx._m(`SQL: UPDATE ${entityName} SET ...`, () => {
5451
+ ctx.db.query(sql).run(...Object.values(transformed), id);
5452
+ });
5068
5453
  const updated = getById(ctx, entityName, id);
5069
5454
  if (hooks?.afterUpdate && updated)
5070
5455
  hooks.afterUpdate(updated);
@@ -5133,15 +5518,11 @@ function deleteWhere(ctx, entityName, conditions) {
5133
5518
  if (ctx.softDeletes) {
5134
5519
  const now = new Date().toISOString();
5135
5520
  const sql2 = `UPDATE "${entityName}" SET "deletedAt" = ? ${clause}`;
5136
- if (ctx.debug)
5137
- console.log("[satidb]", sql2, [now, ...values]);
5138
- const result2 = ctx.db.query(sql2).run(now, ...values);
5521
+ const result2 = ctx._m(`SQL: ${sql2.slice(0, 50)}`, () => ctx.db.query(sql2).run(now, ...values));
5139
5522
  return result2.changes ?? 0;
5140
5523
  }
5141
5524
  const sql = `DELETE FROM "${entityName}" ${clause}`;
5142
- if (ctx.debug)
5143
- console.log("[satidb]", sql, values);
5144
- const result = ctx.db.query(sql).run(...values);
5525
+ const result = ctx._m(`SQL: ${sql.slice(0, 50)}`, () => ctx.db.query(sql).run(...values));
5145
5526
  return result.changes ?? 0;
5146
5527
  }
5147
5528
  function createDeleteBuilder(ctx, entityName) {
@@ -5265,7 +5646,15 @@ class _Database {
5265
5646
  _changeWatermark = 0;
5266
5647
  _pollTimer = null;
5267
5648
  _pollInterval;
5649
+ _measure;
5650
+ _m(label, fn) {
5651
+ if (this._debug)
5652
+ return this._measure.measureSync.assert(label, fn);
5653
+ return fn();
5654
+ }
5268
5655
  constructor(dbFile, schemas, options = {}) {
5656
+ this._debug = options.debug === true;
5657
+ this._measure = createMeasure("satidb");
5269
5658
  this.db = new SqliteDatabase(dbFile);
5270
5659
  if (options.wal !== false)
5271
5660
  this.db.run("PRAGMA journal_mode = WAL");
@@ -5275,7 +5664,6 @@ class _Database {
5275
5664
  this._reactive = options.reactive !== false;
5276
5665
  this._timestamps = options.timestamps === true;
5277
5666
  this._softDeletes = options.softDeletes === true;
5278
- this._debug = options.debug === true;
5279
5667
  this._pollInterval = options.pollInterval ?? 100;
5280
5668
  this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
5281
5669
  this._ctx = {
@@ -5289,74 +5677,77 @@ class _Database {
5289
5677
  softDeletes: this._softDeletes,
5290
5678
  hooks: options.hooks ?? {},
5291
5679
  computed: options.computed ?? {},
5292
- cascade: options.cascade ?? {}
5680
+ cascade: options.cascade ?? {},
5681
+ _m: (label, fn) => this._m(label, fn)
5293
5682
  };
5294
- this.initializeTables();
5683
+ this._m("Init tables", () => this.initializeTables());
5295
5684
  if (this._reactive)
5296
- this.initializeChangeTracking();
5297
- this.runMigrations();
5685
+ this._m("Change tracking", () => this.initializeChangeTracking());
5686
+ this._m("Run migrations", () => this.runMigrations());
5298
5687
  if (options.indexes)
5299
- this.createIndexes(options.indexes);
5688
+ this._m("Create indexes", () => this.createIndexes(options.indexes));
5300
5689
  if (options.unique)
5301
- this.createUniqueConstraints(options.unique);
5690
+ this._m("Unique constraints", () => this.createUniqueConstraints(options.unique));
5302
5691
  for (const entityName of Object.keys(schemas)) {
5303
5692
  const key = entityName;
5304
5693
  const accessor = {
5305
- insert: (data) => insert(this._ctx, entityName, data),
5306
- insertMany: (rows) => insertMany(this._ctx, entityName, rows),
5694
+ insert: (data) => this._m(`${entityName}.insert`, () => insert(this._ctx, entityName, data)),
5695
+ insertMany: (rows) => this._m(`${entityName}.insertMany(${rows.length})`, () => insertMany(this._ctx, entityName, rows)),
5307
5696
  update: (idOrData, data) => {
5308
5697
  if (typeof idOrData === "number")
5309
- return update(this._ctx, entityName, idOrData, data);
5698
+ return this._m(`${entityName}.update(${idOrData})`, () => update(this._ctx, entityName, idOrData, data));
5310
5699
  return createUpdateBuilder(this._ctx, entityName, idOrData);
5311
5700
  },
5312
- upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
5313
- upsertMany: (rows, conditions) => upsertMany(this._ctx, entityName, rows, conditions),
5314
- findOrCreate: (conditions, defaults) => findOrCreate(this._ctx, entityName, conditions, defaults),
5701
+ upsert: (conditions, data) => this._m(`${entityName}.upsert`, () => upsert(this._ctx, entityName, data, conditions)),
5702
+ upsertMany: (rows, conditions) => this._m(`${entityName}.upsertMany(${rows.length})`, () => upsertMany(this._ctx, entityName, rows, conditions)),
5703
+ findOrCreate: (conditions, defaults) => this._m(`${entityName}.findOrCreate`, () => findOrCreate(this._ctx, entityName, conditions, defaults)),
5315
5704
  delete: (id) => {
5316
5705
  if (typeof id === "number") {
5317
- const hooks = this._ctx.hooks[entityName];
5318
- if (hooks?.beforeDelete) {
5319
- const result = hooks.beforeDelete(id);
5320
- if (result === false)
5321
- return;
5322
- }
5323
- const cascadeTargets = this._ctx.cascade[entityName];
5324
- if (cascadeTargets) {
5325
- for (const childTable of cascadeTargets) {
5326
- const rel = this._ctx.relationships.find((r) => r.type === "belongs-to" && r.from === childTable && r.to === entityName);
5327
- if (rel) {
5328
- if (this._softDeletes) {
5329
- const now = new Date().toISOString();
5330
- this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
5331
- } else {
5332
- this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
5706
+ return this._m(`${entityName}.delete(${id})`, () => {
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);
5723
+ }
5333
5724
  }
5334
5725
  }
5335
5726
  }
5336
- }
5337
- if (this._softDeletes) {
5338
- const now = new Date().toISOString();
5339
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
5340
- if (hooks?.afterDelete)
5341
- hooks.afterDelete(id);
5342
- return;
5343
- }
5344
- return deleteEntity(this._ctx, entityName, id);
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);
5735
+ });
5345
5736
  }
5346
5737
  return createDeleteBuilder(this._ctx, entityName);
5347
5738
  },
5348
5739
  restore: (id) => {
5349
5740
  if (!this._softDeletes)
5350
5741
  throw new Error("restore() requires softDeletes: true");
5351
- if (this._debug)
5352
- console.log("[satidb]", `UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`, [id]);
5353
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
5742
+ this._m(`${entityName}.restore(${id})`, () => {
5743
+ this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
5744
+ });
5354
5745
  },
5355
5746
  select: (...cols) => createQueryBuilder(this._ctx, entityName, cols),
5356
- count: () => {
5747
+ count: () => this._m(`${entityName}.count`, () => {
5357
5748
  const row = this.db.query(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ""}`).get();
5358
5749
  return row?.count ?? 0;
5359
- },
5750
+ }),
5360
5751
  on: (event, callback) => {
5361
5752
  return this._registerListener(entityName, event, callback);
5362
5753
  },
@@ -5501,28 +5892,22 @@ class _Database {
5501
5892
  this.db.query('DELETE FROM "_changes" WHERE id <= ?').run(this._changeWatermark);
5502
5893
  }
5503
5894
  transaction(callback) {
5504
- return this.db.transaction(callback)();
5895
+ return this._m("transaction", () => this.db.transaction(callback)());
5505
5896
  }
5506
5897
  close() {
5507
5898
  this._stopPolling();
5508
5899
  this.db.close();
5509
5900
  }
5510
5901
  query(callback) {
5511
- return executeProxyQuery(this.schemas, callback, (sql, params) => {
5512
- if (this._debug)
5513
- console.log("[satidb]", sql, params);
5902
+ return this._m("query(proxy)", () => executeProxyQuery(this.schemas, callback, (sql, params) => {
5514
5903
  return this.db.query(sql).all(...params);
5515
- });
5904
+ }));
5516
5905
  }
5517
5906
  raw(sql, ...params) {
5518
- if (this._debug)
5519
- console.log("[satidb]", sql, params);
5520
- return this.db.query(sql).all(...params);
5907
+ return this._m(`raw: ${sql.slice(0, 60)}`, () => this.db.query(sql).all(...params));
5521
5908
  }
5522
5909
  exec(sql, ...params) {
5523
- if (this._debug)
5524
- console.log("[satidb]", sql, params);
5525
- this.db.run(sql, ...params);
5910
+ this._m(`exec: ${sql.slice(0, 60)}`, () => this.db.run(sql, ...params));
5526
5911
  }
5527
5912
  tables() {
5528
5913
  return Object.keys(this.schemas);
@@ -5531,70 +5916,76 @@ class _Database {
5531
5916
  return this.db.query(`PRAGMA table_info("${tableName}")`).all();
5532
5917
  }
5533
5918
  dump() {
5534
- const result = {};
5535
- for (const tableName of Object.keys(this.schemas)) {
5536
- result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
5537
- }
5538
- return result;
5919
+ return this._m("dump", () => {
5920
+ const result = {};
5921
+ for (const tableName of Object.keys(this.schemas)) {
5922
+ result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
5923
+ }
5924
+ return result;
5925
+ });
5539
5926
  }
5540
5927
  load(data, options) {
5541
- const txn = this.db.transaction(() => {
5542
- for (const [tableName, rows] of Object.entries(data)) {
5543
- if (!this.schemas[tableName])
5544
- continue;
5545
- if (!options?.append) {
5546
- this.db.run(`DELETE FROM "${tableName}"`);
5547
- }
5548
- for (const row of rows) {
5549
- const cols = Object.keys(row).filter((k) => k !== "id");
5550
- const placeholders = cols.map(() => "?").join(", ");
5551
- const values = cols.map((c) => {
5552
- const v = row[c];
5553
- if (v !== null && v !== undefined && typeof v === "object" && !(v instanceof Buffer)) {
5554
- return JSON.stringify(v);
5555
- }
5556
- return v;
5557
- });
5558
- this.db.query(`INSERT INTO "${tableName}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`).run(...values);
5928
+ this._m(`load(${Object.keys(data).join(",")})`, () => {
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);
5947
+ }
5559
5948
  }
5560
- }
5949
+ });
5950
+ txn();
5561
5951
  });
5562
- txn();
5563
5952
  }
5564
5953
  seed(fixtures) {
5565
5954
  this.load(fixtures, { append: true });
5566
5955
  }
5567
5956
  diff() {
5568
- const result = {};
5569
- const systemCols = new Set(["id", "createdAt", "updatedAt", "deletedAt"]);
5570
- for (const [tableName, schema] of Object.entries(this.schemas)) {
5571
- const schemaFields = getStorableFields(schema);
5572
- const schemaColMap = new Map(schemaFields.map((f) => [f.name, zodTypeToSqlType(f.type)]));
5573
- const liveColumns = this.columns(tableName);
5574
- const liveColMap = new Map(liveColumns.map((c) => [c.name, c.type]));
5575
- const added = [];
5576
- const removed = [];
5577
- const typeChanged = [];
5578
- for (const [col, expectedType] of schemaColMap) {
5579
- if (!liveColMap.has(col)) {
5580
- added.push(col);
5581
- } else {
5582
- const actualType = liveColMap.get(col);
5583
- if (actualType !== expectedType) {
5584
- typeChanged.push({ column: col, expected: expectedType, actual: actualType });
5957
+ return this._m("diff", () => {
5958
+ const result = {};
5959
+ const systemCols = new Set(["id", "createdAt", "updatedAt", "deletedAt"]);
5960
+ for (const [tableName, schema] of Object.entries(this.schemas)) {
5961
+ const schemaFields = getStorableFields(schema);
5962
+ const schemaColMap = new Map(schemaFields.map((f) => [f.name, zodTypeToSqlType(f.type)]));
5963
+ const liveColumns = this.columns(tableName);
5964
+ const liveColMap = new Map(liveColumns.map((c) => [c.name, c.type]));
5965
+ const added = [];
5966
+ const removed = [];
5967
+ const typeChanged = [];
5968
+ for (const [col, expectedType] of schemaColMap) {
5969
+ if (!liveColMap.has(col)) {
5970
+ added.push(col);
5971
+ } else {
5972
+ const actualType = liveColMap.get(col);
5973
+ if (actualType !== expectedType) {
5974
+ typeChanged.push({ column: col, expected: expectedType, actual: actualType });
5975
+ }
5585
5976
  }
5586
5977
  }
5587
- }
5588
- for (const col of liveColMap.keys()) {
5589
- if (!systemCols.has(col) && !schemaColMap.has(col)) {
5590
- removed.push(col);
5978
+ for (const col of liveColMap.keys()) {
5979
+ if (!systemCols.has(col) && !schemaColMap.has(col)) {
5980
+ removed.push(col);
5981
+ }
5982
+ }
5983
+ if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
5984
+ result[tableName] = { added, removed, typeChanged };
5591
5985
  }
5592
5986
  }
5593
- if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
5594
- result[tableName] = { added, removed, typeChanged };
5595
- }
5596
- }
5597
- return result;
5987
+ return result;
5988
+ });
5598
5989
  }
5599
5990
  }
5600
5991
  var Database = _Database;
package/package.json CHANGED
@@ -1,57 +1,58 @@
1
- {
2
- "name": "sqlite-zod-orm",
3
- "version": "3.23.0",
4
- "description": "Type-safe SQLite ORM for Bun — Zod schemas, fluent queries, auto relationships, zero SQL",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./src/index.ts",
9
- "exports": {
10
- ".": {
11
- "import": "./dist/index.js",
12
- "types": "./src/index.ts"
13
- }
14
- },
15
- "scripts": {
16
- "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm",
17
- "clean": "rm -rf dist",
18
- "test": "bun test",
19
- "bench": "bun bench/triggers-vs-naive.ts && bun bench/poll-strategy.ts && bun bench/indexes.ts",
20
- "prepublishOnly": "bun run build"
21
- },
22
- "files": [
23
- "src",
24
- "dist",
25
- "README.md"
26
- ],
27
- "keywords": [
28
- "sqlite",
29
- "database",
30
- "bun",
31
- "typescript",
32
- "type-safe",
33
- "orm",
34
- "zod",
35
- "sql",
36
- "query-builder",
37
- "relationships"
38
- ],
39
- "author": "7flash",
40
- "license": "MIT",
41
- "repository": {
42
- "type": "git",
43
- "url": "git@github.com:7flash/sqlite-zod-orm.git"
44
- },
45
- "devDependencies": {
46
- "bun-types": "latest"
47
- },
48
- "peerDependencies": {
49
- "typescript": "^5.0.0"
50
- },
51
- "dependencies": {
52
- "zod": "^3.25.67"
53
- },
54
- "engines": {
55
- "bun": ">=1.0.0"
56
- }
1
+ {
2
+ "name": "sqlite-zod-orm",
3
+ "version": "3.25.0",
4
+ "description": "Type-safe SQLite ORM for Bun — Zod schemas, fluent queries, auto relationships, zero SQL",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./src/index.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm",
17
+ "clean": "rm -rf dist",
18
+ "test": "bun test",
19
+ "bench": "bun bench/triggers-vs-naive.ts && bun bench/poll-strategy.ts && bun bench/indexes.ts",
20
+ "prepublishOnly": "bun run build"
21
+ },
22
+ "files": [
23
+ "src",
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "keywords": [
28
+ "sqlite",
29
+ "database",
30
+ "bun",
31
+ "typescript",
32
+ "type-safe",
33
+ "orm",
34
+ "zod",
35
+ "sql",
36
+ "query-builder",
37
+ "relationships"
38
+ ],
39
+ "author": "7flash",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git@github.com:7flash/sqlite-zod-orm.git"
44
+ },
45
+ "devDependencies": {
46
+ "bun-types": "latest"
47
+ },
48
+ "peerDependencies": {
49
+ "typescript": "^5.0.0"
50
+ },
51
+ "dependencies": {
52
+ "measure-fn": "^3.3.0",
53
+ "zod": "^3.25.67"
54
+ },
55
+ "engines": {
56
+ "bun": ">=1.0.0"
57
+ }
57
58
  }
package/src/context.ts CHANGED
@@ -40,4 +40,10 @@ 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;
43
49
  }
package/src/crud.ts CHANGED
@@ -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.db.query(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.db.query(sql).run(...Object.values(transformed), id);
107
+ });
104
108
 
105
109
  const updated = getById(ctx, entityName, id);
106
110
 
@@ -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.db.query(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.db.query(sql).run(...values));
203
205
  return (result as any).changes ?? 0;
204
206
  }
205
207
 
package/src/database.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * focused modules.
7
7
  */
8
8
  import { Database as SqliteDatabase } from 'bun:sqlite';
9
+ import { createMeasure } from 'measure-fn';
9
10
  import { z } from 'zod';
10
11
  import { QueryBuilder, executeProxyQuery, createQueryBuilder, type ProxyQueryResult } from './query';
11
12
  import type {
@@ -63,7 +64,22 @@ class _Database<Schemas extends SchemaMap> {
63
64
  /** Poll interval in ms. */
64
65
  private _pollInterval: number;
65
66
 
67
+ /** Scoped measure-fn instance for instrumentation. */
68
+ private _measure: ReturnType<typeof createMeasure>;
69
+
70
+ /**
71
+ * Conditional measurement helper — wraps with measure-fn only when debug is on.
72
+ * When debug is off, executes fn directly with zero overhead.
73
+ */
74
+ private _m<T>(label: string, fn: () => T): T {
75
+ if (this._debug) return this._measure.measureSync.assert(label, fn);
76
+ return fn();
77
+ }
78
+
66
79
  constructor(dbFile: string, schemas: Schemas, options: DatabaseOptions = {}) {
80
+ this._debug = options.debug === true;
81
+ this._measure = createMeasure('satidb');
82
+
67
83
  this.db = new SqliteDatabase(dbFile);
68
84
  if (options.wal !== false) this.db.run('PRAGMA journal_mode = WAL');
69
85
  this.db.run('PRAGMA foreign_keys = ON');
@@ -72,7 +88,6 @@ class _Database<Schemas extends SchemaMap> {
72
88
  this._reactive = options.reactive !== false; // default true
73
89
  this._timestamps = options.timestamps === true;
74
90
  this._softDeletes = options.softDeletes === true;
75
- this._debug = options.debug === true;
76
91
  this._pollInterval = options.pollInterval ?? 100;
77
92
  this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
78
93
 
@@ -89,75 +104,78 @@ class _Database<Schemas extends SchemaMap> {
89
104
  hooks: options.hooks ?? {},
90
105
  computed: options.computed ?? {},
91
106
  cascade: options.cascade ?? {},
107
+ _m: <T>(label: string, fn: () => T): T => this._m(label, fn),
92
108
  };
93
109
 
94
- this.initializeTables();
95
- if (this._reactive) this.initializeChangeTracking();
96
- this.runMigrations();
97
- if (options.indexes) this.createIndexes(options.indexes);
98
- if (options.unique) this.createUniqueConstraints(options.unique);
110
+ this._m('Init tables', () => this.initializeTables());
111
+ if (this._reactive) this._m('Change tracking', () => this.initializeChangeTracking());
112
+ this._m('Run migrations', () => this.runMigrations());
113
+ if (options.indexes) this._m('Create indexes', () => this.createIndexes(options.indexes!));
114
+ if (options.unique) this._m('Unique constraints', () => this.createUniqueConstraints(options.unique!));
99
115
 
100
116
  // Create typed entity accessors (db.users, db.posts, etc.)
101
117
  for (const entityName of Object.keys(schemas)) {
102
118
  const key = entityName as keyof Schemas;
103
119
  const accessor: EntityAccessor<Schemas[typeof key]> = {
104
- insert: (data) => insert(this._ctx, entityName, data),
105
- insertMany: (rows: any[]) => insertMany(this._ctx, entityName, rows),
120
+ insert: (data) => this._m(`${entityName}.insert`, () => insert(this._ctx, entityName, data)),
121
+ insertMany: (rows: any[]) => this._m(`${entityName}.insertMany(${rows.length})`, () => insertMany(this._ctx, entityName, rows)),
106
122
  update: (idOrData: any, data?: any) => {
107
- if (typeof idOrData === 'number') return update(this._ctx, entityName, idOrData, data);
123
+ if (typeof idOrData === 'number') return this._m(`${entityName}.update(${idOrData})`, () => update(this._ctx, entityName, idOrData, data));
108
124
  return createUpdateBuilder(this._ctx, entityName, idOrData);
109
125
  },
110
- upsert: (conditions, data) => upsert(this._ctx, entityName, data, conditions),
111
- upsertMany: (rows: any[], conditions?: any) => upsertMany(this._ctx, entityName, rows, conditions),
112
- findOrCreate: (conditions: any, defaults?: any) => findOrCreate(this._ctx, entityName, conditions, defaults),
126
+ upsert: (conditions, data) => this._m(`${entityName}.upsert`, () => upsert(this._ctx, entityName, data, conditions)),
127
+ upsertMany: (rows: any[], conditions?: any) => this._m(`${entityName}.upsertMany(${rows.length})`, () => upsertMany(this._ctx, entityName, rows, conditions)),
128
+ findOrCreate: (conditions: any, defaults?: any) => this._m(`${entityName}.findOrCreate`, () => findOrCreate(this._ctx, entityName, conditions, defaults)),
113
129
  delete: ((id?: any) => {
114
130
  if (typeof id === 'number') {
115
- // beforeDelete hook return false to cancel
116
- const hooks = this._ctx.hooks[entityName];
117
- if (hooks?.beforeDelete) {
118
- const result = hooks.beforeDelete(id);
119
- if (result === false) return;
120
- }
131
+ return this._m(`${entityName}.delete(${id})`, () => {
132
+ // beforeDelete hook — return false to cancel
133
+ const hooks = this._ctx.hooks[entityName];
134
+ if (hooks?.beforeDelete) {
135
+ const result = hooks.beforeDelete(id);
136
+ if (result === false) return;
137
+ }
121
138
 
122
- // Cascade delete children first
123
- const cascadeTargets = this._ctx.cascade[entityName];
124
- if (cascadeTargets) {
125
- for (const childTable of cascadeTargets) {
126
- // Find FK from child → this parent via relationships
127
- const rel = this._ctx.relationships.find(
128
- r => r.type === 'belongs-to' && r.from === childTable && r.to === entityName
129
- );
130
- if (rel) {
131
- if (this._softDeletes) {
132
- const now = new Date().toISOString();
133
- this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
134
- } else {
135
- this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
139
+ // Cascade delete children first
140
+ const cascadeTargets = this._ctx.cascade[entityName];
141
+ if (cascadeTargets) {
142
+ for (const childTable of cascadeTargets) {
143
+ const rel = this._ctx.relationships.find(
144
+ r => r.type === 'belongs-to' && r.from === childTable && r.to === entityName
145
+ );
146
+ if (rel) {
147
+ if (this._softDeletes) {
148
+ const now = new Date().toISOString();
149
+ this.db.query(`UPDATE "${childTable}" SET "deletedAt" = ? WHERE "${rel.foreignKey}" = ?`).run(now, id);
150
+ } else {
151
+ this.db.query(`DELETE FROM "${childTable}" WHERE "${rel.foreignKey}" = ?`).run(id);
152
+ }
136
153
  }
137
154
  }
138
155
  }
139
- }
140
156
 
141
- if (this._softDeletes) {
142
- const now = new Date().toISOString();
143
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
144
- if (hooks?.afterDelete) hooks.afterDelete(id);
145
- return;
146
- }
147
- return deleteEntity(this._ctx, entityName, id);
157
+ if (this._softDeletes) {
158
+ const now = new Date().toISOString();
159
+ this.db.query(`UPDATE "${entityName}" SET "deletedAt" = ? WHERE id = ?`).run(now, id);
160
+ if (hooks?.afterDelete) hooks.afterDelete(id);
161
+ return;
162
+ }
163
+ return deleteEntity(this._ctx, entityName, id);
164
+ });
148
165
  }
149
166
  return createDeleteBuilder(this._ctx, entityName);
150
167
  }) as any,
151
168
  restore: ((id: number) => {
152
169
  if (!this._softDeletes) throw new Error('restore() requires softDeletes: true');
153
- if (this._debug) console.log('[satidb]', `UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`, [id]);
154
- this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
170
+ this._m(`${entityName}.restore(${id})`, () => {
171
+ this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
172
+ });
155
173
  }) as any,
156
174
  select: (...cols: string[]) => createQueryBuilder(this._ctx, entityName, cols),
157
- count: () => {
175
+ count: () => this._m(`${entityName}.count`, () => {
158
176
  const row = this.db.query(`SELECT COUNT(*) as count FROM "${entityName}"${this._softDeletes ? ' WHERE "deletedAt" IS NULL' : ''}`).get() as any;
159
177
  return row?.count ?? 0;
160
- },
178
+ }),
161
179
  on: (event: ChangeEvent, callback: (row: any) => void | Promise<void>) => {
162
180
  return this._registerListener(entityName, event, callback);
163
181
  },
@@ -364,7 +382,7 @@ class _Database<Schemas extends SchemaMap> {
364
382
  // =========================================================================
365
383
 
366
384
  public transaction<T>(callback: () => T): T {
367
- return this.db.transaction(callback)();
385
+ return this._m('transaction', () => this.db.transaction(callback)());
368
386
  }
369
387
 
370
388
  /** Close the database: stops polling and releases the SQLite handle. */
@@ -381,14 +399,13 @@ class _Database<Schemas extends SchemaMap> {
381
399
  public query<T extends Record<string, any> = Record<string, any>>(
382
400
  callback: (ctx: { [K in keyof Schemas]: ProxyColumns<InferSchema<Schemas[K]>> }) => ProxyQueryResult
383
401
  ): T[] {
384
- return executeProxyQuery(
402
+ return this._m('query(proxy)', () => executeProxyQuery(
385
403
  this.schemas,
386
404
  callback as any,
387
405
  (sql: string, params: any[]) => {
388
- if (this._debug) console.log('[satidb]', sql, params);
389
406
  return this.db.query(sql).all(...params) as T[];
390
407
  },
391
- );
408
+ ));
392
409
  }
393
410
 
394
411
  // =========================================================================
@@ -397,14 +414,12 @@ class _Database<Schemas extends SchemaMap> {
397
414
 
398
415
  /** Execute a raw SQL query and return results. */
399
416
  public raw<T = any>(sql: string, ...params: any[]): T[] {
400
- if (this._debug) console.log('[satidb]', sql, params);
401
- return this.db.query(sql).all(...params) as T[];
417
+ return this._m(`raw: ${sql.slice(0, 60)}`, () => this.db.query(sql).all(...params) as T[]);
402
418
  }
403
419
 
404
420
  /** Execute a raw SQL statement (INSERT/UPDATE/DELETE) without returning rows. */
405
421
  public exec(sql: string, ...params: any[]): void {
406
- if (this._debug) console.log('[satidb]', sql, params);
407
- this.db.run(sql, ...params);
422
+ this._m(`exec: ${sql.slice(0, 60)}`, () => this.db.run(sql, ...params));
408
423
  }
409
424
 
410
425
  // =========================================================================
@@ -430,11 +445,13 @@ class _Database<Schemas extends SchemaMap> {
430
445
  * Each key is a table name, value is an array of raw row objects.
431
446
  */
432
447
  public dump(): Record<string, any[]> {
433
- const result: Record<string, any[]> = {};
434
- for (const tableName of Object.keys(this.schemas)) {
435
- result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
436
- }
437
- return result;
448
+ return this._m('dump', () => {
449
+ const result: Record<string, any[]> = {};
450
+ for (const tableName of Object.keys(this.schemas)) {
451
+ result[tableName] = this.db.query(`SELECT * FROM "${tableName}"`).all();
452
+ }
453
+ return result;
454
+ });
438
455
  }
439
456
 
440
457
  /**
@@ -442,28 +459,29 @@ class _Database<Schemas extends SchemaMap> {
442
459
  * Use `{ append: true }` to insert without truncating.
443
460
  */
444
461
  public load(data: Record<string, any[]>, options?: { append?: boolean }): void {
445
- const txn = this.db.transaction(() => {
446
- for (const [tableName, rows] of Object.entries(data)) {
447
- if (!this.schemas[tableName]) continue;
448
- if (!options?.append) {
449
- this.db.run(`DELETE FROM "${tableName}"`);
450
- }
451
- for (const row of rows) {
452
- const cols = Object.keys(row).filter(k => k !== 'id');
453
- const placeholders = cols.map(() => '?').join(', ');
454
- const values = cols.map(c => {
455
- const v = row[c];
456
- // Auto-serialize objects/arrays
457
- if (v !== null && v !== undefined && typeof v === 'object' && !(v instanceof Buffer)) {
458
- return JSON.stringify(v);
459
- }
460
- return v;
461
- });
462
- this.db.query(`INSERT INTO "${tableName}" (${cols.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`).run(...values);
462
+ this._m(`load(${Object.keys(data).join(',')})`, () => {
463
+ const txn = this.db.transaction(() => {
464
+ for (const [tableName, rows] of Object.entries(data)) {
465
+ if (!this.schemas[tableName]) continue;
466
+ if (!options?.append) {
467
+ this.db.run(`DELETE FROM "${tableName}"`);
468
+ }
469
+ for (const row of rows) {
470
+ const cols = Object.keys(row).filter(k => k !== 'id');
471
+ const placeholders = cols.map(() => '?').join(', ');
472
+ const values = cols.map(c => {
473
+ const v = row[c];
474
+ if (v !== null && v !== undefined && typeof v === 'object' && !(v instanceof Buffer)) {
475
+ return JSON.stringify(v);
476
+ }
477
+ return v;
478
+ });
479
+ this.db.query(`INSERT INTO "${tableName}" (${cols.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`).run(...values);
480
+ }
463
481
  }
464
- }
482
+ });
483
+ txn();
465
484
  });
466
- txn();
467
485
  }
468
486
 
469
487
  /**
@@ -483,45 +501,45 @@ class _Database<Schemas extends SchemaMap> {
483
501
  * Returns a diff object per table: { added, removed, typeChanged }.
484
502
  */
485
503
  public diff(): Record<string, { added: string[]; removed: string[]; typeChanged: { column: string; expected: string; actual: string }[] }> {
486
- const result: Record<string, { added: string[]; removed: string[]; typeChanged: { column: string; expected: string; actual: string }[] }> = {};
487
- const systemCols = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt']);
488
-
489
- for (const [tableName, schema] of Object.entries(this.schemas)) {
490
- const schemaFields = getStorableFields(schema);
491
- const schemaColMap = new Map(schemaFields.map(f => [f.name, zodTypeToSqlType(f.type)]));
492
-
493
- const liveColumns = this.columns(tableName);
494
- const liveColMap = new Map(liveColumns.map(c => [c.name, c.type]));
495
-
496
- const added: string[] = [];
497
- const removed: string[] = [];
498
- const typeChanged: { column: string; expected: string; actual: string }[] = [];
499
-
500
- // Columns in schema but not in live DB
501
- for (const [col, expectedType] of schemaColMap) {
502
- if (!liveColMap.has(col)) {
503
- added.push(col);
504
- } else {
505
- const actualType = liveColMap.get(col)!;
506
- if (actualType !== expectedType) {
507
- typeChanged.push({ column: col, expected: expectedType, actual: actualType });
504
+ return this._m('diff', () => {
505
+ const result: Record<string, { added: string[]; removed: string[]; typeChanged: { column: string; expected: string; actual: string }[] }> = {};
506
+ const systemCols = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt']);
507
+
508
+ for (const [tableName, schema] of Object.entries(this.schemas)) {
509
+ const schemaFields = getStorableFields(schema);
510
+ const schemaColMap = new Map(schemaFields.map(f => [f.name, zodTypeToSqlType(f.type)]));
511
+
512
+ const liveColumns = this.columns(tableName);
513
+ const liveColMap = new Map(liveColumns.map(c => [c.name, c.type]));
514
+
515
+ const added: string[] = [];
516
+ const removed: string[] = [];
517
+ const typeChanged: { column: string; expected: string; actual: string }[] = [];
518
+
519
+ for (const [col, expectedType] of schemaColMap) {
520
+ if (!liveColMap.has(col)) {
521
+ added.push(col);
522
+ } else {
523
+ const actualType = liveColMap.get(col)!;
524
+ if (actualType !== expectedType) {
525
+ typeChanged.push({ column: col, expected: expectedType, actual: actualType });
526
+ }
508
527
  }
509
528
  }
510
- }
511
529
 
512
- // Columns in live DB but not in schema (excluding system columns)
513
- for (const col of liveColMap.keys()) {
514
- if (!systemCols.has(col) && !schemaColMap.has(col)) {
515
- removed.push(col);
530
+ for (const col of liveColMap.keys()) {
531
+ if (!systemCols.has(col) && !schemaColMap.has(col)) {
532
+ removed.push(col);
533
+ }
516
534
  }
517
- }
518
535
 
519
- if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
520
- result[tableName] = { added, removed, typeChanged };
536
+ if (added.length > 0 || removed.length > 0 || typeChanged.length > 0) {
537
+ result[tableName] = { added, removed, typeChanged };
538
+ }
521
539
  }
522
- }
523
540
 
524
- return result;
541
+ return result;
542
+ });
525
543
  }
526
544
  }
527
545
 
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.db.query(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 => {