@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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TExistingColumn, TFieldOps, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
1
+ import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
2
2
  import { TMetadataMap } from "@atscript/typescript/utils";
3
3
  import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
4
4
  import { TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
@@ -54,6 +54,176 @@ interface TSqliteDriver {
54
54
  readonly hasVectorExt?: boolean;
55
55
  }
56
56
  //#endregion
57
+ //#region src/better-sqlite3-driver.d.ts
58
+ interface TBetterSqlite3DriverOptions extends Record<string, unknown> {
59
+ /** Load the optional `sqlite-vec` extension. */
60
+ vector?: boolean;
61
+ /** Absolute paths to SQLite loadable extensions, passed to `Database.loadExtension`. */
62
+ loadExtensions?: string[];
63
+ }
64
+ /**
65
+ * {@link TSqliteDriver} implementation backed by `better-sqlite3`.
66
+ *
67
+ * Accepts either a file path (opens a new database) or a pre-created
68
+ * `Database` instance from `better-sqlite3`.
69
+ *
70
+ * ```typescript
71
+ * import { BetterSqlite3Driver } from '@atscript/db-sqlite'
72
+ *
73
+ * // In-memory database
74
+ * const driver = new BetterSqlite3Driver(':memory:')
75
+ *
76
+ * // File-based database
77
+ * const driver = new BetterSqlite3Driver('./my-data.db')
78
+ *
79
+ * // With sqlite-vec extension loaded
80
+ * const driver = new BetterSqlite3Driver('./my-data.db', { vector: true })
81
+ *
82
+ * // Pre-created instance
83
+ * import Database from 'better-sqlite3'
84
+ * const db = new Database(':memory:', { verbose: console.log })
85
+ * const driver = new BetterSqlite3Driver(db)
86
+ * ```
87
+ *
88
+ * Requires `better-sqlite3` to be installed:
89
+ * ```bash
90
+ * pnpm add better-sqlite3
91
+ * ```
92
+ *
93
+ * Vector search support requires the optional `sqlite-vec` package:
94
+ * ```bash
95
+ * pnpm add sqlite-vec
96
+ * ```
97
+ */
98
+ declare class BetterSqlite3Driver implements TSqliteDriver {
99
+ private db;
100
+ readonly hasVectorExt: boolean;
101
+ constructor(pathOrDb: string | import("better-sqlite3").Database, options?: TBetterSqlite3DriverOptions);
102
+ run(sql: string, params?: unknown[]): TSqliteRunResult;
103
+ all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
104
+ get<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
105
+ exec(sql: string): void;
106
+ close(): void;
107
+ }
108
+ //#endregion
109
+ //#region src/tx-gate.d.ts
110
+ /**
111
+ * Per-waiter options for the transaction gate (taken from the adapter's
112
+ * {@link SqliteAdapterOptions} at wait time — the gate itself is per driver).
113
+ */
114
+ interface SqliteTxWaitOptions {
115
+ /**
116
+ * Max milliseconds a waiter blocks for the connection before rejecting with
117
+ * `DbError("TX_WAIT_TIMEOUT")`. Default: unbounded (matches the mysql2 / pg
118
+ * pool defaults).
119
+ */
120
+ transactionWaitTimeoutMs?: number;
121
+ /**
122
+ * Log a warning through `logger` once a waiter has waited this long.
123
+ * Default: 5000 ms. `0` disables the warning.
124
+ */
125
+ transactionWaitWarnMs?: number;
126
+ /** Logger for the wait warning (the adapter passes its own). */
127
+ logger?: TGenericLogger;
128
+ }
129
+ /** Options accepted by `SqliteAdapter` / `createAdapter` for the transaction gate. */
130
+ type SqliteAdapterOptions = Pick<SqliteTxWaitOptions, "transactionWaitTimeoutMs" | "transactionWaitWarnMs">;
131
+ /**
132
+ * One waiter for the gate, created once per `_stmt` / `acquire` call and kept
133
+ * across wake-ups: the timeout and warn timers are armed ONCE with an absolute
134
+ * deadline, so `transactionWaitTimeoutMs` bounds the waiter's TOTAL wait
135
+ * however many transactions run ahead of it, and a re-queued waiter allocates
136
+ * nothing new. `next()` resolves on the next release (or rejects once the
137
+ * deadline passed); `dispose()` clears the timers.
138
+ */
139
+ declare class GateWaiter {
140
+ private readonly _gate;
141
+ private _resolve?;
142
+ private _reject?;
143
+ private _timeoutTimer?;
144
+ private _warnTimer?;
145
+ private _expired?;
146
+ private readonly _startedAt;
147
+ constructor(_gate: SqliteTxGate, opts: SqliteTxWaitOptions | undefined);
148
+ /** Resolves on the NEXT release; rejects with `TX_WAIT_TIMEOUT` once the deadline passed. */
149
+ next(): Promise<void>;
150
+ /** Called by the gate on release: wakes the pending `next()` (a no-op when none is pending). */
151
+ wake(): void;
152
+ dispose(): void;
153
+ }
154
+ /**
155
+ * Logical FIFO mutex around the single synchronous SQLite connection.
156
+ *
157
+ * SQLite has one connection per driver, so two async contexts cannot each
158
+ * own a transaction: a second `BEGIN` fails with "cannot start a transaction
159
+ * within a transaction", and — worse — any statement from another context
160
+ * that runs while a transaction is open executes INSIDE that transaction
161
+ * (rolled back with it, reads its uncommitted rows). The gate serialises:
162
+ *
163
+ * - {@link acquire} / {@link release} — taken by `BEGIN`, released by
164
+ * `COMMIT` / `ROLLBACK`;
165
+ * - {@link runWhenFree} — plain (non-transactional) statements check
166
+ * {@link held} synchronously right before executing and wait otherwise;
167
+ * - {@link runExclusive} — hold the connection without a transaction (schema sync).
168
+ *
169
+ * Fairness: one FIFO waiter list for acquirers and statement waiters alike;
170
+ * a release wakes every current waiter in registration order, each re-checks
171
+ * synchronously and re-queues (ahead of any new arrival) if someone ahead took
172
+ * the gate. A waiter is one object with timers armed once, whatever the
173
+ * number of wake-ups. The fast path (nothing held) allocates nothing.
174
+ */
175
+ declare class SqliteTxGate {
176
+ /** True while a transaction (or an exclusive hold) owns the connection. */
177
+ held: boolean;
178
+ /** `Date.now()` when the current holder took the gate (0 when free). */
179
+ heldSince: number;
180
+ private _waiters;
181
+ /** True when the current async context holds this gate via {@link runExclusive}. */
182
+ get heldByCurrentContext(): boolean;
183
+ /**
184
+ * Waits until the gate is free, then takes it synchronously (no `await`
185
+ * between the check and `held = true`). Pair with {@link release}.
186
+ */
187
+ acquire(opts?: SqliteTxWaitOptions): Promise<void>;
188
+ /** Frees the gate and wakes every waiter (in registration order). */
189
+ release(): void;
190
+ /**
191
+ * Runs one synchronous statement once no transaction holds the connection
192
+ * (immediately when the gate is free). The `held` check and `fn` run in
193
+ * the same synchronous segment, so they cannot interleave with a `BEGIN`
194
+ * from another context.
195
+ */
196
+ runWhenFree<R>(fn: () => R, opts?: SqliteTxWaitOptions): Promise<R>;
197
+ /**
198
+ * Holds the gate for the duration of `fn` WITHOUT opening a transaction
199
+ * (nested transactions inside `fn` still `BEGIN`/`COMMIT` on the held
200
+ * connection without re-acquiring). Re-entrant for the same async context.
201
+ */
202
+ runExclusive<T>(fn: () => Promise<T>, opts?: SqliteTxWaitOptions): Promise<T>;
203
+ /** @internal */
204
+ _enqueue(waiter: GateWaiter): void;
205
+ /** @internal */
206
+ _dequeue(waiter: GateWaiter): void;
207
+ }
208
+ /**
209
+ * Transaction state returned by `SqliteAdapter._beginTransaction`: releases
210
+ * the gate the transaction holds exactly once (a transaction opened inside an
211
+ * exclusive hold owns nothing to release). Ownership across adapters is
212
+ * decided by the core (`_transactionOwner()` = the driver), not here.
213
+ */
214
+ declare class SqliteTxState {
215
+ private readonly _gate;
216
+ private _holdsGate;
217
+ constructor(_gate: SqliteTxGate, _holdsGate: boolean);
218
+ release(): void;
219
+ }
220
+ /**
221
+ * Returns the gate for a driver instance (one gate per driver — every adapter
222
+ * of a space shares its driver, so they share the gate; two driver wrappers
223
+ * around one `Database` would get two gates: construct one driver per database).
224
+ */
225
+ declare function getSqliteTxGate(driver: TSqliteDriver): SqliteTxGate;
226
+ //#endregion
57
227
  //#region src/sqlite-adapter.d.ts
58
228
  /**
59
229
  * SQLite adapter for {@link AtscriptDbTable}.
@@ -83,14 +253,42 @@ declare class SqliteAdapter extends BaseDbAdapter {
83
253
  private _vectorThresholds;
84
254
  /** Partition filter fields per vector index (from @db.search.filter). Field paths. */
85
255
  private _vectorPartitionFields;
86
- constructor(driver: TSqliteDriver);
256
+ /** Per-driver transaction gate (shared by every adapter over this driver). */
257
+ private readonly _gate;
258
+ private readonly _txOptions;
259
+ constructor(driver: TSqliteDriver, options?: SqliteAdapterOptions);
87
260
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
88
261
  formatValue(field: TDbFieldMeta): TValueFormatterPair | undefined;
89
262
  isVectorSearchable(): boolean;
90
263
  private _detectVectorSupport;
91
- protected _beginTransaction(): Promise<unknown>;
92
- protected _commitTransaction(): Promise<void>;
93
- protected _rollbackTransaction(): Promise<void>;
264
+ private _waitOpts;
265
+ /** Every adapter over this driver shares one transaction — and one gate (since 0.1.128). */
266
+ protected _transactionOwner(): unknown;
267
+ /** Inside our own transaction, or holding the connection exclusively. */
268
+ private _ownsConnection;
269
+ /**
270
+ * Runs one synchronous driver statement. Inside our own transaction (or an
271
+ * exclusive hold) it runs immediately; otherwise it waits until no
272
+ * transaction is open on the connection. The `held` check and the statement
273
+ * run in the same synchronous segment, so they cannot interleave with a
274
+ * `BEGIN` from another context.
275
+ */
276
+ private _stmt;
277
+ /**
278
+ * Holds the connection for the duration of `fn` WITHOUT opening a
279
+ * transaction: no other context's statement or transaction can interleave.
280
+ * Meant for the schema entry points that `await` mid-body or toggle
281
+ * connection-level PRAGMAs (`ensureTable`, `recreateTable`, `dropColumns`,
282
+ * `dropTablesByName`, `syncIndexes`) — their DDL must not land inside a
283
+ * request's transaction (PRAGMA changes are no-ops inside one). Inside our
284
+ * own transaction `fn` runs directly; re-entrant for the same async
285
+ * context; nested `withTransaction` calls inside `fn` still BEGIN/COMMIT
286
+ * on the held connection.
287
+ */
288
+ protected _withExclusiveConnection<T>(fn: () => Promise<T>): Promise<T>;
289
+ protected _beginTransaction(): Promise<SqliteTxState>;
290
+ protected _commitTransaction(state: unknown): Promise<void>;
291
+ protected _rollbackTransaction(state: unknown): Promise<void>;
94
292
  /** SQLite does not use schemas — override to always exclude schema. */
95
293
  resolveTableName(): string;
96
294
  /** SQLite enforces FK constraints natively via PRAGMA foreign_keys. */
@@ -113,7 +311,11 @@ declare class SqliteAdapter extends BaseDbAdapter {
113
311
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
114
312
  deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
115
313
  deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
116
- ensureTable(): Promise<void>;
314
+ /**
315
+ * SQLite accepts forward FK references, so a foreign-key cycle is created
316
+ * inline in any order — `opts.deferForeignKeysTo` is accepted and ignored.
317
+ */
318
+ ensureTable(_opts?: TEnsureTableOptions): Promise<void>;
117
319
  private _incrementSeeded;
118
320
  /**
119
321
  * Seeds the sqlite_sequence table for auto-increment fields that have a start value.
@@ -129,6 +331,25 @@ declare class SqliteAdapter extends BaseDbAdapter {
129
331
  dropIndexesForColumns(columns: string[]): Promise<void>;
130
332
  dropTableByName(tableName: string): Promise<void>;
131
333
  dropViewByName(viewName: string): Promise<void>;
334
+ /**
335
+ * Drops a group of mutually referencing tables with FK enforcement off for
336
+ * the duration (`PRAGMA foreign_keys` is a no-op inside a transaction, so
337
+ * the exclusive connection hold — not a `BEGIN` — keeps another context's
338
+ * transaction from overlapping it). Safe because schema sync only passes
339
+ * groups whose referrers are all inside the group.
340
+ */
341
+ dropTablesByName(tableNames: string[]): Promise<void>;
342
+ hasRows(tableName?: string): Promise<boolean>;
343
+ /**
344
+ * Live foreign keys referencing `tableName`: `PRAGMA foreign_key_list` is
345
+ * outbound-only, so the tables in `sqlite_master` are scanned (a loop
346
+ * rather than the `pragma_foreign_key_list` table-valued function, which
347
+ * not every bundled SQLite exposes). Only tables whose CREATE text contains
348
+ * `REFERENCES` can declare a foreign key — the prefilter is a superset, a
349
+ * false positive just costs one PRAGMA.
350
+ */
351
+ getReferencingForeignKeys(tableName: string): Promise<TReferencingForeignKey[]>;
352
+ getObjectKind(name: string): Promise<TDbObjectKind | undefined>;
132
353
  renameTable(oldName: string): Promise<void>;
133
354
  typeMapper(field: TDbFieldMeta): string;
134
355
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
@@ -214,58 +435,6 @@ declare class SqliteAdapter extends BaseDbAdapter {
214
435
  private _dropAllVecTables;
215
436
  }
216
437
  //#endregion
217
- //#region src/better-sqlite3-driver.d.ts
218
- interface TBetterSqlite3DriverOptions extends Record<string, unknown> {
219
- /** Load the optional `sqlite-vec` extension. */
220
- vector?: boolean;
221
- /** Absolute paths to SQLite loadable extensions, passed to `Database.loadExtension`. */
222
- loadExtensions?: string[];
223
- }
224
- /**
225
- * {@link TSqliteDriver} implementation backed by `better-sqlite3`.
226
- *
227
- * Accepts either a file path (opens a new database) or a pre-created
228
- * `Database` instance from `better-sqlite3`.
229
- *
230
- * ```typescript
231
- * import { BetterSqlite3Driver } from '@atscript/db-sqlite'
232
- *
233
- * // In-memory database
234
- * const driver = new BetterSqlite3Driver(':memory:')
235
- *
236
- * // File-based database
237
- * const driver = new BetterSqlite3Driver('./my-data.db')
238
- *
239
- * // With sqlite-vec extension loaded
240
- * const driver = new BetterSqlite3Driver('./my-data.db', { vector: true })
241
- *
242
- * // Pre-created instance
243
- * import Database from 'better-sqlite3'
244
- * const db = new Database(':memory:', { verbose: console.log })
245
- * const driver = new BetterSqlite3Driver(db)
246
- * ```
247
- *
248
- * Requires `better-sqlite3` to be installed:
249
- * ```bash
250
- * pnpm add better-sqlite3
251
- * ```
252
- *
253
- * Vector search support requires the optional `sqlite-vec` package:
254
- * ```bash
255
- * pnpm add sqlite-vec
256
- * ```
257
- */
258
- declare class BetterSqlite3Driver implements TSqliteDriver {
259
- private db;
260
- readonly hasVectorExt: boolean;
261
- constructor(pathOrDb: string | import("better-sqlite3").Database, options?: TBetterSqlite3DriverOptions);
262
- run(sql: string, params?: unknown[]): TSqliteRunResult;
263
- all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
264
- get<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
265
- exec(sql: string): void;
266
- close(): void;
267
- }
268
- //#endregion
269
438
  //#region src/filter-builder.d.ts
270
439
  /**
271
440
  * Translates a uniqu filter expression into a parameterized SQL WHERE clause.
@@ -276,6 +445,8 @@ declare class BetterSqlite3Driver implements TSqliteDriver {
276
445
  declare function buildWhere(filter: FilterExpr$1): TSqlFragment$1;
277
446
  //#endregion
278
447
  //#region src/index.d.ts
279
- declare function createAdapter(connection: string, options?: Record<string, unknown>): DbSpace;
448
+ /** `createAdapter` options: driver options plus the transaction-gate options (since 0.1.128). */
449
+ type TCreateSqliteAdapterOptions = TBetterSqlite3DriverOptions & SqliteAdapterOptions;
450
+ declare function createAdapter(connection: string, options?: TCreateSqliteAdapterOptions): DbSpace;
280
451
  //#endregion
281
- export { BetterSqlite3Driver, SqliteAdapter, type TSqlFragment, type TSqliteDriver, type TSqliteRunResult, buildWhere, createAdapter };
452
+ export { BetterSqlite3Driver, SqliteAdapter, type SqliteAdapterOptions, SqliteTxGate, SqliteTxState, type SqliteTxWaitOptions, type TBetterSqlite3DriverOptions, TCreateSqliteAdapterOptions, type TSqlFragment, type TSqliteDriver, type TSqliteRunResult, buildWhere, createAdapter, getSqliteTxGate };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TExistingColumn, TFieldOps, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
1
+ import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
2
2
  import { TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
3
3
  import { TMetadataMap } from "@atscript/typescript/utils";
4
4
  import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
@@ -54,6 +54,176 @@ interface TSqliteDriver {
54
54
  readonly hasVectorExt?: boolean;
55
55
  }
56
56
  //#endregion
57
+ //#region src/better-sqlite3-driver.d.ts
58
+ interface TBetterSqlite3DriverOptions extends Record<string, unknown> {
59
+ /** Load the optional `sqlite-vec` extension. */
60
+ vector?: boolean;
61
+ /** Absolute paths to SQLite loadable extensions, passed to `Database.loadExtension`. */
62
+ loadExtensions?: string[];
63
+ }
64
+ /**
65
+ * {@link TSqliteDriver} implementation backed by `better-sqlite3`.
66
+ *
67
+ * Accepts either a file path (opens a new database) or a pre-created
68
+ * `Database` instance from `better-sqlite3`.
69
+ *
70
+ * ```typescript
71
+ * import { BetterSqlite3Driver } from '@atscript/db-sqlite'
72
+ *
73
+ * // In-memory database
74
+ * const driver = new BetterSqlite3Driver(':memory:')
75
+ *
76
+ * // File-based database
77
+ * const driver = new BetterSqlite3Driver('./my-data.db')
78
+ *
79
+ * // With sqlite-vec extension loaded
80
+ * const driver = new BetterSqlite3Driver('./my-data.db', { vector: true })
81
+ *
82
+ * // Pre-created instance
83
+ * import Database from 'better-sqlite3'
84
+ * const db = new Database(':memory:', { verbose: console.log })
85
+ * const driver = new BetterSqlite3Driver(db)
86
+ * ```
87
+ *
88
+ * Requires `better-sqlite3` to be installed:
89
+ * ```bash
90
+ * pnpm add better-sqlite3
91
+ * ```
92
+ *
93
+ * Vector search support requires the optional `sqlite-vec` package:
94
+ * ```bash
95
+ * pnpm add sqlite-vec
96
+ * ```
97
+ */
98
+ declare class BetterSqlite3Driver implements TSqliteDriver {
99
+ private db;
100
+ readonly hasVectorExt: boolean;
101
+ constructor(pathOrDb: string | import("better-sqlite3").Database, options?: TBetterSqlite3DriverOptions);
102
+ run(sql: string, params?: unknown[]): TSqliteRunResult;
103
+ all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
104
+ get<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
105
+ exec(sql: string): void;
106
+ close(): void;
107
+ }
108
+ //#endregion
109
+ //#region src/tx-gate.d.ts
110
+ /**
111
+ * Per-waiter options for the transaction gate (taken from the adapter's
112
+ * {@link SqliteAdapterOptions} at wait time — the gate itself is per driver).
113
+ */
114
+ interface SqliteTxWaitOptions {
115
+ /**
116
+ * Max milliseconds a waiter blocks for the connection before rejecting with
117
+ * `DbError("TX_WAIT_TIMEOUT")`. Default: unbounded (matches the mysql2 / pg
118
+ * pool defaults).
119
+ */
120
+ transactionWaitTimeoutMs?: number;
121
+ /**
122
+ * Log a warning through `logger` once a waiter has waited this long.
123
+ * Default: 5000 ms. `0` disables the warning.
124
+ */
125
+ transactionWaitWarnMs?: number;
126
+ /** Logger for the wait warning (the adapter passes its own). */
127
+ logger?: TGenericLogger;
128
+ }
129
+ /** Options accepted by `SqliteAdapter` / `createAdapter` for the transaction gate. */
130
+ type SqliteAdapterOptions = Pick<SqliteTxWaitOptions, "transactionWaitTimeoutMs" | "transactionWaitWarnMs">;
131
+ /**
132
+ * One waiter for the gate, created once per `_stmt` / `acquire` call and kept
133
+ * across wake-ups: the timeout and warn timers are armed ONCE with an absolute
134
+ * deadline, so `transactionWaitTimeoutMs` bounds the waiter's TOTAL wait
135
+ * however many transactions run ahead of it, and a re-queued waiter allocates
136
+ * nothing new. `next()` resolves on the next release (or rejects once the
137
+ * deadline passed); `dispose()` clears the timers.
138
+ */
139
+ declare class GateWaiter {
140
+ private readonly _gate;
141
+ private _resolve?;
142
+ private _reject?;
143
+ private _timeoutTimer?;
144
+ private _warnTimer?;
145
+ private _expired?;
146
+ private readonly _startedAt;
147
+ constructor(_gate: SqliteTxGate, opts: SqliteTxWaitOptions | undefined);
148
+ /** Resolves on the NEXT release; rejects with `TX_WAIT_TIMEOUT` once the deadline passed. */
149
+ next(): Promise<void>;
150
+ /** Called by the gate on release: wakes the pending `next()` (a no-op when none is pending). */
151
+ wake(): void;
152
+ dispose(): void;
153
+ }
154
+ /**
155
+ * Logical FIFO mutex around the single synchronous SQLite connection.
156
+ *
157
+ * SQLite has one connection per driver, so two async contexts cannot each
158
+ * own a transaction: a second `BEGIN` fails with "cannot start a transaction
159
+ * within a transaction", and — worse — any statement from another context
160
+ * that runs while a transaction is open executes INSIDE that transaction
161
+ * (rolled back with it, reads its uncommitted rows). The gate serialises:
162
+ *
163
+ * - {@link acquire} / {@link release} — taken by `BEGIN`, released by
164
+ * `COMMIT` / `ROLLBACK`;
165
+ * - {@link runWhenFree} — plain (non-transactional) statements check
166
+ * {@link held} synchronously right before executing and wait otherwise;
167
+ * - {@link runExclusive} — hold the connection without a transaction (schema sync).
168
+ *
169
+ * Fairness: one FIFO waiter list for acquirers and statement waiters alike;
170
+ * a release wakes every current waiter in registration order, each re-checks
171
+ * synchronously and re-queues (ahead of any new arrival) if someone ahead took
172
+ * the gate. A waiter is one object with timers armed once, whatever the
173
+ * number of wake-ups. The fast path (nothing held) allocates nothing.
174
+ */
175
+ declare class SqliteTxGate {
176
+ /** True while a transaction (or an exclusive hold) owns the connection. */
177
+ held: boolean;
178
+ /** `Date.now()` when the current holder took the gate (0 when free). */
179
+ heldSince: number;
180
+ private _waiters;
181
+ /** True when the current async context holds this gate via {@link runExclusive}. */
182
+ get heldByCurrentContext(): boolean;
183
+ /**
184
+ * Waits until the gate is free, then takes it synchronously (no `await`
185
+ * between the check and `held = true`). Pair with {@link release}.
186
+ */
187
+ acquire(opts?: SqliteTxWaitOptions): Promise<void>;
188
+ /** Frees the gate and wakes every waiter (in registration order). */
189
+ release(): void;
190
+ /**
191
+ * Runs one synchronous statement once no transaction holds the connection
192
+ * (immediately when the gate is free). The `held` check and `fn` run in
193
+ * the same synchronous segment, so they cannot interleave with a `BEGIN`
194
+ * from another context.
195
+ */
196
+ runWhenFree<R>(fn: () => R, opts?: SqliteTxWaitOptions): Promise<R>;
197
+ /**
198
+ * Holds the gate for the duration of `fn` WITHOUT opening a transaction
199
+ * (nested transactions inside `fn` still `BEGIN`/`COMMIT` on the held
200
+ * connection without re-acquiring). Re-entrant for the same async context.
201
+ */
202
+ runExclusive<T>(fn: () => Promise<T>, opts?: SqliteTxWaitOptions): Promise<T>;
203
+ /** @internal */
204
+ _enqueue(waiter: GateWaiter): void;
205
+ /** @internal */
206
+ _dequeue(waiter: GateWaiter): void;
207
+ }
208
+ /**
209
+ * Transaction state returned by `SqliteAdapter._beginTransaction`: releases
210
+ * the gate the transaction holds exactly once (a transaction opened inside an
211
+ * exclusive hold owns nothing to release). Ownership across adapters is
212
+ * decided by the core (`_transactionOwner()` = the driver), not here.
213
+ */
214
+ declare class SqliteTxState {
215
+ private readonly _gate;
216
+ private _holdsGate;
217
+ constructor(_gate: SqliteTxGate, _holdsGate: boolean);
218
+ release(): void;
219
+ }
220
+ /**
221
+ * Returns the gate for a driver instance (one gate per driver — every adapter
222
+ * of a space shares its driver, so they share the gate; two driver wrappers
223
+ * around one `Database` would get two gates: construct one driver per database).
224
+ */
225
+ declare function getSqliteTxGate(driver: TSqliteDriver): SqliteTxGate;
226
+ //#endregion
57
227
  //#region src/sqlite-adapter.d.ts
58
228
  /**
59
229
  * SQLite adapter for {@link AtscriptDbTable}.
@@ -83,14 +253,42 @@ declare class SqliteAdapter extends BaseDbAdapter {
83
253
  private _vectorThresholds;
84
254
  /** Partition filter fields per vector index (from @db.search.filter). Field paths. */
85
255
  private _vectorPartitionFields;
86
- constructor(driver: TSqliteDriver);
256
+ /** Per-driver transaction gate (shared by every adapter over this driver). */
257
+ private readonly _gate;
258
+ private readonly _txOptions;
259
+ constructor(driver: TSqliteDriver, options?: SqliteAdapterOptions);
87
260
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
88
261
  formatValue(field: TDbFieldMeta): TValueFormatterPair | undefined;
89
262
  isVectorSearchable(): boolean;
90
263
  private _detectVectorSupport;
91
- protected _beginTransaction(): Promise<unknown>;
92
- protected _commitTransaction(): Promise<void>;
93
- protected _rollbackTransaction(): Promise<void>;
264
+ private _waitOpts;
265
+ /** Every adapter over this driver shares one transaction — and one gate (since 0.1.128). */
266
+ protected _transactionOwner(): unknown;
267
+ /** Inside our own transaction, or holding the connection exclusively. */
268
+ private _ownsConnection;
269
+ /**
270
+ * Runs one synchronous driver statement. Inside our own transaction (or an
271
+ * exclusive hold) it runs immediately; otherwise it waits until no
272
+ * transaction is open on the connection. The `held` check and the statement
273
+ * run in the same synchronous segment, so they cannot interleave with a
274
+ * `BEGIN` from another context.
275
+ */
276
+ private _stmt;
277
+ /**
278
+ * Holds the connection for the duration of `fn` WITHOUT opening a
279
+ * transaction: no other context's statement or transaction can interleave.
280
+ * Meant for the schema entry points that `await` mid-body or toggle
281
+ * connection-level PRAGMAs (`ensureTable`, `recreateTable`, `dropColumns`,
282
+ * `dropTablesByName`, `syncIndexes`) — their DDL must not land inside a
283
+ * request's transaction (PRAGMA changes are no-ops inside one). Inside our
284
+ * own transaction `fn` runs directly; re-entrant for the same async
285
+ * context; nested `withTransaction` calls inside `fn` still BEGIN/COMMIT
286
+ * on the held connection.
287
+ */
288
+ protected _withExclusiveConnection<T>(fn: () => Promise<T>): Promise<T>;
289
+ protected _beginTransaction(): Promise<SqliteTxState>;
290
+ protected _commitTransaction(state: unknown): Promise<void>;
291
+ protected _rollbackTransaction(state: unknown): Promise<void>;
94
292
  /** SQLite does not use schemas — override to always exclude schema. */
95
293
  resolveTableName(): string;
96
294
  /** SQLite enforces FK constraints natively via PRAGMA foreign_keys. */
@@ -113,7 +311,11 @@ declare class SqliteAdapter extends BaseDbAdapter {
113
311
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
114
312
  deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
115
313
  deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
116
- ensureTable(): Promise<void>;
314
+ /**
315
+ * SQLite accepts forward FK references, so a foreign-key cycle is created
316
+ * inline in any order — `opts.deferForeignKeysTo` is accepted and ignored.
317
+ */
318
+ ensureTable(_opts?: TEnsureTableOptions): Promise<void>;
117
319
  private _incrementSeeded;
118
320
  /**
119
321
  * Seeds the sqlite_sequence table for auto-increment fields that have a start value.
@@ -129,6 +331,25 @@ declare class SqliteAdapter extends BaseDbAdapter {
129
331
  dropIndexesForColumns(columns: string[]): Promise<void>;
130
332
  dropTableByName(tableName: string): Promise<void>;
131
333
  dropViewByName(viewName: string): Promise<void>;
334
+ /**
335
+ * Drops a group of mutually referencing tables with FK enforcement off for
336
+ * the duration (`PRAGMA foreign_keys` is a no-op inside a transaction, so
337
+ * the exclusive connection hold — not a `BEGIN` — keeps another context's
338
+ * transaction from overlapping it). Safe because schema sync only passes
339
+ * groups whose referrers are all inside the group.
340
+ */
341
+ dropTablesByName(tableNames: string[]): Promise<void>;
342
+ hasRows(tableName?: string): Promise<boolean>;
343
+ /**
344
+ * Live foreign keys referencing `tableName`: `PRAGMA foreign_key_list` is
345
+ * outbound-only, so the tables in `sqlite_master` are scanned (a loop
346
+ * rather than the `pragma_foreign_key_list` table-valued function, which
347
+ * not every bundled SQLite exposes). Only tables whose CREATE text contains
348
+ * `REFERENCES` can declare a foreign key — the prefilter is a superset, a
349
+ * false positive just costs one PRAGMA.
350
+ */
351
+ getReferencingForeignKeys(tableName: string): Promise<TReferencingForeignKey[]>;
352
+ getObjectKind(name: string): Promise<TDbObjectKind | undefined>;
132
353
  renameTable(oldName: string): Promise<void>;
133
354
  typeMapper(field: TDbFieldMeta): string;
134
355
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
@@ -214,58 +435,6 @@ declare class SqliteAdapter extends BaseDbAdapter {
214
435
  private _dropAllVecTables;
215
436
  }
216
437
  //#endregion
217
- //#region src/better-sqlite3-driver.d.ts
218
- interface TBetterSqlite3DriverOptions extends Record<string, unknown> {
219
- /** Load the optional `sqlite-vec` extension. */
220
- vector?: boolean;
221
- /** Absolute paths to SQLite loadable extensions, passed to `Database.loadExtension`. */
222
- loadExtensions?: string[];
223
- }
224
- /**
225
- * {@link TSqliteDriver} implementation backed by `better-sqlite3`.
226
- *
227
- * Accepts either a file path (opens a new database) or a pre-created
228
- * `Database` instance from `better-sqlite3`.
229
- *
230
- * ```typescript
231
- * import { BetterSqlite3Driver } from '@atscript/db-sqlite'
232
- *
233
- * // In-memory database
234
- * const driver = new BetterSqlite3Driver(':memory:')
235
- *
236
- * // File-based database
237
- * const driver = new BetterSqlite3Driver('./my-data.db')
238
- *
239
- * // With sqlite-vec extension loaded
240
- * const driver = new BetterSqlite3Driver('./my-data.db', { vector: true })
241
- *
242
- * // Pre-created instance
243
- * import Database from 'better-sqlite3'
244
- * const db = new Database(':memory:', { verbose: console.log })
245
- * const driver = new BetterSqlite3Driver(db)
246
- * ```
247
- *
248
- * Requires `better-sqlite3` to be installed:
249
- * ```bash
250
- * pnpm add better-sqlite3
251
- * ```
252
- *
253
- * Vector search support requires the optional `sqlite-vec` package:
254
- * ```bash
255
- * pnpm add sqlite-vec
256
- * ```
257
- */
258
- declare class BetterSqlite3Driver implements TSqliteDriver {
259
- private db;
260
- readonly hasVectorExt: boolean;
261
- constructor(pathOrDb: string | import("better-sqlite3").Database, options?: TBetterSqlite3DriverOptions);
262
- run(sql: string, params?: unknown[]): TSqliteRunResult;
263
- all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
264
- get<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
265
- exec(sql: string): void;
266
- close(): void;
267
- }
268
- //#endregion
269
438
  //#region src/filter-builder.d.ts
270
439
  /**
271
440
  * Translates a uniqu filter expression into a parameterized SQL WHERE clause.
@@ -276,6 +445,8 @@ declare class BetterSqlite3Driver implements TSqliteDriver {
276
445
  declare function buildWhere(filter: FilterExpr$1): TSqlFragment$1;
277
446
  //#endregion
278
447
  //#region src/index.d.ts
279
- declare function createAdapter(connection: string, options?: Record<string, unknown>): DbSpace;
448
+ /** `createAdapter` options: driver options plus the transaction-gate options (since 0.1.128). */
449
+ type TCreateSqliteAdapterOptions = TBetterSqlite3DriverOptions & SqliteAdapterOptions;
450
+ declare function createAdapter(connection: string, options?: TCreateSqliteAdapterOptions): DbSpace;
280
451
  //#endregion
281
- export { BetterSqlite3Driver, SqliteAdapter, type TSqlFragment, type TSqliteDriver, type TSqliteRunResult, buildWhere, createAdapter };
452
+ export { BetterSqlite3Driver, SqliteAdapter, type SqliteAdapterOptions, SqliteTxGate, SqliteTxState, type SqliteTxWaitOptions, type TBetterSqlite3DriverOptions, TCreateSqliteAdapterOptions, type TSqlFragment, type TSqliteDriver, type TSqliteRunResult, buildWhere, createAdapter, getSqliteTxGate };