@atscript/db-sqlite 0.1.126 → 0.1.128

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.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
- import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace } from "@atscript/db";
3
- import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, geoWindowFromControls, parseRegexString, refActionToSql, renameGeoDistance, toSqlValue } from "@atscript/db-sql-tools";
2
+ import { BaseDbAdapter, DbError, DbSpace } from "@atscript/db";
3
+ import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, geoWindowFromControls, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, toSqlValue } from "@atscript/db-sql-tools";
4
+ import { AsyncLocalStorage } from "node:async_hooks";
4
5
  //#region src/better-sqlite3-driver.ts
5
6
  /**
6
7
  * {@link TSqliteDriver} implementation backed by `better-sqlite3`.
@@ -349,6 +350,212 @@ function buildPrefixedWhere(alias, filter) {
349
350
  }, filter);
350
351
  }
351
352
  //#endregion
353
+ //#region src/tx-gate.ts
354
+ const DEFAULT_WARN_MS = 5e3;
355
+ /** Async context that holds a gate exclusively via {@link SqliteTxGate.runExclusive}. */
356
+ const exclusiveHolds = new AsyncLocalStorage();
357
+ /**
358
+ * One waiter for the gate, created once per `_stmt` / `acquire` call and kept
359
+ * across wake-ups: the timeout and warn timers are armed ONCE with an absolute
360
+ * deadline, so `transactionWaitTimeoutMs` bounds the waiter's TOTAL wait
361
+ * however many transactions run ahead of it, and a re-queued waiter allocates
362
+ * nothing new. `next()` resolves on the next release (or rejects once the
363
+ * deadline passed); `dispose()` clears the timers.
364
+ */
365
+ var GateWaiter = class {
366
+ _gate;
367
+ _resolve;
368
+ _reject;
369
+ _timeoutTimer;
370
+ _warnTimer;
371
+ _expired;
372
+ _startedAt = Date.now();
373
+ constructor(_gate, opts) {
374
+ this._gate = _gate;
375
+ const timeoutMs = opts?.transactionWaitTimeoutMs;
376
+ const warnMs = opts?.transactionWaitWarnMs ?? DEFAULT_WARN_MS;
377
+ const heldSince = this._gate.heldSince;
378
+ if (timeoutMs !== void 0 && timeoutMs >= 0) {
379
+ this._timeoutTimer = setTimeout(() => {
380
+ const waited = Date.now() - this._startedAt;
381
+ const heldFor = heldSince ? Date.now() - heldSince : 0;
382
+ this._expired = new DbError("TX_WAIT_TIMEOUT", [{
383
+ path: "",
384
+ message: `SQLite transaction gate: waited ${waited}ms for the connection (a transaction has been open for ${heldFor}ms) — transactionWaitTimeoutMs exceeded`
385
+ }]);
386
+ this._gate._dequeue(this);
387
+ const reject = this._reject;
388
+ this._resolve = void 0;
389
+ this._reject = void 0;
390
+ reject?.(this._expired);
391
+ }, timeoutMs);
392
+ this._timeoutTimer.unref?.();
393
+ }
394
+ if (warnMs > 0 && opts?.logger) {
395
+ const logger = opts.logger;
396
+ this._warnTimer = setTimeout(() => {
397
+ const waited = Date.now() - this._startedAt;
398
+ const since = heldSince ? new Date(heldSince).toISOString() : "unknown";
399
+ logger.warn(`[atscript-db-sqlite] a statement has been waiting ${waited}ms for an open transaction (held since ${since}); never await external I/O inside withTransaction on SQLite`);
400
+ }, warnMs);
401
+ this._warnTimer.unref?.();
402
+ }
403
+ }
404
+ /** Resolves on the NEXT release; rejects with `TX_WAIT_TIMEOUT` once the deadline passed. */
405
+ next() {
406
+ if (this._expired) return Promise.reject(this._expired);
407
+ return new Promise((resolve, reject) => {
408
+ this._resolve = resolve;
409
+ this._reject = reject;
410
+ this._gate._enqueue(this);
411
+ });
412
+ }
413
+ /** Called by the gate on release: wakes the pending `next()` (a no-op when none is pending). */
414
+ wake() {
415
+ const resolve = this._resolve;
416
+ this._resolve = void 0;
417
+ this._reject = void 0;
418
+ resolve?.();
419
+ }
420
+ dispose() {
421
+ if (this._timeoutTimer !== void 0) clearTimeout(this._timeoutTimer);
422
+ if (this._warnTimer !== void 0) clearTimeout(this._warnTimer);
423
+ }
424
+ };
425
+ /**
426
+ * Logical FIFO mutex around the single synchronous SQLite connection.
427
+ *
428
+ * SQLite has one connection per driver, so two async contexts cannot each
429
+ * own a transaction: a second `BEGIN` fails with "cannot start a transaction
430
+ * within a transaction", and — worse — any statement from another context
431
+ * that runs while a transaction is open executes INSIDE that transaction
432
+ * (rolled back with it, reads its uncommitted rows). The gate serialises:
433
+ *
434
+ * - {@link acquire} / {@link release} — taken by `BEGIN`, released by
435
+ * `COMMIT` / `ROLLBACK`;
436
+ * - {@link runWhenFree} — plain (non-transactional) statements check
437
+ * {@link held} synchronously right before executing and wait otherwise;
438
+ * - {@link runExclusive} — hold the connection without a transaction (schema sync).
439
+ *
440
+ * Fairness: one FIFO waiter list for acquirers and statement waiters alike;
441
+ * a release wakes every current waiter in registration order, each re-checks
442
+ * synchronously and re-queues (ahead of any new arrival) if someone ahead took
443
+ * the gate. A waiter is one object with timers armed once, whatever the
444
+ * number of wake-ups. The fast path (nothing held) allocates nothing.
445
+ */
446
+ var SqliteTxGate = class {
447
+ /** True while a transaction (or an exclusive hold) owns the connection. */
448
+ held = false;
449
+ /** `Date.now()` when the current holder took the gate (0 when free). */
450
+ heldSince = 0;
451
+ _waiters = [];
452
+ /** True when the current async context holds this gate via {@link runExclusive}. */
453
+ get heldByCurrentContext() {
454
+ return exclusiveHolds.getStore() === this;
455
+ }
456
+ /**
457
+ * Waits until the gate is free, then takes it synchronously (no `await`
458
+ * between the check and `held = true`). Pair with {@link release}.
459
+ */
460
+ async acquire(opts) {
461
+ if (this.held) {
462
+ const waiter = new GateWaiter(this, opts);
463
+ try {
464
+ do
465
+ await waiter.next();
466
+ while (this.held);
467
+ } finally {
468
+ waiter.dispose();
469
+ }
470
+ }
471
+ this.held = true;
472
+ this.heldSince = Date.now();
473
+ }
474
+ /** Frees the gate and wakes every waiter (in registration order). */
475
+ release() {
476
+ this.held = false;
477
+ this.heldSince = 0;
478
+ const waiters = this._waiters;
479
+ this._waiters = [];
480
+ for (const w of waiters) w.wake();
481
+ }
482
+ /**
483
+ * Runs one synchronous statement once no transaction holds the connection
484
+ * (immediately when the gate is free). The `held` check and `fn` run in
485
+ * the same synchronous segment, so they cannot interleave with a `BEGIN`
486
+ * from another context.
487
+ */
488
+ async runWhenFree(fn, opts) {
489
+ if (this.held) {
490
+ const waiter = new GateWaiter(this, opts);
491
+ try {
492
+ do
493
+ await waiter.next();
494
+ while (this.held);
495
+ } finally {
496
+ waiter.dispose();
497
+ }
498
+ }
499
+ return fn();
500
+ }
501
+ /**
502
+ * Holds the gate for the duration of `fn` WITHOUT opening a transaction
503
+ * (nested transactions inside `fn` still `BEGIN`/`COMMIT` on the held
504
+ * connection without re-acquiring). Re-entrant for the same async context.
505
+ */
506
+ async runExclusive(fn, opts) {
507
+ if (this.heldByCurrentContext) return fn();
508
+ await this.acquire(opts);
509
+ try {
510
+ return await exclusiveHolds.run(this, fn);
511
+ } finally {
512
+ this.release();
513
+ }
514
+ }
515
+ /** @internal */
516
+ _enqueue(waiter) {
517
+ this._waiters.push(waiter);
518
+ }
519
+ /** @internal */
520
+ _dequeue(waiter) {
521
+ const idx = this._waiters.indexOf(waiter);
522
+ if (idx !== -1) this._waiters.splice(idx, 1);
523
+ }
524
+ };
525
+ /**
526
+ * Transaction state returned by `SqliteAdapter._beginTransaction`: releases
527
+ * the gate the transaction holds exactly once (a transaction opened inside an
528
+ * exclusive hold owns nothing to release). Ownership across adapters is
529
+ * decided by the core (`_transactionOwner()` = the driver), not here.
530
+ */
531
+ var SqliteTxState = class {
532
+ _gate;
533
+ _holdsGate;
534
+ constructor(_gate, _holdsGate) {
535
+ this._gate = _gate;
536
+ this._holdsGate = _holdsGate;
537
+ }
538
+ release() {
539
+ if (!this._holdsGate) return;
540
+ this._holdsGate = false;
541
+ this._gate.release();
542
+ }
543
+ };
544
+ const gates = /* @__PURE__ */ new WeakMap();
545
+ /**
546
+ * Returns the gate for a driver instance (one gate per driver — every adapter
547
+ * of a space shares its driver, so they share the gate; two driver wrappers
548
+ * around one `Database` would get two gates: construct one driver per database).
549
+ */
550
+ function getSqliteTxGate(driver) {
551
+ let gate = gates.get(driver);
552
+ if (!gate) {
553
+ gate = new SqliteTxGate();
554
+ gates.set(driver, gate);
555
+ }
556
+ return gate;
557
+ }
558
+ //#endregion
352
559
  //#region src/sqlite-adapter.ts
353
560
  /**
354
561
  * SQLite adapter for {@link AtscriptDbTable}.
@@ -380,9 +587,14 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
380
587
  _vectorThresholds = /* @__PURE__ */ new Map();
381
588
  /** Partition filter fields per vector index (from @db.search.filter). Field paths. */
382
589
  _vectorPartitionFields = /* @__PURE__ */ new Map();
383
- constructor(driver) {
590
+ /** Per-driver transaction gate (shared by every adapter over this driver). */
591
+ _gate;
592
+ _txOptions;
593
+ constructor(driver, options) {
384
594
  super();
385
595
  this.driver = driver;
596
+ this._gate = getSqliteTxGate(driver);
597
+ this._txOptions = { ...options };
386
598
  this.driver.exec("PRAGMA foreign_keys = ON");
387
599
  }
388
600
  onFieldScanned(field, _type, metadata) {
@@ -444,17 +656,74 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
444
656
  if (!this._supportsVector && this._vectorFields.size > 0) this._log("[atscript-db-sqlite] sqlite-vec extension not available — vector fields will be stored as JSON TEXT (no similarity search).");
445
657
  return this._supportsVector;
446
658
  }
659
+ _waitOpts() {
660
+ return {
661
+ ...this._txOptions,
662
+ logger: this.logger
663
+ };
664
+ }
665
+ /** Every adapter over this driver shares one transaction — and one gate (since 0.1.128). */
666
+ _transactionOwner() {
667
+ return this.driver;
668
+ }
669
+ /** Inside our own transaction, or holding the connection exclusively. */
670
+ _ownsConnection() {
671
+ return this._getTransactionState() !== void 0 || this._gate.heldByCurrentContext;
672
+ }
673
+ /**
674
+ * Runs one synchronous driver statement. Inside our own transaction (or an
675
+ * exclusive hold) it runs immediately; otherwise it waits until no
676
+ * transaction is open on the connection. The `held` check and the statement
677
+ * run in the same synchronous segment, so they cannot interleave with a
678
+ * `BEGIN` from another context.
679
+ */
680
+ async _stmt(fn) {
681
+ if (!this._gate.held || this._ownsConnection()) return fn();
682
+ return this._gate.runWhenFree(fn, this._waitOpts());
683
+ }
684
+ /**
685
+ * Holds the connection for the duration of `fn` WITHOUT opening a
686
+ * transaction: no other context's statement or transaction can interleave.
687
+ * Meant for the schema entry points that `await` mid-body or toggle
688
+ * connection-level PRAGMAs (`ensureTable`, `recreateTable`, `dropColumns`,
689
+ * `dropTablesByName`, `syncIndexes`) — their DDL must not land inside a
690
+ * request's transaction (PRAGMA changes are no-ops inside one). Inside our
691
+ * own transaction `fn` runs directly; re-entrant for the same async
692
+ * context; nested `withTransaction` calls inside `fn` still BEGIN/COMMIT
693
+ * on the held connection.
694
+ */
695
+ async _withExclusiveConnection(fn) {
696
+ if (this._getTransactionState() !== void 0) return fn();
697
+ return this._gate.runExclusive(fn, this._waitOpts());
698
+ }
447
699
  async _beginTransaction() {
448
- this._log("BEGIN");
449
- this.driver.exec("BEGIN");
700
+ const holdsGate = !this._gate.heldByCurrentContext;
701
+ if (holdsGate) await this._gate.acquire(this._waitOpts());
702
+ const state = new SqliteTxState(this._gate, holdsGate);
703
+ try {
704
+ this._log("BEGIN IMMEDIATE");
705
+ this.driver.exec("BEGIN IMMEDIATE");
706
+ } catch (error) {
707
+ state.release();
708
+ throw error;
709
+ }
710
+ return state;
450
711
  }
451
- async _commitTransaction() {
452
- this._log("COMMIT");
453
- this.driver.exec("COMMIT");
712
+ async _commitTransaction(state) {
713
+ try {
714
+ this._log("COMMIT");
715
+ this.driver.exec("COMMIT");
716
+ } finally {
717
+ state.release();
718
+ }
454
719
  }
455
- async _rollbackTransaction() {
456
- this._log("ROLLBACK");
457
- this.driver.exec("ROLLBACK");
720
+ async _rollbackTransaction(state) {
721
+ try {
722
+ this._log("ROLLBACK");
723
+ this.driver.exec("ROLLBACK");
724
+ } finally {
725
+ state.release();
726
+ }
458
727
  }
459
728
  /** SQLite does not use schemas — override to always exclude schema. */
460
729
  resolveTableName() {
@@ -496,7 +765,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
496
765
  async insertOne(data) {
497
766
  const { sql, params } = buildInsert$1(this.resolveTableName(), data);
498
767
  this._log(sql, params);
499
- const result = this._wrapConstraintError(() => this.driver.run(sql, params));
768
+ const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
500
769
  return { insertedId: this._resolveInsertedId(data, result.lastInsertRowid) };
501
770
  }
502
771
  async insertMany(data) {
@@ -505,7 +774,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
505
774
  for (const row of data) {
506
775
  const { sql, params } = buildInsert$1(this.resolveTableName(), row);
507
776
  this._log(sql, params);
508
- const result = this._wrapConstraintError(() => this.driver.run(sql, params));
777
+ const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
509
778
  ids.push(this._resolveInsertedId(row, result.lastInsertRowid));
510
779
  }
511
780
  return {
@@ -522,19 +791,19 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
522
791
  };
523
792
  const { sql, params } = buildSelect$1(this.resolveTableName(), where, controls);
524
793
  this._log(sql, params);
525
- return this.driver.get(sql, params);
794
+ return this._stmt(() => this.driver.get(sql, params));
526
795
  }
527
796
  async findMany(query) {
528
797
  const where = buildWhere(query.filter);
529
798
  const { sql, params } = buildSelect$1(this.resolveTableName(), where, query.controls);
530
799
  this._log(sql, params);
531
- return this.driver.all(sql, params);
800
+ return this._stmt(() => this.driver.all(sql, params));
532
801
  }
533
802
  async count(query) {
534
803
  const where = buildWhere(query.filter);
535
804
  const sql = `SELECT COUNT(*) as cnt FROM "${esc(this.resolveTableName())}" WHERE ${where.sql}`;
536
805
  this._log(sql, where.params);
537
- return this.driver.get(sql, where.params)?.cnt ?? 0;
806
+ return (await this._stmt(() => this.driver.get(sql, where.params)))?.cnt ?? 0;
538
807
  }
539
808
  async aggregate(query) {
540
809
  const where = buildWhere(query.filter);
@@ -542,11 +811,11 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
542
811
  if (query.controls.$count) {
543
812
  const { sql, params } = buildAggregateCount$1(tableName, where, query.controls);
544
813
  this._log(sql, params);
545
- return [{ count: this.driver.get(sql, params)?.count ?? 0 }];
814
+ return [{ count: (await this._stmt(() => this.driver.get(sql, params)))?.count ?? 0 }];
546
815
  }
547
816
  const { sql, params } = buildAggregateSelect$1(tableName, where, query.controls);
548
817
  this._log(sql, params);
549
- return this.driver.all(sql, params);
818
+ return this._stmt(() => this.driver.all(sql, params));
550
819
  }
551
820
  async updateOne(filter, data, ops, expectedVersion) {
552
821
  const where = buildWhere(filter);
@@ -557,7 +826,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
557
826
  params: where.params
558
827
  }, ops, versionColumn, expectedVersion);
559
828
  this._log(sql, params);
560
- const result = this._wrapConstraintError(() => this.driver.run(sql, params));
829
+ const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
561
830
  return {
562
831
  matchedCount: result.changes,
563
832
  modifiedCount: result.changes
@@ -568,7 +837,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
568
837
  const versionColumn = this._table.versionColumn;
569
838
  const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, ops, versionColumn);
570
839
  this._log(sql, params);
571
- const result = this._wrapConstraintError(() => this.driver.run(sql, params));
840
+ const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
572
841
  return {
573
842
  matchedCount: result.changes,
574
843
  modifiedCount: result.changes
@@ -578,12 +847,12 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
578
847
  const where = buildWhere(filter);
579
848
  const tableName = this.resolveTableName();
580
849
  const versionColumn = this._table.versionColumn;
581
- const { sql, params } = buildUpdate$1(tableName, data, {
850
+ const { sql, params } = buildUpdate$1(tableName, fillReplacePayload(data, replaceColumnsFor(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn), {
582
851
  sql: `rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`,
583
852
  params: where.params
584
853
  }, void 0, versionColumn, expectedVersion);
585
854
  this._log(sql, params);
586
- const result = this._wrapConstraintError(() => this.driver.run(sql, params));
855
+ const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
587
856
  return {
588
857
  matchedCount: result.changes,
589
858
  modifiedCount: result.changes
@@ -594,7 +863,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
594
863
  const versionColumn = this._table.versionColumn;
595
864
  const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, versionColumn);
596
865
  this._log(sql, params);
597
- const result = this._wrapConstraintError(() => this.driver.run(sql, params));
866
+ const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
598
867
  return {
599
868
  matchedCount: result.changes,
600
869
  modifiedCount: result.changes
@@ -605,20 +874,26 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
605
874
  const tableName = this.resolveTableName();
606
875
  const sql = `DELETE FROM "${esc(tableName)}" WHERE rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`;
607
876
  this._log(sql, where.params);
608
- return { deletedCount: this._wrapConstraintError(() => this.driver.run(sql, where.params)).changes };
877
+ return { deletedCount: (await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, where.params)))).changes };
609
878
  }
610
879
  async deleteMany(filter) {
611
880
  const where = buildWhere(filter);
612
881
  const { sql, params } = buildDelete$1(this.resolveTableName(), where);
613
882
  this._log(sql, params);
614
- return { deletedCount: this._wrapConstraintError(() => this.driver.run(sql, params)).changes };
883
+ return { deletedCount: (await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)))).changes };
615
884
  }
616
- async ensureTable() {
617
- if (this._table instanceof AtscriptDbView) return this.ensureView();
618
- const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, { typeMapper: (field) => this.typeMapper(field) });
619
- this._log(sql);
620
- this.driver.exec(sql);
621
- this._seedIncrementStart();
885
+ /**
886
+ * SQLite accepts forward FK references, so a foreign-key cycle is created
887
+ * inline in any order — `opts.deferForeignKeysTo` is accepted and ignored.
888
+ */
889
+ async ensureTable(_opts) {
890
+ return this._withExclusiveConnection(async () => {
891
+ if (this._table.isView) return this.ensureView();
892
+ const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, { typeMapper: (field) => this.typeMapper(field) });
893
+ this._log(sql);
894
+ this.driver.exec(sql);
895
+ this._seedIncrementStart();
896
+ });
622
897
  }
623
898
  _incrementSeeded = false;
624
899
  /**
@@ -638,129 +913,213 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
638
913
  }
639
914
  }
640
915
  async ensureView() {
641
- const view = this._table;
642
- const sql = buildCreateView$1(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref));
643
- this._log(sql);
644
- this.driver.exec(sql);
916
+ return this._stmt(() => {
917
+ const view = this._table;
918
+ const sql = buildCreateView$1(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref));
919
+ this._log(sql);
920
+ this.driver.exec(sql);
921
+ });
645
922
  }
646
923
  async getExistingColumns() {
647
924
  return this.getExistingColumnsForTable(this.resolveTableName());
648
925
  }
649
926
  async syncColumns(diff) {
650
- const tableName = this.resolveTableName();
651
- const added = [];
652
- const renamed = [];
653
- for (const { field, oldName } of diff.renamed ?? []) {
654
- const ddl = `ALTER TABLE "${esc(tableName)}" RENAME COLUMN "${esc(oldName)}" TO "${esc(field.physicalName)}"`;
655
- this._log(ddl);
656
- this.driver.exec(ddl);
657
- renamed.push(field.physicalName);
658
- }
659
- for (const field of diff.added) {
660
- const sqlType = this.typeMapper(field);
661
- let ddl = `ALTER TABLE "${esc(tableName)}" ADD COLUMN "${esc(field.physicalName)}" ${sqlType}`;
662
- if (!field.optional && !field.isPrimaryKey) ddl += " NOT NULL";
663
- if (field.defaultValue?.kind === "value") ddl += ` DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
664
- else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValueForType(field.designType)}`;
665
- if (field.collate) ddl += ` COLLATE ${field.collate.toUpperCase()}`;
666
- this._log(ddl);
667
- this.driver.exec(ddl);
668
- added.push(field.physicalName);
669
- }
670
- return {
671
- added,
672
- renamed
673
- };
927
+ return this._stmt(() => {
928
+ const tableName = this.resolveTableName();
929
+ const added = [];
930
+ const renamed = [];
931
+ for (const { field, oldName } of diff.renamed ?? []) {
932
+ const ddl = `ALTER TABLE "${esc(tableName)}" RENAME COLUMN "${esc(oldName)}" TO "${esc(field.physicalName)}"`;
933
+ this._log(ddl);
934
+ this.driver.exec(ddl);
935
+ renamed.push(field.physicalName);
936
+ }
937
+ for (const field of diff.added) {
938
+ const sqlType = this.typeMapper(field);
939
+ let ddl = `ALTER TABLE "${esc(tableName)}" ADD COLUMN "${esc(field.physicalName)}" ${sqlType}`;
940
+ if (!field.optional && !field.isPrimaryKey) ddl += " NOT NULL";
941
+ if (field.defaultValue?.kind === "value") ddl += ` DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
942
+ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValueForType(field.designType)}`;
943
+ if (field.collate) ddl += ` COLLATE ${field.collate.toUpperCase()}`;
944
+ this._log(ddl);
945
+ this.driver.exec(ddl);
946
+ added.push(field.physicalName);
947
+ }
948
+ return {
949
+ added,
950
+ renamed
951
+ };
952
+ });
674
953
  }
675
954
  async recreateTable() {
676
- const tableName = this.resolveTableName();
677
- const tempName = `${tableName}__tmp_${Date.now()}`;
678
- this._dropAllFtsTables(tableName);
679
- this._dropAllVecTables(tableName);
680
- this.driver.exec("PRAGMA foreign_keys = OFF");
681
- this.driver.exec("PRAGMA legacy_alter_table = ON");
682
- try {
683
- const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys, { typeMapper: (field) => this.typeMapper(field) });
684
- this._log(createSql);
685
- this.driver.exec(createSql);
686
- const oldCols = (await this.getExistingColumns()).map((c) => c.name);
687
- const newCols = this._table.fieldDescriptors.filter((f) => !f.ignored).map((f) => f.physicalName);
688
- const oldColSet = new Set(oldCols);
689
- const commonCols = newCols.filter((c) => oldColSet.has(c));
690
- if (commonCols.length > 0) {
691
- const fieldsByName = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
692
- const colNames = commonCols.map((c) => `"${esc(c)}"`).join(", ");
693
- const selectExprs = commonCols.map((c) => {
694
- const field = fieldsByName.get(c);
695
- if (field && !field.optional && !field.isPrimaryKey) {
696
- const fallback = field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType);
697
- return `COALESCE("${esc(c)}", ${fallback}) AS "${esc(c)}"`;
698
- }
699
- return `"${esc(c)}"`;
700
- }).join(", ");
701
- const copySql = `INSERT INTO "${esc(tempName)}" (${colNames}) SELECT ${selectExprs} FROM "${esc(tableName)}"`;
702
- this._log(copySql);
703
- this.driver.exec(copySql);
955
+ return this._withExclusiveConnection(async () => {
956
+ const tableName = this.resolveTableName();
957
+ const tempName = `${tableName}__tmp_${Date.now()}`;
958
+ this._dropAllFtsTables(tableName);
959
+ this._dropAllVecTables(tableName);
960
+ this.driver.exec("PRAGMA foreign_keys = OFF");
961
+ this.driver.exec("PRAGMA legacy_alter_table = ON");
962
+ try {
963
+ const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys, { typeMapper: (field) => this.typeMapper(field) });
964
+ this._log(createSql);
965
+ this.driver.exec(createSql);
966
+ const oldCols = (await this.getExistingColumns()).map((c) => c.name);
967
+ const newCols = this._table.fieldDescriptors.filter((f) => !f.ignored).map((f) => f.physicalName);
968
+ const oldColSet = new Set(oldCols);
969
+ const commonCols = newCols.filter((c) => oldColSet.has(c));
970
+ if (commonCols.length > 0) {
971
+ const fieldsByName = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
972
+ const colNames = commonCols.map((c) => `"${esc(c)}"`).join(", ");
973
+ const selectExprs = commonCols.map((c) => {
974
+ const field = fieldsByName.get(c);
975
+ if (field && !field.optional && !field.isPrimaryKey) {
976
+ const fallback = field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType);
977
+ return `COALESCE("${esc(c)}", ${fallback}) AS "${esc(c)}"`;
978
+ }
979
+ return `"${esc(c)}"`;
980
+ }).join(", ");
981
+ const copySql = `INSERT INTO "${esc(tempName)}" (${colNames}) SELECT ${selectExprs} FROM "${esc(tableName)}"`;
982
+ this._log(copySql);
983
+ this.driver.exec(copySql);
984
+ }
985
+ const oldName = `${tableName}__old_${Date.now()}`;
986
+ this.driver.exec(`ALTER TABLE "${esc(tableName)}" RENAME TO "${esc(oldName)}"`);
987
+ this.driver.exec(`ALTER TABLE "${esc(tempName)}" RENAME TO "${esc(tableName)}"`);
988
+ this.driver.exec(`DROP TABLE IF EXISTS "${esc(oldName)}"`);
989
+ } finally {
990
+ this.driver.exec("PRAGMA legacy_alter_table = OFF");
991
+ this.driver.exec("PRAGMA foreign_keys = ON");
704
992
  }
705
- const oldName = `${tableName}__old_${Date.now()}`;
706
- this.driver.exec(`ALTER TABLE "${esc(tableName)}" RENAME TO "${esc(oldName)}"`);
707
- this.driver.exec(`ALTER TABLE "${esc(tempName)}" RENAME TO "${esc(tableName)}"`);
708
- this.driver.exec(`DROP TABLE IF EXISTS "${esc(oldName)}"`);
709
- } finally {
710
- this.driver.exec("PRAGMA legacy_alter_table = OFF");
711
- this.driver.exec("PRAGMA foreign_keys = ON");
712
- }
993
+ });
713
994
  }
714
995
  async dropTable() {
715
- const tableName = this.resolveTableName();
716
- this._dropAllFtsTables(tableName);
717
- this._dropAllVecTables(tableName);
718
- const ddl = `DROP TABLE IF EXISTS "${esc(tableName)}"`;
719
- this._log(ddl);
720
- this.driver.exec(ddl);
996
+ return this._stmt(() => {
997
+ const tableName = this.resolveTableName();
998
+ this._dropAllFtsTables(tableName);
999
+ this._dropAllVecTables(tableName);
1000
+ const ddl = `DROP TABLE IF EXISTS "${esc(tableName)}"`;
1001
+ this._log(ddl);
1002
+ this.driver.exec(ddl);
1003
+ });
721
1004
  }
722
1005
  async dropColumns(columns) {
723
- await this.withTransaction(async () => {
724
- const tableName = this.resolveTableName();
725
- for (const col of columns) {
726
- const ddl = `ALTER TABLE "${esc(tableName)}" DROP COLUMN "${esc(col)}"`;
727
- this._log(ddl);
728
- this.driver.exec(ddl);
729
- }
1006
+ return this._withExclusiveConnection(async () => {
1007
+ await this.withTransaction(async () => {
1008
+ const tableName = this.resolveTableName();
1009
+ for (const col of columns) {
1010
+ const ddl = `ALTER TABLE "${esc(tableName)}" DROP COLUMN "${esc(col)}"`;
1011
+ this._log(ddl);
1012
+ this.driver.exec(ddl);
1013
+ }
1014
+ });
730
1015
  });
731
1016
  }
732
1017
  async dropIndexesForColumns(columns) {
733
- const tableName = this.resolveTableName();
734
- const dropped = new Set(columns);
735
- const indexes = this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => i.name.startsWith("atscript__"));
736
- for (const index of indexes) if (this.driver.all(`PRAGMA index_info("${esc(index.name)}")`).some((c) => c.name !== null && dropped.has(c.name))) {
737
- const sql = `DROP INDEX IF EXISTS "${esc(index.name)}"`;
738
- this._log(sql);
739
- this.driver.exec(sql);
740
- }
741
- for (const name of this._listShadowTables(tableName, "fts")) if (this.driver.all(`PRAGMA table_info("${esc(name)}")`).some((c) => dropped.has(c.name))) this._dropFtsTable(name);
742
- for (const name of this._listShadowTables(tableName, "vec")) {
743
- const triggerSql = this.driver.all(`SELECT sql FROM sqlite_master WHERE type='trigger' AND name = ?`, [`${name}__ai`])[0]?.sql ?? "";
744
- if (columns.some((c) => triggerSql.includes(`"${esc(c)}"`))) this._dropVecTable(name);
745
- }
1018
+ return this._stmt(() => {
1019
+ const tableName = this.resolveTableName();
1020
+ const dropped = new Set(columns);
1021
+ const indexes = this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => i.name.startsWith("atscript__"));
1022
+ for (const index of indexes) if (this.driver.all(`PRAGMA index_info("${esc(index.name)}")`).some((c) => c.name !== null && dropped.has(c.name))) {
1023
+ const sql = `DROP INDEX IF EXISTS "${esc(index.name)}"`;
1024
+ this._log(sql);
1025
+ this.driver.exec(sql);
1026
+ }
1027
+ for (const name of this._listShadowTables(tableName, "fts")) if (this.driver.all(`PRAGMA table_info("${esc(name)}")`).some((c) => dropped.has(c.name))) this._dropFtsTable(name);
1028
+ for (const name of this._listShadowTables(tableName, "vec")) {
1029
+ const triggerSql = this.driver.all(`SELECT sql FROM sqlite_master WHERE type='trigger' AND name = ?`, [`${name}__ai`])[0]?.sql ?? "";
1030
+ if (columns.some((c) => triggerSql.includes(`"${esc(c)}"`))) this._dropVecTable(name);
1031
+ }
1032
+ });
746
1033
  }
747
1034
  async dropTableByName(tableName) {
748
- this._dropAllFtsTables(tableName);
749
- this._dropAllVecTables(tableName);
750
- const ddl = `DROP TABLE IF EXISTS "${esc(tableName)}"`;
751
- this._log(ddl);
752
- this.driver.exec(ddl);
1035
+ return this._stmt(() => {
1036
+ this._dropAllFtsTables(tableName);
1037
+ this._dropAllVecTables(tableName);
1038
+ const ddl = `DROP TABLE IF EXISTS "${esc(tableName)}"`;
1039
+ this._log(ddl);
1040
+ this.driver.exec(ddl);
1041
+ });
753
1042
  }
754
1043
  async dropViewByName(viewName) {
755
- const ddl = `DROP VIEW IF EXISTS "${esc(viewName)}"`;
756
- this._log(ddl);
757
- this.driver.exec(ddl);
1044
+ return this._stmt(() => {
1045
+ const ddl = `DROP VIEW IF EXISTS "${esc(viewName)}"`;
1046
+ this._log(ddl);
1047
+ this.driver.exec(ddl);
1048
+ });
1049
+ }
1050
+ /**
1051
+ * Drops a group of mutually referencing tables with FK enforcement off for
1052
+ * the duration (`PRAGMA foreign_keys` is a no-op inside a transaction, so
1053
+ * the exclusive connection hold — not a `BEGIN` — keeps another context's
1054
+ * transaction from overlapping it). Safe because schema sync only passes
1055
+ * groups whose referrers are all inside the group.
1056
+ */
1057
+ async dropTablesByName(tableNames) {
1058
+ return this._withExclusiveConnection(async () => {
1059
+ this.driver.exec("PRAGMA foreign_keys = OFF");
1060
+ try {
1061
+ for (const name of tableNames) await this.dropTableByName(name);
1062
+ } finally {
1063
+ this.driver.exec("PRAGMA foreign_keys = ON");
1064
+ }
1065
+ });
1066
+ }
1067
+ async hasRows(tableName) {
1068
+ return this._stmt(() => {
1069
+ const sql = `SELECT 1 AS one FROM "${esc(tableName ?? this.resolveTableName())}" LIMIT 1`;
1070
+ this._log(sql);
1071
+ return this.driver.get(sql) != null;
1072
+ });
1073
+ }
1074
+ /**
1075
+ * Live foreign keys referencing `tableName`: `PRAGMA foreign_key_list` is
1076
+ * outbound-only, so the tables in `sqlite_master` are scanned (a loop
1077
+ * rather than the `pragma_foreign_key_list` table-valued function, which
1078
+ * not every bundled SQLite exposes). Only tables whose CREATE text contains
1079
+ * `REFERENCES` can declare a foreign key — the prefilter is a superset, a
1080
+ * false positive just costs one PRAGMA.
1081
+ */
1082
+ async getReferencingForeignKeys(tableName) {
1083
+ return this._stmt(() => {
1084
+ const out = [];
1085
+ const tables = this.driver.all(`SELECT name FROM sqlite_master
1086
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND sql LIKE '%REFERENCES%'`);
1087
+ for (const { name } of tables) {
1088
+ const rows = this.driver.all(`PRAGMA foreign_key_list("${esc(name)}")`);
1089
+ const byId = /* @__PURE__ */ new Map();
1090
+ for (const row of rows) {
1091
+ if (row.table !== tableName) continue;
1092
+ let fk = byId.get(row.id);
1093
+ if (!fk) {
1094
+ fk = {
1095
+ table: name,
1096
+ fields: [],
1097
+ targetFields: []
1098
+ };
1099
+ byId.set(row.id, fk);
1100
+ }
1101
+ fk.fields.push(row.from);
1102
+ fk.targetFields.push(row.to);
1103
+ }
1104
+ out.push(...byId.values());
1105
+ }
1106
+ return out;
1107
+ });
1108
+ }
1109
+ async getObjectKind(name) {
1110
+ return this._stmt(() => {
1111
+ const row = this.driver.get(`SELECT type FROM sqlite_master WHERE name = ? AND type IN ('table', 'view')`, [name]);
1112
+ if (!row) return;
1113
+ return row.type === "view" ? "view" : "table";
1114
+ });
758
1115
  }
759
1116
  async renameTable(oldName) {
760
- const newName = this.resolveTableName();
761
- const ddl = `ALTER TABLE "${esc(oldName)}" RENAME TO "${esc(newName)}"`;
762
- this._log(ddl);
763
- this.driver.exec(ddl);
1117
+ return this._stmt(() => {
1118
+ const newName = this.resolveTableName();
1119
+ const ddl = `ALTER TABLE "${esc(oldName)}" RENAME TO "${esc(newName)}"`;
1120
+ this._log(ddl);
1121
+ this.driver.exec(ddl);
1122
+ });
764
1123
  }
765
1124
  typeMapper(field) {
766
1125
  if (field.encrypted) return "TEXT";
@@ -769,37 +1128,41 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
769
1128
  return sqliteTypeFromDesignType(field.designType);
770
1129
  }
771
1130
  async getExistingColumnsForTable(tableName) {
772
- return this.driver.all(`PRAGMA table_info("${esc(tableName)}")`).map((r) => ({
773
- name: r.name,
774
- type: r.type,
775
- notnull: r.notnull === 1,
776
- pk: r.pk > 0,
777
- dflt_value: normalizeSqliteDefault(r.dflt_value)
778
- }));
1131
+ return this._stmt(() => {
1132
+ return this.driver.all(`PRAGMA table_info("${esc(tableName)}")`).map((r) => ({
1133
+ name: r.name,
1134
+ type: r.type,
1135
+ notnull: r.notnull === 1,
1136
+ pk: r.pk > 0,
1137
+ dflt_value: normalizeSqliteDefault(r.dflt_value)
1138
+ }));
1139
+ });
779
1140
  }
780
1141
  async syncIndexes() {
781
- const tableName = this.resolveTableName();
782
- await this.syncIndexesWithDiff({
783
- listExisting: async () => this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => !i.name.startsWith("sqlite_")).map((i) => ({
784
- name: i.name,
785
- columns: this.driver.all(`PRAGMA index_info("${esc(i.name)}")`).map((c) => c.name).filter((n) => n !== null)
786
- })),
787
- createIndex: async (index) => {
788
- const unique = index.type === "unique" ? "UNIQUE " : "";
789
- const cols = index.fields.map((f) => `"${esc(f.name)}" ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
790
- const sql = `CREATE ${unique}INDEX IF NOT EXISTS "${esc(index.key)}" ON "${esc(tableName)}" (${cols})`;
791
- this._log(sql);
792
- this.driver.exec(sql);
793
- },
794
- dropIndex: async (name) => {
795
- const sql = `DROP INDEX IF EXISTS "${esc(name)}"`;
796
- this._log(sql);
797
- this.driver.exec(sql);
798
- },
799
- shouldSkipType: (type) => type === "fulltext" || type === "geo"
1142
+ return this._withExclusiveConnection(async () => {
1143
+ const tableName = this.resolveTableName();
1144
+ await this.syncIndexesWithDiff({
1145
+ listExisting: async () => this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => !i.name.startsWith("sqlite_")).map((i) => ({
1146
+ name: i.name,
1147
+ columns: this.driver.all(`PRAGMA index_info("${esc(i.name)}")`).map((c) => c.name).filter((n) => n !== null)
1148
+ })),
1149
+ createIndex: async (index) => {
1150
+ const unique = index.type === "unique" ? "UNIQUE " : "";
1151
+ const cols = index.fields.map((f) => `"${esc(f.name)}" ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
1152
+ const sql = `CREATE ${unique}INDEX IF NOT EXISTS "${esc(index.key)}" ON "${esc(tableName)}" (${cols})`;
1153
+ this._log(sql);
1154
+ this.driver.exec(sql);
1155
+ },
1156
+ dropIndex: async (name) => {
1157
+ const sql = `DROP INDEX IF EXISTS "${esc(name)}"`;
1158
+ this._log(sql);
1159
+ this.driver.exec(sql);
1160
+ },
1161
+ shouldSkipType: (type) => type === "fulltext" || type === "geo"
1162
+ });
1163
+ this._syncFtsIndexes(tableName);
1164
+ this._syncVecIndexes(tableName);
800
1165
  });
801
- this._syncFtsIndexes(tableName);
802
- this._syncVecIndexes(tableName);
803
1166
  }
804
1167
  getSearchIndexes() {
805
1168
  const indexes = [];
@@ -838,7 +1201,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
838
1201
  params.push(controls.$skip);
839
1202
  }
840
1203
  this._log(sql, params);
841
- return this.driver.all(sql, params);
1204
+ return this._stmt(() => this.driver.all(sql, params));
842
1205
  }
843
1206
  async searchWithCount(text, query, indexName) {
844
1207
  if (!text.trim()) return {
@@ -851,7 +1214,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
851
1214
  this._log(countSql, base.params);
852
1215
  return {
853
1216
  data,
854
- count: this.driver.get(countSql, base.params)?.cnt ?? 0
1217
+ count: (await this._stmt(() => this.driver.get(countSql, base.params)))?.cnt ?? 0
855
1218
  };
856
1219
  }
857
1220
  /** Builds FTS table name from index name: `<table>__fts__<indexName>`. */
@@ -956,18 +1319,20 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
956
1319
  async vectorSearch(vector, query, indexName) {
957
1320
  if (!this._detectVectorSupport()) throw new Error("Vector search requires the sqlite-vec extension. Construct BetterSqlite3Driver with { vector: true }.");
958
1321
  const base = this._buildVectorSearchBase(vector, query, indexName);
959
- return this._runVectorSearch(base);
1322
+ return this._stmt(() => this._runVectorSearch(base));
960
1323
  }
961
1324
  async vectorSearchWithCount(vector, query, indexName) {
962
1325
  if (!this._detectVectorSupport()) throw new Error("Vector search requires the sqlite-vec extension. Construct BetterSqlite3Driver with { vector: true }.");
963
1326
  const base = this._buildVectorSearchBase(vector, query, indexName);
964
- const data = this._runVectorSearch(base);
965
1327
  const countSql = `SELECT COUNT(*) AS cnt ${base.fromWhere}`;
966
- this._log(countSql, base.params);
967
- return {
968
- data,
969
- count: this.driver.get(countSql, base.params)?.cnt ?? 0
970
- };
1328
+ return this._stmt(() => {
1329
+ const data = this._runVectorSearch(base);
1330
+ this._log(countSql, base.params);
1331
+ return {
1332
+ data,
1333
+ count: this.driver.get(countSql, base.params)?.cnt ?? 0
1334
+ };
1335
+ });
971
1336
  }
972
1337
  _runVectorSearch(base) {
973
1338
  let sql = `SELECT * ${base.fromWhere} ORDER BY _distance ASC LIMIT ?`;
@@ -1098,7 +1463,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1098
1463
  async geoSearch(point, query, indexName) {
1099
1464
  const { sql, params } = this._buildGeoSearchSelect(this._prepareGeoSearch(point, query, indexName));
1100
1465
  this._log(sql, params);
1101
- return this.driver.all(sql, params).map((row) => renameGeoDistance(row));
1466
+ return (await this._stmt(() => this.driver.all(sql, params))).map((row) => renameGeoDistance(row));
1102
1467
  }
1103
1468
  async geoSearchWithCount(point, query, indexName) {
1104
1469
  const ctx = this._prepareGeoSearch(point, query, indexName);
@@ -1106,8 +1471,10 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1106
1471
  const countFrag = buildGeoSearchCount(sqliteDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window);
1107
1472
  this._log(sql, params);
1108
1473
  this._log(countFrag.sql, countFrag.params);
1109
- const rows = this.driver.all(sql, params);
1110
- const countRow = this.driver.get(countFrag.sql, countFrag.params);
1474
+ const { rows, countRow } = await this._stmt(() => ({
1475
+ rows: this.driver.all(sql, params),
1476
+ countRow: this.driver.get(countFrag.sql, countFrag.params)
1477
+ }));
1111
1478
  return {
1112
1479
  data: rows.map((row) => renameGeoDistance(row)),
1113
1480
  count: countRow?.cnt ?? 0
@@ -1240,8 +1607,13 @@ function normalizeSqliteDefault(value) {
1240
1607
  //#endregion
1241
1608
  //#region src/index.ts
1242
1609
  function createAdapter(connection, options) {
1243
- const driver = new BetterSqlite3Driver(connection, options);
1244
- return new DbSpace(() => new SqliteAdapter(driver));
1610
+ const { transactionWaitTimeoutMs, transactionWaitWarnMs, ...driverOptions } = options ?? {};
1611
+ const driver = new BetterSqlite3Driver(connection, driverOptions);
1612
+ const adapterOptions = {
1613
+ transactionWaitTimeoutMs,
1614
+ transactionWaitWarnMs
1615
+ };
1616
+ return new DbSpace(() => new SqliteAdapter(driver, adapterOptions));
1245
1617
  }
1246
1618
  //#endregion
1247
- export { BetterSqlite3Driver, SqliteAdapter, buildWhere, createAdapter };
1619
+ export { BetterSqlite3Driver, SqliteAdapter, SqliteTxGate, SqliteTxState, buildWhere, createAdapter, getSqliteTxGate };