@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.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.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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.
|
|
449
|
-
this.
|
|
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
|
-
|
|
453
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
this.
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
|
|
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.
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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.
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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.
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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.
|
|
1110
|
-
|
|
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
|
|
1244
|
-
|
|
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 };
|