@atscript/db-sqlite 0.1.127 → 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.cjs +548 -173
- package/dist/index.d.cts +231 -60
- package/dist/index.d.mts +231 -60
- package/dist/index.mjs +548 -176
- package/package.json +10 -10
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
|
-
|
|
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.
|
|
450
|
-
this.
|
|
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
|
-
|
|
454
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
this.
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const
|
|
745
|
-
|
|
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.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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.
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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.
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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.
|
|
1111
|
-
|
|
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
|
|
1245
|
-
|
|
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;
|