@atscript/db-sqlite 0.1.127 → 0.1.129

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