sqlite-zod-orm 3.7.1 → 3.7.3
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 +42 -21
- package/package.json +1 -1
- package/src/query-builder.ts +83 -39
package/dist/index.js
CHANGED
|
@@ -361,31 +361,45 @@ class QueryBuilder {
|
|
|
361
361
|
subscribe(callback, options = {}) {
|
|
362
362
|
const { interval = this.defaultPollInterval, immediate = true } = options;
|
|
363
363
|
const fingerprintSQL = this.buildFingerprintSQL();
|
|
364
|
-
let
|
|
365
|
-
|
|
364
|
+
let lastCount = null;
|
|
365
|
+
let lastMax = null;
|
|
366
|
+
let lastInMemoryRev = null;
|
|
367
|
+
let stopped = false;
|
|
368
|
+
const poll = async () => {
|
|
369
|
+
if (stopped)
|
|
370
|
+
return;
|
|
366
371
|
try {
|
|
372
|
+
const rev = this.revisionGetter?.() ?? "0";
|
|
373
|
+
const inMemoryChanged = rev !== lastInMemoryRev;
|
|
374
|
+
lastInMemoryRev = rev;
|
|
367
375
|
const fpRows = this.executor(fingerprintSQL.sql, fingerprintSQL.params, true);
|
|
368
376
|
const fpRow = fpRows[0];
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
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) {
|
|
373
383
|
const rows = this.all();
|
|
374
|
-
callback(rows);
|
|
384
|
+
await callback(rows);
|
|
375
385
|
}
|
|
376
386
|
} catch {}
|
|
387
|
+
if (!stopped)
|
|
388
|
+
setTimeout(poll, interval);
|
|
377
389
|
};
|
|
378
390
|
if (immediate) {
|
|
379
391
|
poll();
|
|
392
|
+
} else {
|
|
393
|
+
setTimeout(poll, interval);
|
|
380
394
|
}
|
|
381
|
-
const timer = setInterval(poll, interval);
|
|
382
395
|
return () => {
|
|
383
|
-
|
|
396
|
+
stopped = true;
|
|
384
397
|
};
|
|
385
398
|
}
|
|
386
399
|
each(callback, options = {}) {
|
|
387
400
|
const { interval = this.defaultPollInterval } = options;
|
|
388
|
-
const
|
|
401
|
+
const userWhere = this.buildWhereClause();
|
|
402
|
+
const maxRows = this.executor(`SELECT MAX(id) as _max FROM ${this.tableName} ${userWhere.sql ? `WHERE ${userWhere.sql}` : ""}`, userWhere.params, true);
|
|
389
403
|
let lastMaxId = maxRows[0]?._max ?? 0;
|
|
390
404
|
let lastRevision = this.revisionGetter?.() ?? "0";
|
|
391
405
|
let stopped = false;
|
|
@@ -395,12 +409,15 @@ class QueryBuilder {
|
|
|
395
409
|
const rev = this.revisionGetter?.() ?? "0";
|
|
396
410
|
if (rev !== lastRevision) {
|
|
397
411
|
lastRevision = rev;
|
|
398
|
-
const
|
|
399
|
-
|
|
412
|
+
const params = [...userWhere.params, lastMaxId];
|
|
413
|
+
const whereClause = userWhere.sql ? `WHERE ${userWhere.sql} AND id > ? ORDER BY id ASC` : `WHERE id > ? ORDER BY id ASC`;
|
|
414
|
+
const sql = `SELECT * FROM ${this.tableName} ${whereClause}`;
|
|
415
|
+
const newRows = this.executor(sql, params, false);
|
|
416
|
+
for (const row of newRows) {
|
|
400
417
|
if (stopped)
|
|
401
418
|
return;
|
|
402
|
-
await callback(
|
|
403
|
-
lastMaxId =
|
|
419
|
+
await callback(row);
|
|
420
|
+
lastMaxId = row.id;
|
|
404
421
|
}
|
|
405
422
|
}
|
|
406
423
|
if (!stopped)
|
|
@@ -411,14 +428,13 @@ class QueryBuilder {
|
|
|
411
428
|
stopped = true;
|
|
412
429
|
};
|
|
413
430
|
}
|
|
414
|
-
|
|
431
|
+
buildWhereClause() {
|
|
415
432
|
const params = [];
|
|
416
|
-
let sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}`;
|
|
417
433
|
if (this.iqo.whereAST) {
|
|
418
434
|
const compiled = compileAST(this.iqo.whereAST);
|
|
419
|
-
sql
|
|
420
|
-
|
|
421
|
-
|
|
435
|
+
return { sql: compiled.sql, params: compiled.params };
|
|
436
|
+
}
|
|
437
|
+
if (this.iqo.wheres.length > 0) {
|
|
422
438
|
const whereParts = [];
|
|
423
439
|
for (const w of this.iqo.wheres) {
|
|
424
440
|
if (w.operator === "IN") {
|
|
@@ -435,9 +451,14 @@ class QueryBuilder {
|
|
|
435
451
|
params.push(transformValueForStorage(w.value));
|
|
436
452
|
}
|
|
437
453
|
}
|
|
438
|
-
|
|
454
|
+
return { sql: whereParts.join(" AND "), params };
|
|
439
455
|
}
|
|
440
|
-
return { sql, params };
|
|
456
|
+
return { sql: "", params: [] };
|
|
457
|
+
}
|
|
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 };
|
|
441
462
|
}
|
|
442
463
|
then(onfulfilled, onrejected) {
|
|
443
464
|
try {
|
package/package.json
CHANGED
package/src/query-builder.ts
CHANGED
|
@@ -479,6 +479,9 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
479
479
|
* in-memory revision counter to detect ALL changes (inserts, updates, deletes)
|
|
480
480
|
* with zero disk overhead.
|
|
481
481
|
*
|
|
482
|
+
* Uses a self-scheduling async loop: each callback (sync or async) completes
|
|
483
|
+
* before the next poll starts. No overlapping polls.
|
|
484
|
+
*
|
|
482
485
|
* ```ts
|
|
483
486
|
* const unsub = db.messages.select()
|
|
484
487
|
* .where({ groupId: 1 })
|
|
@@ -492,52 +495,68 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
492
495
|
* unsub();
|
|
493
496
|
* ```
|
|
494
497
|
*
|
|
495
|
-
* @param callback Called with the full result set whenever the data changes.
|
|
498
|
+
* @param callback Called with the full result set whenever the data changes. Async callbacks are awaited.
|
|
496
499
|
* @param options `interval` in ms (default 500). Set `immediate` to false to skip the first call.
|
|
497
|
-
* @returns An unsubscribe function
|
|
500
|
+
* @returns An unsubscribe function.
|
|
498
501
|
*/
|
|
499
502
|
subscribe(
|
|
500
|
-
callback: (rows: T[]) => void
|
|
503
|
+
callback: (rows: T[]) => void | Promise<void>,
|
|
501
504
|
options: { interval?: number; immediate?: boolean } = {},
|
|
502
505
|
): () => void {
|
|
503
506
|
const { interval = this.defaultPollInterval, immediate = true } = options;
|
|
504
507
|
|
|
505
508
|
// Build the fingerprint SQL (COUNT + MAX(id)) using the same WHERE
|
|
506
509
|
const fingerprintSQL = this.buildFingerprintSQL();
|
|
507
|
-
let
|
|
510
|
+
let lastCount: number | null = null;
|
|
511
|
+
let lastMax: number | null = null;
|
|
512
|
+
let lastInMemoryRev: string | null = null;
|
|
513
|
+
let stopped = false;
|
|
508
514
|
|
|
509
|
-
const poll = () => {
|
|
515
|
+
const poll = async () => {
|
|
516
|
+
if (stopped) return;
|
|
510
517
|
try {
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
//
|
|
515
|
-
//
|
|
518
|
+
// Two-signal change detection:
|
|
519
|
+
// 1. In-memory revision (table-specific) → catches same-process writes
|
|
520
|
+
// 2. COUNT+MAX fingerprint (table-specific) → catches cross-process inserts/deletes
|
|
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.
|
|
516
525
|
const rev = this.revisionGetter?.() ?? '0';
|
|
517
|
-
const currentFingerprint = `${fpRow?._cnt ?? 0}:${fpRow?._max ?? 0}:${rev}`;
|
|
518
526
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
527
|
+
// Fast path: in-memory revision changed → our CRUD wrote to this table
|
|
528
|
+
const inMemoryChanged = rev !== lastInMemoryRev;
|
|
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) {
|
|
522
542
|
const rows = this.all();
|
|
523
|
-
callback(rows);
|
|
543
|
+
await callback(rows);
|
|
524
544
|
}
|
|
525
545
|
} catch {
|
|
526
546
|
// Silently skip on error (table might be in transition)
|
|
527
547
|
}
|
|
548
|
+
// Self-scheduling: next poll only after this one completes
|
|
549
|
+
if (!stopped) setTimeout(poll, interval);
|
|
528
550
|
};
|
|
529
551
|
|
|
530
552
|
// Immediate first execution
|
|
531
553
|
if (immediate) {
|
|
532
554
|
poll();
|
|
555
|
+
} else {
|
|
556
|
+
setTimeout(poll, interval);
|
|
533
557
|
}
|
|
534
558
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// Return unsubscribe function
|
|
538
|
-
return () => {
|
|
539
|
-
clearInterval(timer);
|
|
540
|
-
};
|
|
559
|
+
return () => { stopped = true; };
|
|
541
560
|
}
|
|
542
561
|
|
|
543
562
|
/**
|
|
@@ -548,10 +567,19 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
548
567
|
* `WHERE id > watermark` is rebuilt each poll with the latest value,
|
|
549
568
|
* so it's always O(new_rows) — not O(table_size).
|
|
550
569
|
*
|
|
570
|
+
* Composes with the query builder chain: any `.where()` conditions
|
|
571
|
+
* are combined with the watermark clause.
|
|
572
|
+
*
|
|
551
573
|
* ```ts
|
|
574
|
+
* // All new messages
|
|
552
575
|
* const unsub = db.messages.select().each((msg) => {
|
|
553
576
|
* console.log('New:', msg.text);
|
|
554
577
|
* });
|
|
578
|
+
*
|
|
579
|
+
* // Only new messages by Alice
|
|
580
|
+
* const unsub2 = db.messages.select()
|
|
581
|
+
* .where({ author: 'Alice' })
|
|
582
|
+
* .each((msg) => console.log(msg.text));
|
|
555
583
|
* ```
|
|
556
584
|
*
|
|
557
585
|
* @param callback Called once per new row. Async callbacks are awaited.
|
|
@@ -564,9 +592,14 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
564
592
|
): () => void {
|
|
565
593
|
const { interval = this.defaultPollInterval } = options;
|
|
566
594
|
|
|
567
|
-
//
|
|
595
|
+
// Compile the user's WHERE clause (if any) so we can combine with watermark
|
|
596
|
+
const userWhere = this.buildWhereClause();
|
|
597
|
+
|
|
598
|
+
// Initialize watermark to current max id, respecting user's WHERE clause
|
|
568
599
|
const maxRows = this.executor(
|
|
569
|
-
`SELECT MAX(id) as _max FROM ${this.tableName}
|
|
600
|
+
`SELECT MAX(id) as _max FROM ${this.tableName} ${userWhere.sql ? `WHERE ${userWhere.sql}` : ''}`,
|
|
601
|
+
userWhere.params,
|
|
602
|
+
true
|
|
570
603
|
);
|
|
571
604
|
let lastMaxId: number = (maxRows[0] as any)?._max ?? 0;
|
|
572
605
|
let lastRevision = this.revisionGetter?.() ?? '0';
|
|
@@ -579,16 +612,20 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
579
612
|
if (rev !== lastRevision) {
|
|
580
613
|
lastRevision = rev;
|
|
581
614
|
|
|
582
|
-
//
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
615
|
+
// Combine user WHERE with watermark: WHERE (user_conditions) AND id > ?
|
|
616
|
+
const params = [...userWhere.params, lastMaxId];
|
|
617
|
+
const whereClause = userWhere.sql
|
|
618
|
+
? `WHERE ${userWhere.sql} AND id > ? ORDER BY id ASC`
|
|
619
|
+
: `WHERE id > ? ORDER BY id ASC`;
|
|
620
|
+
const sql = `SELECT * FROM ${this.tableName} ${whereClause}`;
|
|
587
621
|
|
|
588
|
-
|
|
622
|
+
// raw=false → rows go through transform + get .update()/.delete() methods
|
|
623
|
+
const newRows = this.executor(sql, params, false);
|
|
624
|
+
|
|
625
|
+
for (const row of newRows) {
|
|
589
626
|
if (stopped) return;
|
|
590
|
-
await callback(
|
|
591
|
-
lastMaxId = (
|
|
627
|
+
await callback(row as T);
|
|
628
|
+
lastMaxId = (row as any).id;
|
|
592
629
|
}
|
|
593
630
|
}
|
|
594
631
|
|
|
@@ -599,16 +636,16 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
599
636
|
return () => { stopped = true; };
|
|
600
637
|
}
|
|
601
638
|
|
|
602
|
-
/**
|
|
603
|
-
private
|
|
639
|
+
/** Compile the IQO's WHERE conditions into a SQL fragment + params (without the WHERE keyword). */
|
|
640
|
+
private buildWhereClause(): { sql: string; params: any[] } {
|
|
604
641
|
const params: any[] = [];
|
|
605
|
-
let sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}`;
|
|
606
642
|
|
|
607
643
|
if (this.iqo.whereAST) {
|
|
608
644
|
const compiled = compileAST(this.iqo.whereAST);
|
|
609
|
-
sql
|
|
610
|
-
|
|
611
|
-
|
|
645
|
+
return { sql: compiled.sql, params: compiled.params };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (this.iqo.wheres.length > 0) {
|
|
612
649
|
const whereParts: string[] = [];
|
|
613
650
|
for (const w of this.iqo.wheres) {
|
|
614
651
|
if (w.operator === 'IN') {
|
|
@@ -625,10 +662,17 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
625
662
|
params.push(transformValueForStorage(w.value));
|
|
626
663
|
}
|
|
627
664
|
}
|
|
628
|
-
|
|
665
|
+
return { sql: whereParts.join(' AND '), params };
|
|
629
666
|
}
|
|
630
667
|
|
|
631
|
-
return { sql, params };
|
|
668
|
+
return { sql: '', params: [] };
|
|
669
|
+
}
|
|
670
|
+
|
|
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 };
|
|
632
676
|
}
|
|
633
677
|
|
|
634
678
|
// ---------- Thenable (async/await support) ----------
|