@photostructure/sqlite 0.3.0 → 0.4.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 +52 -16
  2. package/README.md +5 -4
  3. package/binding.gyp +2 -2
  4. package/dist/index.cjs +158 -11
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +284 -89
  7. package/dist/index.d.mts +284 -89
  8. package/dist/index.d.ts +284 -89
  9. package/dist/index.mjs +155 -10
  10. package/dist/index.mjs.map +1 -1
  11. package/package.json +71 -62
  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 +228 -0
  25. package/src/index.ts +83 -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,59 +1,89 @@
1
1
  #include "aggregate_function.h"
2
2
 
3
+ #include <cinttypes>
3
4
  #include <cmath>
4
5
  #include <cstring>
5
6
  #include <limits>
6
7
  #include <unordered_map>
7
8
  #include <vector>
8
9
 
10
+ #include "shims/node_errors.h"
9
11
  #include "sqlite_impl.h"
10
12
 
11
- namespace photostructure::sqlite {
13
+ // Maximum safe integer for JavaScript numbers (2^53 - 1)
14
+ static constexpr int64_t kMaxSafeJsInteger = 9007199254740991LL;
12
15
 
13
- // Static member definitions for ValueStorage
14
- std::unordered_map<int32_t, Napi::Reference<Napi::Value>>
15
- ValueStorage::storage_;
16
- std::mutex ValueStorage::mutex_;
17
- std::atomic<int32_t> ValueStorage::next_id_{0};
16
+ namespace photostructure::sqlite {
18
17
 
19
18
  // ValueStorage implementation
20
- int32_t ValueStorage::Store([[maybe_unused]] Napi::Env env, Napi::Value value) {
21
- const std::lock_guard<std::mutex> lock(mutex_);
22
- const int32_t id = ++next_id_;
23
-
24
- // Try direct napi_value persistence instead of Napi::Reference
19
+ int32_t ValueStorage::Store(Napi::Env env, Napi::Value value) {
20
+ AddonData *addon_data = GetAddonData(env);
21
+ if (!addon_data)
22
+ throw Napi::Error::New(env, "Addon data not found");
23
+
24
+ std::lock_guard<std::mutex> lock(addon_data->value_storage_mutex);
25
+ const int32_t id =
26
+ addon_data->next_value_id.fetch_add(1, std::memory_order_relaxed);
25
27
  try {
26
- storage_[id] = Napi::Reference<Napi::Value>::New(value, 1);
28
+ addon_data->value_storage[id] = Napi::Reference<Napi::Value>::New(value, 1);
27
29
  } catch (...) {
28
30
  // If Reference creation fails, throw to let caller handle
29
31
  throw;
30
32
  }
31
-
32
33
  return id;
33
34
  }
34
35
 
35
36
  Napi::Value ValueStorage::Get(Napi::Env env, int32_t id) {
36
- const std::lock_guard<std::mutex> lock(mutex_);
37
- auto it = storage_.find(id);
38
- return (it != storage_.end()) ? it->second.Value() : env.Undefined();
37
+ AddonData *addon_data = GetAddonData(env);
38
+ if (!addon_data)
39
+ return env.Null();
40
+
41
+ std::lock_guard<std::mutex> lock(addon_data->value_storage_mutex);
42
+ auto it = addon_data->value_storage.find(id);
43
+ if (it == addon_data->value_storage.end() || it->second.IsEmpty()) {
44
+ return env.Null();
45
+ }
46
+ return it->second.Value();
39
47
  }
40
48
 
41
- void ValueStorage::Remove(int32_t id) {
42
- const std::lock_guard<std::mutex> lock(mutex_);
43
- auto it = storage_.find(id);
44
- if (it != storage_.end()) {
45
- it->second.Reset();
46
- storage_.erase(it);
49
+ void ValueStorage::Remove(Napi::Env env, int32_t id) {
50
+ AddonData *addon_data = GetAddonData(env);
51
+ if (!addon_data)
52
+ return;
53
+
54
+ std::lock_guard<std::mutex> lock(addon_data->value_storage_mutex);
55
+ auto it = addon_data->value_storage.find(id);
56
+ if (it != addon_data->value_storage.end()) {
57
+ // Don't call Reset() here - it's unsafe during environment teardown on
58
+ // musl. The Cleanup() hook will reset all remaining references safely. For
59
+ // refs removed during normal operation, they'll be cleaned up by GC.
60
+ addon_data->value_storage.erase(it);
47
61
  }
48
62
  }
49
63
 
50
- CustomAggregate::CustomAggregate(Napi::Env env,
51
- [[maybe_unused]] DatabaseSync *db,
64
+ void CustomAggregate::CleanupHook(void *arg) {
65
+ // Called before environment teardown - safe to Reset() references here
66
+ auto *self = static_cast<CustomAggregate *>(arg);
67
+
68
+ if (!self->start_fn_.IsEmpty())
69
+ self->start_fn_.Reset();
70
+ if (!self->object_ref_.IsEmpty())
71
+ self->object_ref_.Reset();
72
+ if (!self->step_fn_.IsEmpty())
73
+ self->step_fn_.Reset();
74
+ if (!self->inverse_fn_.IsEmpty())
75
+ self->inverse_fn_.Reset();
76
+ if (!self->result_fn_.IsEmpty())
77
+ self->result_fn_.Reset();
78
+ }
79
+
80
+ CustomAggregate::CustomAggregate(Napi::Env env, DatabaseSync *db,
52
81
  bool use_bigint_args, Napi::Value start,
53
82
  Napi::Function step_fn,
54
83
  Napi::Function inverse_fn,
55
84
  Napi::Function result_fn)
56
- : env_(env), use_bigint_args_(use_bigint_args), async_context_(nullptr) {
85
+ : env_(env), db_(db), use_bigint_args_(use_bigint_args),
86
+ async_context_(nullptr) {
57
87
  // Handle start value based on type
58
88
  if (start.IsNull()) {
59
89
  start_type_ = PRIMITIVE_NULL;
@@ -91,47 +121,35 @@ CustomAggregate::CustomAggregate(Napi::Env env,
91
121
  result_fn_ = Napi::Reference<Napi::Function>::New(result_fn, 1);
92
122
  }
93
123
 
124
+ // Register cleanup hook to Reset() references before environment teardown.
125
+ // This is required for worker thread support per Node-API best practices.
126
+ // See:
127
+ // https://nodejs.github.io/node-addon-examples/special-topics/context-awareness/
128
+ napi_add_env_cleanup_hook(env_, CleanupHook, this);
129
+
94
130
  // Don't create async context immediately - we'll create it lazily if needed
95
131
  async_context_ = nullptr;
96
132
  }
97
133
 
98
134
  CustomAggregate::~CustomAggregate() {
135
+ // Remove cleanup hook if still registered
136
+ napi_remove_env_cleanup_hook(env_, CleanupHook, this);
137
+
138
+ // Don't call Reset() on references here - CleanupHook already handled them,
139
+ // or the environment is being torn down and Reset() would be unsafe.
140
+
99
141
  // Check if environment is still valid before N-API operations.
100
- // During shutdown, env_ may be torn down and N-API operations would crash.
101
- // Try to create a handle scope - if this fails, env is invalid.
102
142
  napi_handle_scope scope;
103
143
  napi_status status = napi_open_handle_scope(env_, &scope);
104
144
 
105
145
  if (status == napi_ok) {
106
- // Safe to do N-API operations
107
- // Only clean up our own function references
108
- // Do NOT touch any aggregate contexts - SQLite owns those
109
- if (!step_fn_.IsEmpty()) {
110
- step_fn_.Reset();
111
- }
112
- if (!inverse_fn_.IsEmpty()) {
113
- inverse_fn_.Reset();
114
- }
115
- if (!result_fn_.IsEmpty()) {
116
- result_fn_.Reset();
117
- }
118
- if (!start_fn_.IsEmpty()) {
119
- start_fn_.Reset();
120
- }
121
- if (start_type_ == OBJECT && !object_ref_.IsEmpty()) {
122
- object_ref_.Reset();
123
- }
124
-
125
146
  // Clean up async context if it was created
126
147
  if (async_context_ != nullptr) {
127
148
  napi_async_destroy(env_, async_context_);
128
149
  async_context_ = nullptr;
129
150
  }
130
-
131
151
  napi_close_handle_scope(env_, scope);
132
152
  }
133
- // If status != napi_ok, env is invalid - skip cleanup.
134
- // References will be leaked, but that's better than crashing.
135
153
  }
136
154
 
137
155
  void CustomAggregate::xStep(sqlite3_context *ctx, int argc,
@@ -144,7 +162,10 @@ void CustomAggregate::xInverse(sqlite3_context *ctx, int argc,
144
162
  xStepBase(ctx, argc, argv, &CustomAggregate::inverse_fn_);
145
163
  }
146
164
 
147
- void CustomAggregate::xFinal(sqlite3_context *ctx) { xValueBase(ctx, true); }
165
+ void CustomAggregate::xFinal(sqlite3_context *ctx) {
166
+ xValueBase(ctx, true);
167
+ DestroyAggregateData(ctx);
168
+ }
148
169
 
149
170
  void CustomAggregate::xValue(sqlite3_context *ctx) { xValueBase(ctx, false); }
150
171
 
@@ -177,7 +198,25 @@ void CustomAggregate::xStepBase(
177
198
 
178
199
  if (!state->is_initialized) {
179
200
  // Initialize with the proper start value
180
- Napi::Value start_val = self->GetStartValue();
201
+ Napi::Value start_val;
202
+
203
+ // If start is a function, call it to get the initial value
204
+ if (self->start_type_ == FUNCTION) {
205
+ Napi::Function start_func = self->start_fn_.Value();
206
+ napi_value result;
207
+ napi_status status = napi_call_function(
208
+ self->env_, self->env_.Undefined(), start_func, 0, nullptr, &result);
209
+
210
+ if (status != napi_ok || self->env_.IsExceptionPending()) {
211
+ // JavaScript exception is pending - let it propagate
212
+ self->db_->SetIgnoreNextSQLiteError(true);
213
+ sqlite3_result_error(ctx, "", 0);
214
+ return;
215
+ }
216
+ start_val = Napi::Value(self->env_, result);
217
+ } else {
218
+ start_val = self->GetStartValue();
219
+ }
181
220
 
182
221
  // Store the start value in the appropriate type
183
222
  if (start_val.IsNumber()) {
@@ -205,9 +244,9 @@ void CustomAggregate::xStepBase(
205
244
  std::min(buffer.Length(), sizeof(state->string_buffer) - 1);
206
245
  memcpy(state->string_buffer, buffer.Data(), copy_len);
207
246
  state->string_length = copy_len;
208
- } else if (start_val.IsObject() && !start_val.IsArray() &&
247
+ } else if ((start_val.IsObject() || start_val.IsArray()) &&
209
248
  !start_val.IsBuffer()) {
210
- // Store objects as JSON strings
249
+ // Store objects and arrays as JSON strings
211
250
  state->type = AggregateValue::OBJECT_JSON;
212
251
  // Use JSON.stringify to serialize the object
213
252
  std::string json_str = SafeJsonStringify(self->env_, start_val);
@@ -229,6 +268,7 @@ void CustomAggregate::xStepBase(
229
268
  }
230
269
 
231
270
  state->is_initialized = true;
271
+ state->xvalue_called = false;
232
272
  }
233
273
 
234
274
  // Get the JavaScript function
@@ -258,11 +298,17 @@ void CustomAggregate::xStepBase(
258
298
  case AggregateValue::BOOLEAN:
259
299
  current_value = Napi::Boolean::New(self->env_, state->bool_value);
260
300
  break;
261
- case AggregateValue::BUFFER:
262
- current_value = Napi::Buffer<uint8_t>::Copy(
263
- self->env_, reinterpret_cast<const uint8_t *>(state->string_buffer),
264
- state->string_length);
301
+ case AggregateValue::BUFFER: {
302
+ // Return Uint8Array to match Node.js node:sqlite behavior
303
+ auto array_buffer =
304
+ Napi::ArrayBuffer::New(self->env_, state->string_length);
305
+ if (state->string_length > 0) {
306
+ memcpy(array_buffer.Data(), state->string_buffer, state->string_length);
307
+ }
308
+ current_value = Napi::Uint8Array::New(self->env_, state->string_length,
309
+ array_buffer, 0);
265
310
  break;
311
+ }
266
312
  case AggregateValue::OBJECT_JSON: {
267
313
  // Parse JSON back to object using JSON.parse
268
314
  try {
@@ -288,7 +334,17 @@ void CustomAggregate::xStepBase(
288
334
 
289
335
  // Convert SQLite values to JavaScript
290
336
  for (int i = 0; i < argc; ++i) {
291
- js_args.push_back(self->SqliteValueToJS(argv[i]));
337
+ Napi::Value js_val = self->SqliteValueToJS(argv[i]);
338
+
339
+ // Check if SqliteValueToJS threw an exception (e.g., ERR_OUT_OF_RANGE)
340
+ if (self->env_.IsExceptionPending()) {
341
+ // Ignore the SQLite error because a JavaScript exception is pending
342
+ self->db_->SetIgnoreNextSQLiteError(true);
343
+ sqlite3_result_error(ctx, "", 0);
344
+ return;
345
+ }
346
+
347
+ js_args.push_back(js_val);
292
348
  }
293
349
 
294
350
  // Convert to napi_value array
@@ -303,8 +359,10 @@ void CustomAggregate::xStepBase(
303
359
  napi_call_function(self->env_, self->env_.Undefined(), func,
304
360
  raw_args.size(), raw_args.data(), &result);
305
361
 
306
- if (status != napi_ok) {
307
- sqlite3_result_error(ctx, "Error calling aggregate step function", -1);
362
+ if (status != napi_ok || self->env_.IsExceptionPending()) {
363
+ // JavaScript exception is pending - let it propagate
364
+ self->db_->SetIgnoreNextSQLiteError(true);
365
+ sqlite3_result_error(ctx, "", 0);
308
366
  return;
309
367
  }
310
368
 
@@ -348,11 +406,11 @@ void CustomAggregate::xStepBase(
348
406
  std::min(buffer.Length(), sizeof(state->string_buffer) - 1);
349
407
  memcpy(state->string_buffer, buffer.Data(), copy_len);
350
408
  state->string_length = copy_len;
351
- } else if (result_val.IsObject() && !result_val.IsArray() &&
409
+ } else if ((result_val.IsObject() || result_val.IsArray()) &&
352
410
  !result_val.IsBuffer()) {
353
- // Store objects as JSON strings
411
+ // Store objects and arrays as JSON strings
354
412
  state->type = AggregateValue::OBJECT_JSON;
355
- // Use JSON.stringify to serialize the object
413
+ // Use JSON.stringify to serialize the object/array
356
414
  std::string json_str = SafeJsonStringify(self->env_, result_val);
357
415
 
358
416
  // If JSON is too long, use a simpler representation
@@ -408,11 +466,17 @@ void CustomAggregate::xValueBase(sqlite3_context *ctx, bool is_final) {
408
466
  case AggregateValue::BOOLEAN:
409
467
  current_value = Napi::Boolean::New(self->env_, state->bool_value);
410
468
  break;
411
- case AggregateValue::BUFFER:
412
- current_value = Napi::Buffer<uint8_t>::Copy(
413
- self->env_, reinterpret_cast<const uint8_t *>(state->string_buffer),
414
- state->string_length);
469
+ case AggregateValue::BUFFER: {
470
+ // Return Uint8Array to match Node.js node:sqlite behavior
471
+ auto array_buffer =
472
+ Napi::ArrayBuffer::New(self->env_, state->string_length);
473
+ if (state->string_length > 0) {
474
+ memcpy(array_buffer.Data(), state->string_buffer, state->string_length);
475
+ }
476
+ current_value = Napi::Uint8Array::New(self->env_, state->string_length,
477
+ array_buffer, 0);
415
478
  break;
479
+ }
416
480
  case AggregateValue::OBJECT_JSON: {
417
481
  // Parse JSON back to object using JSON.parse
418
482
  try {
@@ -435,9 +499,24 @@ void CustomAggregate::xValueBase(sqlite3_context *ctx, bool is_final) {
435
499
  break;
436
500
  }
437
501
 
438
- // Apply result function if provided
502
+ // For window functions, xValue is called for each row and xFinal is called at
503
+ // the end. We should only call the result function once per actual result
504
+ // row.
505
+ // - xValue (is_final=false): Called for each row in window functions, call
506
+ // result function
507
+ // - xFinal (is_final=true): For regular aggregates, call result function
508
+ // For window aggregates (xvalue_called=true), skip
509
+ // result function
510
+ bool should_call_result = !is_final || !state->xvalue_called;
511
+
512
+ if (!is_final) {
513
+ // Mark that xValue was called (this is a window function)
514
+ state->xvalue_called = true;
515
+ }
516
+
517
+ // Apply result function if provided and appropriate
439
518
  Napi::Value final_value = current_value;
440
- if (!self->result_fn_.IsEmpty()) {
519
+ if (should_call_result && !self->result_fn_.IsEmpty()) {
441
520
  Napi::Function result_func = self->result_fn_.Value();
442
521
 
443
522
  std::vector<napi_value> args = {current_value};
@@ -447,8 +526,10 @@ void CustomAggregate::xValueBase(sqlite3_context *ctx, bool is_final) {
447
526
  napi_call_function(self->env_, self->env_.Undefined(), result_func, 1,
448
527
  args.data(), &result);
449
528
 
450
- if (status != napi_ok) {
451
- sqlite3_result_error(ctx, "Error calling aggregate result function", -1);
529
+ if (status != napi_ok || self->env_.IsExceptionPending()) {
530
+ // JavaScript exception is pending - let it propagate
531
+ self->db_->SetIgnoreNextSQLiteError(true);
532
+ sqlite3_result_error(ctx, "", 0);
452
533
  return;
453
534
  }
454
535
 
@@ -457,6 +538,14 @@ void CustomAggregate::xValueBase(sqlite3_context *ctx, bool is_final) {
457
538
 
458
539
  // Convert the final JavaScript value to SQLite result
459
540
  self->JSValueToSqliteResult(ctx, final_value);
541
+
542
+ // Check if JSValueToSqliteResult threw an exception (e.g., ERR_OUT_OF_RANGE)
543
+ if (self->env_.IsExceptionPending()) {
544
+ // Ignore the SQLite error because a JavaScript exception is pending
545
+ self->db_->SetIgnoreNextSQLiteError(true);
546
+ sqlite3_result_error(ctx, "", 0);
547
+ return;
548
+ }
460
549
  }
461
550
 
462
551
  CustomAggregate::AggregateData *
@@ -500,13 +589,16 @@ CustomAggregate::GetAggregate(sqlite3_context *ctx) {
500
589
  }
501
590
 
502
591
  void CustomAggregate::DestroyAggregateData(sqlite3_context *ctx) {
592
+ CustomAggregate *self =
593
+ static_cast<CustomAggregate *>(sqlite3_user_data(ctx));
503
594
  AggregateData *agg = static_cast<AggregateData *>(
504
595
  sqlite3_aggregate_context(ctx, sizeof(AggregateData)));
505
596
 
506
- if (agg && agg->initialized) {
507
- ValueStorage::Remove(agg->value_id);
508
- agg->initialized = false;
597
+ if (!self || !agg || !agg->initialized) {
598
+ return;
509
599
  }
600
+ ValueStorage::Remove(self->env_, agg->value_id);
601
+ agg->initialized = false;
510
602
  }
511
603
 
512
604
  Napi::Value CustomAggregate::SqliteValueToJS(sqlite3_value *value) {
@@ -514,13 +606,24 @@ Napi::Value CustomAggregate::SqliteValueToJS(sqlite3_value *value) {
514
606
  case SQLITE_NULL:
515
607
  return env_.Null();
516
608
 
517
- case SQLITE_INTEGER:
609
+ case SQLITE_INTEGER: {
610
+ sqlite3_int64 int_val = sqlite3_value_int64(value);
518
611
  if (use_bigint_args_) {
519
- return Napi::BigInt::New(
520
- env_, static_cast<int64_t>(sqlite3_value_int64(value)));
612
+ return Napi::BigInt::New(env_, static_cast<int64_t>(int_val));
613
+ } else if (std::abs(int_val) <= kMaxSafeJsInteger) {
614
+ return Napi::Number::New(env_, static_cast<double>(int_val));
521
615
  } else {
522
- return Napi::Number::New(env_, sqlite3_value_int64(value));
616
+ // Value is outside safe integer range for JavaScript numbers
617
+ // Throw ERR_OUT_OF_RANGE directly - we're in a valid N-API context
618
+ char error_msg[128];
619
+ snprintf(error_msg, sizeof(error_msg),
620
+ "Value is too large to be represented as a JavaScript number: "
621
+ "%" PRId64,
622
+ static_cast<int64_t>(int_val));
623
+ node::THROW_ERR_OUT_OF_RANGE(env_, error_msg);
624
+ return env_.Undefined(); // Return undefined, exception is pending
523
625
  }
626
+ }
524
627
 
525
628
  case SQLITE_FLOAT:
526
629
  return Napi::Number::New(env_, sqlite3_value_double(value));
@@ -534,11 +637,14 @@ Napi::Value CustomAggregate::SqliteValueToJS(sqlite3_value *value) {
534
637
  case SQLITE_BLOB: {
535
638
  const void *blob = sqlite3_value_blob(value);
536
639
  int bytes = sqlite3_value_bytes(value);
640
+ // Return Uint8Array to match Node.js node:sqlite behavior
537
641
  if (blob && bytes > 0) {
538
- return Napi::Buffer<uint8_t>::Copy(
539
- env_, static_cast<const uint8_t *>(blob), static_cast<size_t>(bytes));
642
+ auto array_buffer = Napi::ArrayBuffer::New(env_, bytes);
643
+ memcpy(array_buffer.Data(), blob, bytes);
644
+ return Napi::Uint8Array::New(env_, bytes, array_buffer, 0);
540
645
  } else {
541
- return Napi::Buffer<uint8_t>::New(env_, 0);
646
+ auto array_buffer = Napi::ArrayBuffer::New(env_, 0);
647
+ return Napi::Uint8Array::New(env_, 0, array_buffer, 0);
542
648
  }
543
649
  }
544
650
 
@@ -549,34 +655,17 @@ Napi::Value CustomAggregate::SqliteValueToJS(sqlite3_value *value) {
549
655
 
550
656
  void CustomAggregate::JSValueToSqliteResult(sqlite3_context *ctx,
551
657
  Napi::Value value) {
552
- if (value.IsNull()) {
553
- sqlite3_result_null(ctx);
554
- } else if (value.IsUndefined()) {
658
+ if (value.IsNull() || value.IsUndefined()) {
555
659
  sqlite3_result_null(ctx);
556
- } else if (value.IsNumber()) {
557
- double num = value.As<Napi::Number>().DoubleValue();
558
- if (std::isnan(num)) {
559
- sqlite3_result_null(ctx);
560
- } else if (std::floor(num) == num &&
561
- num >= std::numeric_limits<int64_t>::min() &&
562
- num <= std::numeric_limits<int64_t>::max()) {
563
- sqlite3_result_int64(ctx, static_cast<int64_t>(num));
564
- } else {
565
- sqlite3_result_double(ctx, num);
566
- }
567
660
  } else if (value.IsBoolean()) {
661
+ // Extension over Node.js: Convert booleans to 0/1
568
662
  sqlite3_result_int(ctx, value.As<Napi::Boolean>().Value() ? 1 : 0);
569
- } else if (value.IsBigInt()) {
570
- bool lossless;
571
- int64_t val = value.As<Napi::BigInt>().Int64Value(&lossless);
572
- if (lossless) {
573
- sqlite3_result_int64(ctx, val);
574
- } else {
575
- sqlite3_result_error(ctx, "BigInt value is too large for SQLite", -1);
576
- }
663
+ } else if (value.IsNumber()) {
664
+ // Match Node.js: numbers are stored as doubles
665
+ sqlite3_result_double(ctx, value.As<Napi::Number>().DoubleValue());
577
666
  } else if (value.IsString()) {
578
667
  std::string str = value.As<Napi::String>().Utf8Value();
579
- sqlite3_result_text(ctx, str.c_str(), SafeCastToInt(str.length()),
668
+ sqlite3_result_text(ctx, str.c_str(), static_cast<int>(str.length()),
580
669
  SQLITE_TRANSIENT);
581
670
  } else if (value.IsDataView()) {
582
671
  // IMPORTANT: Check DataView BEFORE IsBuffer() because N-API's IsBuffer()
@@ -591,21 +680,40 @@ void CustomAggregate::JSValueToSqliteResult(sqlite3_context *ctx,
591
680
  if (arrayBuffer.Data() != nullptr && byteLength > 0) {
592
681
  const uint8_t *data =
593
682
  static_cast<const uint8_t *>(arrayBuffer.Data()) + byteOffset;
594
- sqlite3_result_blob(ctx, data, SafeCastToInt(byteLength),
683
+ sqlite3_result_blob(ctx, data, static_cast<int>(byteLength),
595
684
  SQLITE_TRANSIENT);
596
685
  } else {
597
686
  sqlite3_result_zeroblob(ctx, 0);
598
687
  }
599
- } else if (value.IsBuffer()) {
600
- // Handles both Node.js Buffer and TypedArrays (Uint8Array, etc.)
601
- Napi::Buffer<uint8_t> buffer = value.As<Napi::Buffer<uint8_t>>();
602
- sqlite3_result_blob(ctx, buffer.Data(), SafeCastToInt(buffer.Length()),
603
- SQLITE_TRANSIENT);
688
+ } else if (value.IsTypedArray()) {
689
+ // Handles Uint8Array and other TypedArrays (but not DataView, handled
690
+ // above)
691
+ Napi::TypedArray arr = value.As<Napi::TypedArray>();
692
+ Napi::ArrayBuffer buf = arr.ArrayBuffer();
693
+ sqlite3_result_blob(
694
+ ctx, static_cast<const uint8_t *>(buf.Data()) + arr.ByteOffset(),
695
+ static_cast<int>(arr.ByteLength()), SQLITE_TRANSIENT);
696
+ } else if (value.IsBigInt()) {
697
+ // Check BigInt - must fit in int64
698
+ bool lossless;
699
+ int64_t bigint_val = value.As<Napi::BigInt>().Int64Value(&lossless);
700
+ if (!lossless) {
701
+ // BigInt too large for SQLite - throw ERR_OUT_OF_RANGE
702
+ node::THROW_ERR_OUT_OF_RANGE(
703
+ env_,
704
+ "BigInt value is too large to be represented as a SQLite integer");
705
+ return;
706
+ }
707
+ sqlite3_result_int64(ctx, static_cast<sqlite3_int64>(bigint_val));
708
+ } else if (value.IsPromise()) {
709
+ // Promises are not supported
710
+ sqlite3_result_error(
711
+ ctx, "Asynchronous user-defined functions are not supported", -1);
604
712
  } else {
605
- // For other types (objects, functions, etc.), convert to string
606
- std::string str = value.ToString().Utf8Value();
607
- sqlite3_result_text(ctx, str.c_str(), SafeCastToInt(str.length()),
608
- SQLITE_TRANSIENT);
713
+ // Unsupported type
714
+ sqlite3_result_error(
715
+ ctx, "Returned JavaScript value cannot be converted to a SQLite value",
716
+ -1);
609
717
  }
610
718
  }
611
719
 
@@ -19,15 +19,10 @@ class DatabaseSync;
19
19
  // Thread-safe external storage for N-API values
20
20
  // This solves the problem of storing N-API objects in SQLite-allocated memory
21
21
  class ValueStorage {
22
- private:
23
- static std::unordered_map<int32_t, Napi::Reference<Napi::Value>> storage_;
24
- static std::mutex mutex_;
25
- static std::atomic<int32_t> next_id_;
26
-
27
22
  public:
28
23
  static int32_t Store(Napi::Env env, Napi::Value value);
29
24
  static Napi::Value Get(Napi::Env env, int32_t id);
30
- static void Remove(int32_t id);
25
+ static void Remove(Napi::Env env, int32_t id);
31
26
  };
32
27
 
33
28
  class CustomAggregate {
@@ -46,6 +41,8 @@ public:
46
41
  static void xDestroy(void *self);
47
42
 
48
43
  private:
44
+ // Environment cleanup hook - called before environment teardown
45
+ static void CleanupHook(void *arg);
49
46
  // Comprehensive aggregate value storage (no Napi::Reference)
50
47
  struct AggregateValue {
51
48
  enum Type {
@@ -59,6 +56,7 @@ private:
59
56
  };
60
57
  Type type;
61
58
  bool is_initialized;
59
+ bool xvalue_called; // Track if xValue was called (for window functions)
62
60
  union {
63
61
  double number_value;
64
62
  bool bool_value;
@@ -94,6 +92,7 @@ private:
94
92
  Napi::Value GetStartValue();
95
93
 
96
94
  Napi::Env env_;
95
+ DatabaseSync *db_;
97
96
  bool use_bigint_args_;
98
97
 
99
98
  // Storage for start value - handle primitives and objects
package/src/binding.cpp CHANGED
@@ -2,6 +2,7 @@
2
2
  #include <napi.h>
3
3
  #include <set>
4
4
 
5
+ #include "aggregate_function.h"
5
6
  #include "sqlite_impl.h"
6
7
 
7
8
  namespace photostructure::sqlite {
@@ -17,20 +18,23 @@ void CleanupAddonData([[maybe_unused]] napi_env env, void *finalize_data,
17
18
  addon_data->databases.clear();
18
19
  }
19
20
 
20
- // Clean up persistent references to prevent memory leaks
21
- if (!addon_data->databaseSyncConstructor.IsEmpty()) {
22
- addon_data->databaseSyncConstructor.Reset();
23
- }
24
- if (!addon_data->statementSyncConstructor.IsEmpty()) {
25
- addon_data->statementSyncConstructor.Reset();
26
- }
27
- if (!addon_data->statementSyncIteratorConstructor.IsEmpty()) {
28
- addon_data->statementSyncIteratorConstructor.Reset();
29
- }
30
- if (!addon_data->sessionConstructor.IsEmpty()) {
31
- addon_data->sessionConstructor.Reset();
21
+ // Clean up ValueStorage references
22
+ {
23
+ std::lock_guard<std::mutex> lock(addon_data->value_storage_mutex);
24
+ for (auto &[id, ref] : addon_data->value_storage) {
25
+ if (!ref.IsEmpty()) {
26
+ ref.Reset();
27
+ }
28
+ }
29
+ addon_data->value_storage.clear();
32
30
  }
33
31
 
32
+ // Let Napi::FunctionReference destructors handle cleanup naturally.
33
+ // Explicitly calling Reset() during worker termination causes JIT corruption
34
+ // on Alpine/musl. The references will be cleaned up when addon_data is
35
+ // deleted. See: nodejs/node-addon-api#660,
36
+ // P02-investigate-flaky-native-crashes.md
37
+
34
38
  delete addon_data;
35
39
  }
36
40
 
@@ -75,6 +79,13 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
75
79
  return exports;
76
80
  }
77
81
 
82
+ // Cache Object.create for creating objects with null prototype
83
+ Napi::Object global_object = env.Global().Get("Object").As<Napi::Object>();
84
+ Napi::Function object_create =
85
+ global_object.Get("create").As<Napi::Function>();
86
+ addon_data->objectCreateFn =
87
+ Napi::Reference<Napi::Function>::New(object_create);
88
+
78
89
  DatabaseSync::Init(env, exports);
79
90
  StatementSync::Init(env, exports);
80
91
  StatementSyncIterator::Init(env, exports);
@@ -217,18 +228,16 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
217
228
  }
218
229
  }
219
230
  if (db == nullptr) {
220
- Napi::TypeError::New(
221
- env, "The \"sourceDb\" argument must be a DatabaseSync object")
222
- .ThrowAsJavaScriptException();
231
+ Napi::TypeError error = Napi::TypeError::New(
232
+ env, "The \"sourceDb\" argument must be an object.");
233
+ error.Set("code", Napi::String::New(env, "ERR_INVALID_ARG_TYPE"));
234
+ error.ThrowAsJavaScriptException();
223
235
  return env.Undefined();
224
236
  }
225
237
 
226
- // Validate destination path is provided
227
- if (info.Length() < 2) {
228
- Napi::TypeError::New(env, "The \"destination\" argument is required")
229
- .ThrowAsJavaScriptException();
230
- return env.Undefined();
231
- }
238
+ // Validate path is provided and valid - delegates to
239
+ // ValidateDatabasePath which will throw ERR_INVALID_ARG_TYPE with the
240
+ // proper message
232
241
 
233
242
  // Delegate to instance method: db.backup(destination, options?)
234
243
  std::vector<napi_value> args;