@nextlyhq/adapter-sqlite 0.0.2-alpha.7 → 0.0.2-alpha.71

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,572 +1,896 @@
1
- 'use strict';
2
-
3
- var adapterDrizzle = require('@nextlyhq/adapter-drizzle');
4
- var types = require('@nextlyhq/adapter-drizzle/types');
5
- var versionCheck = require('@nextlyhq/adapter-drizzle/version-check');
6
- var betterSqlite3 = require('drizzle-orm/better-sqlite3');
7
-
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");
8
8
  // src/index.ts
9
- var VERSION = "0.1.0";
10
- var SQLITE_ERROR_CODES = {
11
- // Constraint violations
12
- SQLITE_CONSTRAINT: "constraint",
13
- SQLITE_CONSTRAINT_UNIQUE: "unique_violation",
14
- SQLITE_CONSTRAINT_PRIMARYKEY: "unique_violation",
15
- SQLITE_CONSTRAINT_FOREIGNKEY: "foreign_key_violation",
16
- SQLITE_CONSTRAINT_NOTNULL: "not_null_violation",
17
- SQLITE_CONSTRAINT_CHECK: "check_violation",
18
- // Busy/locked errors
19
- SQLITE_BUSY: "timeout",
20
- SQLITE_LOCKED: "timeout",
21
- // Connection errors
22
- SQLITE_CANTOPEN: "connection",
23
- SQLITE_NOTADB: "connection",
24
- SQLITE_CORRUPT: "connection",
25
- // Query errors
26
- SQLITE_ERROR: "query",
27
- SQLITE_MISUSE: "query",
28
- 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"
29
68
  };
30
- var DEFAULT_CONFIG = {
31
- busyTimeout: 5e3,
32
- wal: true,
33
- foreignKeys: true
69
+ /**
70
+ * Default configuration values.
71
+ */
72
+ const DEFAULT_CONFIG = {
73
+ busyTimeout: 5e3,
74
+ wal: true,
75
+ foreignKeys: true,
76
+ connectionTimeoutMs: 15e3
34
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
+ */
35
89
  function sanitizeSqliteValue(v) {
36
- if (v === void 0) return null;
37
- if (typeof v === "boolean") return v ? 1 : 0;
38
- if (v instanceof Date) return v.toISOString();
39
- if (v !== null && typeof v === "object") return JSON.stringify(v);
40
- 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;
41
96
  }
42
- var SqliteAdapter = class extends adapterDrizzle.DrizzleAdapter {
43
- /**
44
- * The database dialect - always 'sqlite' for this adapter.
45
- */
46
- dialect = "sqlite";
47
- /**
48
- * Adapter configuration.
49
- */
50
- config;
51
- /**
52
- * better-sqlite3 Database instance.
53
- */
54
- db = null;
55
- /**
56
- * Connection state flag.
57
- */
58
- connected = false;
59
- /**
60
- * Serialization queue for concurrent `transaction()` calls.
61
- *
62
- * better-sqlite3 is synchronous, single-connection, and rejects nested
63
- * `BEGIN` with "cannot start a transaction within a transaction".
64
- * When two `await adapter.transaction(...)` calls overlap (e.g. a bulk
65
- * mutation calling `Promise.allSettled` with N per-row updates), the
66
- * second one tries to BEGIN while the first is still open and the
67
- * driver throws.
68
- *
69
- * Postgres/MySQL avoid this by allocating a fresh client/connection
70
- * per transaction from a pool — there is no equivalent for
71
- * better-sqlite3, so we serialize at the JS layer instead. Every
72
- * `transaction()` invocation chains onto this promise; only one
73
- * BEGIN→COMMIT/ROLLBACK section runs at any moment, preserving
74
- * write-isolation semantics that callers expect from a transactional
75
- * adapter.
76
- *
77
- * Performance impact is the natural floor: SQLite already serializes
78
- * writers at the file-lock level, so the queue removes failed-call
79
- * surface area without changing the achievable concurrency.
80
- */
81
- transactionQueue = Promise.resolve();
82
- /**
83
- * Creates a new SQLite adapter instance.
84
- *
85
- * @param config - Adapter configuration
86
- */
87
- constructor(config) {
88
- super();
89
- this.config = config;
90
- }
91
- /**
92
- * Connect to the SQLite database.
93
- *
94
- * @remarks
95
- * This method initializes the database connection. For SQLite, this
96
- * opens the database file or creates an in-memory database.
97
- * Also configures WAL mode and foreign keys based on config.
98
- *
99
- * @throws {DatabaseError} If connection fails
100
- */
101
- async connect() {
102
- if (this.connected && this.db) {
103
- return;
104
- }
105
- try {
106
- const BetterSqlite3 = await import('better-sqlite3');
107
- const Database = BetterSqlite3.default;
108
- let dbPath;
109
- if (this.config.memory) {
110
- dbPath = ":memory:";
111
- } else if (this.config.url) {
112
- dbPath = this.config.url.replace(/^file:/, "");
113
- } else {
114
- dbPath = ":memory:";
115
- }
116
- if (dbPath !== ":memory:" && !this.config.readonly) {
117
- const fs = await import('fs');
118
- const path = await import('path');
119
- const dir = path.dirname(path.resolve(dbPath));
120
- await fs.promises.mkdir(dir, { recursive: true });
121
- }
122
- this.db = new Database(dbPath, {
123
- readonly: this.config.readonly ?? false,
124
- timeout: this.config.busyTimeout ?? DEFAULT_CONFIG.busyTimeout
125
- });
126
- if ((this.config.wal ?? DEFAULT_CONFIG.wal) && dbPath !== ":memory:" && !this.config.readonly) {
127
- this.db.pragma("journal_mode = WAL");
128
- }
129
- if (this.config.foreignKeys ?? DEFAULT_CONFIG.foreignKeys) {
130
- this.db.pragma("foreign_keys = ON");
131
- }
132
- await versionCheck.checkDialectVersion(this.db, "sqlite");
133
- this.connected = true;
134
- if (this.config.logger?.info) {
135
- this.config.logger.info("SQLite connection established", {
136
- url: dbPath === ":memory:" ? "in-memory" : dbPath,
137
- wal: this.config.wal ?? DEFAULT_CONFIG.wal,
138
- foreignKeys: this.config.foreignKeys ?? DEFAULT_CONFIG.foreignKeys
139
- });
140
- }
141
- } catch (error) {
142
- if (this.db) {
143
- try {
144
- this.db.close();
145
- } catch {
146
- }
147
- this.db = null;
148
- }
149
- throw this.classifyError(error);
150
- }
151
- }
152
- /**
153
- * Disconnect from the SQLite database.
154
- *
155
- * @remarks
156
- * This method closes the database connection and releases resources.
157
- */
158
- // better-sqlite3 is synchronous; method is async to satisfy the DatabaseAdapter contract.
159
- // eslint-disable-next-line @typescript-eslint/require-await
160
- async disconnect() {
161
- if (!this.db) {
162
- return;
163
- }
164
- try {
165
- this.db.close();
166
- if (this.config.logger?.info) {
167
- this.config.logger.info("SQLite connection closed");
168
- }
169
- } finally {
170
- this.db = null;
171
- this.connected = false;
172
- }
173
- }
174
- /**
175
- * Check if connected to the database.
176
- */
177
- isConnected() {
178
- return this.connected && this.db !== null;
179
- }
180
- /**
181
- * Get connection pool statistics.
182
- *
183
- * @remarks
184
- * SQLite doesn't use connection pooling, so this returns null.
185
- * The database is single-file with a single connection.
186
- */
187
- getPoolStats() {
188
- return null;
189
- }
190
- /**
191
- * Execute a raw SQL query.
192
- *
193
- * @param sql - SQL query string with $1, $2 placeholders (converted to ? for SQLite)
194
- * @param params - Query parameters
195
- * @returns Query results
196
- *
197
- * @throws {DatabaseError} If query execution fails
198
- */
199
- // better-sqlite3 is synchronous; method is async to satisfy the DatabaseAdapter contract.
200
- // eslint-disable-next-line @typescript-eslint/require-await
201
- async executeQuery(sql, params = []) {
202
- const db = this.ensureDb();
203
- const startTime = Date.now();
204
- try {
205
- const convertedSql = this.convertPlaceholders(sql);
206
- const trimmedSql = convertedSql.trim().toUpperCase();
207
- const isPragmaQuery = trimmedSql.startsWith("PRAGMA") && !trimmedSql.includes("=");
208
- const isSelect = trimmedSql.startsWith("SELECT") || isPragmaQuery || trimmedSql.startsWith("WITH");
209
- const hasReturning = trimmedSql.includes("RETURNING");
210
- let result;
211
- const sanitizedParams = params.map(sanitizeSqliteValue);
212
- if (isSelect || hasReturning) {
213
- const stmt = db.prepare(convertedSql);
214
- result = stmt.all(...sanitizedParams);
215
- } else {
216
- const stmt = db.prepare(convertedSql);
217
- const runResult = stmt.run(...sanitizedParams);
218
- result = [
219
- {
220
- changes: runResult.changes,
221
- lastInsertRowid: runResult.lastInsertRowid
222
- }
223
- ];
224
- }
225
- if (this.config.logger?.query) {
226
- const durationMs = Date.now() - startTime;
227
- this.config.logger.query(convertedSql, params, durationMs);
228
- }
229
- return result;
230
- } catch (error) {
231
- throw this.classifyError(error, sql);
232
- }
233
- }
234
- /**
235
- * Execute work within a transaction.
236
- *
237
- * @param work - Function containing transactional operations
238
- * @param options - Transaction options (isolation level not fully supported in SQLite)
239
- * @returns Result of the work function
240
- *
241
- * @remarks
242
- * Uses better-sqlite3's native transaction handling which:
243
- * - Automatically commits on success
244
- * - Automatically rolls back on error
245
- * - Converts nested transactions to savepoints
246
- *
247
- * Note: SQLite uses DEFERRED, IMMEDIATE, or EXCLUSIVE transaction modes
248
- * rather than isolation levels. This adapter uses IMMEDIATE by default.
249
- */
250
- async transaction(work, _options) {
251
- const run = async () => this.runTransaction(work);
252
- const next = this.transactionQueue.then(run, run);
253
- this.transactionQueue = next.catch(() => void 0);
254
- return next;
255
- }
256
- /**
257
- * Inner transaction body. Kept private so callers always go through
258
- * the serialized `transaction()` queue above and the BEGIN/COMMIT
259
- * pair is never invoked outside of it.
260
- */
261
- async runTransaction(work) {
262
- const db = this.ensureDb();
263
- const startTime = Date.now();
264
- try {
265
- const ctx = this.createTransactionContext(db);
266
- db.exec("BEGIN IMMEDIATE");
267
- try {
268
- const result = await work(ctx);
269
- db.exec("COMMIT");
270
- if (this.config.logger?.debug) {
271
- const durationMs = Date.now() - startTime;
272
- this.config.logger.debug("Transaction committed", {
273
- durationMs
274
- });
275
- }
276
- return result;
277
- } catch (error) {
278
- try {
279
- db.exec("ROLLBACK");
280
- } catch {
281
- }
282
- throw error;
283
- }
284
- } catch (error) {
285
- throw this.classifyError(error);
286
- }
287
- }
288
- /**
289
- * Get SQLite database capabilities.
290
- *
291
- * @remarks
292
- * SQLite capabilities:
293
- * - JSON support (not JSONB)
294
- * - No arrays
295
- * - No native ILIKE
296
- * - RETURNING clause (3.35+)
297
- * - Savepoints supported
298
- * - ON CONFLICT supported
299
- */
300
- getCapabilities() {
301
- return {
302
- dialect: "sqlite",
303
- supportsJsonb: false,
304
- // SQLite uses JSON, not JSONB
305
- supportsJson: true,
306
- supportsArrays: false,
307
- // SQLite doesn't support array types
308
- supportsGeneratedColumns: true,
309
- // SQLite 3.31+
310
- supportsFts: true,
311
- // SQLite FTS5
312
- supportsIlike: false,
313
- // No native ILIKE, use LOWER() LIKE
314
- supportsReturning: true,
315
- // SQLite 3.35+
316
- supportsSavepoints: true,
317
- // SQLite supports savepoints
318
- supportsOnConflict: true,
319
- // ON CONFLICT clause
320
- maxParamsPerQuery: 999,
321
- // SQLite SQLITE_MAX_VARIABLE_NUMBER default
322
- maxIdentifierLength: 128
323
- // SQLite doesn't have a strict limit
324
- };
325
- }
326
- /**
327
- * Override insertMany for bulk insert optimization.
328
- *
329
- * @remarks
330
- * Uses a single multi-row INSERT statement for better performance.
331
- */
332
- async insertMany(table, data, options) {
333
- if (data.length === 0) {
334
- return [];
335
- }
336
- const db = this.ensureDb();
337
- if (data.length === 1) {
338
- const result = await this.insert(table, data[0], options);
339
- return [result];
340
- }
341
- const columns = Object.keys(data[0]);
342
- const params = [];
343
- const valuesClauses = [];
344
- for (const record of data) {
345
- const placeholders = [];
346
- for (const col of columns) {
347
- params.push(sanitizeSqliteValue(record[col]));
348
- placeholders.push("?");
349
- }
350
- valuesClauses.push(`(${placeholders.join(", ")})`);
351
- }
352
- const columnList = columns.map((col) => this.escapeIdentifier(col)).join(", ");
353
- let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columnList}) VALUES ${valuesClauses.join(", ")}`;
354
- if (options?.returning) {
355
- const returning = options.returning === "*" ? "*" : options.returning.map((col) => this.escapeIdentifier(col)).join(", ");
356
- sql += ` RETURNING ${returning}`;
357
- } else {
358
- sql += " RETURNING *";
359
- }
360
- try {
361
- const stmt = db.prepare(sql);
362
- const rows = stmt.all(...params);
363
- return rows;
364
- } catch (error) {
365
- throw this.handleQueryError(error, "insertMany", table);
366
- }
367
- }
368
- // ============================================================
369
- // Protected Helper Methods
370
- // ============================================================
371
- /**
372
- * Ensures database is connected and returns it.
373
- *
374
- * @throws {DatabaseError} If not connected
375
- */
376
- ensureDb() {
377
- if (!this.db) {
378
- throw types.createDatabaseError({
379
- kind: "connection",
380
- message: "SqliteAdapter is not connected. Call connect() first."
381
- });
382
- }
383
- return this.db;
384
- }
385
- /**
386
- * Return the typed Drizzle instance for SQLite.
387
- * Guarded for server-only usage and requires an active connection.
388
- *
389
- * @param schema - Optional schema for relational queries (db.query.*)
390
- * @returns Drizzle ORM instance wrapping the better-sqlite3 connection
391
- * @throws {Error} If called in browser or not connected
392
- */
393
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
394
- getDrizzle(schema) {
395
- if (typeof window !== "undefined") {
396
- throw new Error("getDrizzle() is server-only");
397
- }
398
- const db = this.ensureDb();
399
- return schema ? betterSqlite3.drizzle(db, { schema }) : betterSqlite3.drizzle(db);
400
- }
401
- /**
402
- * Convert $1, $2 placeholders to ? for better-sqlite3.
403
- *
404
- * @param sql - SQL with PostgreSQL-style placeholders
405
- * @returns SQL with ? placeholders
406
- */
407
- convertPlaceholders(sql) {
408
- return sql.replace(/\$\d+/g, "?");
409
- }
410
- /**
411
- * Creates a TransactionContext for the given database connection.
412
- */
413
- createTransactionContext(db) {
414
- return {
415
- // eslint-disable-next-line @typescript-eslint/require-await
416
- execute: async (sql, params = []) => {
417
- const convertedSql = this.convertPlaceholders(sql);
418
- const trimmedSql = convertedSql.trim().toUpperCase();
419
- const isSelect = trimmedSql.startsWith("SELECT") || trimmedSql.includes("RETURNING");
420
- const sanitizedParams = params.map(sanitizeSqliteValue);
421
- if (isSelect) {
422
- const stmt = db.prepare(convertedSql);
423
- return stmt.all(...sanitizedParams);
424
- } else {
425
- const stmt = db.prepare(convertedSql);
426
- const result = stmt.run(...sanitizedParams);
427
- return [
428
- {
429
- changes: result.changes,
430
- lastInsertRowid: result.lastInsertRowid
431
- }
432
- ];
433
- }
434
- },
435
- // eslint-disable-next-line @typescript-eslint/require-await
436
- insert: async (table, data, options) => {
437
- const columns = Object.keys(data);
438
- const values = Object.values(data).map(sanitizeSqliteValue);
439
- const placeholders = values.map(() => "?").join(", ");
440
- let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES (${placeholders})`;
441
- if (options?.returning) {
442
- const returning = options.returning === "*" ? "*" : options.returning.map((col) => this.escapeIdentifier(col)).join(", ");
443
- sql += ` RETURNING ${returning}`;
444
- } else {
445
- sql += " RETURNING *";
446
- }
447
- const stmt = db.prepare(sql);
448
- const rows = stmt.all(...values);
449
- return rows[0];
450
- },
451
- // eslint-disable-next-line @typescript-eslint/require-await
452
- insertMany: async (table, data, options) => {
453
- if (data.length === 0) return [];
454
- const columns = Object.keys(data[0]);
455
- const allValues = [];
456
- const valuesClauses = [];
457
- for (const record of data) {
458
- const placeholders = [];
459
- for (const col of columns) {
460
- allValues.push(sanitizeSqliteValue(record[col]));
461
- placeholders.push("?");
462
- }
463
- valuesClauses.push(`(${placeholders.join(", ")})`);
464
- }
465
- let sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES ${valuesClauses.join(", ")}`;
466
- if (options?.returning) {
467
- const returning = options.returning === "*" ? "*" : options.returning.map((col) => this.escapeIdentifier(col)).join(", ");
468
- sql += ` RETURNING ${returning}`;
469
- } else {
470
- sql += " RETURNING *";
471
- }
472
- const stmt = db.prepare(sql);
473
- return stmt.all(...allValues);
474
- },
475
- // TransactionContext CRUD methods delegate to the adapter's CRUD
476
- // which uses Drizzle query API via the TableResolver.
477
- select: async (table, options) => {
478
- return this.select(table, options);
479
- },
480
- selectOne: async (table, options) => {
481
- return this.selectOne(table, options);
482
- },
483
- update: async (table, data, where, options) => {
484
- return this.update(table, data, where, options);
485
- },
486
- delete: async (table, where, _options) => {
487
- return this.delete(table, where);
488
- },
489
- upsert: async (table, data, options) => {
490
- return this.upsert(table, data, options);
491
- },
492
- // eslint-disable-next-line @typescript-eslint/require-await
493
- savepoint: async (name) => {
494
- db.exec(`SAVEPOINT ${this.escapeIdentifier(name)}`);
495
- },
496
- // eslint-disable-next-line @typescript-eslint/require-await
497
- rollbackToSavepoint: async (name) => {
498
- db.exec(`ROLLBACK TO SAVEPOINT ${this.escapeIdentifier(name)}`);
499
- },
500
- // eslint-disable-next-line @typescript-eslint/require-await
501
- releaseSavepoint: async (name) => {
502
- db.exec(`RELEASE SAVEPOINT ${this.escapeIdentifier(name)}`);
503
- }
504
- };
505
- }
506
- /**
507
- * Classifies a SQLite error into a DatabaseError.
508
- *
509
- * @param error - Original error from better-sqlite3
510
- * @param sql - SQL statement that caused the error (optional)
511
- * @returns DatabaseError with proper classification
512
- */
513
- classifyError(error, sql) {
514
- if (types.isDatabaseError(error)) return error;
515
- const sqliteError = error;
516
- let kind = "unknown";
517
- if (sqliteError.code) {
518
- kind = SQLITE_ERROR_CODES[sqliteError.code] || "unknown";
519
- } else if (sqliteError.message) {
520
- const msg = sqliteError.message.toUpperCase();
521
- if (msg.includes("UNIQUE CONSTRAINT")) {
522
- kind = "unique_violation";
523
- } else if (msg.includes("FOREIGN KEY CONSTRAINT")) {
524
- kind = "foreign_key_violation";
525
- } else if (msg.includes("NOT NULL CONSTRAINT")) {
526
- kind = "not_null_violation";
527
- } else if (msg.includes("CHECK CONSTRAINT")) {
528
- kind = "check_violation";
529
- } else if (msg.includes("BUSY") || msg.includes("LOCKED")) {
530
- kind = "timeout";
531
- } else if (msg.includes("SQLITE_CANTOPEN") || msg.includes("UNABLE TO OPEN")) {
532
- kind = "connection";
533
- }
534
- }
535
- let message = sqliteError.message ?? String(error);
536
- if (sql && kind === "query") {
537
- message = `Query failed: ${message}`;
538
- }
539
- return types.createDatabaseError({
540
- kind,
541
- message,
542
- code: sqliteError.code,
543
- cause: error instanceof Error ? error : void 0
544
- });
545
- }
546
- /**
547
- * Override handleQueryError to use SQLite-specific classification.
548
- */
549
- handleQueryError(error, operation, table) {
550
- const dbError = this.classifyError(error);
551
- if (!dbError.message.includes(operation)) {
552
- dbError.message = `${operation} operation failed on table '${table}': ${dbError.message}`;
553
- }
554
- if (!dbError.table) {
555
- dbError.table = table;
556
- }
557
- return dbError;
558
- }
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
+ }
559
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
+ */
560
800
  function createSqliteAdapter(config) {
561
- return new SqliteAdapter(config);
801
+ return new SqliteAdapter(config);
562
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
+ */
563
817
  function isSqliteAdapter(value) {
564
- return value instanceof SqliteAdapter;
818
+ return value instanceof SqliteAdapter;
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
+ */
825
+ function openScope(parent) {
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;
839
+ }
840
+ /** The outermost transaction `scope` runs inside; itself for that one. */
841
+ function outermost(scope) {
842
+ let at = scope;
843
+ while (at.parent) at = at.parent;
844
+ return at;
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
+ */
853
+ function undone(scope, writesBefore) {
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
+ });
873
+ }
874
+ /** Chain `run` onto a scope's queue, keeping the queue itself unrejected. */
875
+ function enqueueIn(scope, run) {
876
+ const next = scope.queue.then(run, run);
877
+ scope.queue = next.catch(() => void 0);
878
+ return next;
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
+ */
885
+ async function settleNested(scope) {
886
+ scope.active = false;
887
+ const index = scope.open.indexOf(scope);
888
+ if (index !== -1) scope.open.splice(index, 1);
889
+ await scope.queue;
565
890
  }
566
-
567
891
  exports.SqliteAdapter = SqliteAdapter;
568
892
  exports.VERSION = VERSION;
569
893
  exports.createSqliteAdapter = createSqliteAdapter;
570
894
  exports.isSqliteAdapter = isSqliteAdapter;
571
- //# sourceMappingURL=index.cjs.map
895
+
572
896
  //# sourceMappingURL=index.cjs.map