sqlite-zod-orm 3.22.0 → 3.24.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, {
@@ -4620,6 +5003,18 @@ class QueryBuilder {
4620
5003
  const result = this.executor(`SELECT changes() as c`, [], true);
4621
5004
  return result[0]?.c ?? 0;
4622
5005
  }
5006
+ increment(column, amount = 1) {
5007
+ const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
5008
+ const whereMatch = selectSql.match(/WHERE (.+?)(?:\s+ORDER|\s+LIMIT|\s+GROUP|\s+HAVING|$)/s);
5009
+ const wherePart = whereMatch ? whereMatch[1] : "1=1";
5010
+ const sql = `UPDATE "${this.tableName}" SET "${column}" = "${column}" + ? WHERE ${wherePart}`;
5011
+ this.executor(sql, [amount, ...params], true);
5012
+ const result = this.executor(`SELECT changes() as c`, [], true);
5013
+ return result[0]?.c ?? 0;
5014
+ }
5015
+ decrement(column, amount = 1) {
5016
+ return this.increment(column, -amount);
5017
+ }
4623
5018
  then(onfulfilled, onrejected) {
4624
5019
  try {
4625
5020
  const result = this.all();
@@ -5253,7 +5648,15 @@ class _Database {
5253
5648
  _changeWatermark = 0;
5254
5649
  _pollTimer = null;
5255
5650
  _pollInterval;
5651
+ _measure;
5652
+ _m(label, fn) {
5653
+ if (this._debug)
5654
+ return this._measure.measureSync.assert(label, fn);
5655
+ return fn();
5656
+ }
5256
5657
  constructor(dbFile, schemas, options = {}) {
5658
+ this._debug = options.debug === true;
5659
+ this._measure = createMeasure("satidb");
5257
5660
  this.db = new SqliteDatabase(dbFile);
5258
5661
  if (options.wal !== false)
5259
5662
  this.db.run("PRAGMA journal_mode = WAL");
@@ -5263,7 +5666,6 @@ class _Database {
5263
5666
  this._reactive = options.reactive !== false;
5264
5667
  this._timestamps = options.timestamps === true;
5265
5668
  this._softDeletes = options.softDeletes === true;
5266
- this._debug = options.debug === true;
5267
5669
  this._pollInterval = options.pollInterval ?? 100;
5268
5670
  this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
5269
5671
  this._ctx = {
@@ -5279,19 +5681,19 @@ class _Database {
5279
5681
  computed: options.computed ?? {},
5280
5682
  cascade: options.cascade ?? {}
5281
5683
  };
5282
- this.initializeTables();
5684
+ this._m("Init tables", () => this.initializeTables());
5283
5685
  if (this._reactive)
5284
- this.initializeChangeTracking();
5285
- this.runMigrations();
5686
+ this._m("Change tracking", () => this.initializeChangeTracking());
5687
+ this._m("Run migrations", () => this.runMigrations());
5286
5688
  if (options.indexes)
5287
- this.createIndexes(options.indexes);
5689
+ this._m("Create indexes", () => this.createIndexes(options.indexes));
5288
5690
  if (options.unique)
5289
- this.createUniqueConstraints(options.unique);
5691
+ this._m("Unique constraints", () => this.createUniqueConstraints(options.unique));
5290
5692
  for (const entityName of Object.keys(schemas)) {
5291
5693
  const key = entityName;
5292
5694
  const accessor = {
5293
- insert: (data) => insert(this._ctx, entityName, data),
5294
- insertMany: (rows) => insertMany(this._ctx, entityName, rows),
5695
+ insert: (data) => this._m(`${entityName}.insert`, () => insert(this._ctx, entityName, data)),
5696
+ insertMany: (rows) => this._m(`${entityName}.insertMany(${rows.length})`, () => insertMany(this._ctx, entityName, rows)),
5295
5697
  update: (idOrData, data) => {
5296
5698
  if (typeof idOrData === "number")
5297
5699
  return update(this._ctx, entityName, idOrData, data);
@@ -5336,8 +5738,6 @@ class _Database {
5336
5738
  restore: (id) => {
5337
5739
  if (!this._softDeletes)
5338
5740
  throw new Error("restore() requires softDeletes: true");
5339
- if (this._debug)
5340
- console.log("[satidb]", `UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`, [id]);
5341
5741
  this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
5342
5742
  },
5343
5743
  select: (...cols) => createQueryBuilder(this._ctx, entityName, cols),
package/package.json CHANGED
@@ -1,57 +1,58 @@
1
- {
2
- "name": "sqlite-zod-orm",
3
- "version": "3.22.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.24.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/builder.ts CHANGED
@@ -572,6 +572,34 @@ export class QueryBuilder<T extends Record<string, any>, TResult extends Record<
572
572
  return (result[0] as any)?.c ?? 0;
573
573
  }
574
574
 
575
+ /**
576
+ * Atomically increment a numeric column for matching rows.
577
+ * Returns the number of affected rows.
578
+ * ```ts
579
+ * db.users.select().where({ id: 1 }).increment('score', 10)
580
+ * ```
581
+ */
582
+ increment(column: keyof T & string, amount: number = 1): number {
583
+ const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
584
+ const whereMatch = selectSql.match(/WHERE (.+?)(?:\s+ORDER|\s+LIMIT|\s+GROUP|\s+HAVING|$)/s);
585
+ const wherePart = whereMatch ? whereMatch[1] : '1=1';
586
+
587
+ const sql = `UPDATE "${this.tableName}" SET "${column}" = "${column}" + ? WHERE ${wherePart}`;
588
+ this.executor(sql, [amount, ...params], true);
589
+ const result = this.executor(`SELECT changes() as c`, [], true);
590
+ return (result[0] as any)?.c ?? 0;
591
+ }
592
+
593
+ /**
594
+ * Atomically decrement a numeric column for matching rows.
595
+ * Returns the number of affected rows.
596
+ * ```ts
597
+ * db.users.select().where({ id: 1 }).decrement('score', 5)
598
+ * ```
599
+ */
600
+ decrement(column: keyof T & string, amount: number = 1): number {
601
+ return this.increment(column, -amount);
602
+ }
575
603
 
576
604
  // ---------- Thenable (async/await support) ----------
577
605
 
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
 
@@ -91,18 +106,18 @@ class _Database<Schemas extends SchemaMap> {
91
106
  cascade: options.cascade ?? {},
92
107
  };
93
108
 
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);
109
+ this._m('Init tables', () => this.initializeTables());
110
+ if (this._reactive) this._m('Change tracking', () => this.initializeChangeTracking());
111
+ this._m('Run migrations', () => this.runMigrations());
112
+ if (options.indexes) this._m('Create indexes', () => this.createIndexes(options.indexes!));
113
+ if (options.unique) this._m('Unique constraints', () => this.createUniqueConstraints(options.unique!));
99
114
 
100
115
  // Create typed entity accessors (db.users, db.posts, etc.)
101
116
  for (const entityName of Object.keys(schemas)) {
102
117
  const key = entityName as keyof Schemas;
103
118
  const accessor: EntityAccessor<Schemas[typeof key]> = {
104
- insert: (data) => insert(this._ctx, entityName, data),
105
- insertMany: (rows: any[]) => insertMany(this._ctx, entityName, rows),
119
+ insert: (data) => this._m(`${entityName}.insert`, () => insert(this._ctx, entityName, data)),
120
+ insertMany: (rows: any[]) => this._m(`${entityName}.insertMany(${rows.length})`, () => insertMany(this._ctx, entityName, rows)),
106
121
  update: (idOrData: any, data?: any) => {
107
122
  if (typeof idOrData === 'number') return update(this._ctx, entityName, idOrData, data);
108
123
  return createUpdateBuilder(this._ctx, entityName, idOrData);
@@ -150,7 +165,7 @@ class _Database<Schemas extends SchemaMap> {
150
165
  }) as any,
151
166
  restore: ((id: number) => {
152
167
  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]);
168
+ // debug log replaced by measure-fn instrumentation
154
169
  this.db.query(`UPDATE "${entityName}" SET "deletedAt" = NULL WHERE id = ?`).run(id);
155
170
  }) as any,
156
171
  select: (...cols: string[]) => createQueryBuilder(this._ctx, entityName, cols),