@photostructure/sqlite 0.3.0 → 0.5.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +65 -16
  2. package/README.md +5 -10
  3. package/binding.gyp +2 -2
  4. package/dist/index.cjs +314 -11
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +346 -89
  7. package/dist/index.d.mts +346 -89
  8. package/dist/index.d.ts +346 -89
  9. package/dist/index.mjs +311 -10
  10. package/dist/index.mjs.map +1 -1
  11. package/package.json +72 -63
  12. package/prebuilds/darwin-arm64/@photostructure+sqlite.glibc.node +0 -0
  13. package/prebuilds/darwin-x64/@photostructure+sqlite.glibc.node +0 -0
  14. package/prebuilds/linux-arm64/@photostructure+sqlite.glibc.node +0 -0
  15. package/prebuilds/linux-arm64/@photostructure+sqlite.musl.node +0 -0
  16. package/prebuilds/linux-x64/@photostructure+sqlite.glibc.node +0 -0
  17. package/prebuilds/linux-x64/@photostructure+sqlite.musl.node +0 -0
  18. package/prebuilds/test_extension.so +0 -0
  19. package/prebuilds/win32-arm64/@photostructure+sqlite.glibc.node +0 -0
  20. package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
  21. package/src/aggregate_function.cpp +222 -114
  22. package/src/aggregate_function.h +5 -6
  23. package/src/binding.cpp +30 -21
  24. package/src/enhance.ts +552 -0
  25. package/src/index.ts +84 -9
  26. package/src/shims/node_errors.h +34 -15
  27. package/src/shims/sqlite_errors.h +34 -8
  28. package/src/sql-tag-store.ts +6 -9
  29. package/src/sqlite_impl.cpp +1044 -394
  30. package/src/sqlite_impl.h +46 -7
  31. package/src/transaction.ts +178 -0
  32. package/src/types/database-sync-instance.ts +6 -40
  33. package/src/types/pragma-options.ts +23 -0
  34. package/src/types/statement-sync-instance.ts +38 -12
  35. package/src/types/transaction.ts +72 -0
  36. package/src/upstream/node_sqlite.cc +143 -43
  37. package/src/upstream/node_sqlite.h +15 -11
  38. package/src/upstream/sqlite3.c +102 -58
  39. package/src/upstream/sqlite3.h +5 -5
  40. package/src/user_function.cpp +138 -141
  41. package/src/user_function.h +3 -0
@@ -1,18 +1,29 @@
1
1
  #include "user_function.h"
2
2
 
3
+ #include <cinttypes>
3
4
  #include <climits>
4
5
  #include <cmath>
5
6
  #include <limits>
6
7
  #include <stdexcept>
7
8
 
9
+ #include "shims/node_errors.h"
8
10
  #include "sqlite_impl.h"
9
11
 
12
+ // Maximum safe integer for JavaScript numbers (2^53 - 1)
13
+ static constexpr int64_t kMaxSafeJsInteger = 9007199254740991LL;
14
+
10
15
  namespace photostructure::sqlite {
11
16
 
12
17
  UserDefinedFunction::UserDefinedFunction(Napi::Env env, Napi::Function fn,
13
18
  DatabaseSync *db, bool use_bigint_args)
14
19
  : env_(env), fn_(Napi::Reference<Napi::Function>::New(fn, 1)), db_(db),
15
20
  use_bigint_args_(use_bigint_args), async_context_(nullptr) {
21
+ // Register cleanup hook to Reset() reference before environment teardown.
22
+ // This is required for worker thread support per Node-API best practices.
23
+ // See:
24
+ // https://nodejs.github.io/node-addon-examples/special-topics/context-awareness/
25
+ napi_add_env_cleanup_hook(env_, CleanupHook, this);
26
+
16
27
  // Create async context for callbacks
17
28
  const napi_status status = napi_async_init(
18
29
  env, nullptr, Napi::String::New(env, "SQLiteUserFunction"),
@@ -24,27 +35,31 @@ UserDefinedFunction::UserDefinedFunction(Napi::Env env, Napi::Function fn,
24
35
  }
25
36
 
26
37
  UserDefinedFunction::~UserDefinedFunction() noexcept {
27
- // Check if environment is still valid before N-API operations.
28
- // During shutdown, env_ may be torn down and N-API operations would crash.
29
- // Try to create a handle scope - if this fails, env is invalid.
38
+ // Remove cleanup hook if still registered
39
+ napi_remove_env_cleanup_hook(env_, CleanupHook, this);
40
+
41
+ // Don't call fn_.Reset() here - CleanupHook already handled it,
42
+ // or the environment is being torn down and Reset() would be unsafe.
43
+
44
+ // Clean up async context if environment is still valid
30
45
  napi_handle_scope scope;
31
46
  napi_status status = napi_open_handle_scope(env_, &scope);
32
47
 
33
48
  if (status == napi_ok) {
34
- // Safe to do N-API operations
35
- if (!fn_.IsEmpty()) {
36
- fn_.Reset();
37
- }
38
-
39
49
  if (async_context_ != nullptr) {
40
50
  napi_async_destroy(env_, async_context_);
41
51
  async_context_ = nullptr;
42
52
  }
43
-
44
53
  napi_close_handle_scope(env_, scope);
45
54
  }
46
- // If status != napi_ok, env is invalid - skip cleanup.
47
- // References will be leaked, but that's better than crashing.
55
+ }
56
+
57
+ void UserDefinedFunction::CleanupHook(void *arg) {
58
+ // Called before environment teardown - safe to Reset() here
59
+ auto *self = static_cast<UserDefinedFunction *>(arg);
60
+ if (!self->fn_.IsEmpty()) {
61
+ self->fn_.Reset();
62
+ }
48
63
  }
49
64
 
50
65
  void UserDefinedFunction::xFunc(sqlite3_context *ctx, int argc,
@@ -57,73 +72,78 @@ void UserDefinedFunction::xFunc(sqlite3_context *ctx, int argc,
57
72
 
58
73
  UserDefinedFunction *self = static_cast<UserDefinedFunction *>(user_data);
59
74
 
75
+ Napi::HandleScope scope(self->env_);
76
+ Napi::CallbackScope callback_scope(self->env_, self->async_context_);
77
+
78
+ // Check if function reference is still valid
79
+ if (self->fn_.IsEmpty()) {
80
+ sqlite3_result_error(ctx, "Function reference is no longer valid", -1);
81
+ return;
82
+ }
83
+
84
+ Napi::Value fn_value;
60
85
  try {
61
- Napi::HandleScope scope(self->env_);
62
- Napi::CallbackScope callback_scope(self->env_, self->async_context_);
86
+ fn_value = self->fn_.Value();
87
+ } catch (const Napi::Error &e) {
88
+ sqlite3_result_error(ctx, "Failed to retrieve function reference", -1);
89
+ return;
90
+ }
63
91
 
64
- // Check if function reference is still valid
65
- if (self->fn_.IsEmpty()) {
66
- sqlite3_result_error(ctx, "Function reference is no longer valid", -1);
67
- return;
68
- }
92
+ // Additional check for function validity
93
+ if (!fn_value.IsFunction()) {
94
+ sqlite3_result_error(ctx, "Invalid function reference - not a function",
95
+ -1);
96
+ return;
97
+ }
69
98
 
70
- Napi::Value fn_value;
71
- try {
72
- fn_value = self->fn_.Value();
73
- } catch (const Napi::Error &e) {
74
- sqlite3_result_error(ctx, "Failed to retrieve function reference", -1);
75
- return;
76
- }
99
+ Napi::Function fn = fn_value.As<Napi::Function>();
77
100
 
78
- // Additional check for function validity
79
- if (!fn_value.IsFunction()) {
80
- sqlite3_result_error(ctx, "Invalid function reference - not a function",
81
- -1);
101
+ // Convert SQLite arguments to JavaScript values
102
+ std::vector<napi_value> js_args;
103
+ js_args.reserve(argc);
104
+
105
+ for (int i = 0; i < argc; i++) {
106
+ Napi::Value js_val = self->SqliteValueToJS(argv[i]);
107
+
108
+ // Check if SqliteValueToJS threw an exception (e.g., ERR_OUT_OF_RANGE)
109
+ if (self->env_.IsExceptionPending()) {
110
+ // Ignore the SQLite error because a JavaScript exception is pending
111
+ self->db_->SetIgnoreNextSQLiteError(true);
112
+ sqlite3_result_error(ctx, "", 0);
82
113
  return;
83
114
  }
84
115
 
85
- Napi::Function fn = fn_value.As<Napi::Function>();
116
+ js_args.push_back(js_val);
117
+ }
86
118
 
87
- // Convert SQLite arguments to JavaScript values
88
- std::vector<napi_value> js_args;
89
- js_args.reserve(argc);
119
+ // Call the JavaScript function
120
+ napi_value js_result;
121
+ napi_value js_func = fn;
122
+ napi_value this_arg = self->env_.Undefined();
90
123
 
91
- for (int i = 0; i < argc; i++) {
92
- Napi::Value js_val = self->SqliteValueToJS(argv[i]);
93
- js_args.push_back(js_val);
94
- }
124
+ napi_status status =
125
+ napi_call_function(self->env_, this_arg, js_func, js_args.size(),
126
+ js_args.data(), &js_result);
95
127
 
96
- // Call the JavaScript function with safer exception handling
97
- napi_value js_result;
98
- napi_value js_func = fn;
99
- napi_value this_arg = self->env_.Undefined();
100
-
101
- napi_status status =
102
- napi_call_function(self->env_, this_arg, js_func, js_args.size(),
103
- js_args.data(), &js_result);
104
-
105
- if (status != napi_ok || self->env_.IsExceptionPending()) {
106
- // Handle JavaScript exception by setting a generic SQLite error
107
- if (self->env_.IsExceptionPending()) {
108
- sqlite3_result_error(ctx, "JavaScript exception in user function", -1);
109
- } else {
110
- sqlite3_result_error(ctx, "Failed to call user function", -1);
111
- }
112
- return;
113
- }
128
+ if (status != napi_ok || self->env_.IsExceptionPending()) {
129
+ // JavaScript exception is pending - let it propagate
130
+ // Ignore the SQLite error because the JavaScript exception takes precedence
131
+ self->db_->SetIgnoreNextSQLiteError(true);
132
+ sqlite3_result_error(ctx, "", 0);
133
+ return;
134
+ }
114
135
 
115
- Napi::Value result(self->env_, js_result);
136
+ Napi::Value result(self->env_, js_result);
116
137
 
117
- // Convert result back to SQLite
118
- self->JSValueToSqliteResult(ctx, result);
138
+ // Convert result back to SQLite
139
+ self->JSValueToSqliteResult(ctx, result);
119
140
 
120
- } catch (const Napi::Error &e) {
121
- // Handle JavaScript errors by setting a generic SQLite error
122
- sqlite3_result_error(ctx, "JavaScript exception in user function", -1);
123
- } catch (const std::exception &e) {
124
- sqlite3_result_error(ctx, e.what(), -1);
125
- } catch (...) {
126
- sqlite3_result_error(ctx, "Unknown error in user-defined function", -1);
141
+ // Check if JSValueToSqliteResult threw an exception (e.g., ERR_OUT_OF_RANGE)
142
+ if (self->env_.IsExceptionPending()) {
143
+ // Ignore the SQLite error because a JavaScript exception is pending
144
+ self->db_->SetIgnoreNextSQLiteError(true);
145
+ sqlite3_result_error(ctx, "", 0);
146
+ return;
127
147
  }
128
148
  }
129
149
 
@@ -140,19 +160,18 @@ Napi::Value UserDefinedFunction::SqliteValueToJS(sqlite3_value *value) {
140
160
 
141
161
  if (use_bigint_args_) {
142
162
  return Napi::BigInt::New(env_, static_cast<int64_t>(int_val));
143
- } else if (int_val >= INT32_MIN && int_val <= INT32_MAX) {
144
- return Napi::Number::New(env_, static_cast<int32_t>(int_val));
145
- } else {
146
- // Large integers that don't fit in int32 but aren't using BigInt
147
- // Check if the value can be safely represented as a JavaScript number
148
- if (int_val < -0x1FFFFFFFFFFFFF || int_val > 0x1FFFFFFFFFFFFF) {
149
- // Value is outside safe integer range for JavaScript numbers
150
- std::string error_msg =
151
- "Value is too large to be represented as a JavaScript number: " +
152
- std::to_string(int_val);
153
- throw std::runtime_error(error_msg);
154
- }
163
+ } else if (std::abs(int_val) <= kMaxSafeJsInteger) {
155
164
  return Napi::Number::New(env_, static_cast<double>(int_val));
165
+ } else {
166
+ // Value is outside safe integer range for JavaScript numbers
167
+ // Throw ERR_OUT_OF_RANGE directly - we're in a valid N-API context
168
+ char error_msg[128];
169
+ snprintf(error_msg, sizeof(error_msg),
170
+ "Value is too large to be represented as a JavaScript number: "
171
+ "%" PRId64,
172
+ static_cast<int64_t>(int_val));
173
+ node::THROW_ERR_OUT_OF_RANGE(env_, error_msg);
174
+ return env_.Undefined(); // Return undefined, exception is pending
156
175
  }
157
176
  }
158
177
 
@@ -170,11 +189,14 @@ Napi::Value UserDefinedFunction::SqliteValueToJS(sqlite3_value *value) {
170
189
  case SQLITE_BLOB: {
171
190
  const void *blob = sqlite3_value_blob(value);
172
191
  int bytes = sqlite3_value_bytes(value);
192
+ // Return Uint8Array to match Node.js node:sqlite behavior
173
193
  if (blob && bytes > 0) {
174
- return Napi::Buffer<uint8_t>::Copy(
175
- env_, static_cast<const uint8_t *>(blob), static_cast<size_t>(bytes));
194
+ auto array_buffer = Napi::ArrayBuffer::New(env_, bytes);
195
+ memcpy(array_buffer.Data(), blob, bytes);
196
+ return Napi::Uint8Array::New(env_, bytes, array_buffer, 0);
176
197
  } else {
177
- return Napi::Buffer<uint8_t>::New(env_, 0);
198
+ auto array_buffer = Napi::ArrayBuffer::New(env_, 0);
199
+ return Napi::Uint8Array::New(env_, 0, array_buffer, 0);
178
200
  }
179
201
  }
180
202
 
@@ -189,48 +211,15 @@ void UserDefinedFunction::JSValueToSqliteResult(sqlite3_context *ctx,
189
211
  if (value.IsNull() || value.IsUndefined()) {
190
212
  sqlite3_result_null(ctx);
191
213
  } else if (value.IsBoolean()) {
192
- // Check boolean BEFORE number, since boolean values can also be numbers
193
- bool bool_val = value.As<Napi::Boolean>().Value();
194
- sqlite3_result_int(ctx, bool_val ? 1 : 0);
195
- } else if (value.IsBigInt()) {
196
- // Check BigInt BEFORE number to handle large integers properly
197
- bool lossless;
198
- int64_t bigint_val = value.As<Napi::BigInt>().Int64Value(&lossless);
199
- if (lossless) {
200
- sqlite3_result_int64(ctx, static_cast<sqlite3_int64>(bigint_val));
201
- } else {
202
- // BigInt too large, convert to text representation
203
- std::string bigint_str = value.As<Napi::BigInt>().ToString().Utf8Value();
204
- try {
205
- sqlite3_result_text(ctx, bigint_str.c_str(),
206
- SafeCastToInt(bigint_str.length()),
207
- SQLITE_TRANSIENT);
208
- } catch (const std::overflow_error &) {
209
- sqlite3_result_error(ctx, "BigInt string representation too long", -1);
210
- }
211
- }
214
+ // Extension over Node.js: Convert booleans to 0/1
215
+ sqlite3_result_int(ctx, value.As<Napi::Boolean>().Value() ? 1 : 0);
212
216
  } else if (value.IsNumber()) {
213
- double num_val = value.As<Napi::Number>().DoubleValue();
214
-
215
- // Check if it's an integer value
216
- // Note: We cast INT64_MIN/MAX to double to avoid implicit conversion
217
- // warnings
218
- if (std::abs(num_val - std::floor(num_val)) <
219
- std::numeric_limits<double>::epsilon() &&
220
- num_val >= static_cast<double>(INT64_MIN) &&
221
- num_val <= static_cast<double>(INT64_MAX)) {
222
- sqlite3_result_int64(ctx, static_cast<sqlite3_int64>(num_val));
223
- } else {
224
- sqlite3_result_double(ctx, num_val);
225
- }
217
+ // Match Node.js: numbers are stored as doubles
218
+ sqlite3_result_double(ctx, value.As<Napi::Number>().DoubleValue());
226
219
  } else if (value.IsString()) {
227
220
  std::string str_val = value.As<Napi::String>().Utf8Value();
228
- try {
229
- sqlite3_result_text(ctx, str_val.c_str(), SafeCastToInt(str_val.length()),
230
- SQLITE_TRANSIENT);
231
- } catch (const std::overflow_error &) {
232
- sqlite3_result_error(ctx, "String value too long", -1);
233
- }
221
+ sqlite3_result_text(ctx, str_val.c_str(),
222
+ static_cast<int>(str_val.length()), SQLITE_TRANSIENT);
234
223
  } else if (value.IsDataView()) {
235
224
  // IMPORTANT: Check DataView BEFORE IsBuffer() because N-API's IsBuffer()
236
225
  // returns true for ALL ArrayBufferViews (including DataView), but
@@ -244,33 +233,41 @@ void UserDefinedFunction::JSValueToSqliteResult(sqlite3_context *ctx,
244
233
  if (arrayBuffer.Data() != nullptr && byteLength > 0) {
245
234
  const uint8_t *data =
246
235
  static_cast<const uint8_t *>(arrayBuffer.Data()) + byteOffset;
247
- try {
248
- sqlite3_result_blob(ctx, data, SafeCastToInt(byteLength),
249
- SQLITE_TRANSIENT);
250
- } catch (const std::overflow_error &) {
251
- sqlite3_result_error(ctx, "DataView too large", -1);
252
- }
236
+ sqlite3_result_blob(ctx, data, static_cast<int>(byteLength),
237
+ SQLITE_TRANSIENT);
253
238
  } else {
254
239
  sqlite3_result_zeroblob(ctx, 0);
255
240
  }
256
- } else if (value.IsBuffer()) {
257
- // Handles both Node.js Buffer and TypedArrays (Uint8Array, etc.)
258
- Napi::Buffer<uint8_t> buffer = value.As<Napi::Buffer<uint8_t>>();
259
- try {
260
- sqlite3_result_blob(ctx, buffer.Data(), SafeCastToInt(buffer.Length()),
261
- SQLITE_TRANSIENT);
262
- } catch (const std::overflow_error &) {
263
- sqlite3_result_error(ctx, "Buffer too large", -1);
241
+ } else if (value.IsTypedArray()) {
242
+ // Handles Uint8Array and other TypedArrays (but not DataView, handled
243
+ // above)
244
+ Napi::TypedArray arr = value.As<Napi::TypedArray>();
245
+ Napi::ArrayBuffer buf = arr.ArrayBuffer();
246
+ sqlite3_result_blob(
247
+ ctx, static_cast<const uint8_t *>(buf.Data()) + arr.ByteOffset(),
248
+ static_cast<int>(arr.ByteLength()), SQLITE_TRANSIENT);
249
+ } else if (value.IsBigInt()) {
250
+ // Check BigInt - must fit in int64
251
+ bool lossless;
252
+ int64_t bigint_val = value.As<Napi::BigInt>().Int64Value(&lossless);
253
+ if (!lossless) {
254
+ // BigInt too large for SQLite - throw ERR_OUT_OF_RANGE
255
+ node::THROW_ERR_OUT_OF_RANGE(
256
+ env_,
257
+ "BigInt value is too large to be represented as a SQLite integer");
258
+ return;
264
259
  }
260
+ sqlite3_result_int64(ctx, static_cast<sqlite3_int64>(bigint_val));
261
+ } else if (value.IsPromise()) {
262
+ // Promises are not supported - must use sqlite3_result_error for this one
263
+ // because it's an ERR_SQLITE_ERROR per the test expectations
264
+ sqlite3_result_error(
265
+ ctx, "Asynchronous user-defined functions are not supported", -1);
265
266
  } else {
266
- // For any other type, convert to string
267
- std::string str_val = value.ToString().Utf8Value();
268
- try {
269
- sqlite3_result_text(ctx, str_val.c_str(), SafeCastToInt(str_val.length()),
270
- SQLITE_TRANSIENT);
271
- } catch (const std::overflow_error &) {
272
- sqlite3_result_error(ctx, "Converted string value too long", -1);
273
- }
267
+ // Unsupported type - must use sqlite3_result_error
268
+ sqlite3_result_error(
269
+ ctx, "Returned JavaScript value cannot be converted to a SQLite value",
270
+ -1);
274
271
  }
275
272
  }
276
273
 
@@ -24,6 +24,9 @@ public:
24
24
  static void xDestroy(void *self);
25
25
 
26
26
  private:
27
+ // Environment cleanup hook - called before environment teardown
28
+ static void CleanupHook(void *arg);
29
+
27
30
  Napi::Env env_;
28
31
  Napi::FunctionReference fn_;
29
32
  DatabaseSync *db_;