sqlite-zod-orm 3.7.0 → 3.7.2
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 +53 -11
- package/package.json +1 -1
- package/src/query-builder.ts +105 -17
package/dist/index.js
CHANGED
|
@@ -362,7 +362,10 @@ class QueryBuilder {
|
|
|
362
362
|
const { interval = this.defaultPollInterval, immediate = true } = options;
|
|
363
363
|
const fingerprintSQL = this.buildFingerprintSQL();
|
|
364
364
|
let lastFingerprint = null;
|
|
365
|
-
|
|
365
|
+
let stopped = false;
|
|
366
|
+
const poll = async () => {
|
|
367
|
+
if (stopped)
|
|
368
|
+
return;
|
|
366
369
|
try {
|
|
367
370
|
const fpRows = this.executor(fingerprintSQL.sql, fingerprintSQL.params, true);
|
|
368
371
|
const fpRow = fpRows[0];
|
|
@@ -371,26 +374,60 @@ class QueryBuilder {
|
|
|
371
374
|
if (currentFingerprint !== lastFingerprint) {
|
|
372
375
|
lastFingerprint = currentFingerprint;
|
|
373
376
|
const rows = this.all();
|
|
374
|
-
callback(rows);
|
|
377
|
+
await callback(rows);
|
|
375
378
|
}
|
|
376
379
|
} catch {}
|
|
380
|
+
if (!stopped)
|
|
381
|
+
setTimeout(poll, interval);
|
|
377
382
|
};
|
|
378
383
|
if (immediate) {
|
|
379
384
|
poll();
|
|
385
|
+
} else {
|
|
386
|
+
setTimeout(poll, interval);
|
|
380
387
|
}
|
|
381
|
-
const timer = setInterval(poll, interval);
|
|
382
388
|
return () => {
|
|
383
|
-
|
|
389
|
+
stopped = true;
|
|
384
390
|
};
|
|
385
391
|
}
|
|
386
|
-
|
|
392
|
+
each(callback, options = {}) {
|
|
393
|
+
const { interval = this.defaultPollInterval } = options;
|
|
394
|
+
const userWhere = this.buildWhereClause();
|
|
395
|
+
const maxRows = this.executor(`SELECT MAX(id) as _max FROM ${this.tableName} ${userWhere.sql ? `WHERE ${userWhere.sql}` : ""}`, userWhere.params, true);
|
|
396
|
+
let lastMaxId = maxRows[0]?._max ?? 0;
|
|
397
|
+
let lastRevision = this.revisionGetter?.() ?? "0";
|
|
398
|
+
let stopped = false;
|
|
399
|
+
const poll = async () => {
|
|
400
|
+
if (stopped)
|
|
401
|
+
return;
|
|
402
|
+
const rev = this.revisionGetter?.() ?? "0";
|
|
403
|
+
if (rev !== lastRevision) {
|
|
404
|
+
lastRevision = rev;
|
|
405
|
+
const params = [...userWhere.params, lastMaxId];
|
|
406
|
+
const whereClause = userWhere.sql ? `WHERE ${userWhere.sql} AND id > ? ORDER BY id ASC` : `WHERE id > ? ORDER BY id ASC`;
|
|
407
|
+
const sql = `SELECT * FROM ${this.tableName} ${whereClause}`;
|
|
408
|
+
const newRows = this.executor(sql, params, false);
|
|
409
|
+
for (const row of newRows) {
|
|
410
|
+
if (stopped)
|
|
411
|
+
return;
|
|
412
|
+
await callback(row);
|
|
413
|
+
lastMaxId = row.id;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (!stopped)
|
|
417
|
+
setTimeout(poll, interval);
|
|
418
|
+
};
|
|
419
|
+
setTimeout(poll, interval);
|
|
420
|
+
return () => {
|
|
421
|
+
stopped = true;
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
buildWhereClause() {
|
|
387
425
|
const params = [];
|
|
388
|
-
let sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}`;
|
|
389
426
|
if (this.iqo.whereAST) {
|
|
390
427
|
const compiled = compileAST(this.iqo.whereAST);
|
|
391
|
-
sql
|
|
392
|
-
|
|
393
|
-
|
|
428
|
+
return { sql: compiled.sql, params: compiled.params };
|
|
429
|
+
}
|
|
430
|
+
if (this.iqo.wheres.length > 0) {
|
|
394
431
|
const whereParts = [];
|
|
395
432
|
for (const w of this.iqo.wheres) {
|
|
396
433
|
if (w.operator === "IN") {
|
|
@@ -407,9 +444,14 @@ class QueryBuilder {
|
|
|
407
444
|
params.push(transformValueForStorage(w.value));
|
|
408
445
|
}
|
|
409
446
|
}
|
|
410
|
-
|
|
447
|
+
return { sql: whereParts.join(" AND "), params };
|
|
411
448
|
}
|
|
412
|
-
return { sql, params };
|
|
449
|
+
return { sql: "", params: [] };
|
|
450
|
+
}
|
|
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 };
|
|
413
455
|
}
|
|
414
456
|
then(onfulfilled, onrejected) {
|
|
415
457
|
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,12 +495,12 @@ 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;
|
|
@@ -505,8 +508,10 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
505
508
|
// Build the fingerprint SQL (COUNT + MAX(id)) using the same WHERE
|
|
506
509
|
const fingerprintSQL = this.buildFingerprintSQL();
|
|
507
510
|
let lastFingerprint: string | null = null;
|
|
511
|
+
let stopped = false;
|
|
508
512
|
|
|
509
|
-
const poll = () => {
|
|
513
|
+
const poll = async () => {
|
|
514
|
+
if (stopped) return;
|
|
510
515
|
try {
|
|
511
516
|
// Run lightweight fingerprint check
|
|
512
517
|
const fpRows = this.executor(fingerprintSQL.sql, fingerprintSQL.params, true);
|
|
@@ -520,36 +525,112 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
520
525
|
lastFingerprint = currentFingerprint;
|
|
521
526
|
// Fingerprint changed → re-execute the full query
|
|
522
527
|
const rows = this.all();
|
|
523
|
-
callback(rows);
|
|
528
|
+
await callback(rows);
|
|
524
529
|
}
|
|
525
530
|
} catch {
|
|
526
531
|
// Silently skip on error (table might be in transition)
|
|
527
532
|
}
|
|
533
|
+
// Self-scheduling: next poll only after this one completes
|
|
534
|
+
if (!stopped) setTimeout(poll, interval);
|
|
528
535
|
};
|
|
529
536
|
|
|
530
537
|
// Immediate first execution
|
|
531
538
|
if (immediate) {
|
|
532
539
|
poll();
|
|
540
|
+
} else {
|
|
541
|
+
setTimeout(poll, interval);
|
|
533
542
|
}
|
|
534
543
|
|
|
535
|
-
|
|
544
|
+
return () => { stopped = true; };
|
|
545
|
+
}
|
|
536
546
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
547
|
+
/**
|
|
548
|
+
* Stream new rows one at a time via a watermark (last seen id).
|
|
549
|
+
*
|
|
550
|
+
* Unlike `.subscribe()` (which gives you an array snapshot), `.each()`
|
|
551
|
+
* calls your callback once per new row, in insertion order. The SQL
|
|
552
|
+
* `WHERE id > watermark` is rebuilt each poll with the latest value,
|
|
553
|
+
* so it's always O(new_rows) — not O(table_size).
|
|
554
|
+
*
|
|
555
|
+
* Composes with the query builder chain: any `.where()` conditions
|
|
556
|
+
* are combined with the watermark clause.
|
|
557
|
+
*
|
|
558
|
+
* ```ts
|
|
559
|
+
* // All new messages
|
|
560
|
+
* const unsub = db.messages.select().each((msg) => {
|
|
561
|
+
* console.log('New:', msg.text);
|
|
562
|
+
* });
|
|
563
|
+
*
|
|
564
|
+
* // Only new messages by Alice
|
|
565
|
+
* const unsub2 = db.messages.select()
|
|
566
|
+
* .where({ author: 'Alice' })
|
|
567
|
+
* .each((msg) => console.log(msg.text));
|
|
568
|
+
* ```
|
|
569
|
+
*
|
|
570
|
+
* @param callback Called once per new row. Async callbacks are awaited.
|
|
571
|
+
* @param options `interval` in ms (default: pollInterval).
|
|
572
|
+
* @returns Unsubscribe function.
|
|
573
|
+
*/
|
|
574
|
+
each(
|
|
575
|
+
callback: (row: T) => void | Promise<void>,
|
|
576
|
+
options: { interval?: number } = {},
|
|
577
|
+
): () => void {
|
|
578
|
+
const { interval = this.defaultPollInterval } = options;
|
|
579
|
+
|
|
580
|
+
// Compile the user's WHERE clause (if any) so we can combine with watermark
|
|
581
|
+
const userWhere = this.buildWhereClause();
|
|
582
|
+
|
|
583
|
+
// Initialize watermark to current max id, respecting user's WHERE clause
|
|
584
|
+
const maxRows = this.executor(
|
|
585
|
+
`SELECT MAX(id) as _max FROM ${this.tableName} ${userWhere.sql ? `WHERE ${userWhere.sql}` : ''}`,
|
|
586
|
+
userWhere.params,
|
|
587
|
+
true
|
|
588
|
+
);
|
|
589
|
+
let lastMaxId: number = (maxRows[0] as any)?._max ?? 0;
|
|
590
|
+
let lastRevision = this.revisionGetter?.() ?? '0';
|
|
591
|
+
let stopped = false;
|
|
592
|
+
|
|
593
|
+
const poll = async () => {
|
|
594
|
+
if (stopped) return;
|
|
595
|
+
|
|
596
|
+
const rev = this.revisionGetter?.() ?? '0';
|
|
597
|
+
if (rev !== lastRevision) {
|
|
598
|
+
lastRevision = rev;
|
|
599
|
+
|
|
600
|
+
// Combine user WHERE with watermark: WHERE (user_conditions) AND id > ?
|
|
601
|
+
const params = [...userWhere.params, lastMaxId];
|
|
602
|
+
const whereClause = userWhere.sql
|
|
603
|
+
? `WHERE ${userWhere.sql} AND id > ? ORDER BY id ASC`
|
|
604
|
+
: `WHERE id > ? ORDER BY id ASC`;
|
|
605
|
+
const sql = `SELECT * FROM ${this.tableName} ${whereClause}`;
|
|
606
|
+
|
|
607
|
+
// raw=false → rows go through transform + get .update()/.delete() methods
|
|
608
|
+
const newRows = this.executor(sql, params, false);
|
|
609
|
+
|
|
610
|
+
for (const row of newRows) {
|
|
611
|
+
if (stopped) return;
|
|
612
|
+
await callback(row as T);
|
|
613
|
+
lastMaxId = (row as any).id;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (!stopped) setTimeout(poll, interval);
|
|
540
618
|
};
|
|
619
|
+
|
|
620
|
+
setTimeout(poll, interval);
|
|
621
|
+
return () => { stopped = true; };
|
|
541
622
|
}
|
|
542
623
|
|
|
543
|
-
/**
|
|
544
|
-
private
|
|
624
|
+
/** Compile the IQO's WHERE conditions into a SQL fragment + params (without the WHERE keyword). */
|
|
625
|
+
private buildWhereClause(): { sql: string; params: any[] } {
|
|
545
626
|
const params: any[] = [];
|
|
546
|
-
let sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}`;
|
|
547
627
|
|
|
548
628
|
if (this.iqo.whereAST) {
|
|
549
629
|
const compiled = compileAST(this.iqo.whereAST);
|
|
550
|
-
sql
|
|
551
|
-
|
|
552
|
-
|
|
630
|
+
return { sql: compiled.sql, params: compiled.params };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (this.iqo.wheres.length > 0) {
|
|
553
634
|
const whereParts: string[] = [];
|
|
554
635
|
for (const w of this.iqo.wheres) {
|
|
555
636
|
if (w.operator === 'IN') {
|
|
@@ -566,10 +647,17 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
566
647
|
params.push(transformValueForStorage(w.value));
|
|
567
648
|
}
|
|
568
649
|
}
|
|
569
|
-
|
|
650
|
+
return { sql: whereParts.join(' AND '), params };
|
|
570
651
|
}
|
|
571
652
|
|
|
572
|
-
return { sql, params };
|
|
653
|
+
return { sql: '', params: [] };
|
|
654
|
+
}
|
|
655
|
+
|
|
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 };
|
|
573
661
|
}
|
|
574
662
|
|
|
575
663
|
// ---------- Thenable (async/await support) ----------
|