sqlite-zod-orm 3.7.1 → 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 +30 -16
- package/package.json +1 -1
- package/src/query-builder.ts +58 -29
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,21 +374,25 @@ 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 = {}) {
|
|
387
393
|
const { interval = this.defaultPollInterval } = options;
|
|
388
|
-
const
|
|
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);
|
|
389
396
|
let lastMaxId = maxRows[0]?._max ?? 0;
|
|
390
397
|
let lastRevision = this.revisionGetter?.() ?? "0";
|
|
391
398
|
let stopped = false;
|
|
@@ -395,12 +402,15 @@ class QueryBuilder {
|
|
|
395
402
|
const rev = this.revisionGetter?.() ?? "0";
|
|
396
403
|
if (rev !== lastRevision) {
|
|
397
404
|
lastRevision = rev;
|
|
398
|
-
const
|
|
399
|
-
|
|
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) {
|
|
400
410
|
if (stopped)
|
|
401
411
|
return;
|
|
402
|
-
await callback(
|
|
403
|
-
lastMaxId =
|
|
412
|
+
await callback(row);
|
|
413
|
+
lastMaxId = row.id;
|
|
404
414
|
}
|
|
405
415
|
}
|
|
406
416
|
if (!stopped)
|
|
@@ -411,14 +421,13 @@ class QueryBuilder {
|
|
|
411
421
|
stopped = true;
|
|
412
422
|
};
|
|
413
423
|
}
|
|
414
|
-
|
|
424
|
+
buildWhereClause() {
|
|
415
425
|
const params = [];
|
|
416
|
-
let sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}`;
|
|
417
426
|
if (this.iqo.whereAST) {
|
|
418
427
|
const compiled = compileAST(this.iqo.whereAST);
|
|
419
|
-
sql
|
|
420
|
-
|
|
421
|
-
|
|
428
|
+
return { sql: compiled.sql, params: compiled.params };
|
|
429
|
+
}
|
|
430
|
+
if (this.iqo.wheres.length > 0) {
|
|
422
431
|
const whereParts = [];
|
|
423
432
|
for (const w of this.iqo.wheres) {
|
|
424
433
|
if (w.operator === "IN") {
|
|
@@ -435,9 +444,14 @@ class QueryBuilder {
|
|
|
435
444
|
params.push(transformValueForStorage(w.value));
|
|
436
445
|
}
|
|
437
446
|
}
|
|
438
|
-
|
|
447
|
+
return { sql: whereParts.join(" AND "), params };
|
|
439
448
|
}
|
|
440
|
-
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 };
|
|
441
455
|
}
|
|
442
456
|
then(onfulfilled, onrejected) {
|
|
443
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,24 +525,23 @@ 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
|
-
|
|
536
|
-
|
|
537
|
-
// Return unsubscribe function
|
|
538
|
-
return () => {
|
|
539
|
-
clearInterval(timer);
|
|
540
|
-
};
|
|
544
|
+
return () => { stopped = true; };
|
|
541
545
|
}
|
|
542
546
|
|
|
543
547
|
/**
|
|
@@ -548,10 +552,19 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
548
552
|
* `WHERE id > watermark` is rebuilt each poll with the latest value,
|
|
549
553
|
* so it's always O(new_rows) — not O(table_size).
|
|
550
554
|
*
|
|
555
|
+
* Composes with the query builder chain: any `.where()` conditions
|
|
556
|
+
* are combined with the watermark clause.
|
|
557
|
+
*
|
|
551
558
|
* ```ts
|
|
559
|
+
* // All new messages
|
|
552
560
|
* const unsub = db.messages.select().each((msg) => {
|
|
553
561
|
* console.log('New:', msg.text);
|
|
554
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));
|
|
555
568
|
* ```
|
|
556
569
|
*
|
|
557
570
|
* @param callback Called once per new row. Async callbacks are awaited.
|
|
@@ -564,9 +577,14 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
564
577
|
): () => void {
|
|
565
578
|
const { interval = this.defaultPollInterval } = options;
|
|
566
579
|
|
|
567
|
-
//
|
|
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
|
|
568
584
|
const maxRows = this.executor(
|
|
569
|
-
`SELECT MAX(id) as _max FROM ${this.tableName}
|
|
585
|
+
`SELECT MAX(id) as _max FROM ${this.tableName} ${userWhere.sql ? `WHERE ${userWhere.sql}` : ''}`,
|
|
586
|
+
userWhere.params,
|
|
587
|
+
true
|
|
570
588
|
);
|
|
571
589
|
let lastMaxId: number = (maxRows[0] as any)?._max ?? 0;
|
|
572
590
|
let lastRevision = this.revisionGetter?.() ?? '0';
|
|
@@ -579,16 +597,20 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
579
597
|
if (rev !== lastRevision) {
|
|
580
598
|
lastRevision = rev;
|
|
581
599
|
|
|
582
|
-
//
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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);
|
|
587
609
|
|
|
588
|
-
for (const
|
|
610
|
+
for (const row of newRows) {
|
|
589
611
|
if (stopped) return;
|
|
590
|
-
await callback(
|
|
591
|
-
lastMaxId = (
|
|
612
|
+
await callback(row as T);
|
|
613
|
+
lastMaxId = (row as any).id;
|
|
592
614
|
}
|
|
593
615
|
}
|
|
594
616
|
|
|
@@ -599,16 +621,16 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
599
621
|
return () => { stopped = true; };
|
|
600
622
|
}
|
|
601
623
|
|
|
602
|
-
/**
|
|
603
|
-
private
|
|
624
|
+
/** Compile the IQO's WHERE conditions into a SQL fragment + params (without the WHERE keyword). */
|
|
625
|
+
private buildWhereClause(): { sql: string; params: any[] } {
|
|
604
626
|
const params: any[] = [];
|
|
605
|
-
let sql = `SELECT COUNT(*) as _cnt, MAX(id) as _max FROM ${this.tableName}`;
|
|
606
627
|
|
|
607
628
|
if (this.iqo.whereAST) {
|
|
608
629
|
const compiled = compileAST(this.iqo.whereAST);
|
|
609
|
-
sql
|
|
610
|
-
|
|
611
|
-
|
|
630
|
+
return { sql: compiled.sql, params: compiled.params };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (this.iqo.wheres.length > 0) {
|
|
612
634
|
const whereParts: string[] = [];
|
|
613
635
|
for (const w of this.iqo.wheres) {
|
|
614
636
|
if (w.operator === 'IN') {
|
|
@@ -625,10 +647,17 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
625
647
|
params.push(transformValueForStorage(w.value));
|
|
626
648
|
}
|
|
627
649
|
}
|
|
628
|
-
|
|
650
|
+
return { sql: whereParts.join(' AND '), params };
|
|
629
651
|
}
|
|
630
652
|
|
|
631
|
-
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 };
|
|
632
661
|
}
|
|
633
662
|
|
|
634
663
|
// ---------- Thenable (async/await support) ----------
|