@nextlyhq/adapter-sqlite 0.0.2-alpha.68 → 0.0.2-alpha.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,865 +1,896 @@
1
- 'use strict';
2
-
3
- var async_hooks = require('async_hooks');
4
- var adapterDrizzle = require('@nextlyhq/adapter-drizzle');
5
- var types = require('@nextlyhq/adapter-drizzle/types');
6
- var versionCheck = require('@nextlyhq/adapter-drizzle/version-check');
7
- var betterSqlite3 = require('drizzle-orm/better-sqlite3');
8
- var sqliteCore = require('drizzle-orm/sqlite-core');
9
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_async_hooks = require("node:async_hooks");
3
+ let _nextlyhq_adapter_drizzle = require("@nextlyhq/adapter-drizzle");
4
+ let _nextlyhq_adapter_drizzle_types = require("@nextlyhq/adapter-drizzle/types");
5
+ let _nextlyhq_adapter_drizzle_version_check = require("@nextlyhq/adapter-drizzle/version-check");
6
+ let drizzle_orm_better_sqlite3 = require("drizzle-orm/better-sqlite3");
7
+ let drizzle_orm_sqlite_core = require("drizzle-orm/sqlite-core");
10
8
  // src/index.ts
11
- var VERSION = "0.1.0";
12
- var SQLITE_ERROR_CODES = {
13
- // Constraint violations
14
- SQLITE_CONSTRAINT: "constraint",
15
- SQLITE_CONSTRAINT_UNIQUE: "unique_violation",
16
- SQLITE_CONSTRAINT_PRIMARYKEY: "unique_violation",
17
- SQLITE_CONSTRAINT_FOREIGNKEY: "foreign_key_violation",
18
- SQLITE_CONSTRAINT_NOTNULL: "not_null_violation",
19
- SQLITE_CONSTRAINT_CHECK: "check_violation",
20
- // Busy/locked errors
21
- SQLITE_BUSY: "timeout",
22
- SQLITE_LOCKED: "timeout",
23
- // Connection errors
24
- SQLITE_CANTOPEN: "connection",
25
- SQLITE_NOTADB: "connection",
26
- SQLITE_CORRUPT: "connection",
27
- // Query errors
28
- SQLITE_ERROR: "query",
29
- SQLITE_MISUSE: "query",
30
- SQLITE_RANGE: "query"
9
+ /**
10
+ * @nextlyhq/adapter-sqlite
11
+ *
12
+ * SQLite database adapter for Nextly.
13
+ * Extends DrizzleAdapter from @nextlyhq/adapter-drizzle to provide SQLite-specific functionality.
14
+ *
15
+ * @remarks
16
+ * This adapter uses the better-sqlite3 package for database connectivity and provides:
17
+ * - Synchronous API wrapped for async interface compatibility
18
+ * - Transactions the adapter runs itself (BEGIN IMMEDIATE / COMMIT / ROLLBACK), with nested calls run as savepoints
19
+ * - CRUD operations with RETURNING clause support (SQLite 3.35+)
20
+ * - WAL mode for better concurrent read performance
21
+ * - In-memory and file-based database support
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * import { createSqliteAdapter } from '@nextlyhq/adapter-sqlite';
26
+ *
27
+ * const adapter = createSqliteAdapter({
28
+ * url: 'file:./data/app.db',
29
+ * });
30
+ *
31
+ * await adapter.connect();
32
+ *
33
+ * // Query data
34
+ * const users = await adapter.select('users', {
35
+ * where: { and: [{ column: 'status', op: '=', value: 'active' }] },
36
+ * limit: 10,
37
+ * });
38
+ *
39
+ * await adapter.disconnect();
40
+ * ```
41
+ *
42
+ * @packageDocumentation
43
+ */
44
+ /**
45
+ * Package version
46
+ */
47
+ const VERSION = "0.1.0";
48
+ /**
49
+ * SQLite error codes mapping to DatabaseErrorKind.
50
+ *
51
+ * @see https://sqlite.org/rescode.html
52
+ */
53
+ const SQLITE_ERROR_CODES = {
54
+ SQLITE_CONSTRAINT: "constraint",
55
+ SQLITE_CONSTRAINT_UNIQUE: "unique_violation",
56
+ SQLITE_CONSTRAINT_PRIMARYKEY: "unique_violation",
57
+ SQLITE_CONSTRAINT_FOREIGNKEY: "foreign_key_violation",
58
+ SQLITE_CONSTRAINT_NOTNULL: "not_null_violation",
59
+ SQLITE_CONSTRAINT_CHECK: "check_violation",
60
+ SQLITE_BUSY: "timeout",
61
+ SQLITE_LOCKED: "timeout",
62
+ SQLITE_CANTOPEN: "connection",
63
+ SQLITE_NOTADB: "connection",
64
+ SQLITE_CORRUPT: "connection",
65
+ SQLITE_ERROR: "query",
66
+ SQLITE_MISUSE: "query",
67
+ SQLITE_RANGE: "query"
31
68
  };
32
- var DEFAULT_CONFIG = {
33
- busyTimeout: 5e3,
34
- wal: true,
35
- foreignKeys: true
69
+ /**
70
+ * Default configuration values.
71
+ */
72
+ const DEFAULT_CONFIG = {
73
+ busyTimeout: 5e3,
74
+ wal: true,
75
+ foreignKeys: true,
76
+ connectionTimeoutMs: 15e3
36
77
  };
78
+ /**
79
+ * Converts a JavaScript value to a type that better-sqlite3 can bind.
80
+ * better-sqlite3 only accepts: number, string, bigint, Buffer, null.
81
+ *
82
+ * These conversions have to match what the schema declares, because the rows
83
+ * they write are read back through it. Every column here that takes a `Date`
84
+ * is `integer({ mode: "timestamp" })` — the system columns and user date
85
+ * fields in `runtime-schema-generator.ts`, and the core tables, whose date
86
+ * columns are all INTEGER. So a date is bound as unix seconds, which is what
87
+ * that mode reads.
88
+ */
37
89
  function sanitizeSqliteValue(v) {
38
- if (v === void 0) return null;
39
- if (typeof v === "boolean") return v ? 1 : 0;
40
- if (Buffer.isBuffer(v) || ArrayBuffer.isView(v)) return v;
41
- if (v instanceof Date) return Math.floor(v.getTime() / 1e3);
42
- if (v !== null && typeof v === "object") return JSON.stringify(v);
43
- return v;
90
+ if (v === void 0) return null;
91
+ if (typeof v === "boolean") return v ? 1 : 0;
92
+ if (Buffer.isBuffer(v) || ArrayBuffer.isView(v)) return v;
93
+ if (v instanceof Date) return Math.floor(v.getTime() / 1e3);
94
+ if (v !== null && typeof v === "object") return JSON.stringify(v);
95
+ return v;
44
96
  }
45
- var SqliteAdapter = class extends adapterDrizzle.DrizzleAdapter {
46
- // getDrizzle memoization: drizzle v1's constructor builds a relational
47
- // query builder per table in the relations config (~40 tables), and the
48
- // service layer resolves an instance on every db access — construct once
49
- // per relations object (identity-stable: the schema registry caches it
50
- // and hands out a NEW object on invalidation, which naturally misses
51
- // this cache and produces a fresh instance).
52
- drizzleByRelations = /* @__PURE__ */ new WeakMap();
53
- drizzleBare;
54
- /**
55
- * The database dialect - always 'sqlite' for this adapter.
56
- */
57
- dialect = "sqlite";
58
- /**
59
- * Adapter configuration.
60
- */
61
- config;
62
- /**
63
- * better-sqlite3 Database instance.
64
- */
65
- db = null;
66
- /**
67
- * Connection state flag.
68
- */
69
- connected = false;
70
- /**
71
- * Serialization queue for concurrent `transaction()` calls.
72
- *
73
- * better-sqlite3 is synchronous, single-connection, and rejects nested
74
- * `BEGIN` with "cannot start a transaction within a transaction".
75
- * When two `await adapter.transaction(...)` calls overlap (e.g. a bulk
76
- * mutation calling `Promise.allSettled` with N per-row updates), the
77
- * second one tries to BEGIN while the first is still open and the
78
- * driver throws.
79
- *
80
- * Postgres/MySQL avoid this by allocating a fresh client/connection
81
- * per transaction from a pool — there is no equivalent for
82
- * better-sqlite3, so we serialize at the JS layer instead. Every
83
- * `transaction()` invocation chains onto this promise; only one
84
- * BEGIN→COMMIT/ROLLBACK section runs at any moment, preserving
85
- * write-isolation semantics that callers expect from a transactional
86
- * adapter.
87
- *
88
- * Performance impact is the natural floor: SQLite already serializes
89
- * writers at the file-lock level, so the queue removes failed-call
90
- * surface area without changing the achievable concurrency.
91
- */
92
- transactionQueue = Promise.resolve();
93
- /**
94
- * The transaction the current async chain is running inside, if any.
95
- *
96
- * A `transaction()` call made from INSIDE a transaction's work — a store
97
- * writing on behalf of the caller, a plugin's write inside its own
98
- * `ctx.db.transaction` — used to queue behind the very transaction that
99
- * was waiting for it, and neither ever settled: that hung every later
100
- * transaction on the instance too. Such a call now runs as a SAVEPOINT of
101
- * the active transaction instead, so it commits or rolls back with it and
102
- * its own failure undoes only its own part.
103
- */
104
- transactionScope = new async_hooks.AsyncLocalStorage();
105
- /**
106
- * Creates a new SQLite adapter instance.
107
- *
108
- * @param config - Adapter configuration
109
- */
110
- constructor(config) {
111
- super();
112
- this.config = config;
113
- }
114
- /**
115
- * Connect to the SQLite database.
116
- *
117
- * @remarks
118
- * This method initializes the database connection. For SQLite, this
119
- * opens the database file or creates an in-memory database.
120
- * Also configures WAL mode and foreign keys based on config.
121
- *
122
- * @throws {DatabaseError} If connection fails
123
- */
124
- async connect() {
125
- if (this.connected && this.db) {
126
- return;
127
- }
128
- try {
129
- const BetterSqlite3 = await import('better-sqlite3');
130
- const Database = BetterSqlite3.default;
131
- let dbPath;
132
- if (this.config.memory) {
133
- dbPath = ":memory:";
134
- } else if (this.config.url) {
135
- dbPath = this.config.url.replace(/^file:/, "");
136
- } else {
137
- dbPath = ":memory:";
138
- }
139
- if (dbPath !== ":memory:" && !this.config.readonly) {
140
- const fs = await import('fs');
141
- const path = await import('path');
142
- const dir = path.dirname(path.resolve(dbPath));
143
- await fs.promises.mkdir(dir, { recursive: true });
144
- }
145
- this.db = new Database(dbPath, {
146
- readonly: this.config.readonly ?? false,
147
- timeout: this.config.busyTimeout ?? DEFAULT_CONFIG.busyTimeout
148
- });
149
- if ((this.config.wal ?? DEFAULT_CONFIG.wal) && dbPath !== ":memory:" && !this.config.readonly) {
150
- this.db.pragma("journal_mode = WAL");
151
- }
152
- if (this.config.foreignKeys ?? DEFAULT_CONFIG.foreignKeys) {
153
- this.db.pragma("foreign_keys = ON");
154
- }
155
- await versionCheck.checkDialectVersion(this.db, "sqlite");
156
- this.connected = true;
157
- if (this.config.logger?.info) {
158
- this.config.logger.info("SQLite connection established", {
159
- url: dbPath === ":memory:" ? "in-memory" : dbPath,
160
- wal: this.config.wal ?? DEFAULT_CONFIG.wal,
161
- foreignKeys: this.config.foreignKeys ?? DEFAULT_CONFIG.foreignKeys
162
- });
163
- }
164
- } catch (error) {
165
- if (this.db) {
166
- try {
167
- this.db.close();
168
- } catch {
169
- }
170
- this.db = null;
171
- }
172
- throw this.classifyError(error);
173
- }
174
- }
175
- /**
176
- * Disconnect from the SQLite database.
177
- *
178
- * @remarks
179
- * This method closes the database connection and releases resources.
180
- */
181
- // better-sqlite3 is synchronous; method is async to satisfy the DatabaseAdapter contract.
182
- // eslint-disable-next-line @typescript-eslint/require-await
183
- async disconnect() {
184
- this.drizzleBare = void 0;
185
- this.drizzleByRelations = /* @__PURE__ */ new WeakMap();
186
- if (!this.db) {
187
- return;
188
- }
189
- try {
190
- this.db.close();
191
- if (this.config.logger?.info) {
192
- this.config.logger.info("SQLite connection closed");
193
- }
194
- } finally {
195
- this.db = null;
196
- this.connected = false;
197
- }
198
- }
199
- /**
200
- * Check if connected to the database.
201
- */
202
- isConnected() {
203
- return this.connected && this.db !== null;
204
- }
205
- /**
206
- * Get connection pool statistics.
207
- *
208
- * @remarks
209
- * SQLite doesn't use connection pooling, so this returns null.
210
- * The database is single-file with a single connection.
211
- */
212
- getPoolStats() {
213
- return null;
214
- }
215
- /**
216
- * Execute a raw SQL query.
217
- *
218
- * @param sql - SQL query string with $1, $2 placeholders (converted to ? for SQLite)
219
- * @param params - Query parameters
220
- * @returns Query results
221
- *
222
- * @throws {DatabaseError} If query execution fails
223
- */
224
- // better-sqlite3 is synchronous; method is async to satisfy the DatabaseAdapter contract.
225
- // eslint-disable-next-line @typescript-eslint/require-await
226
- async executeQuery(sql, params = []) {
227
- const db = this.ensureDb();
228
- const startTime = Date.now();
229
- try {
230
- const convertedSql = this.convertPlaceholders(sql);
231
- const trimmedSql = convertedSql.trim().toUpperCase();
232
- const isPragmaQuery = trimmedSql.startsWith("PRAGMA") && !trimmedSql.includes("=");
233
- const isSelect = trimmedSql.startsWith("SELECT") || isPragmaQuery || trimmedSql.startsWith("WITH");
234
- const hasReturning = trimmedSql.includes("RETURNING");
235
- let result;
236
- const sanitizedParams = params.map(sanitizeSqliteValue);
237
- if (isSelect || hasReturning) {
238
- const stmt = db.prepare(convertedSql);
239
- result = stmt.all(...sanitizedParams);
240
- } else {
241
- const stmt = db.prepare(convertedSql);
242
- const runResult = stmt.run(...sanitizedParams);
243
- result = [
244
- {
245
- changes: runResult.changes,
246
- lastInsertRowid: runResult.lastInsertRowid
247
- }
248
- ];
249
- }
250
- if (this.config.logger?.query) {
251
- const durationMs = Date.now() - startTime;
252
- this.config.logger.query(convertedSql, params, durationMs);
253
- }
254
- return result;
255
- } catch (error) {
256
- throw this.classifyError(error, sql);
257
- }
258
- }
259
- /**
260
- * Execute work within a transaction.
261
- *
262
- * @param work - Function containing transactional operations
263
- * @param options - Transaction options (isolation level not fully supported in SQLite)
264
- * @returns Result of the work function
265
- *
266
- * @remarks
267
- * The adapter manages the transaction itself, because better-sqlite3's
268
- * `db.transaction()` does not take async work: it runs `BEGIN IMMEDIATE`,
269
- * then `COMMIT` when `work` resolves or `ROLLBACK` when it throws.
270
- * - Top-level calls run one at a time.
271
- * - A call made inside an open transaction's work joins it as a savepoint
272
- * (`SAVEPOINT` / `RELEASE` / `ROLLBACK TO`).
273
- * - Nested calls of one scope run one at a time.
274
- * - A call made later from a savepoint that has already released joins the
275
- * innermost scope still open.
276
- * - Nested work started and not awaited finishes before the enclosing scope
277
- * commits or rolls back.
278
- *
279
- * Note: SQLite uses DEFERRED, IMMEDIATE, or EXCLUSIVE transaction modes
280
- * rather than isolation levels. This adapter uses IMMEDIATE.
281
- */
282
- async transaction(work, _options) {
283
- const own = this.transactionScope.getStore();
284
- const parent = this.nestingScope();
285
- if (own && parent !== own) own.writesAfterClosing += 1;
286
- if (parent) {
287
- return enqueueIn(parent, () => this.runSavepoint(work, parent));
288
- }
289
- const run = async () => this.runTransaction(work);
290
- const next = this.transactionQueue.then(run, run);
291
- this.transactionQueue = next.catch(() => void 0);
292
- const { result, effects } = await next;
293
- await this.runHeldEffects(effects);
294
- return result;
295
- }
296
- /**
297
- * Hold `effect` until the transaction enclosing this async context commits,
298
- * or run it now when none does.
299
- *
300
- * The effect belongs to the scope this async context runs in, even one
301
- * that has already released or rolled back: a service whose own
302
- * transaction nested as a savepoint registers from its caller's scope once
303
- * that savepoint has released, and the change is undoable for as long as
304
- * any scope around it is. The outermost transaction runs the effect once
305
- * its COMMIT succeeds, unless a scope between the effect's scope and the
306
- * outermost transaction rolled back, and drops every effect on ROLLBACK or
307
- * a failed COMMIT. A context whose outermost transaction has already
308
- * finished is outside any transaction, so its effect runs now, unless what
309
- * it announces was undone and it has run no transaction since.
310
- */
311
- async afterCommit(effect, onDeferredFailure) {
312
- const scope = this.transactionScope.getStore();
313
- if (!scope) {
314
- await effect();
315
- return;
316
- }
317
- const root = outermost(scope);
318
- if (!root.finished) {
319
- root.held.push({
320
- effect: this.inRegisteringContext(effect),
321
- onDeferredFailure,
322
- scope,
323
- writesBefore: scope.writesAfterClosing
324
- });
325
- return;
326
- }
327
- if (!undone(scope, scope.writesAfterClosing)) await effect();
328
- }
329
- /**
330
- * `effect`, bound to the async context it is registered from.
331
- *
332
- * A held effect runs after the outermost COMMIT, from the code that ran the
333
- * outermost transaction, so without this it sees that code's context rather
334
- * than its own: whatever its caller pinned around the write is gone. That
335
- * includes who is calling. A scope pinned for a scoped API key lost there
336
- * leaves a hook's nested call judged by the key's owner instead of the key,
337
- * and a person's frame lost there leaves the hook told of nobody.
338
- *
339
- * The adapter's own transaction scope is the one store it leaves: the
340
- * effect runs once that transaction has finished, outside it, as an effect
341
- * held to the outermost commit always has.
342
- */
343
- inRegisteringContext(effect) {
344
- return async_hooks.AsyncResource.bind(() => this.transactionScope.exit(effect));
345
- }
346
- /**
347
- * Run the effects a committed transaction held, in order. A failure is
348
- * reported, never thrown: the change is durable, and reporting the
349
- * transaction as failed would invite a retry of something already done.
350
- */
351
- async runHeldEffects(effects) {
352
- for (const { effect, onDeferredFailure } of effects) {
353
- try {
354
- await effect();
355
- } catch (error) {
356
- this.reportHeldEffectFailure(error, onDeferredFailure);
357
- }
358
- }
359
- }
360
- /** Hand a held effect's failure to its reporter, or to the adapter's log. */
361
- reportHeldEffectFailure(error, onDeferredFailure) {
362
- try {
363
- if (onDeferredFailure) {
364
- onDeferredFailure(error);
365
- return;
366
- }
367
- } catch {
368
- }
369
- const failure = error instanceof Error ? error : new Error(String(error));
370
- if (this.config.logger?.error) {
371
- this.config.logger.error(failure, { phase: "after-commit" });
372
- } else {
373
- console.error("An effect held until commit failed:", failure);
374
- }
375
- }
376
- /**
377
- * Whether a `transaction()` call made here would join an open transaction
378
- * as a savepoint. On SQLite it would: the connection is the only one, so a
379
- * transaction open on it encloses whatever the async chain that opened it
380
- * starts.
381
- */
382
- inTransaction() {
383
- return this.nestingScope() !== void 0;
384
- }
385
- /**
386
- * Whether the calling async flow is inside a transaction that has not
387
- * finished, judged from the transaction this flow was started inside,
388
- * never from the connection: another request's open transaction answers
389
- * false here, where its own `transaction()` queues behind that one.
390
- *
391
- * A flow started inside a transaction, even inside a savepoint that has
392
- * since released, stays inside it until its outermost transaction has
393
- * committed or rolled back: until then whatever it writes on the one
394
- * connection lands in that transaction.
395
- */
396
- transactionOpenInFlow() {
397
- const own = this.transactionScope.getStore();
398
- return own !== void 0 && !outermost(own).finished;
399
- }
400
- /**
401
- * The scope a `transaction()` call made here nests in, or none when it
402
- * opens a transaction of its own.
403
- *
404
- * A call made later from inside a savepoint that has since released runs
405
- * inside the innermost scope open on the connection. Queued on an enclosing
406
- * scope, or on the instance, it would wait behind the scope running there,
407
- * which may be the one awaiting it. Savepoints on one connection form a
408
- * stack, so whatever opens now is inside that innermost scope anyway; when
409
- * none is open the call queues on the instance.
410
- *
411
- * The innermost open scope need not be related to the call. When it is a
412
- * sibling savepoint that later rolls back, the call's write rolls back with
413
- * it, although the call itself already resolved. Waiting for an unrelated
414
- * scope instead could wait on the very scope that awaits the call.
415
- */
416
- nestingScope() {
417
- const own = this.transactionScope.getStore();
418
- return own?.active ? own : own?.open.at(-1);
419
- }
420
- /**
421
- * Run `work` as a savepoint of the active transaction: released when it
422
- * resolves, rolled back to when it throws.
423
- */
424
- async runSavepoint(work, parent) {
425
- const db = this.ensureDb();
426
- const depth = parent.depth + 1;
427
- const name = `nextly_sp_${depth}`;
428
- try {
429
- const ctx = this.createTransactionContext(db);
430
- db.exec(`SAVEPOINT ${name}`);
431
- const scope = openScope(parent);
432
- try {
433
- const result = await this.transactionScope.run(scope, () => work(ctx));
434
- await settleNested(scope);
435
- db.exec(`RELEASE ${name}`);
436
- return result;
437
- } catch (error) {
438
- await settleNested(scope);
439
- scope.rolledBack = true;
440
- try {
441
- db.exec(`ROLLBACK TO ${name}`);
442
- db.exec(`RELEASE ${name}`);
443
- } catch {
444
- }
445
- throw error;
446
- }
447
- } catch (error) {
448
- if (types.isApplicationError(error)) throw error;
449
- throw this.classifyError(error);
450
- }
451
- }
452
- /**
453
- * Inner transaction body. Kept private so callers always go through
454
- * the serialized `transaction()` queue above and the BEGIN/COMMIT
455
- * pair is never invoked outside of it.
456
- */
457
- async runTransaction(work) {
458
- const db = this.ensureDb();
459
- const startTime = Date.now();
460
- try {
461
- const ctx = this.createTransactionContext(db);
462
- db.exec("BEGIN IMMEDIATE");
463
- const scope = openScope();
464
- try {
465
- const result = await this.transactionScope.run(scope, () => work(ctx));
466
- await settleNested(scope);
467
- db.exec("COMMIT");
468
- scope.finished = true;
469
- if (this.config.logger?.debug) {
470
- const durationMs = Date.now() - startTime;
471
- this.config.logger.debug("Transaction committed", {
472
- durationMs
473
- });
474
- }
475
- return {
476
- result,
477
- effects: scope.held.filter(
478
- (held) => !undone(held.scope, held.writesBefore)
479
- )
480
- };
481
- } catch (error) {
482
- await settleNested(scope);
483
- scope.rolledBack = true;
484
- scope.finished = true;
485
- scope.held.length = 0;
486
- try {
487
- db.exec("ROLLBACK");
488
- } catch {
489
- }
490
- throw error;
491
- }
492
- } catch (error) {
493
- if (types.isApplicationError(error)) throw error;
494
- throw this.classifyError(error);
495
- }
496
- }
497
- /**
498
- * Get SQLite database capabilities.
499
- *
500
- * @remarks
501
- * SQLite capabilities:
502
- * - JSON support (not JSONB)
503
- * - No arrays
504
- * - No native ILIKE
505
- * - RETURNING clause (3.35+)
506
- * - Savepoints supported
507
- * - ON CONFLICT supported
508
- */
509
- getCapabilities() {
510
- return {
511
- dialect: "sqlite",
512
- supportsJsonb: false,
513
- // SQLite uses JSON, not JSONB
514
- supportsJson: true,
515
- supportsArrays: false,
516
- // SQLite doesn't support array types
517
- supportsGeneratedColumns: true,
518
- // SQLite 3.31+
519
- supportsFts: true,
520
- // SQLite FTS5
521
- supportsIlike: false,
522
- // No native ILIKE, use LOWER() LIKE
523
- supportsReturning: true,
524
- // SQLite 3.35+
525
- supportsSavepoints: true,
526
- // SQLite supports savepoints
527
- supportsOnConflict: true,
528
- // ON CONFLICT clause
529
- maxParamsPerQuery: 999,
530
- // SQLite SQLITE_MAX_VARIABLE_NUMBER default
531
- maxIdentifierLength: 128
532
- // SQLite doesn't have a strict limit
533
- };
534
- }
535
- /**
536
- * Override insertMany for bulk insert optimization.
537
- *
538
- * @remarks
539
- * Uses a single multi-row INSERT statement for better performance.
540
- */
541
- async insertMany(table, data, options) {
542
- if (data.length === 0) {
543
- return [];
544
- }
545
- const db = this.ensureDb();
546
- if (data.length === 1) {
547
- const result = await this.insert(table, data[0], options);
548
- return [result];
549
- }
550
- const columns = Object.keys(data[0]);
551
- const params = [];
552
- const valuesClauses = [];
553
- for (const record of data) {
554
- const placeholders = [];
555
- for (const col of columns) {
556
- params.push(sanitizeSqliteValue(record[col]));
557
- placeholders.push("?");
558
- }
559
- valuesClauses.push(`(${placeholders.join(", ")})`);
560
- }
561
- const columnList = columns.map((col) => this.escapeIdentifier(col)).join(", ");
562
- let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columnList}) VALUES ${valuesClauses.join(", ")}`;
563
- if (options?.returning) {
564
- const returning = options.returning === "*" ? "*" : options.returning.map((col) => this.escapeIdentifier(col)).join(", ");
565
- sql += ` RETURNING ${returning}`;
566
- } else {
567
- sql += " RETURNING *";
568
- }
569
- try {
570
- const stmt = db.prepare(sql);
571
- const rows = stmt.all(...params);
572
- const tableObj = this.getTableObject(table);
573
- return rows.map((r) => this.mapRowFromRawSql(tableObj, r));
574
- } catch (error) {
575
- throw this.handleQueryError(error, "insertMany", table);
576
- }
577
- }
578
- // ============================================================
579
- // Protected Helper Methods
580
- // ============================================================
581
- /**
582
- * Ensures database is connected and returns it.
583
- *
584
- * @throws {DatabaseError} If not connected
585
- */
586
- ensureDb() {
587
- if (!this.db) {
588
- throw types.createDatabaseError({
589
- kind: "connection",
590
- message: "SqliteAdapter is not connected. Call connect() first."
591
- });
592
- }
593
- return this.db;
594
- }
595
- /**
596
- * Return the typed Drizzle instance for SQLite.
597
- * Guarded for server-only usage and requires an active connection.
598
- *
599
- * @param schema - Optional schema for relational queries (db.query.*)
600
- * @returns Drizzle ORM instance wrapping the better-sqlite3 connection
601
- * @throws {Error} If called in browser or not connected
602
- */
603
- getDrizzle(relations) {
604
- if (typeof window !== "undefined") {
605
- throw new Error("getDrizzle() is server-only");
606
- }
607
- const db = this.ensureDb();
608
- if (!relations) {
609
- this.drizzleBare ??= betterSqlite3.drizzle({ client: db });
610
- return this.drizzleBare;
611
- }
612
- let cached = this.drizzleByRelations.get(relations);
613
- if (!cached) {
614
- cached = betterSqlite3.drizzle({ client: db, relations });
615
- this.drizzleByRelations.set(relations, cached);
616
- }
617
- return cached;
618
- }
619
- /**
620
- * Convert $1, $2 placeholders to ? for better-sqlite3.
621
- *
622
- * @param sql - SQL with PostgreSQL-style placeholders
623
- * @returns SQL with ? placeholders
624
- */
625
- convertPlaceholders(sql) {
626
- return sql.replace(/\$\d+/g, "?");
627
- }
628
- /**
629
- * Creates a TransactionContext for the given database connection.
630
- */
631
- /** A table-level `primaryKey({ columns })`, read through the SQLite table config. */
632
- compositePrimaryKey(tableObj) {
633
- return sqliteCore.getTableConfig(
634
- tableObj
635
- ).primaryKeys.flatMap((key) => key.columns);
636
- }
637
- createTransactionContext(db) {
638
- const txHandles = this.transactionDrizzleHandles(
639
- (relations) => relations ? betterSqlite3.drizzle({ client: db, relations }) : betterSqlite3.drizzle({ client: db })
640
- );
641
- const txDb = txHandles.bare;
642
- return {
643
- drizzleWithRelations: txHandles.withRelations,
644
- // SQLite has no row-level locking and needs none here: `withTransaction`
645
- // opens SQLite transactions with BEGIN IMMEDIATE, which takes the write
646
- // lock up front and serializes writers for the whole transaction.
647
- lockRow: () => Promise.resolve(),
648
- // `run`, not `all`: better-sqlite3 throws on a statement that returns no
649
- // rows, which is every statement this method exists to carry. The Drizzle
650
- // instance is the transaction-bound one, so the statement runs inside this
651
- // transaction.
652
- //
653
- // better-sqlite3 is synchronous, so this resolves an already-settled
654
- // promise rather than being declared `async` over a body that never
655
- // awaits.
656
- runStatement: (statement) => {
657
- txDb().run(statement);
658
- return Promise.resolve();
659
- },
660
- // `all`, not `run`: this is the reading half, and better-sqlite3 returns
661
- // rows only from `all`. Synchronous, so the promise is already settled.
662
- queryStatement: (statement) => Promise.resolve(txDb().all(statement)),
663
- // eslint-disable-next-line @typescript-eslint/require-await
664
- execute: async (sql, params = []) => {
665
- const convertedSql = this.convertPlaceholders(sql);
666
- const trimmedSql = convertedSql.trim().toUpperCase();
667
- const isSelect = trimmedSql.startsWith("SELECT") || trimmedSql.includes("RETURNING");
668
- const sanitizedParams = params.map(sanitizeSqliteValue);
669
- if (isSelect) {
670
- const stmt = db.prepare(convertedSql);
671
- return stmt.all(...sanitizedParams);
672
- } else {
673
- const stmt = db.prepare(convertedSql);
674
- const result = stmt.run(...sanitizedParams);
675
- return [
676
- {
677
- changes: result.changes,
678
- lastInsertRowid: result.lastInsertRowid
679
- }
680
- ];
681
- }
682
- },
683
- // Keys only, unlike the PostgreSQL and MySQL raw paths, which also encode
684
- // their date values. `sanitizeSqliteValue` below binds a `Date` as unix
685
- // seconds, and an instant carries no zone -- so what SQLite stores does
686
- // not depend on the server's timezone and there is nothing to correct.
687
- // eslint-disable-next-line @typescript-eslint/require-await
688
- insert: async (table, data, options) => {
689
- const mapped = this.mapKeysToSqlColumns(
690
- this.getTableObject(table),
691
- data
692
- );
693
- const columns = Object.keys(mapped);
694
- const values = Object.values(mapped).map(sanitizeSqliteValue);
695
- const placeholders = values.map(() => "?").join(", ");
696
- let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES (${placeholders})`;
697
- const ret = options?.returning;
698
- const returningEmpty = Array.isArray(ret) && ret.length === 0;
699
- if (!returningEmpty) {
700
- const returning = !ret || ret === "*" ? "*" : this.mapColumnNamesToSql(this.getTableObject(table), ret).map((col) => this.escapeIdentifier(col)).join(", ");
701
- sql += ` RETURNING ${returning}`;
702
- }
703
- const stmt = db.prepare(sql);
704
- if (returningEmpty) {
705
- stmt.run(...values);
706
- return void 0;
707
- }
708
- const rows = stmt.all(...values);
709
- return this.mapRowFromRawSql(this.getTableObject(table), rows[0]);
710
- },
711
- // eslint-disable-next-line @typescript-eslint/require-await
712
- insertMany: async (table, data, options) => {
713
- if (data.length === 0) return [];
714
- const tableObj = this.getTableObject(table);
715
- const mappedRecords = data.map(
716
- (r) => this.mapKeysToSqlColumns(tableObj, r)
717
- );
718
- const columns = Object.keys(mappedRecords[0]);
719
- const allValues = [];
720
- const valuesClauses = [];
721
- for (const record of mappedRecords) {
722
- const placeholders = [];
723
- for (const col of columns) {
724
- allValues.push(sanitizeSqliteValue(record[col]));
725
- placeholders.push("?");
726
- }
727
- valuesClauses.push(`(${placeholders.join(", ")})`);
728
- }
729
- let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES ${valuesClauses.join(", ")}`;
730
- const ret = options?.returning;
731
- const returningEmpty = Array.isArray(ret) && ret.length === 0;
732
- if (!returningEmpty) {
733
- const returning = !ret || ret === "*" ? "*" : this.mapColumnNamesToSql(this.getTableObject(table), ret).map((col) => this.escapeIdentifier(col)).join(", ");
734
- sql += ` RETURNING ${returning}`;
735
- }
736
- const stmt = db.prepare(sql);
737
- if (returningEmpty) {
738
- stmt.run(...allValues);
739
- return [];
740
- }
741
- return stmt.all(...allValues).map(
742
- (r) => this.mapRowFromRawSql(tableObj, r)
743
- );
744
- },
745
- // Adapter-built, as `insert` above is; `transactionUpdate` says why.
746
- // A column the model does not declare binds as every other value on
747
- // this path does, through `sanitizeSqliteValue`. `all` only when the
748
- // statement carries RETURNING: better-sqlite3 throws on `all` for a
749
- // statement that returns no rows, and on `run` for one that does.
750
- update: this.transactionUpdate(
751
- txDb,
752
- (statement, returnsRows) => returnsRows ? txDb().all(statement) : (txDb().run(statement), void 0),
753
- sanitizeSqliteValue
754
- ),
755
- ...this.createTransactionForwarders(txDb),
756
- // eslint-disable-next-line @typescript-eslint/require-await
757
- savepoint: async (name) => {
758
- db.exec(`SAVEPOINT ${this.escapeIdentifier(name)}`);
759
- },
760
- // eslint-disable-next-line @typescript-eslint/require-await
761
- rollbackToSavepoint: async (name) => {
762
- db.exec(`ROLLBACK TO SAVEPOINT ${this.escapeIdentifier(name)}`);
763
- },
764
- // eslint-disable-next-line @typescript-eslint/require-await
765
- releaseSavepoint: async (name) => {
766
- db.exec(`RELEASE SAVEPOINT ${this.escapeIdentifier(name)}`);
767
- }
768
- };
769
- }
770
- /**
771
- * Classifies a SQLite error into a DatabaseError.
772
- *
773
- * @param error - Original error from better-sqlite3
774
- * @param sql - SQL statement that caused the error (optional)
775
- * @returns DatabaseError with proper classification
776
- */
777
- // fallow-ignore-next-line complexity
778
- classifyError(error, sql) {
779
- if (types.isDatabaseError(error)) return error;
780
- const sqliteError = this.driverErrorIn(
781
- error,
782
- (candidate) => typeof candidate.code === "string" && candidate.code !== ""
783
- );
784
- let kind = "unknown";
785
- if (sqliteError.code) {
786
- kind = SQLITE_ERROR_CODES[sqliteError.code] || "unknown";
787
- } else if (sqliteError.message) {
788
- const msg = sqliteError.message.toUpperCase();
789
- if (msg.includes("UNIQUE CONSTRAINT")) {
790
- kind = "unique_violation";
791
- } else if (msg.includes("FOREIGN KEY CONSTRAINT")) {
792
- kind = "foreign_key_violation";
793
- } else if (msg.includes("NOT NULL CONSTRAINT")) {
794
- kind = "not_null_violation";
795
- } else if (msg.includes("CHECK CONSTRAINT")) {
796
- kind = "check_violation";
797
- } else if (msg.includes("BUSY") || msg.includes("LOCKED")) {
798
- kind = "timeout";
799
- } else if (msg.includes("SQLITE_CANTOPEN") || msg.includes("UNABLE TO OPEN")) {
800
- kind = "connection";
801
- }
802
- }
803
- let message = sqliteError.message ?? String(error);
804
- if (sql && kind === "query") {
805
- message = `Query failed: ${message}`;
806
- }
807
- return types.createDatabaseError({
808
- kind,
809
- message,
810
- code: sqliteError.code,
811
- cause: error instanceof Error ? error : void 0
812
- });
813
- }
97
+ /**
98
+ * SQLite database adapter for Nextly.
99
+ *
100
+ * Extends the base DrizzleAdapter to provide SQLite-specific functionality
101
+ * using the better-sqlite3 package.
102
+ *
103
+ * @remarks
104
+ * SQLite has some differences from PostgreSQL:
105
+ * - Synchronous API (wrapped for async compatibility)
106
+ * - No connection pooling (single file/connection)
107
+ * - RETURNING clause supported (SQLite 3.35+)
108
+ * - Savepoints supported via nested transactions
109
+ * - No native ILIKE (uses LOWER() LIKE workaround)
110
+ * - JSON support (not JSONB)
111
+ * - No array types
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const adapter = new SqliteAdapter({
116
+ * url: 'file:./data.db',
117
+ * });
118
+ *
119
+ * await adapter.connect();
120
+ * ```
121
+ */
122
+ var SqliteAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
123
+ drizzleByRelations = /* @__PURE__ */ new WeakMap();
124
+ drizzleBare;
125
+ /**
126
+ * The database dialect - always 'sqlite' for this adapter.
127
+ */
128
+ dialect = "sqlite";
129
+ /**
130
+ * Adapter configuration.
131
+ */
132
+ config;
133
+ /**
134
+ * better-sqlite3 Database instance.
135
+ */
136
+ db = null;
137
+ /**
138
+ * Connection state flag.
139
+ */
140
+ connected = false;
141
+ /**
142
+ * Serialization queue for concurrent `transaction()` calls.
143
+ *
144
+ * better-sqlite3 is synchronous, single-connection, and rejects nested
145
+ * `BEGIN` with "cannot start a transaction within a transaction".
146
+ * When two `await adapter.transaction(...)` calls overlap (e.g. a bulk
147
+ * mutation calling `Promise.allSettled` with N per-row updates), the
148
+ * second one tries to BEGIN while the first is still open and the
149
+ * driver throws.
150
+ *
151
+ * Postgres/MySQL avoid this by allocating a fresh client/connection
152
+ * per transaction from a pool — there is no equivalent for
153
+ * better-sqlite3, so we serialize at the JS layer instead. Every
154
+ * `transaction()` invocation chains onto this promise; only one
155
+ * BEGIN→COMMIT/ROLLBACK section runs at any moment, preserving
156
+ * write-isolation semantics that callers expect from a transactional
157
+ * adapter.
158
+ *
159
+ * Performance impact is the natural floor: SQLite already serializes
160
+ * writers at the file-lock level, so the queue removes failed-call
161
+ * surface area without changing the achievable concurrency.
162
+ */
163
+ transactionQueue = Promise.resolve();
164
+ /**
165
+ * The transaction the current async chain is running inside, if any.
166
+ *
167
+ * A `transaction()` call made from INSIDE a transaction's work — a store
168
+ * writing on behalf of the caller, a plugin's write inside its own
169
+ * `ctx.db.transaction` — used to queue behind the very transaction that
170
+ * was waiting for it, and neither ever settled: that hung every later
171
+ * transaction on the instance too. Such a call now runs as a SAVEPOINT of
172
+ * the active transaction instead, so it commits or rolls back with it and
173
+ * its own failure undoes only its own part.
174
+ */
175
+ transactionScope = new node_async_hooks.AsyncLocalStorage();
176
+ /**
177
+ * Creates a new SQLite adapter instance.
178
+ *
179
+ * @param config - Adapter configuration
180
+ */
181
+ constructor(config) {
182
+ super();
183
+ this.config = config;
184
+ }
185
+ /**
186
+ * Connect to the SQLite database.
187
+ *
188
+ * @remarks
189
+ * This method initializes the database connection. For SQLite, this
190
+ * opens the database file or creates an in-memory database.
191
+ * Also configures WAL mode and foreign keys based on config.
192
+ *
193
+ * @throws {DatabaseError} If connection fails
194
+ */
195
+ async connect() {
196
+ if (this.connected && this.db) return;
197
+ try {
198
+ const Database = (await import("better-sqlite3")).default;
199
+ let dbPath;
200
+ if (this.config.memory) dbPath = ":memory:";
201
+ else if (this.config.url) dbPath = this.config.url.replace(/^file:/, "");
202
+ else dbPath = ":memory:";
203
+ if (dbPath !== ":memory:" && !this.config.readonly) {
204
+ const fs = await import("node:fs");
205
+ const path = await import("node:path");
206
+ const dir = path.dirname(path.resolve(dbPath));
207
+ await fs.promises.mkdir(dir, { recursive: true });
208
+ }
209
+ this.db = new Database(dbPath, {
210
+ readonly: this.config.readonly ?? false,
211
+ timeout: this.config.busyTimeout ?? DEFAULT_CONFIG.busyTimeout
212
+ });
213
+ if ((this.config.wal ?? DEFAULT_CONFIG.wal) && dbPath !== ":memory:" && !this.config.readonly) this.db.pragma("journal_mode = WAL");
214
+ if (this.config.foreignKeys ?? DEFAULT_CONFIG.foreignKeys) this.db.pragma("foreign_keys = ON");
215
+ await (0, _nextlyhq_adapter_drizzle_version_check.checkDialectVersion)(this.db, "sqlite");
216
+ this.connected = true;
217
+ if (this.config.logger?.info) this.config.logger.info("SQLite connection established", {
218
+ url: dbPath === ":memory:" ? "in-memory" : dbPath,
219
+ wal: this.config.wal ?? DEFAULT_CONFIG.wal,
220
+ foreignKeys: this.config.foreignKeys ?? DEFAULT_CONFIG.foreignKeys
221
+ });
222
+ } catch (error) {
223
+ if (this.db) {
224
+ try {
225
+ this.db.close();
226
+ } catch {}
227
+ this.db = null;
228
+ }
229
+ throw this.classifyError(error);
230
+ }
231
+ }
232
+ /**
233
+ * Disconnect from the SQLite database.
234
+ *
235
+ * @remarks
236
+ * This method closes the database connection and releases resources.
237
+ */
238
+ async disconnect() {
239
+ this.drizzleBare = void 0;
240
+ this.drizzleByRelations = /* @__PURE__ */ new WeakMap();
241
+ if (!this.db) return;
242
+ try {
243
+ this.db.close();
244
+ if (this.config.logger?.info) this.config.logger.info("SQLite connection closed");
245
+ } finally {
246
+ this.db = null;
247
+ this.connected = false;
248
+ }
249
+ }
250
+ /**
251
+ * Check if connected to the database.
252
+ */
253
+ isConnected() {
254
+ return this.connected && this.db !== null;
255
+ }
256
+ /**
257
+ * Get connection pool statistics.
258
+ *
259
+ * @remarks
260
+ * SQLite doesn't use connection pooling, so this returns null.
261
+ * The database is single-file with a single connection.
262
+ */
263
+ getPoolStats() {
264
+ return null;
265
+ }
266
+ /**
267
+ * Execute a raw SQL query.
268
+ *
269
+ * @param sql - SQL query string with $1, $2 placeholders (converted to ? for SQLite)
270
+ * @param params - Query parameters
271
+ * @returns Query results
272
+ *
273
+ * @throws {DatabaseError} If query execution fails
274
+ */
275
+ async executeQuery(sql, params = []) {
276
+ const db = this.ensureDb();
277
+ const startTime = Date.now();
278
+ try {
279
+ const convertedSql = this.convertPlaceholders(sql);
280
+ const trimmedSql = convertedSql.trim().toUpperCase();
281
+ const isPragmaQuery = trimmedSql.startsWith("PRAGMA") && !trimmedSql.includes("=");
282
+ const isSelect = trimmedSql.startsWith("SELECT") || isPragmaQuery || trimmedSql.startsWith("WITH");
283
+ const hasReturning = trimmedSql.includes("RETURNING");
284
+ let result;
285
+ const sanitizedParams = params.map(sanitizeSqliteValue);
286
+ if (isSelect || hasReturning) result = db.prepare(convertedSql).all(...sanitizedParams);
287
+ else {
288
+ const runResult = db.prepare(convertedSql).run(...sanitizedParams);
289
+ result = [{
290
+ changes: runResult.changes,
291
+ lastInsertRowid: runResult.lastInsertRowid
292
+ }];
293
+ }
294
+ if (this.config.logger?.query) {
295
+ const durationMs = Date.now() - startTime;
296
+ this.config.logger.query(convertedSql, params, durationMs);
297
+ }
298
+ return result;
299
+ } catch (error) {
300
+ throw this.classifyError(error, sql);
301
+ }
302
+ }
303
+ /**
304
+ * Execute work within a transaction.
305
+ *
306
+ * @param work - Function containing transactional operations
307
+ * @param options - Transaction options (isolation level not fully supported in SQLite)
308
+ * @returns Result of the work function
309
+ *
310
+ * @remarks
311
+ * The adapter manages the transaction itself, because better-sqlite3's
312
+ * `db.transaction()` does not take async work: it runs `BEGIN IMMEDIATE`,
313
+ * then `COMMIT` when `work` resolves or `ROLLBACK` when it throws.
314
+ * - Top-level calls run one at a time.
315
+ * - A call made inside an open transaction's work joins it as a savepoint
316
+ * (`SAVEPOINT` / `RELEASE` / `ROLLBACK TO`).
317
+ * - Nested calls of one scope run one at a time.
318
+ * - A call made later from a savepoint that has already released joins the
319
+ * innermost scope still open.
320
+ * - Nested work started and not awaited finishes before the enclosing scope
321
+ * commits or rolls back.
322
+ *
323
+ * Note: SQLite uses DEFERRED, IMMEDIATE, or EXCLUSIVE transaction modes
324
+ * rather than isolation levels. This adapter uses IMMEDIATE.
325
+ */
326
+ /** How long a transaction waits for the connection: `pool.connectionTimeoutMs`. */
327
+ transactionWaitMs() {
328
+ return this.config.pool?.connectionTimeoutMs ?? DEFAULT_CONFIG.connectionTimeoutMs;
329
+ }
330
+ async transaction(work, _options) {
331
+ const own = this.transactionScope.getStore();
332
+ const parent = this.nestingScope();
333
+ if (own && parent !== own) own.writesAfterClosing += 1;
334
+ if (parent) return enqueueIn(parent, () => this.runSavepoint(work, parent));
335
+ const run = async () => this.runTransaction(work);
336
+ const previous = this.transactionQueue;
337
+ const next = waitForTurn(previous, this.transactionWaitMs()).then(run);
338
+ this.transactionQueue = previous.then(() => next).catch(() => void 0);
339
+ const { result, effects } = await next;
340
+ await this.runHeldEffects(effects);
341
+ return result;
342
+ }
343
+ /**
344
+ * Hold `effect` until the transaction enclosing this async context commits,
345
+ * or run it now when none does.
346
+ *
347
+ * The effect belongs to the scope this async context runs in, even one
348
+ * that has already released or rolled back: a service whose own
349
+ * transaction nested as a savepoint registers from its caller's scope once
350
+ * that savepoint has released, and the change is undoable for as long as
351
+ * any scope around it is. The outermost transaction runs the effect once
352
+ * its COMMIT succeeds, unless a scope between the effect's scope and the
353
+ * outermost transaction rolled back, and drops every effect on ROLLBACK or
354
+ * a failed COMMIT. A context whose outermost transaction has already
355
+ * finished is outside any transaction, so its effect runs now, unless what
356
+ * it announces was undone and it has run no transaction since.
357
+ */
358
+ async afterCommit(effect, onDeferredFailure) {
359
+ const scope = this.transactionScope.getStore();
360
+ if (!scope) {
361
+ await effect();
362
+ return;
363
+ }
364
+ const root = outermost(scope);
365
+ if (!root.finished) {
366
+ root.held.push({
367
+ effect: this.inRegisteringContext(effect),
368
+ onDeferredFailure,
369
+ scope,
370
+ writesBefore: scope.writesAfterClosing
371
+ });
372
+ return;
373
+ }
374
+ if (!undone(scope, scope.writesAfterClosing)) await effect();
375
+ }
376
+ /**
377
+ * `effect`, bound to the async context it is registered from.
378
+ *
379
+ * A held effect runs after the outermost COMMIT, from the code that ran the
380
+ * outermost transaction, so without this it sees that code's context rather
381
+ * than its own: whatever its caller pinned around the write is gone. That
382
+ * includes who is calling. A scope pinned for a scoped API key lost there
383
+ * leaves a hook's nested call judged by the key's owner instead of the key,
384
+ * and a person's frame lost there leaves the hook told of nobody.
385
+ *
386
+ * The adapter's own transaction scope is the one store it leaves: the
387
+ * effect runs once that transaction has finished, outside it, as an effect
388
+ * held to the outermost commit always has.
389
+ */
390
+ inRegisteringContext(effect) {
391
+ return node_async_hooks.AsyncResource.bind(() => this.transactionScope.exit(effect));
392
+ }
393
+ /**
394
+ * Run the effects a committed transaction held, in order. A failure is
395
+ * reported, never thrown: the change is durable, and reporting the
396
+ * transaction as failed would invite a retry of something already done.
397
+ */
398
+ async runHeldEffects(effects) {
399
+ for (const { effect, onDeferredFailure } of effects) try {
400
+ await effect();
401
+ } catch (error) {
402
+ this.reportHeldEffectFailure(error, onDeferredFailure);
403
+ }
404
+ }
405
+ /** Hand a held effect's failure to its reporter, or to the adapter's log. */
406
+ reportHeldEffectFailure(error, onDeferredFailure) {
407
+ try {
408
+ if (onDeferredFailure) {
409
+ onDeferredFailure(error);
410
+ return;
411
+ }
412
+ } catch {}
413
+ const failure = error instanceof Error ? error : new Error(String(error));
414
+ if (this.config.logger?.error) this.config.logger.error(failure, { phase: "after-commit" });
415
+ else console.error("An effect held until commit failed:", failure);
416
+ }
417
+ /**
418
+ * Whether a `transaction()` call made here would join an open transaction
419
+ * as a savepoint. On SQLite it would: the connection is the only one, so a
420
+ * transaction open on it encloses whatever the async chain that opened it
421
+ * starts.
422
+ */
423
+ inTransaction() {
424
+ return this.nestingScope() !== void 0;
425
+ }
426
+ /**
427
+ * Whether the calling async flow is inside a transaction that has not
428
+ * finished, judged from the transaction this flow was started inside,
429
+ * never from the connection: another request's open transaction answers
430
+ * false here, where its own `transaction()` queues behind that one.
431
+ *
432
+ * A flow started inside a transaction, even inside a savepoint that has
433
+ * since released, stays inside it until its outermost transaction has
434
+ * committed or rolled back: until then whatever it writes on the one
435
+ * connection lands in that transaction.
436
+ */
437
+ transactionOpenInFlow() {
438
+ const own = this.transactionScope.getStore();
439
+ return own !== void 0 && !outermost(own).finished;
440
+ }
441
+ /**
442
+ * The scope a `transaction()` call made here nests in, or none when it
443
+ * opens a transaction of its own.
444
+ *
445
+ * A call made later from inside a savepoint that has since released runs
446
+ * inside the innermost scope open on the connection. Queued on an enclosing
447
+ * scope, or on the instance, it would wait behind the scope running there,
448
+ * which may be the one awaiting it. Savepoints on one connection form a
449
+ * stack, so whatever opens now is inside that innermost scope anyway; when
450
+ * none is open the call queues on the instance.
451
+ *
452
+ * The innermost open scope need not be related to the call. When it is a
453
+ * sibling savepoint that later rolls back, the call's write rolls back with
454
+ * it, although the call itself already resolved. Waiting for an unrelated
455
+ * scope instead could wait on the very scope that awaits the call.
456
+ */
457
+ nestingScope() {
458
+ const own = this.transactionScope.getStore();
459
+ return own?.active ? own : own?.open.at(-1);
460
+ }
461
+ /**
462
+ * Run `work` as a savepoint of the active transaction: released when it
463
+ * resolves, rolled back to when it throws.
464
+ */
465
+ async runSavepoint(work, parent) {
466
+ const db = this.ensureDb();
467
+ const name = `nextly_sp_${parent.depth + 1}`;
468
+ try {
469
+ const ctx = this.createTransactionContext(db);
470
+ db.exec(`SAVEPOINT ${name}`);
471
+ const scope = openScope(parent);
472
+ try {
473
+ const result = await this.transactionScope.run(scope, () => work(ctx));
474
+ await settleNested(scope);
475
+ db.exec(`RELEASE ${name}`);
476
+ return result;
477
+ } catch (error) {
478
+ await settleNested(scope);
479
+ scope.rolledBack = true;
480
+ try {
481
+ db.exec(`ROLLBACK TO ${name}`);
482
+ db.exec(`RELEASE ${name}`);
483
+ } catch {}
484
+ throw error;
485
+ }
486
+ } catch (error) {
487
+ if ((0, _nextlyhq_adapter_drizzle_types.isApplicationError)(error)) throw error;
488
+ throw this.classifyError(error);
489
+ }
490
+ }
491
+ /**
492
+ * Inner transaction body. Kept private so callers always go through
493
+ * the serialized `transaction()` queue above and the BEGIN/COMMIT
494
+ * pair is never invoked outside of it.
495
+ */
496
+ async runTransaction(work) {
497
+ const db = this.ensureDb();
498
+ const startTime = Date.now();
499
+ try {
500
+ const ctx = this.createTransactionContext(db);
501
+ db.exec("BEGIN IMMEDIATE");
502
+ const scope = openScope();
503
+ try {
504
+ const result = await this.transactionScope.run(scope, () => work(ctx));
505
+ await settleNested(scope);
506
+ db.exec("COMMIT");
507
+ scope.finished = true;
508
+ if (this.config.logger?.debug) {
509
+ const durationMs = Date.now() - startTime;
510
+ this.config.logger.debug("Transaction committed", { durationMs });
511
+ }
512
+ return {
513
+ result,
514
+ effects: scope.held.filter((held) => !undone(held.scope, held.writesBefore))
515
+ };
516
+ } catch (error) {
517
+ await settleNested(scope);
518
+ scope.rolledBack = true;
519
+ scope.finished = true;
520
+ scope.held.length = 0;
521
+ try {
522
+ db.exec("ROLLBACK");
523
+ } catch {}
524
+ throw error;
525
+ }
526
+ } catch (error) {
527
+ if ((0, _nextlyhq_adapter_drizzle_types.isApplicationError)(error)) throw error;
528
+ throw this.classifyError(error);
529
+ }
530
+ }
531
+ /**
532
+ * Get SQLite database capabilities.
533
+ *
534
+ * @remarks
535
+ * SQLite capabilities:
536
+ * - JSON support (not JSONB)
537
+ * - No arrays
538
+ * - No native ILIKE
539
+ * - RETURNING clause (3.35+)
540
+ * - Savepoints supported
541
+ * - ON CONFLICT supported
542
+ */
543
+ getCapabilities() {
544
+ return {
545
+ dialect: "sqlite",
546
+ supportsJsonb: false,
547
+ supportsJson: true,
548
+ supportsArrays: false,
549
+ supportsGeneratedColumns: true,
550
+ supportsFts: true,
551
+ supportsIlike: false,
552
+ supportsReturning: true,
553
+ supportsSavepoints: true,
554
+ supportsOnConflict: true,
555
+ maxParamsPerQuery: 999,
556
+ maxIdentifierLength: 128
557
+ };
558
+ }
559
+ /**
560
+ * Override insertMany for bulk insert optimization.
561
+ *
562
+ * @remarks
563
+ * Uses a single multi-row INSERT statement for better performance.
564
+ */
565
+ async insertMany(table, data, options) {
566
+ if (data.length === 0) return [];
567
+ const db = this.ensureDb();
568
+ if (data.length === 1) return [await this.insert(table, data[0], options)];
569
+ const columns = Object.keys(data[0]);
570
+ const params = [];
571
+ const valuesClauses = [];
572
+ for (const record of data) {
573
+ const placeholders = [];
574
+ for (const col of columns) {
575
+ params.push(sanitizeSqliteValue(record[col]));
576
+ placeholders.push("?");
577
+ }
578
+ valuesClauses.push(`(${placeholders.join(", ")})`);
579
+ }
580
+ const columnList = columns.map((col) => this.escapeIdentifier(col)).join(", ");
581
+ let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columnList}) VALUES ${valuesClauses.join(", ")}`;
582
+ if (options?.returning) {
583
+ const returning = options.returning === "*" ? "*" : options.returning.map((col) => this.escapeIdentifier(col)).join(", ");
584
+ sql += ` RETURNING ${returning}`;
585
+ } else sql += " RETURNING *";
586
+ try {
587
+ const rows = db.prepare(sql).all(...params);
588
+ const tableObj = this.getTableObject(table);
589
+ return rows.map((r) => this.mapRowFromRawSql(tableObj, r));
590
+ } catch (error) {
591
+ throw this.handleQueryError(error, "insertMany", table);
592
+ }
593
+ }
594
+ /**
595
+ * Ensures database is connected and returns it.
596
+ *
597
+ * @throws {DatabaseError} If not connected
598
+ */
599
+ ensureDb() {
600
+ if (!this.db) throw (0, _nextlyhq_adapter_drizzle_types.createDatabaseError)({
601
+ kind: "connection",
602
+ message: "SqliteAdapter is not connected. Call connect() first."
603
+ });
604
+ return this.db;
605
+ }
606
+ /**
607
+ * Return the typed Drizzle instance for SQLite.
608
+ * Guarded for server-only usage and requires an active connection.
609
+ *
610
+ * @param schema - Optional schema for relational queries (db.query.*)
611
+ * @returns Drizzle ORM instance wrapping the better-sqlite3 connection
612
+ * @throws {Error} If called in browser or not connected
613
+ */
614
+ getDrizzle(relations) {
615
+ if (typeof window !== "undefined") throw new Error("getDrizzle() is server-only");
616
+ const db = this.ensureDb();
617
+ if (!relations) {
618
+ this.drizzleBare ??= (0, drizzle_orm_better_sqlite3.drizzle)({ client: db });
619
+ return this.drizzleBare;
620
+ }
621
+ let cached = this.drizzleByRelations.get(relations);
622
+ if (!cached) {
623
+ cached = (0, drizzle_orm_better_sqlite3.drizzle)({
624
+ client: db,
625
+ relations
626
+ });
627
+ this.drizzleByRelations.set(relations, cached);
628
+ }
629
+ return cached;
630
+ }
631
+ /**
632
+ * Convert $1, $2 placeholders to ? for better-sqlite3.
633
+ *
634
+ * @param sql - SQL with PostgreSQL-style placeholders
635
+ * @returns SQL with ? placeholders
636
+ */
637
+ convertPlaceholders(sql) {
638
+ return sql.replace(/\$\d+/g, "?");
639
+ }
640
+ /**
641
+ * Creates a TransactionContext for the given database connection.
642
+ */
643
+ /** A table-level `primaryKey({ columns })`, read through the SQLite table config. */
644
+ compositePrimaryKey(tableObj) {
645
+ return (0, drizzle_orm_sqlite_core.getTableConfig)(tableObj).primaryKeys.flatMap((key) => key.columns);
646
+ }
647
+ createTransactionContext(db) {
648
+ const txHandles = this.transactionDrizzleHandles((relations) => relations ? (0, drizzle_orm_better_sqlite3.drizzle)({
649
+ client: db,
650
+ relations
651
+ }) : (0, drizzle_orm_better_sqlite3.drizzle)({ client: db }));
652
+ const txDb = txHandles.bare;
653
+ return {
654
+ drizzleWithRelations: txHandles.withRelations,
655
+ lockRow: () => Promise.resolve(),
656
+ runStatement: (statement) => {
657
+ txDb().run(statement);
658
+ return Promise.resolve();
659
+ },
660
+ queryStatement: (statement) => Promise.resolve(txDb().all(statement)),
661
+ execute: async (sql, params = []) => {
662
+ const convertedSql = this.convertPlaceholders(sql);
663
+ const trimmedSql = convertedSql.trim().toUpperCase();
664
+ const isSelect = trimmedSql.startsWith("SELECT") || trimmedSql.includes("RETURNING");
665
+ const sanitizedParams = params.map(sanitizeSqliteValue);
666
+ if (isSelect) return db.prepare(convertedSql).all(...sanitizedParams);
667
+ else {
668
+ const result = db.prepare(convertedSql).run(...sanitizedParams);
669
+ return [{
670
+ changes: result.changes,
671
+ lastInsertRowid: result.lastInsertRowid
672
+ }];
673
+ }
674
+ },
675
+ insert: async (table, data, options) => {
676
+ const mapped = this.mapKeysToSqlColumns(this.getTableObject(table), data);
677
+ const columns = Object.keys(mapped);
678
+ const values = Object.values(mapped).map(sanitizeSqliteValue);
679
+ const placeholders = values.map(() => "?").join(", ");
680
+ let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES (${placeholders})`;
681
+ const ret = options?.returning;
682
+ const returningEmpty = Array.isArray(ret) && ret.length === 0;
683
+ if (!returningEmpty) {
684
+ const returning = !ret || ret === "*" ? "*" : this.mapColumnNamesToSql(this.getTableObject(table), ret).map((col) => this.escapeIdentifier(col)).join(", ");
685
+ sql += ` RETURNING ${returning}`;
686
+ }
687
+ const stmt = db.prepare(sql);
688
+ if (returningEmpty) {
689
+ stmt.run(...values);
690
+ return;
691
+ }
692
+ const rows = stmt.all(...values);
693
+ return this.mapRowFromRawSql(this.getTableObject(table), rows[0]);
694
+ },
695
+ insertMany: async (table, data, options) => {
696
+ if (data.length === 0) return [];
697
+ const tableObj = this.getTableObject(table);
698
+ const mappedRecords = data.map((r) => this.mapKeysToSqlColumns(tableObj, r));
699
+ const columns = Object.keys(mappedRecords[0]);
700
+ const allValues = [];
701
+ const valuesClauses = [];
702
+ for (const record of mappedRecords) {
703
+ const placeholders = [];
704
+ for (const col of columns) {
705
+ allValues.push(sanitizeSqliteValue(record[col]));
706
+ placeholders.push("?");
707
+ }
708
+ valuesClauses.push(`(${placeholders.join(", ")})`);
709
+ }
710
+ let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES ${valuesClauses.join(", ")}`;
711
+ const ret = options?.returning;
712
+ const returningEmpty = Array.isArray(ret) && ret.length === 0;
713
+ if (!returningEmpty) {
714
+ const returning = !ret || ret === "*" ? "*" : this.mapColumnNamesToSql(this.getTableObject(table), ret).map((col) => this.escapeIdentifier(col)).join(", ");
715
+ sql += ` RETURNING ${returning}`;
716
+ }
717
+ const stmt = db.prepare(sql);
718
+ if (returningEmpty) {
719
+ stmt.run(...allValues);
720
+ return [];
721
+ }
722
+ return stmt.all(...allValues).map((r) => this.mapRowFromRawSql(tableObj, r));
723
+ },
724
+ update: this.transactionUpdate(txDb, (statement, returnsRows) => returnsRows ? txDb().all(statement) : (txDb().run(statement), void 0), sanitizeSqliteValue),
725
+ ...this.createTransactionForwarders(txDb),
726
+ savepoint: async (name) => {
727
+ db.exec(`SAVEPOINT ${this.escapeIdentifier(name)}`);
728
+ },
729
+ rollbackToSavepoint: async (name) => {
730
+ db.exec(`ROLLBACK TO SAVEPOINT ${this.escapeIdentifier(name)}`);
731
+ },
732
+ releaseSavepoint: async (name) => {
733
+ db.exec(`RELEASE SAVEPOINT ${this.escapeIdentifier(name)}`);
734
+ }
735
+ };
736
+ }
737
+ /**
738
+ * Classifies a SQLite error into a DatabaseError.
739
+ *
740
+ * @param error - Original error from better-sqlite3
741
+ * @param sql - SQL statement that caused the error (optional)
742
+ * @returns DatabaseError with proper classification
743
+ */
744
+ classifyError(error, sql) {
745
+ if ((0, _nextlyhq_adapter_drizzle_types.isDatabaseError)(error)) return error;
746
+ const sqliteError = this.driverErrorIn(error, (candidate) => typeof candidate.code === "string" && candidate.code !== "");
747
+ let kind = "unknown";
748
+ if (sqliteError.code) kind = SQLITE_ERROR_CODES[sqliteError.code] || "unknown";
749
+ else if (sqliteError.message) {
750
+ const msg = sqliteError.message.toUpperCase();
751
+ if (msg.includes("UNIQUE CONSTRAINT")) kind = "unique_violation";
752
+ else if (msg.includes("FOREIGN KEY CONSTRAINT")) kind = "foreign_key_violation";
753
+ else if (msg.includes("NOT NULL CONSTRAINT")) kind = "not_null_violation";
754
+ else if (msg.includes("CHECK CONSTRAINT")) kind = "check_violation";
755
+ else if (msg.includes("BUSY") || msg.includes("LOCKED")) kind = "timeout";
756
+ else if (msg.includes("SQLITE_CANTOPEN") || msg.includes("UNABLE TO OPEN")) kind = "connection";
757
+ }
758
+ let message = sqliteError.message ?? String(error);
759
+ if (sql && kind === "query") message = `Query failed: ${message}`;
760
+ return (0, _nextlyhq_adapter_drizzle_types.createDatabaseError)({
761
+ kind,
762
+ message,
763
+ code: sqliteError.code,
764
+ cause: error instanceof Error ? error : void 0
765
+ });
766
+ }
814
767
  };
768
+ /**
769
+ * Create a SQLite database adapter.
770
+ *
771
+ * @param config - SQLite adapter configuration
772
+ * @returns A new SqliteAdapter instance
773
+ *
774
+ * @example
775
+ * ```typescript
776
+ * // Simple usage with file path
777
+ * const adapter = createSqliteAdapter({
778
+ * url: 'file:./data.db',
779
+ * });
780
+ *
781
+ * // In-memory database
782
+ * const memAdapter = createSqliteAdapter({
783
+ * memory: true,
784
+ * });
785
+ *
786
+ * // Full configuration
787
+ * const adapter = createSqliteAdapter({
788
+ * url: 'file:./data.db',
789
+ * wal: true,
790
+ * foreignKeys: true,
791
+ * busyTimeout: 5000,
792
+ * logger: {
793
+ * query: (sql, params, duration) => console.log(`Query: ${sql}`),
794
+ * },
795
+ * });
796
+ *
797
+ * await adapter.connect();
798
+ * ```
799
+ */
815
800
  function createSqliteAdapter(config) {
816
- return new SqliteAdapter(config);
801
+ return new SqliteAdapter(config);
817
802
  }
803
+ /**
804
+ * Type guard to check if a value is a SqliteAdapter.
805
+ *
806
+ * @param value - Value to check
807
+ * @returns True if value is a SqliteAdapter instance
808
+ *
809
+ * @example
810
+ * ```typescript
811
+ * if (isSqliteAdapter(adapter)) {
812
+ * // TypeScript knows adapter is SqliteAdapter
813
+ * console.log('Using SQLite');
814
+ * }
815
+ * ```
816
+ */
818
817
  function isSqliteAdapter(value) {
819
- return value instanceof SqliteAdapter;
818
+ return value instanceof SqliteAdapter;
820
819
  }
820
+ /**
821
+ * Open the scope of a savepoint inside `parent`, or of an outermost
822
+ * transaction when there is none, and record it among the connection's open
823
+ * scopes.
824
+ */
821
825
  function openScope(parent) {
822
- const scope = {
823
- active: true,
824
- depth: parent ? parent.depth + 1 : 0,
825
- parent,
826
- rolledBack: false,
827
- writesAfterClosing: 0,
828
- finished: false,
829
- held: [],
830
- queue: Promise.resolve(),
831
- open: parent ? parent.open : []
832
- };
833
- scope.open.push(scope);
834
- return scope;
826
+ const scope = {
827
+ active: true,
828
+ depth: parent ? parent.depth + 1 : 0,
829
+ parent,
830
+ rolledBack: false,
831
+ writesAfterClosing: 0,
832
+ finished: false,
833
+ held: [],
834
+ queue: Promise.resolve(),
835
+ open: parent ? parent.open : []
836
+ };
837
+ scope.open.push(scope);
838
+ return scope;
835
839
  }
840
+ /** The outermost transaction `scope` runs inside; itself for that one. */
836
841
  function outermost(scope) {
837
- let at = scope;
838
- while (at.parent) at = at.parent;
839
- return at;
842
+ let at = scope;
843
+ while (at.parent) at = at.parent;
844
+ return at;
840
845
  }
846
+ /**
847
+ * Whether an effect registered from `scope` describes a write that was
848
+ * undone: `scope`, or a scope it runs inside, rolled back, and no
849
+ * transaction had run from its context since it closed when the effect was
850
+ * registered (`writesBefore`). An effect registered before such a write
851
+ * still describes the undone one.
852
+ */
841
853
  function undone(scope, writesBefore) {
842
- if (writesBefore > 0) return false;
843
- for (let at = scope; at; at = at.parent) {
844
- if (at.rolledBack) return true;
845
- }
846
- return false;
854
+ if (writesBefore > 0) return false;
855
+ for (let at = scope; at; at = at.parent) if (at.rolledBack) return true;
856
+ return false;
857
+ }
858
+ /**
859
+ * Settles once `turn` does, or rejects with the pool-wait timeout error once
860
+ * `boundMs` passes first. A bound of 0 waits without limit, as pg-pool reads a
861
+ * `connectionTimeoutMillis` of 0.
862
+ */
863
+ function waitForTurn(turn, boundMs) {
864
+ if (boundMs <= 0) return turn.then(() => void 0);
865
+ return new Promise((resolve, reject) => {
866
+ const timer = setTimeout(() => reject((0, _nextlyhq_adapter_drizzle_types.poolWaitTimeoutError)(boundMs)), boundMs);
867
+ timer.unref();
868
+ turn.then(() => {
869
+ clearTimeout(timer);
870
+ resolve();
871
+ });
872
+ });
847
873
  }
874
+ /** Chain `run` onto a scope's queue, keeping the queue itself unrejected. */
848
875
  function enqueueIn(scope, run) {
849
- const next = scope.queue.then(run, run);
850
- scope.queue = next.catch(() => void 0);
851
- return next;
876
+ const next = scope.queue.then(run, run);
877
+ scope.queue = next.catch(() => void 0);
878
+ return next;
852
879
  }
880
+ /**
881
+ * Close a scope to new nested calls and wait for those already queued. A call
882
+ * arriving after this joins the innermost scope still open on the connection,
883
+ * or, when none is, goes to the instance queue behind the transaction.
884
+ */
853
885
  async function settleNested(scope) {
854
- scope.active = false;
855
- const index = scope.open.indexOf(scope);
856
- if (index !== -1) scope.open.splice(index, 1);
857
- await scope.queue;
886
+ scope.active = false;
887
+ const index = scope.open.indexOf(scope);
888
+ if (index !== -1) scope.open.splice(index, 1);
889
+ await scope.queue;
858
890
  }
859
-
860
891
  exports.SqliteAdapter = SqliteAdapter;
861
892
  exports.VERSION = VERSION;
862
893
  exports.createSqliteAdapter = createSqliteAdapter;
863
894
  exports.isSqliteAdapter = isSqliteAdapter;
864
- //# sourceMappingURL=index.cjs.map
895
+
865
896
  //# sourceMappingURL=index.cjs.map