sqlite-zod-orm 3.7.2 → 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 -14
- package/package.json +1 -1
- package/src/database.ts +42 -12
- package/src/query-builder.ts +6 -20
package/dist/index.js
CHANGED
|
@@ -360,19 +360,15 @@ class QueryBuilder {
|
|
|
360
360
|
}
|
|
361
361
|
subscribe(callback, options = {}) {
|
|
362
362
|
const { interval = this.defaultPollInterval, immediate = true } = options;
|
|
363
|
-
|
|
364
|
-
let lastFingerprint = null;
|
|
363
|
+
let lastRevision = null;
|
|
365
364
|
let stopped = false;
|
|
366
365
|
const poll = async () => {
|
|
367
366
|
if (stopped)
|
|
368
367
|
return;
|
|
369
368
|
try {
|
|
370
|
-
const fpRows = this.executor(fingerprintSQL.sql, fingerprintSQL.params, true);
|
|
371
|
-
const fpRow = fpRows[0];
|
|
372
369
|
const rev = this.revisionGetter?.() ?? "0";
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
lastFingerprint = currentFingerprint;
|
|
370
|
+
if (rev !== lastRevision) {
|
|
371
|
+
lastRevision = rev;
|
|
376
372
|
const rows = this.all();
|
|
377
373
|
await callback(rows);
|
|
378
374
|
}
|
|
@@ -448,11 +444,6 @@ class QueryBuilder {
|
|
|
448
444
|
}
|
|
449
445
|
return { sql: "", params: [] };
|
|
450
446
|
}
|
|
451
|
-
buildFingerprintSQL() {
|
|
452
|
-
const where = this.buildWhereClause();
|
|
453
|
-
const sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}${where.sql ? ` WHERE ${where.sql}` : ""}`;
|
|
454
|
-
return { sql, params: where.params };
|
|
455
|
-
}
|
|
456
447
|
then(onfulfilled, onrejected) {
|
|
457
448
|
try {
|
|
458
449
|
const result = this.all();
|
|
@@ -4741,6 +4732,7 @@ class _Database {
|
|
|
4741
4732
|
this.pollInterval = options.pollInterval ?? 500;
|
|
4742
4733
|
this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
|
|
4743
4734
|
this.initializeTables();
|
|
4735
|
+
this.initializeChangeTracking();
|
|
4744
4736
|
this.runMigrations();
|
|
4745
4737
|
if (options.indexes)
|
|
4746
4738
|
this.createIndexes(options.indexes);
|
|
@@ -4775,6 +4767,24 @@ class _Database {
|
|
|
4775
4767
|
this.db.run(`CREATE TABLE IF NOT EXISTS ${entityName} (id INTEGER PRIMARY KEY AUTOINCREMENT, ${allCols}${allConstraints})`);
|
|
4776
4768
|
}
|
|
4777
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
|
+
}
|
|
4778
4788
|
runMigrations() {
|
|
4779
4789
|
for (const [entityName, schema] of Object.entries(this.schemas)) {
|
|
4780
4790
|
const existingColumns = this.db.query(`PRAGMA table_info(${entityName})`).all();
|
|
@@ -4802,8 +4812,9 @@ class _Database {
|
|
|
4802
4812
|
}
|
|
4803
4813
|
_getRevision(entityName) {
|
|
4804
4814
|
const rev = this._revisions[entityName] ?? 0;
|
|
4805
|
-
const
|
|
4806
|
-
|
|
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}`;
|
|
4807
4818
|
}
|
|
4808
4819
|
insert(entityName, data) {
|
|
4809
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,36 +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 lastFingerprint: string | null = null;
|
|
508
|
+
let lastRevision: string | null = null;
|
|
511
509
|
let stopped = false;
|
|
512
510
|
|
|
513
511
|
const poll = async () => {
|
|
514
512
|
if (stopped) return;
|
|
515
513
|
try {
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
// Include revision in fingerprint (combines in-memory counter + PRAGMA data_version).
|
|
520
|
-
// This detects ALL changes: same-process and cross-process.
|
|
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.
|
|
521
517
|
const rev = this.revisionGetter?.() ?? '0';
|
|
522
|
-
const currentFingerprint = `${fpRow?._cnt ?? 0}:${fpRow?._max ?? 0}:${rev}`;
|
|
523
518
|
|
|
524
|
-
if (
|
|
525
|
-
|
|
526
|
-
// Fingerprint changed → re-execute the full query
|
|
519
|
+
if (rev !== lastRevision) {
|
|
520
|
+
lastRevision = rev;
|
|
527
521
|
const rows = this.all();
|
|
528
522
|
await callback(rows);
|
|
529
523
|
}
|
|
530
524
|
} catch {
|
|
531
525
|
// Silently skip on error (table might be in transition)
|
|
532
526
|
}
|
|
533
|
-
// Self-scheduling: next poll only after this one completes
|
|
534
527
|
if (!stopped) setTimeout(poll, interval);
|
|
535
528
|
};
|
|
536
529
|
|
|
537
|
-
// Immediate first execution
|
|
538
530
|
if (immediate) {
|
|
539
531
|
poll();
|
|
540
532
|
} else {
|
|
@@ -653,12 +645,6 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
653
645
|
return { sql: '', params: [] };
|
|
654
646
|
}
|
|
655
647
|
|
|
656
|
-
/** Build a lightweight fingerprint query (COUNT + MAX(id)) that shares the same WHERE clause. */
|
|
657
|
-
private buildFingerprintSQL(): { sql: string; params: any[] } {
|
|
658
|
-
const where = this.buildWhereClause();
|
|
659
|
-
const sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}${where.sql ? ` WHERE ${where.sql}` : ''}`;
|
|
660
|
-
return { sql, params: where.params };
|
|
661
|
-
}
|
|
662
648
|
|
|
663
649
|
// ---------- Thenable (async/await support) ----------
|
|
664
650
|
|