sqlite-zod-orm 3.7.3 → 3.8.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 +25 -21
- package/package.json +1 -1
- package/src/database.ts +42 -12
- package/src/query-builder.ts +6 -35
package/dist/index.js
CHANGED
|
@@ -360,26 +360,15 @@ class QueryBuilder {
|
|
|
360
360
|
}
|
|
361
361
|
subscribe(callback, options = {}) {
|
|
362
362
|
const { interval = this.defaultPollInterval, immediate = true } = options;
|
|
363
|
-
|
|
364
|
-
let lastCount = null;
|
|
365
|
-
let lastMax = null;
|
|
366
|
-
let lastInMemoryRev = null;
|
|
363
|
+
let lastRevision = null;
|
|
367
364
|
let stopped = false;
|
|
368
365
|
const poll = async () => {
|
|
369
366
|
if (stopped)
|
|
370
367
|
return;
|
|
371
368
|
try {
|
|
372
369
|
const rev = this.revisionGetter?.() ?? "0";
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const fpRows = this.executor(fingerprintSQL.sql, fingerprintSQL.params, true);
|
|
376
|
-
const fpRow = fpRows[0];
|
|
377
|
-
const cnt = fpRow?._cnt ?? 0;
|
|
378
|
-
const max = fpRow?._max ?? 0;
|
|
379
|
-
const fpChanged = cnt !== lastCount || max !== lastMax;
|
|
380
|
-
lastCount = cnt;
|
|
381
|
-
lastMax = max;
|
|
382
|
-
if (inMemoryChanged || fpChanged) {
|
|
370
|
+
if (rev !== lastRevision) {
|
|
371
|
+
lastRevision = rev;
|
|
383
372
|
const rows = this.all();
|
|
384
373
|
await callback(rows);
|
|
385
374
|
}
|
|
@@ -455,11 +444,6 @@ class QueryBuilder {
|
|
|
455
444
|
}
|
|
456
445
|
return { sql: "", params: [] };
|
|
457
446
|
}
|
|
458
|
-
buildFingerprintSQL() {
|
|
459
|
-
const where = this.buildWhereClause();
|
|
460
|
-
const sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}${where.sql ? ` WHERE ${where.sql}` : ""}`;
|
|
461
|
-
return { sql, params: where.params };
|
|
462
|
-
}
|
|
463
447
|
then(onfulfilled, onrejected) {
|
|
464
448
|
try {
|
|
465
449
|
const result = this.all();
|
|
@@ -4748,6 +4732,7 @@ class _Database {
|
|
|
4748
4732
|
this.pollInterval = options.pollInterval ?? 500;
|
|
4749
4733
|
this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
|
|
4750
4734
|
this.initializeTables();
|
|
4735
|
+
this.initializeChangeTracking();
|
|
4751
4736
|
this.runMigrations();
|
|
4752
4737
|
if (options.indexes)
|
|
4753
4738
|
this.createIndexes(options.indexes);
|
|
@@ -4782,6 +4767,24 @@ class _Database {
|
|
|
4782
4767
|
this.db.run(`CREATE TABLE IF NOT EXISTS ${entityName} (id INTEGER PRIMARY KEY AUTOINCREMENT, ${allCols}${allConstraints})`);
|
|
4783
4768
|
}
|
|
4784
4769
|
}
|
|
4770
|
+
initializeChangeTracking() {
|
|
4771
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS _satidb_changes (
|
|
4772
|
+
tbl TEXT PRIMARY KEY,
|
|
4773
|
+
seq INTEGER NOT NULL DEFAULT 0
|
|
4774
|
+
)`);
|
|
4775
|
+
for (const entityName of Object.keys(this.schemas)) {
|
|
4776
|
+
this.db.run(`INSERT OR IGNORE INTO _satidb_changes (tbl, seq) VALUES (?, 0)`, entityName);
|
|
4777
|
+
for (const op2 of ["insert", "update", "delete"]) {
|
|
4778
|
+
const triggerName = `_satidb_${entityName}_${op2}`;
|
|
4779
|
+
const event = op2.toUpperCase();
|
|
4780
|
+
this.db.run(`CREATE TRIGGER IF NOT EXISTS ${triggerName}
|
|
4781
|
+
AFTER ${event} ON ${entityName}
|
|
4782
|
+
BEGIN
|
|
4783
|
+
UPDATE _satidb_changes SET seq = seq + 1 WHERE tbl = '${entityName}';
|
|
4784
|
+
END`);
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4785
4788
|
runMigrations() {
|
|
4786
4789
|
for (const [entityName, schema] of Object.entries(this.schemas)) {
|
|
4787
4790
|
const existingColumns = this.db.query(`PRAGMA table_info(${entityName})`).all();
|
|
@@ -4809,8 +4812,9 @@ class _Database {
|
|
|
4809
4812
|
}
|
|
4810
4813
|
_getRevision(entityName) {
|
|
4811
4814
|
const rev = this._revisions[entityName] ?? 0;
|
|
4812
|
-
const
|
|
4813
|
-
|
|
4815
|
+
const row = this.db.query("SELECT seq FROM _satidb_changes WHERE tbl = ?").get(entityName);
|
|
4816
|
+
const seq = row?.seq ?? 0;
|
|
4817
|
+
return `${rev}:${seq}`;
|
|
4814
4818
|
}
|
|
4815
4819
|
insert(entityName, data) {
|
|
4816
4820
|
const schema = this.schemas[entityName];
|
package/package.json
CHANGED
package/src/database.ts
CHANGED
|
@@ -31,8 +31,8 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
31
31
|
private options: DatabaseOptions;
|
|
32
32
|
private pollInterval: number;
|
|
33
33
|
|
|
34
|
-
/** In-memory revision counter per table —
|
|
35
|
-
*
|
|
34
|
+
/** In-memory revision counter per table — same-process fast path.
|
|
35
|
+
* Complements the trigger-based _satidb_changes table for cross-process detection. */
|
|
36
36
|
private _revisions: Record<string, number> = {};
|
|
37
37
|
|
|
38
38
|
constructor(dbFile: string, schemas: Schemas, options: DatabaseOptions = {}) {
|
|
@@ -44,6 +44,7 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
44
44
|
this.pollInterval = options.pollInterval ?? 500;
|
|
45
45
|
this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
|
|
46
46
|
this.initializeTables();
|
|
47
|
+
this.initializeChangeTracking();
|
|
47
48
|
this.runMigrations();
|
|
48
49
|
if (options.indexes) this.createIndexes(options.indexes);
|
|
49
50
|
|
|
@@ -89,6 +90,36 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Initialize per-table change tracking using triggers.
|
|
95
|
+
*
|
|
96
|
+
* Creates a `_satidb_changes` table with one row per user table and a monotonic `seq` counter.
|
|
97
|
+
* INSERT/UPDATE/DELETE triggers on each user table auto-increment the seq.
|
|
98
|
+
* This enables table-specific, cross-process change detection — no PRAGMA data_version needed.
|
|
99
|
+
*/
|
|
100
|
+
private initializeChangeTracking(): void {
|
|
101
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS _satidb_changes (
|
|
102
|
+
tbl TEXT PRIMARY KEY,
|
|
103
|
+
seq INTEGER NOT NULL DEFAULT 0
|
|
104
|
+
)`);
|
|
105
|
+
|
|
106
|
+
for (const entityName of Object.keys(this.schemas)) {
|
|
107
|
+
// Ensure a row exists for this table
|
|
108
|
+
this.db.run(`INSERT OR IGNORE INTO _satidb_changes (tbl, seq) VALUES (?, 0)`, entityName);
|
|
109
|
+
|
|
110
|
+
// Create triggers (idempotent via IF NOT EXISTS)
|
|
111
|
+
for (const op of ['insert', 'update', 'delete'] as const) {
|
|
112
|
+
const triggerName = `_satidb_${entityName}_${op}`;
|
|
113
|
+
const event = op.toUpperCase();
|
|
114
|
+
this.db.run(`CREATE TRIGGER IF NOT EXISTS ${triggerName}
|
|
115
|
+
AFTER ${event} ON ${entityName}
|
|
116
|
+
BEGIN
|
|
117
|
+
UPDATE _satidb_changes SET seq = seq + 1 WHERE tbl = '${entityName}';
|
|
118
|
+
END`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
92
123
|
private runMigrations(): void {
|
|
93
124
|
for (const [entityName, schema] of Object.entries(this.schemas)) {
|
|
94
125
|
const existingColumns = this.db.query(`PRAGMA table_info(${entityName})`).all() as any[];
|
|
@@ -115,28 +146,27 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
115
146
|
}
|
|
116
147
|
|
|
117
148
|
// ===========================================================================
|
|
118
|
-
// Revision Tracking (in-memory
|
|
149
|
+
// Revision Tracking (trigger-based + in-memory fast path)
|
|
119
150
|
// ===========================================================================
|
|
120
151
|
|
|
121
|
-
/** Bump the revision counter
|
|
152
|
+
/** Bump the in-memory revision counter. Called by our CRUD methods (same-process fast path). */
|
|
122
153
|
private _bumpRevision(entityName: string): void {
|
|
123
154
|
this._revisions[entityName] = (this._revisions[entityName] ?? 0) + 1;
|
|
124
155
|
}
|
|
125
156
|
|
|
126
157
|
/**
|
|
127
|
-
* Get
|
|
158
|
+
* Get the change sequence for a table.
|
|
128
159
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* - PRAGMA data_version: catches writes from OTHER processes (SQLite bumps it
|
|
132
|
-
* whenever another connection commits, but NOT for the current connection)
|
|
160
|
+
* Reads from `_satidb_changes` — a per-table seq counter bumped by triggers
|
|
161
|
+
* on every INSERT/UPDATE/DELETE, regardless of which connection performed the write.
|
|
133
162
|
*
|
|
134
|
-
*
|
|
163
|
+
* Combined with the in-memory counter for instant same-process detection.
|
|
135
164
|
*/
|
|
136
165
|
public _getRevision(entityName: string): string {
|
|
137
166
|
const rev = this._revisions[entityName] ?? 0;
|
|
138
|
-
const
|
|
139
|
-
|
|
167
|
+
const row = this.db.query('SELECT seq FROM _satidb_changes WHERE tbl = ?').get(entityName) as any;
|
|
168
|
+
const seq = row?.seq ?? 0;
|
|
169
|
+
return `${rev}:${seq}`;
|
|
140
170
|
}
|
|
141
171
|
|
|
142
172
|
// ===========================================================================
|
package/src/query-builder.ts
CHANGED
|
@@ -505,51 +505,28 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
505
505
|
): () => void {
|
|
506
506
|
const { interval = this.defaultPollInterval, immediate = true } = options;
|
|
507
507
|
|
|
508
|
-
|
|
509
|
-
const fingerprintSQL = this.buildFingerprintSQL();
|
|
510
|
-
let lastCount: number | null = null;
|
|
511
|
-
let lastMax: number | null = null;
|
|
512
|
-
let lastInMemoryRev: string | null = null;
|
|
508
|
+
let lastRevision: string | null = null;
|
|
513
509
|
let stopped = false;
|
|
514
510
|
|
|
515
511
|
const poll = async () => {
|
|
516
512
|
if (stopped) return;
|
|
517
513
|
try {
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
// Note: cross-process UPDATEs that don't change count/max are only caught
|
|
523
|
-
// by PRAGMA data_version, which is database-wide. We accept this tradeoff
|
|
524
|
-
// to avoid re-querying on writes to OTHER tables.
|
|
514
|
+
// Single check: revision combines in-memory counter (same-process)
|
|
515
|
+
// + trigger-based seq from _satidb_changes (cross-process).
|
|
516
|
+
// Both are table-specific — no false positives from other tables.
|
|
525
517
|
const rev = this.revisionGetter?.() ?? '0';
|
|
526
518
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
lastInMemoryRev = rev;
|
|
530
|
-
|
|
531
|
-
// Check table-specific fingerprint (COUNT + MAX(id))
|
|
532
|
-
const fpRows = this.executor(fingerprintSQL.sql, fingerprintSQL.params, true);
|
|
533
|
-
const fpRow = fpRows[0] as any;
|
|
534
|
-
const cnt = fpRow?._cnt ?? 0;
|
|
535
|
-
const max = fpRow?._max ?? 0;
|
|
536
|
-
const fpChanged = cnt !== lastCount || max !== lastMax;
|
|
537
|
-
lastCount = cnt;
|
|
538
|
-
lastMax = max;
|
|
539
|
-
|
|
540
|
-
// Fire callback only if THIS table actually changed
|
|
541
|
-
if (inMemoryChanged || fpChanged) {
|
|
519
|
+
if (rev !== lastRevision) {
|
|
520
|
+
lastRevision = rev;
|
|
542
521
|
const rows = this.all();
|
|
543
522
|
await callback(rows);
|
|
544
523
|
}
|
|
545
524
|
} catch {
|
|
546
525
|
// Silently skip on error (table might be in transition)
|
|
547
526
|
}
|
|
548
|
-
// Self-scheduling: next poll only after this one completes
|
|
549
527
|
if (!stopped) setTimeout(poll, interval);
|
|
550
528
|
};
|
|
551
529
|
|
|
552
|
-
// Immediate first execution
|
|
553
530
|
if (immediate) {
|
|
554
531
|
poll();
|
|
555
532
|
} else {
|
|
@@ -668,12 +645,6 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
668
645
|
return { sql: '', params: [] };
|
|
669
646
|
}
|
|
670
647
|
|
|
671
|
-
/** Build a lightweight fingerprint query (COUNT + MAX(id)) that shares the same WHERE clause. */
|
|
672
|
-
private buildFingerprintSQL(): { sql: string; params: any[] } {
|
|
673
|
-
const where = this.buildWhereClause();
|
|
674
|
-
const sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}${where.sql ? ` WHERE ${where.sql}` : ''}`;
|
|
675
|
-
return { sql, params: where.params };
|
|
676
|
-
}
|
|
677
648
|
|
|
678
649
|
// ---------- Thenable (async/await support) ----------
|
|
679
650
|
|