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 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
- const poll = () => {
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
- clearInterval(timer);
389
+ stopped = true;
384
390
  };
385
391
  }
386
392
  each(callback, options = {}) {
387
393
  const { interval = this.defaultPollInterval } = options;
388
- const maxRows = this.executor(`SELECT MAX(id) as _max FROM ${this.tableName}`, [], true);
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 newRows = this.executor(`SELECT * FROM ${this.tableName} WHERE id > ? ORDER BY id ASC`, [lastMaxId], true);
399
- for (const rawRow of newRows) {
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(rawRow);
403
- lastMaxId = rawRow.id;
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
- buildFingerprintSQL() {
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 += ` WHERE ${compiled.sql}`;
420
- params.push(...compiled.params);
421
- } else if (this.iqo.wheres.length > 0) {
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
- sql += ` WHERE ${whereParts.join(" AND ")}`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.7.1",
3
+ "version": "3.7.2",
4
4
  "description": "Type-safe SQLite ORM for Bun — Zod schemas, fluent queries, auto relationships, zero SQL",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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 that clears the polling interval.
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
- const timer = setInterval(poll, interval);
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
- // Initialize watermark to current max id
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}`, [], true
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
- // Fetch only new rows since watermark O(new_rows)
583
- const newRows = this.executor(
584
- `SELECT * FROM ${this.tableName} WHERE id > ? ORDER BY id ASC`,
585
- [lastMaxId], true
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 rawRow of newRows) {
610
+ for (const row of newRows) {
589
611
  if (stopped) return;
590
- await callback(rawRow as unknown as T);
591
- lastMaxId = (rawRow as any).id;
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
- /** Build a lightweight fingerprint query (COUNT + MAX(id)) that shares the same WHERE clause. */
603
- private buildFingerprintSQL(): { sql: string; params: any[] } {
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 += ` WHERE ${compiled.sql}`;
610
- params.push(...compiled.params);
611
- } else if (this.iqo.wheres.length > 0) {
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
- sql += ` WHERE ${whereParts.join(' AND ')}`;
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) ----------