@photostructure/sqlite 0.5.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/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";
@@ -204,6 +210,82 @@ DatabaseSync.prototype = _DatabaseSync.prototype;
204
210
  return new SQLTagStore(this, capacity);
205
211
  };
206
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
+
207
289
  // NOTE: .pragma() and .transaction() are NOT added to the prototype by default.
208
290
  // This keeps DatabaseSync 100% API-compatible with node:sqlite.
209
291
  // Users who want better-sqlite3-style methods should use enhance():
@@ -324,6 +324,8 @@ Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
324
324
  InstanceMethod("createSession", &DatabaseSync::CreateSession),
325
325
  InstanceMethod("applyChangeset", &DatabaseSync::ApplyChangeset),
326
326
  InstanceMethod("setAuthorizer", &DatabaseSync::SetAuthorizer),
327
+ InstanceMethod("getLimit", &DatabaseSync::GetLimit),
328
+ InstanceMethod("setLimit", &DatabaseSync::SetLimit),
327
329
  InstanceMethod("backup", &DatabaseSync::Backup),
328
330
  InstanceMethod("location", &DatabaseSync::LocationMethod),
329
331
  InstanceAccessor("isOpen", &DatabaseSync::IsOpenGetter, nullptr),
@@ -561,6 +563,69 @@ DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
561
563
  }
562
564
  config.set_enable_defensive(defensive_val.As<Napi::Boolean>().Value());
563
565
  }
566
+
567
+ // Validate and parse 'limits' option
568
+ Napi::Value limits_val = options.Get("limits");
569
+ if (!limits_val.IsUndefined()) {
570
+ if (!limits_val.IsObject() || limits_val.IsNull()) {
571
+ node::THROW_ERR_INVALID_ARG_TYPE(
572
+ info.Env(), "The \"options.limits\" argument must be an object.");
573
+ return;
574
+ }
575
+
576
+ Napi::Object limits_obj = limits_val.As<Napi::Object>();
577
+
578
+ // Limit name to SQLite limit ID mapping (matches upstream
579
+ // kLimitMapping)
580
+ static const struct {
581
+ const char *name;
582
+ int id;
583
+ } kLimitNames[] = {
584
+ {"length", SQLITE_LIMIT_LENGTH},
585
+ {"sqlLength", SQLITE_LIMIT_SQL_LENGTH},
586
+ {"column", SQLITE_LIMIT_COLUMN},
587
+ {"exprDepth", SQLITE_LIMIT_EXPR_DEPTH},
588
+ {"compoundSelect", SQLITE_LIMIT_COMPOUND_SELECT},
589
+ {"vdbeOp", SQLITE_LIMIT_VDBE_OP},
590
+ {"functionArg", SQLITE_LIMIT_FUNCTION_ARG},
591
+ {"attach", SQLITE_LIMIT_ATTACHED},
592
+ {"likePatternLength", SQLITE_LIMIT_LIKE_PATTERN_LENGTH},
593
+ {"variableNumber", SQLITE_LIMIT_VARIABLE_NUMBER},
594
+ {"triggerDepth", SQLITE_LIMIT_TRIGGER_DEPTH},
595
+ };
596
+
597
+ for (const auto &limit : kLimitNames) {
598
+ Napi::Value val = limits_obj.Get(limit.name);
599
+ if (!val.IsUndefined()) {
600
+ if (!val.IsNumber()) {
601
+ std::string msg = std::string("The \"options.limits.") +
602
+ limit.name + "\" argument must be an integer.";
603
+ node::THROW_ERR_INVALID_ARG_TYPE(info.Env(), msg.c_str());
604
+ return;
605
+ }
606
+
607
+ double dval = val.As<Napi::Number>().DoubleValue();
608
+ if (dval != std::trunc(dval) || std::isinf(dval) ||
609
+ std::isnan(dval)) {
610
+ std::string msg = std::string("The \"options.limits.") +
611
+ limit.name + "\" argument must be an integer.";
612
+ node::THROW_ERR_INVALID_ARG_TYPE(info.Env(), msg.c_str());
613
+ return;
614
+ }
615
+
616
+ int limit_val = static_cast<int>(dval);
617
+ if (limit_val < 0) {
618
+ std::string msg = std::string("The \"options.limits.") +
619
+ limit.name +
620
+ "\" argument must be non-negative.";
621
+ node::THROW_ERR_OUT_OF_RANGE(info.Env(), msg.c_str());
622
+ return;
623
+ }
624
+
625
+ config.set_initial_limit(limit.id, limit_val);
626
+ }
627
+ }
628
+ }
564
629
  }
565
630
 
566
631
  // Store configuration for later use
@@ -1007,6 +1072,14 @@ void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
1007
1072
  throw ex;
1008
1073
  }
1009
1074
  }
1075
+
1076
+ // Apply initial limits from constructor options
1077
+ for (size_t i = 0; i < config_.initial_limits().size(); i++) {
1078
+ if (config_.initial_limits()[i].has_value()) {
1079
+ sqlite3_limit(connection_, static_cast<int>(i),
1080
+ *config_.initial_limits()[i]);
1081
+ }
1082
+ }
1010
1083
  }
1011
1084
 
1012
1085
  void DatabaseSync::InternalClose() {
@@ -1957,6 +2030,11 @@ StatementSync::~StatementSync() {
1957
2030
  // See: commit 4da0638, nodejs/node-addon-api#660
1958
2031
  }
1959
2032
 
2033
+ inline int StatementSync::ResetStatement() {
2034
+ reset_generation_++;
2035
+ return sqlite3_reset(statement_);
2036
+ }
2037
+
1960
2038
  Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
1961
2039
  Napi::Env env = info.Env();
1962
2040
 
@@ -1994,7 +2072,7 @@ Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
1994
2072
  // correct value. This fixes an issue where RETURNING queries would
1995
2073
  // report changes: 0 on the first call.
1996
2074
  // See: https://github.com/nodejs/node/issues/57344
1997
- int result = sqlite3_reset(statement_);
2075
+ int result = ResetStatement();
1998
2076
 
1999
2077
  if (result != SQLITE_OK) {
2000
2078
  std::string error = sqlite3_errmsg(database_->connection());
@@ -2073,15 +2151,15 @@ Napi::Value StatementSync::Get(const Napi::CallbackInfo &info) {
2073
2151
  Napi::Value value = CreateResult();
2074
2152
  // Reset statement after fetching result to release locks (like Node.js
2075
2153
  // OnScopeLeave)
2076
- sqlite3_reset(statement_);
2154
+ ResetStatement();
2077
2155
  return value;
2078
2156
  } else if (result == SQLITE_DONE) {
2079
2157
  // Reset statement to release locks even when no rows returned
2080
- sqlite3_reset(statement_);
2158
+ ResetStatement();
2081
2159
  return env.Undefined();
2082
2160
  } else {
2083
2161
  // Reset statement before throwing to release locks
2084
- sqlite3_reset(statement_);
2162
+ ResetStatement();
2085
2163
  std::string error = sqlite3_errmsg(database_->connection());
2086
2164
  ThrowEnhancedSqliteErrorWithDB(env, database_, database_->connection(),
2087
2165
  result, error);
@@ -2089,7 +2167,7 @@ Napi::Value StatementSync::Get(const Napi::CallbackInfo &info) {
2089
2167
  }
2090
2168
  } catch (const std::exception &e) {
2091
2169
  // Reset statement on exception to release locks
2092
- sqlite3_reset(statement_);
2170
+ ResetStatement();
2093
2171
  ThrowErrSqliteErrorWithDb(env, database_, e.what());
2094
2172
  return env.Undefined();
2095
2173
  }
@@ -2132,11 +2210,11 @@ Napi::Value StatementSync::All(const Napi::CallbackInfo &info) {
2132
2210
  results.Set(index++, CreateResult());
2133
2211
  } else if (result == SQLITE_DONE) {
2134
2212
  // Reset statement to release locks (like Node.js OnScopeLeave)
2135
- sqlite3_reset(statement_);
2213
+ ResetStatement();
2136
2214
  break;
2137
2215
  } else {
2138
2216
  // Reset statement before throwing to release locks
2139
- sqlite3_reset(statement_);
2217
+ ResetStatement();
2140
2218
  std::string error = sqlite3_errmsg(database_->connection());
2141
2219
  node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
2142
2220
  return env.Undefined();
@@ -2146,7 +2224,7 @@ Napi::Value StatementSync::All(const Napi::CallbackInfo &info) {
2146
2224
  return results;
2147
2225
  } catch (const std::exception &e) {
2148
2226
  // Reset statement on exception to release locks
2149
- sqlite3_reset(statement_);
2227
+ ResetStatement();
2150
2228
  node::THROW_ERR_SQLITE_ERROR(env, e.what());
2151
2229
  return env.Undefined();
2152
2230
  }
@@ -2170,7 +2248,7 @@ Napi::Value StatementSync::Iterate(const Napi::CallbackInfo &info) {
2170
2248
  }
2171
2249
 
2172
2250
  // Reset the statement first
2173
- int r = sqlite3_reset(statement_);
2251
+ int r = ResetStatement();
2174
2252
  if (r != SQLITE_OK) {
2175
2253
  node::THROW_ERR_SQLITE_ERROR(info.Env(),
2176
2254
  sqlite3_errmsg(database_->connection()));
@@ -2839,7 +2917,7 @@ void StatementSync::Reset() {
2839
2917
  return; // Silent return, error should have been caught earlier
2840
2918
  }
2841
2919
 
2842
- sqlite3_reset(statement_);
2920
+ ResetStatement();
2843
2921
  sqlite3_clear_bindings(statement_);
2844
2922
  }
2845
2923
 
@@ -2912,6 +2990,7 @@ StatementSyncIterator::~StatementSyncIterator() {}
2912
2990
  void StatementSyncIterator::SetStatement(StatementSync *stmt) {
2913
2991
  stmt_ = stmt;
2914
2992
  done_ = false;
2993
+ statement_reset_generation_ = stmt->reset_generation_;
2915
2994
  }
2916
2995
 
2917
2996
  Napi::Value StatementSyncIterator::Next(const Napi::CallbackInfo &info) {
@@ -2927,6 +3006,11 @@ Napi::Value StatementSyncIterator::Next(const Napi::CallbackInfo &info) {
2927
3006
  return env.Undefined();
2928
3007
  }
2929
3008
 
3009
+ if (statement_reset_generation_ != stmt_->reset_generation_) {
3010
+ node::THROW_ERR_INVALID_STATE(env, "iterator was invalidated");
3011
+ return env.Undefined();
3012
+ }
3013
+
2930
3014
  if (done_) {
2931
3015
  Napi::Object result = CreateObjectWithNullPrototype(env);
2932
3016
  result.Set("done", true);
@@ -3675,6 +3759,45 @@ int DatabaseSync::AuthorizerCallback(void *user_data, int action_code,
3675
3759
  }
3676
3760
  }
3677
3761
 
3762
+ Napi::Value DatabaseSync::GetLimit(const Napi::CallbackInfo &info) {
3763
+ Napi::Env env = info.Env();
3764
+
3765
+ if (!IsOpen()) {
3766
+ node::THROW_ERR_INVALID_STATE(env, "database is not open");
3767
+ return env.Undefined();
3768
+ }
3769
+
3770
+ if (info.Length() < 1 || !info[0].IsNumber()) {
3771
+ node::THROW_ERR_INVALID_ARG_TYPE(env,
3772
+ "The limit ID argument must be a number.");
3773
+ return env.Undefined();
3774
+ }
3775
+
3776
+ int limit_id = info[0].As<Napi::Number>().Int32Value();
3777
+ int current_value = sqlite3_limit(connection_, limit_id, -1);
3778
+ return Napi::Number::New(env, current_value);
3779
+ }
3780
+
3781
+ Napi::Value DatabaseSync::SetLimit(const Napi::CallbackInfo &info) {
3782
+ Napi::Env env = info.Env();
3783
+
3784
+ if (!IsOpen()) {
3785
+ node::THROW_ERR_INVALID_STATE(env, "database is not open");
3786
+ return env.Undefined();
3787
+ }
3788
+
3789
+ if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
3790
+ node::THROW_ERR_INVALID_ARG_TYPE(
3791
+ env, "The limit ID and value arguments must be numbers.");
3792
+ return env.Undefined();
3793
+ }
3794
+
3795
+ int limit_id = info[0].As<Napi::Number>().Int32Value();
3796
+ int new_value = info[1].As<Napi::Number>().Int32Value();
3797
+ int old_value = sqlite3_limit(connection_, limit_id, new_value);
3798
+ return Napi::Number::New(env, old_value);
3799
+ }
3800
+
3678
3801
  // Thread validation implementations
3679
3802
  bool DatabaseSync::ValidateThread(Napi::Env env) const {
3680
3803
  if (std::this_thread::get_id() != creation_thread_) {
package/src/sqlite_impl.h CHANGED
@@ -4,6 +4,7 @@
4
4
  #include <napi.h>
5
5
  #include <sqlite3.h>
6
6
 
7
+ #include <array>
7
8
  #include <atomic>
8
9
  #include <climits>
9
10
  #include <map>
@@ -119,6 +120,14 @@ public:
119
120
  bool get_open_uri() const { return open_uri_; }
120
121
  void set_open_uri(bool flag) { open_uri_ = flag; }
121
122
 
123
+ static constexpr size_t kNumLimits = 11;
124
+ void set_initial_limit(int sqlite_limit_id, int value) {
125
+ initial_limits_.at(sqlite_limit_id) = value;
126
+ }
127
+ const std::array<std::optional<int>, kNumLimits> &initial_limits() const {
128
+ return initial_limits_;
129
+ }
130
+
122
131
  private:
123
132
  std::string location_;
124
133
  bool read_only_ = false;
@@ -131,6 +140,7 @@ private:
131
140
  bool allow_unknown_named_params_ = false;
132
141
  bool defensive_ = true; // Node.js v25+ defaults to true
133
142
  bool open_uri_ = false;
143
+ std::array<std::optional<int>, kNumLimits> initial_limits_{};
134
144
  };
135
145
 
136
146
  // Main database class
@@ -179,6 +189,10 @@ public:
179
189
  // Backup support
180
190
  Napi::Value Backup(const Napi::CallbackInfo &info);
181
191
 
192
+ // Limits API
193
+ Napi::Value GetLimit(const Napi::CallbackInfo &info);
194
+ Napi::Value SetLimit(const Napi::CallbackInfo &info);
195
+
182
196
  // Authorization API
183
197
  Napi::Value SetAuthorizer(const Napi::CallbackInfo &info);
184
198
 
@@ -311,6 +325,10 @@ private:
311
325
  // Bare named parameters mapping (bare name -> full name with prefix)
312
326
  std::optional<std::map<std::string, std::string>> bare_named_params_;
313
327
 
328
+ // Generation counter for iterator invalidation
329
+ uint64_t reset_generation_ = 0;
330
+ inline int ResetStatement();
331
+
314
332
  bool ValidateThread(Napi::Env env) const;
315
333
  friend class DatabaseSync;
316
334
  friend class StatementSyncIterator;
@@ -335,6 +353,7 @@ private:
335
353
 
336
354
  StatementSync *stmt_;
337
355
  bool done_;
356
+ uint64_t statement_reset_generation_ = 0;
338
357
  };
339
358
 
340
359
  // Session class for SQLite changesets
@@ -164,6 +164,49 @@ export interface DatabaseSyncInstance {
164
164
  | null,
165
165
  ): void;
166
166
 
167
+ /**
168
+ * An object with getters and setters for each SQLite limit.
169
+ * Setting a property changes the limit immediately.
170
+ * Setting a property to `Infinity` resets the limit to its compile-time maximum.
171
+ * @see https://sqlite.org/c3ref/limit.html
172
+ */
173
+ readonly limits: DatabaseSyncLimits;
174
+
175
+ /** @internal Native method to get a SQLite limit by ID. */
176
+ getLimit(limitId: number): number;
177
+ /** @internal Native method to set a SQLite limit by ID. Returns old value. */
178
+ setLimit(limitId: number, value: number): number;
179
+
167
180
  /** Dispose of the database resources using the explicit resource management protocol. */
168
181
  [Symbol.dispose](): void;
169
182
  }
183
+
184
+ /**
185
+ * Represents the configurable SQLite limits for a database connection.
186
+ * Each property corresponds to a SQLite limit constant.
187
+ * @see https://sqlite.org/c3ref/limit.html
188
+ */
189
+ export interface DatabaseSyncLimits {
190
+ /** Maximum length of any string or BLOB or table row, in bytes. */
191
+ length: number;
192
+ /** Maximum length of an SQL statement, in bytes. */
193
+ sqlLength: number;
194
+ /** Maximum number of columns in a table, result set, or index. */
195
+ column: number;
196
+ /** Maximum depth of the parse tree on any expression. */
197
+ exprDepth: number;
198
+ /** Maximum number of terms in a compound SELECT statement. */
199
+ compoundSelect: number;
200
+ /** Maximum number of instructions in a virtual machine program. */
201
+ vdbeOp: number;
202
+ /** Maximum number of arguments on a function. */
203
+ functionArg: number;
204
+ /** Maximum number of attached databases. */
205
+ attach: number;
206
+ /** Maximum length of the pattern argument to the LIKE or GLOB operators. */
207
+ likePatternLength: number;
208
+ /** Maximum index number of any parameter in an SQL statement. */
209
+ variableNumber: number;
210
+ /** Maximum depth of recursion for triggers. */
211
+ triggerDepth: number;
212
+ }
@@ -66,4 +66,23 @@ export interface DatabaseSyncOptions {
66
66
  * @default true
67
67
  */
68
68
  readonly open?: boolean;
69
+ /**
70
+ * An object specifying initial SQLite limits to set when opening the database.
71
+ * Each property corresponds to a SQLite limit constant. Only integer values are
72
+ * accepted (no Infinity). Omitted properties retain their default values.
73
+ * @see https://sqlite.org/c3ref/limit.html
74
+ */
75
+ readonly limits?: {
76
+ readonly length?: number;
77
+ readonly sqlLength?: number;
78
+ readonly column?: number;
79
+ readonly exprDepth?: number;
80
+ readonly compoundSelect?: number;
81
+ readonly vdbeOp?: number;
82
+ readonly functionArg?: number;
83
+ readonly attach?: number;
84
+ readonly likePatternLength?: number;
85
+ readonly variableNumber?: number;
86
+ readonly triggerDepth?: number;
87
+ };
69
88
  }