sqlite-zod-orm 3.3.0 → 3.3.1

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/README.md CHANGED
@@ -236,30 +236,55 @@ unsub();
236
236
  ┌──────────────────────────────────────────────────┐
237
237
  │ Every {interval}ms: │
238
238
  │ │
239
- │ 1. Check in-memory revision counter (free)
239
+ │ 1. Check revision (in-memory + data_version)
240
240
  │ 2. Run: SELECT COUNT(*), MAX(id) │
241
241
  │ FROM users WHERE role = 'admin' │
242
242
  │ │
243
- │ 3. Combine into fingerprint: "count:max:rev"
243
+ │ 3. Combine into fingerprint: "count:max:rev:dv"
244
244
  │ │
245
245
  │ 4. If fingerprint changed → re-run full query │
246
246
  │ and call your callback │
247
247
  └──────────────────────────────────────────────────┘
248
248
  ```
249
249
 
250
- Since `_Database` controls all writes, it bumps an in-memory revision counter on every insert, update, and delete. The fingerprint includes this counter, so **all changes are always detected** — no triggers, no WAL, no `_changes` table.
250
+ Two signals combine to detect **all** changes from **any** source:
251
251
 
252
- | Operation | Detected | How |
252
+ | Signal | Catches | How |
253
253
  |---|---|---|
254
- | INSERT | | MAX(id) increases + revision bumps |
255
- | DELETE | | COUNT changes + revision bumps |
256
- | UPDATE | ✅ | revision bumps |
254
+ | **In-memory revision** | Same-process writes | Bumped by CRUD methods |
255
+ | **PRAGMA data_version** | Cross-process writes | SQLite bumps it on external commits |
256
+
257
+ | Operation | Detected | Source |
258
+ |---|---|---|
259
+ | INSERT | ✅ | Same or other process |
260
+ | DELETE | ✅ | Same or other process |
261
+ | UPDATE | ✅ | Same or other process |
262
+
263
+ No triggers. No `_changes` table. Zero disk overhead. WAL mode is enabled by default for concurrent read/write.
264
+
265
+ ### Multi-process example
266
+
267
+ ```typescript
268
+ // Process A — watches for new/edited messages
269
+ const unsub = db.messages.select()
270
+ .orderBy('id', 'asc')
271
+ .subscribe((messages) => {
272
+ console.log('Messages:', messages);
273
+ }, { interval: 200 });
274
+
275
+ // Process B — writes to the same DB file (different process)
276
+ // sqlite3 chat.db "INSERT INTO messages (text, author) VALUES ('hello', 'Bob')"
277
+ // → Process A's callback fires with updated message list!
278
+ ```
279
+
280
+ Run `bun examples/messages-demo.ts` for a full working demo.
257
281
 
258
282
  **Use cases:**
259
283
  - Live dashboards (poll every 1-5s)
260
284
  - Real-time chat / message lists
261
285
  - Auto-refreshing data tables
262
286
  - Watching filtered subsets of data
287
+ - Cross-process data synchronization
263
288
 
264
289
  ---
265
290
 
package/dist/index.js CHANGED
@@ -334,7 +334,7 @@ class QueryBuilder {
334
334
  try {
335
335
  const fpRows = this.executor(fingerprintSQL.sql, fingerprintSQL.params, true);
336
336
  const fpRow = fpRows[0];
337
- const rev = this.revisionGetter?.() ?? 0;
337
+ const rev = this.revisionGetter?.() ?? "0";
338
338
  const currentFingerprint = `${fpRow?._cnt ?? 0}:${fpRow?._max ?? 0}:${rev}`;
339
339
  if (currentFingerprint !== lastFingerprint) {
340
340
  lastFingerprint = currentFingerprint;
@@ -4659,6 +4659,7 @@ class _Database {
4659
4659
  _revisions = {};
4660
4660
  constructor(dbFile, schemas, options = {}) {
4661
4661
  this.db = new SqliteDatabase(dbFile);
4662
+ this.db.run("PRAGMA journal_mode = WAL");
4662
4663
  this.db.run("PRAGMA foreign_keys = ON");
4663
4664
  this.schemas = schemas;
4664
4665
  this.options = options;
@@ -4724,7 +4725,9 @@ class _Database {
4724
4725
  this._revisions[entityName] = (this._revisions[entityName] ?? 0) + 1;
4725
4726
  }
4726
4727
  _getRevision(entityName) {
4727
- return this._revisions[entityName] ?? 0;
4728
+ const rev = this._revisions[entityName] ?? 0;
4729
+ const dataVersion = this.db.query("PRAGMA data_version").get()?.data_version ?? 0;
4730
+ return `${rev}:${dataVersion}`;
4728
4731
  }
4729
4732
  insert(entityName, data) {
4730
4733
  const schema = this.schemas[entityName];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
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/database.ts CHANGED
@@ -36,6 +36,7 @@ class _Database<Schemas extends SchemaMap> {
36
36
 
37
37
  constructor(dbFile: string, schemas: Schemas, options: DatabaseOptions = {}) {
38
38
  this.db = new SqliteDatabase(dbFile);
39
+ this.db.run('PRAGMA journal_mode = WAL'); // WAL enables concurrent read + write
39
40
  this.db.run('PRAGMA foreign_keys = ON');
40
41
  this.schemas = schemas;
41
42
  this.options = options;
@@ -112,7 +113,7 @@ class _Database<Schemas extends SchemaMap> {
112
113
  }
113
114
 
114
115
  // ===========================================================================
115
- // Revision Tracking (in-memory, zero overhead)
116
+ // Revision Tracking (in-memory + cross-process)
116
117
  // ===========================================================================
117
118
 
118
119
  /** Bump the revision counter for a table. Called on every write. */
@@ -120,9 +121,20 @@ class _Database<Schemas extends SchemaMap> {
120
121
  this._revisions[entityName] = (this._revisions[entityName] ?? 0) + 1;
121
122
  }
122
123
 
123
- /** Get the current revision for a table. Used by QueryBuilder.subscribe() fingerprint. */
124
- public _getRevision(entityName: string): number {
125
- return this._revisions[entityName] ?? 0;
124
+ /**
125
+ * Get a composite revision string for a table.
126
+ *
127
+ * Combines two signals:
128
+ * - In-memory counter: catches writes from THIS process (our CRUD methods bump it)
129
+ * - PRAGMA data_version: catches writes from OTHER processes (SQLite bumps it
130
+ * whenever another connection commits, but NOT for the current connection)
131
+ *
132
+ * Together they detect ALL changes regardless of source, with zero disk overhead.
133
+ */
134
+ public _getRevision(entityName: string): string {
135
+ const rev = this._revisions[entityName] ?? 0;
136
+ const dataVersion = (this.db.query('PRAGMA data_version').get() as any)?.data_version ?? 0;
137
+ return `${rev}:${dataVersion}`;
126
138
  }
127
139
 
128
140
  // ===========================================================================
@@ -167,7 +167,7 @@ export class QueryBuilder<T extends Record<string, any>> {
167
167
  private singleExecutor: (sql: string, params: any[], raw: boolean) => any | null;
168
168
  private joinResolver: ((fromTable: string, toTable: string) => { fk: string; pk: string } | null) | null;
169
169
  private conditionResolver: ((conditions: Record<string, any>) => Record<string, any>) | null;
170
- private revisionGetter: (() => number) | null;
170
+ private revisionGetter: (() => string) | null;
171
171
 
172
172
  constructor(
173
173
  tableName: string,
@@ -175,7 +175,7 @@ export class QueryBuilder<T extends Record<string, any>> {
175
175
  singleExecutor: (sql: string, params: any[], raw: boolean) => any | null,
176
176
  joinResolver?: ((fromTable: string, toTable: string) => { fk: string; pk: string } | null) | null,
177
177
  conditionResolver?: ((conditions: Record<string, any>) => Record<string, any>) | null,
178
- revisionGetter?: (() => number) | null,
178
+ revisionGetter?: (() => string) | null,
179
179
  ) {
180
180
  this.tableName = tableName;
181
181
  this.executor = executor;
@@ -459,9 +459,9 @@ export class QueryBuilder<T extends Record<string, any>> {
459
459
  // Run lightweight fingerprint check
460
460
  const fpRows = this.executor(fingerprintSQL.sql, fingerprintSQL.params, true);
461
461
  const fpRow = fpRows[0] as any;
462
- // Include in-memory revision in fingerprint.
463
- // This ensures ALL changes (insert/update/delete) are detected.
464
- const rev = this.revisionGetter?.() ?? 0;
462
+ // Include revision in fingerprint (combines in-memory counter + PRAGMA data_version).
463
+ // This detects ALL changes: same-process and cross-process.
464
+ const rev = this.revisionGetter?.() ?? '0';
465
465
  const currentFingerprint = `${fpRow?._cnt ?? 0}:${fpRow?._max ?? 0}:${rev}`;
466
466
 
467
467
  if (currentFingerprint !== lastFingerprint) {