@photostructure/sqlite 0.4.0 → 1.0.0

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/src/enhance.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  * compatible database, including `node:sqlite` DatabaseSync and this package's
4
4
  * DatabaseSync.
5
5
  *
6
- * This module provides the `enhance()` function which adds `.pragma()` and
7
- * `.transaction()` methods to database instances that don't have them (e.g.,
8
- * node:sqlite DatabaseSync).
6
+ * This module provides the `enhance()` function which adds `.pragma()`,
7
+ * `.transaction()`, and statement modes (`.pluck()`, `.raw()`, `.expand()`)
8
+ * to database instances that don't have them (e.g., node:sqlite DatabaseSync).
9
9
  */
10
10
 
11
11
  import { createTransaction } from "./transaction";
@@ -21,10 +21,88 @@ export interface EnhanceableDatabaseSync {
21
21
  exec(sql: string): void;
22
22
  /** Prepare a statement that can return results */
23
23
  prepare(sql: string): { all(): unknown[] };
24
+ /** Whether the database connection is open */
25
+ readonly isOpen?: boolean;
24
26
  /** Whether a transaction is currently active */
25
27
  readonly isTransaction: boolean;
26
28
  }
27
29
 
30
+ /**
31
+ * Statement mode matching better-sqlite3's mutually exclusive modes.
32
+ * - "flat": Default — rows as `{ column: value }` objects
33
+ * - "pluck": First column value only
34
+ * - "raw": Rows as arrays of values
35
+ * - "expand": Rows namespaced by table, e.g. `{ users: { id: 1 }, posts: { title: "..." } }`
36
+ */
37
+ type StatementMode = "flat" | "pluck" | "raw" | "expand";
38
+
39
+ /**
40
+ * A statement enhanced with better-sqlite3-style `.pluck()`, `.raw()`, and
41
+ * `.expand()` methods. These are mutually exclusive — enabling one disables
42
+ * the others.
43
+ */
44
+ export interface EnhancedStatementMethods {
45
+ /**
46
+ * Causes the statement to return only the first column value of each row.
47
+ *
48
+ * When plucking is turned on, raw and expand modes are turned off.
49
+ *
50
+ * @param toggle Enable (true) or disable (false) pluck mode. Defaults to true.
51
+ * @returns The same statement for chaining.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const count = db.prepare("SELECT COUNT(*) FROM users").pluck().get();
56
+ * // Returns: 42 (not { "COUNT(*)": 42 })
57
+ *
58
+ * const names = db.prepare("SELECT name FROM users").pluck().all();
59
+ * // Returns: ["Alice", "Bob"] (not [{ name: "Alice" }, { name: "Bob" }])
60
+ * ```
61
+ */
62
+ pluck(toggle?: boolean): this;
63
+
64
+ /**
65
+ * Causes the statement to return rows as arrays of values instead of objects.
66
+ *
67
+ * When raw mode is turned on, pluck and expand modes are turned off.
68
+ *
69
+ * @param toggle Enable (true) or disable (false) raw mode. Defaults to true.
70
+ * @returns The same statement for chaining.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const rows = db.prepare("SELECT id, name FROM users").raw().all();
75
+ * // Returns: [[1, "Alice"], [2, "Bob"]] (not [{ id: 1, name: "Alice" }, ...])
76
+ * ```
77
+ */
78
+ raw(toggle?: boolean): this;
79
+
80
+ /**
81
+ * Causes the statement to return data namespaced by table. Each key in a row
82
+ * object will be a table name, and each corresponding value will be a nested
83
+ * object containing that table's columns. Columns from expressions or
84
+ * subqueries are placed under the special `$` namespace.
85
+ *
86
+ * When expand mode is turned on, pluck and raw modes are turned off.
87
+ *
88
+ * Requires the statement to have a `.columns()` method (available on real
89
+ * statements but not minimal mocks).
90
+ *
91
+ * @param toggle Enable (true) or disable (false) expand mode. Defaults to true.
92
+ * @returns The same statement for chaining.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const rows = db.prepare("SELECT u.id, u.name, p.title FROM users u JOIN posts p ON ...").expand().all();
97
+ * // Returns: [{ users: { id: 1, name: "Alice" }, posts: { title: "Hello" } }]
98
+ * ```
99
+ */
100
+ expand(toggle?: boolean): this;
101
+
102
+ /** The database instance this statement was prepared from. */
103
+ readonly database: EnhanceableDatabaseSync;
104
+ }
105
+
28
106
  /**
29
107
  * Interface for an enhanced database with pragma() and transaction() methods.
30
108
  */
@@ -63,10 +141,248 @@ export interface EnhancedMethods {
63
141
  }
64
142
 
65
143
  /**
66
- * A database instance that has been enhanced with pragma() and transaction() methods.
144
+ * A database instance that has been enhanced with pragma(), transaction(),
145
+ * and statement modes (pluck/raw/expand) on statements returned by prepare().
146
+ */
147
+ export type EnhancedDatabaseSync<T extends EnhanceableDatabaseSync> = Omit<
148
+ T,
149
+ "prepare"
150
+ > &
151
+ EnhancedMethods & {
152
+ prepare(
153
+ ...args: Parameters<T["prepare"]>
154
+ ): ReturnType<T["prepare"]> & EnhancedStatementMethods;
155
+ };
156
+
157
+ /** Symbol to track whether prepare() has been wrapped */
158
+ const ENHANCED_PREPARE = Symbol.for("@photostructure/sqlite:enhancedPrepare");
159
+
160
+ /**
161
+ * Extract the first column value from a row object or array.
162
+ */
163
+ function extractFirstColumn(row: unknown): unknown {
164
+ if (row == null) return row;
165
+ if (Array.isArray(row)) return row[0];
166
+ const keys = Object.keys(row as Record<string, unknown>);
167
+ return keys.length > 0
168
+ ? (row as Record<string, unknown>)[keys[0]!]
169
+ : undefined;
170
+ }
171
+
172
+ /**
173
+ * Build a column-to-table mapping from statement metadata, used by expand mode.
174
+ * Returns an array parallel to column order: each entry is the table name
175
+ * (or "$" for expressions/subqueries) and the column name.
67
176
  */
68
- export type EnhancedDatabaseSync<T extends EnhanceableDatabaseSync> = T &
69
- EnhancedMethods;
177
+ function buildColumnTableMap(
178
+ stmt: any,
179
+ ): Array<{ table: string; column: string }> | undefined {
180
+ if (typeof stmt.columns !== "function") return undefined;
181
+ const cols: Array<{ name: string; table: string | null }> = stmt.columns();
182
+ return cols.map((c) => ({
183
+ table: c.table ?? "$",
184
+ column: c.name,
185
+ }));
186
+ }
187
+
188
+ /**
189
+ * Transform a row array into a table-namespaced expanded object.
190
+ * Uses array indices to match columns, avoiding data loss from duplicate names.
191
+ */
192
+ function expandRowFromArray(
193
+ row: unknown[],
194
+ columnMap: Array<{ table: string; column: string }>,
195
+ ): Record<string, Record<string, unknown>> {
196
+ const result: Record<string, Record<string, unknown>> = {};
197
+ for (let i = 0; i < columnMap.length && i < row.length; i++) {
198
+ const { table, column } = columnMap[i]!; // eslint-disable-line security/detect-object-injection
199
+ // eslint-disable-next-line security/detect-object-injection -- table/column from our own columnMap
200
+ result[table] ??= {};
201
+ result[table]![column] = row[i]; // eslint-disable-line security/detect-object-injection
202
+ }
203
+ return result;
204
+ }
205
+
206
+ /**
207
+ * Transform a flat row object into a table-namespaced expanded object.
208
+ * Fallback for mocks without setReturnArrays — cannot handle duplicate column
209
+ * names correctly, but mocks typically don't produce them.
210
+ */
211
+ function expandRowFromObject(
212
+ row: Record<string, unknown>,
213
+ columnMap: Array<{ table: string; column: string }>,
214
+ ): Record<string, Record<string, unknown>> {
215
+ const result: Record<string, Record<string, unknown>> = {};
216
+ const keys = Object.keys(row);
217
+ for (let i = 0; i < keys.length && i < columnMap.length; i++) {
218
+ const { table, column } = columnMap[i]!; // eslint-disable-line security/detect-object-injection
219
+ // eslint-disable-next-line security/detect-object-injection -- table/column from our own columnMap
220
+ result[table] ??= {};
221
+ result[table]![column] = row[keys[i]!]; // eslint-disable-line security/detect-object-injection
222
+ }
223
+ return result;
224
+ }
225
+
226
+ /**
227
+ * Validate a boolean toggle argument, matching better-sqlite3's pattern.
228
+ */
229
+ function validateToggle(value: unknown): boolean {
230
+ const use = value === undefined ? true : value;
231
+ if (typeof use !== "boolean") {
232
+ throw new TypeError("Expected first argument to be a boolean");
233
+ }
234
+ return use;
235
+ }
236
+
237
+ /**
238
+ * Transform a row based on the current statement mode.
239
+ */
240
+ function transformRow(
241
+ row: unknown,
242
+ mode: StatementMode,
243
+ columnMap: Array<{ table: string; column: string }> | undefined,
244
+ ): unknown {
245
+ if (row == null || mode === "flat") return row;
246
+ if (mode === "pluck") return extractFirstColumn(row);
247
+ if (mode === "expand") {
248
+ // columnMap is guaranteed non-null here — setMode() throws if columns() is
249
+ // unavailable, so we'll never reach this branch with columnMap == null.
250
+ // Prefer array-based expand (used when setReturnArrays is available)
251
+ // to correctly handle duplicate column names across tables.
252
+ // Fall back to object-based expand for mocks without setReturnArrays.
253
+ if (Array.isArray(row)) {
254
+ return expandRowFromArray(row, columnMap!);
255
+ }
256
+ return expandRowFromObject(row as Record<string, unknown>, columnMap!);
257
+ }
258
+ // "raw" mode is handled natively by setReturnArrays(), so no transform needed
259
+ return row;
260
+ }
261
+
262
+ /**
263
+ * Add `.pluck()`, `.raw()`, and `.expand()` methods to a statement instance.
264
+ * These modes are mutually exclusive — enabling one disables the others,
265
+ * matching better-sqlite3's behavior.
266
+ */
267
+ function enhanceStatement<S extends { all(): unknown[] }>(
268
+ stmt: S,
269
+ ): S & EnhancedStatementMethods {
270
+ // Idempotency: if already enhanced, return as-is
271
+ if (typeof (stmt as any).pluck === "function") {
272
+ return stmt as S & EnhancedStatementMethods;
273
+ }
274
+
275
+ let mode: StatementMode = "flat";
276
+ let columnMap: Array<{ table: string; column: string }> | undefined;
277
+
278
+ // Cast to any to avoid TypeScript strictness around bound method signatures.
279
+ // At runtime these are native C++ methods that accept variadic bind parameters.
280
+ const originalGet: any =
281
+ typeof (stmt as any).get === "function"
282
+ ? (stmt as any).get.bind(stmt)
283
+ : undefined;
284
+ const originalAll: any = (stmt as any).all.bind(stmt);
285
+ const originalIterate: any =
286
+ typeof (stmt as any).iterate === "function"
287
+ ? (stmt as any).iterate.bind(stmt)
288
+ : undefined;
289
+
290
+ // Toggle helper matching better-sqlite3's mode switching:
291
+ // enable(true) sets to target mode; enable(false) resets to flat ONLY if
292
+ // currently in that mode, otherwise leaves mode unchanged.
293
+ function setMode(target: StatementMode, use: boolean): void {
294
+ if (use) {
295
+ mode = target;
296
+ // Cache column map on first expand() call
297
+ if (target === "expand" && columnMap == null) {
298
+ columnMap = buildColumnTableMap(stmt);
299
+ if (columnMap == null) {
300
+ throw new TypeError(
301
+ "expand() requires the statement to have a columns() method",
302
+ );
303
+ }
304
+ }
305
+ } else if (mode === target) {
306
+ mode = "flat";
307
+ }
308
+ // If use=false and mode !== target, no-op (matches better-sqlite3)
309
+
310
+ // Sync native array mode: both raw and expand need arrays from the native layer.
311
+ // Expand needs arrays to correctly handle duplicate column names across tables.
312
+ if (typeof (stmt as any).setReturnArrays === "function") {
313
+ (stmt as any).setReturnArrays(mode === "raw" || mode === "expand");
314
+ }
315
+ }
316
+
317
+ Object.defineProperty(stmt, "pluck", {
318
+ value: function pluck(toggle?: boolean): S {
319
+ setMode("pluck", validateToggle(toggle));
320
+ return stmt;
321
+ },
322
+ writable: true,
323
+ configurable: true,
324
+ enumerable: false,
325
+ });
326
+
327
+ Object.defineProperty(stmt, "raw", {
328
+ value: function raw(toggle?: boolean): S {
329
+ setMode("raw", validateToggle(toggle));
330
+ return stmt;
331
+ },
332
+ writable: true,
333
+ configurable: true,
334
+ enumerable: false,
335
+ });
336
+
337
+ Object.defineProperty(stmt, "expand", {
338
+ value: function expand(toggle?: boolean): S {
339
+ setMode("expand", validateToggle(toggle));
340
+ return stmt;
341
+ },
342
+ writable: true,
343
+ configurable: true,
344
+ enumerable: false,
345
+ });
346
+
347
+ if (originalGet != null) {
348
+ Object.defineProperty(stmt, "get", {
349
+ value: (...params: any[]) => {
350
+ const row = originalGet(...params);
351
+ return transformRow(row, mode, columnMap);
352
+ },
353
+ writable: true,
354
+ configurable: true,
355
+ enumerable: false,
356
+ });
357
+ }
358
+
359
+ Object.defineProperty(stmt, "all", {
360
+ value: (...params: any[]) => {
361
+ const rows = originalAll(...params);
362
+ if (mode === "flat" || mode === "raw") return rows;
363
+ return rows.map((row: unknown) => transformRow(row, mode, columnMap));
364
+ },
365
+ writable: true,
366
+ configurable: true,
367
+ enumerable: false,
368
+ });
369
+
370
+ if (originalIterate != null) {
371
+ Object.defineProperty(stmt, "iterate", {
372
+ value: function* (...params: any[]) {
373
+ const iter = originalIterate(...params);
374
+ for (const row of iter) {
375
+ yield transformRow(row, mode, columnMap);
376
+ }
377
+ },
378
+ writable: true,
379
+ configurable: true,
380
+ enumerable: false,
381
+ });
382
+ }
383
+
384
+ return stmt as S & EnhancedStatementMethods;
385
+ }
70
386
 
71
387
  /**
72
388
  * Implementation of pragma() that works on any EnhanceableDatabaseSync.
@@ -95,23 +411,12 @@ function pragmaImpl(
95
411
  }
96
412
 
97
413
  const stmt = this.prepare(`PRAGMA ${source}`);
98
- const rows = stmt.all() as Record<string, unknown>[];
99
414
 
100
415
  if (simple) {
101
- // Return the first column of the first row, or undefined if no rows
102
- const firstRow = rows[0];
103
- if (firstRow == null) {
104
- return undefined;
105
- }
106
- const keys = Object.keys(firstRow);
107
- const firstKey = keys[0];
108
- if (firstKey == null) {
109
- return undefined;
110
- }
111
- return firstRow[firstKey];
416
+ return extractFirstColumn(stmt.all()[0]);
112
417
  }
113
418
 
114
- return rows;
419
+ return stmt.all();
115
420
  }
116
421
 
117
422
  /**
@@ -139,71 +444,93 @@ function hasEnhancedMethods(
139
444
  }
140
445
 
141
446
  /**
142
- * Ensures that `.pragma()` and `.transaction()` methods are available on the
143
- * given database.
447
+ * Ensures that `.pragma()`, `.transaction()`, and statement modes
448
+ * (`.pluck()`, `.raw()`, `.expand()`) are available on the given database.
144
449
  *
145
450
  * This function can enhance:
146
451
  * - `node:sqlite` DatabaseSync instances (adds the methods)
147
- * - `@photostructure/sqlite` DatabaseSync instances (no-op, already has these
148
- * methods)
452
+ * - `@photostructure/sqlite` DatabaseSync instances (adds the methods)
149
453
  * - Any object with compatible `exec()`, `prepare()`, and `isTransaction`
150
454
  *
151
455
  * The enhancement is done by adding methods directly to the instance, not the
152
456
  * prototype, so it won't affect other instances or the original class.
153
457
  *
154
458
  * @param db The database instance to enhance
155
- * @returns The same instance with `.pragma()` and `.transaction()` methods
156
- * guaranteed
459
+ * @returns The same instance with `.pragma()`, `.transaction()`, and
460
+ * `.pluck()` / `.raw()` / `.expand()` (on prepared statements) guaranteed
157
461
  *
158
462
  * @example
159
463
  * ```typescript
160
- * // With node:sqlite
161
- * import { DatabaseSync } from 'node:sqlite';
162
- * import { enhance } from '@photostructure/sqlite';
464
+ * import { DatabaseSync, enhance } from '@photostructure/sqlite';
163
465
  *
164
466
  * const db = enhance(new DatabaseSync(':memory:'));
165
467
  *
166
- * // Now you can use better-sqlite3-style methods
468
+ * // better-sqlite3-style pragma
167
469
  * db.pragma('journal_mode = wal');
470
+ *
471
+ * // better-sqlite3-style transactions
168
472
  * const insertMany = db.transaction((items) => {
169
473
  * for (const item of items) insert.run(item);
170
474
  * });
171
- * ```
172
475
  *
173
- * @example
174
- * ```typescript
175
- * // With @photostructure/sqlite (no-op, already enhanced)
176
- * import { DatabaseSync, enhance } from '@photostructure/sqlite';
177
- *
178
- * const db = enhance(new DatabaseSync(':memory:'));
179
- * // db already had these methods, enhance() just returns it unchanged
476
+ * // better-sqlite3-style pluck
477
+ * const count = db.prepare("SELECT COUNT(*) FROM users").pluck().get();
478
+ * const names = db.prepare("SELECT name FROM users").pluck().all();
180
479
  * ```
181
480
  */
182
481
  export function enhance<T extends EnhanceableDatabaseSync>(
183
482
  db: T,
184
483
  ): EnhancedDatabaseSync<T> {
185
- // If already enhanced, return as-is
186
- if (hasEnhancedMethods(db)) {
187
- return db;
484
+ // Add pragma and transaction if not already present
485
+ if (!hasEnhancedMethods(db)) {
486
+ // Using Object.defineProperty to make them non-enumerable like native methods
487
+ Object.defineProperty(db, "pragma", {
488
+ value: pragmaImpl,
489
+ writable: true,
490
+ configurable: true,
491
+ enumerable: false,
492
+ });
493
+
494
+ Object.defineProperty(db, "transaction", {
495
+ value: transactionImpl,
496
+ writable: true,
497
+ configurable: true,
498
+ enumerable: false,
499
+ });
188
500
  }
189
501
 
190
- // Add methods directly to the instance
191
- // Using Object.defineProperty to make them non-enumerable like native methods
192
- Object.defineProperty(db, "pragma", {
193
- value: pragmaImpl,
194
- writable: true,
195
- configurable: true,
196
- enumerable: false,
197
- });
502
+ // Wrap prepare() to add pluck() to returned statements
503
+ // eslint-disable-next-line security/detect-object-injection -- ENHANCED_PREPARE is a Symbol
504
+ if (!(db as any)[ENHANCED_PREPARE]) {
505
+ const originalPrepare: any = db.prepare.bind(db);
198
506
 
199
- Object.defineProperty(db, "transaction", {
200
- value: transactionImpl,
201
- writable: true,
202
- configurable: true,
203
- enumerable: false,
204
- });
507
+ Object.defineProperty(db, "prepare", {
508
+ value: (...args: any[]) => {
509
+ const stmt = originalPrepare(...args);
510
+ enhanceStatement(stmt);
511
+ // Add stmt.database back-reference for better-sqlite3 compat
512
+ Object.defineProperty(stmt, "database", {
513
+ value: db,
514
+ writable: false,
515
+ configurable: true,
516
+ enumerable: false,
517
+ });
518
+ return stmt;
519
+ },
520
+ writable: true,
521
+ configurable: true,
522
+ enumerable: false,
523
+ });
524
+
525
+ Object.defineProperty(db, ENHANCED_PREPARE, {
526
+ value: true,
527
+ writable: false,
528
+ configurable: false,
529
+ enumerable: false,
530
+ });
531
+ }
205
532
 
206
- return db as EnhancedDatabaseSync<T>;
533
+ return db as unknown as EnhancedDatabaseSync<T>;
207
534
  }
208
535
 
209
536
  /**
package/src/index.ts CHANGED
@@ -3,7 +3,10 @@ import nodeGypBuild from "node-gyp-build";
3
3
  import { join } from "node:path";
4
4
  import { _dirname } from "./dirname";
5
5
  import { SQLTagStore } from "./sql-tag-store";
6
- import { DatabaseSyncInstance } from "./types/database-sync-instance";
6
+ import {
7
+ DatabaseSyncInstance,
8
+ DatabaseSyncLimits,
9
+ } from "./types/database-sync-instance";
7
10
  import { DatabaseSyncOptions } from "./types/database-sync-options";
8
11
  import { SQLTagStoreInstance } from "./types/sql-tag-store-instance";
9
12
  import { SqliteAuthorizationActions } from "./types/sqlite-authorization-actions";
@@ -15,7 +18,10 @@ import { StatementSyncInstance } from "./types/statement-sync-instance";
15
18
 
16
19
  export type { AggregateOptions } from "./types/aggregate-options";
17
20
  export type { ChangesetApplyOptions } from "./types/changeset-apply-options";
18
- export type { DatabaseSyncInstance } from "./types/database-sync-instance";
21
+ export type {
22
+ DatabaseSyncInstance,
23
+ DatabaseSyncLimits,
24
+ } from "./types/database-sync-instance";
19
25
  export type { DatabaseSyncOptions } from "./types/database-sync-options";
20
26
  export type { PragmaOptions } from "./types/pragma-options";
21
27
  export type { SessionOptions } from "./types/session-options";
@@ -39,6 +45,7 @@ export {
39
45
  type EnhanceableDatabaseSync,
40
46
  type EnhancedDatabaseSync,
41
47
  type EnhancedMethods,
48
+ type EnhancedStatementMethods,
42
49
  } from "./enhance";
43
50
 
44
51
  // Use _dirname() helper that works in both CJS/ESM and Jest
@@ -203,6 +210,82 @@ DatabaseSync.prototype = _DatabaseSync.prototype;
203
210
  return new SQLTagStore(this, capacity);
204
211
  };
205
212
 
213
+ // Limit name to SQLite limit ID mapping (matches upstream kLimitMapping order)
214
+ const LIMIT_MAPPING: ReadonlyArray<{
215
+ name: keyof DatabaseSyncLimits;
216
+ id: number;
217
+ }> = [
218
+ { name: "length", id: 0 },
219
+ { name: "sqlLength", id: 1 },
220
+ { name: "column", id: 2 },
221
+ { name: "exprDepth", id: 3 },
222
+ { name: "compoundSelect", id: 4 },
223
+ { name: "vdbeOp", id: 5 },
224
+ { name: "functionArg", id: 6 },
225
+ { name: "attach", id: 7 },
226
+ { name: "likePatternLength", id: 8 },
227
+ { name: "variableNumber", id: 9 },
228
+ { name: "triggerDepth", id: 10 },
229
+ ];
230
+
231
+ const INT_MAX = 2147483647;
232
+
233
+ // WeakMap to cache limits objects per database instance
234
+ const limitsCache = new WeakMap<DatabaseSyncInstance, DatabaseSyncLimits>();
235
+
236
+ function validateLimitValue(value: unknown): number {
237
+ if (typeof value !== "number" || Number.isNaN(value)) {
238
+ throw new TypeError(
239
+ "Limit value must be a non-negative integer or Infinity.",
240
+ );
241
+ }
242
+ if (value === Infinity) {
243
+ return INT_MAX;
244
+ }
245
+ if (!Number.isFinite(value) || value !== Math.trunc(value)) {
246
+ throw new TypeError(
247
+ "Limit value must be a non-negative integer or Infinity.",
248
+ );
249
+ }
250
+ if (value < 0) {
251
+ throw new RangeError("Limit value must be non-negative.");
252
+ }
253
+ return value;
254
+ }
255
+
256
+ function createLimitsObject(db: DatabaseSyncInstance): DatabaseSyncLimits {
257
+ const obj = Object.create(null) as DatabaseSyncLimits;
258
+ for (const { name, id } of LIMIT_MAPPING) {
259
+ Object.defineProperty(obj, name, {
260
+ get() {
261
+ return db.getLimit(id);
262
+ },
263
+ set(value: unknown) {
264
+ const validated = validateLimitValue(value);
265
+ db.setLimit(id, validated);
266
+ },
267
+ enumerable: true,
268
+ configurable: false,
269
+ });
270
+ }
271
+ return obj;
272
+ }
273
+
274
+ if (!Object.getOwnPropertyDescriptor(DatabaseSync.prototype, "limits")) {
275
+ Object.defineProperty(DatabaseSync.prototype, "limits", {
276
+ get(this: DatabaseSyncInstance) {
277
+ let obj = limitsCache.get(this);
278
+ if (obj == null) {
279
+ obj = createLimitsObject(this);
280
+ limitsCache.set(this, obj);
281
+ }
282
+ return obj;
283
+ },
284
+ enumerable: true,
285
+ configurable: true,
286
+ });
287
+ }
288
+
206
289
  // NOTE: .pragma() and .transaction() are NOT added to the prototype by default.
207
290
  // This keeps DatabaseSync 100% API-compatible with node:sqlite.
208
291
  // Users who want better-sqlite3-style methods should use enhance():