@photostructure/sqlite 0.0.1 → 0.2.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 (56) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/README.md +45 -484
  3. package/SECURITY.md +27 -84
  4. package/binding.gyp +69 -22
  5. package/dist/index.cjs +185 -18
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +552 -100
  8. package/dist/index.d.mts +552 -100
  9. package/dist/index.d.ts +552 -100
  10. package/dist/index.mjs +183 -18
  11. package/dist/index.mjs.map +1 -1
  12. package/package.json +51 -41
  13. package/prebuilds/darwin-arm64/@photostructure+sqlite.glibc.node +0 -0
  14. package/prebuilds/darwin-x64/@photostructure+sqlite.glibc.node +0 -0
  15. package/prebuilds/linux-arm64/@photostructure+sqlite.glibc.node +0 -0
  16. package/prebuilds/linux-arm64/@photostructure+sqlite.musl.node +0 -0
  17. package/prebuilds/linux-x64/@photostructure+sqlite.glibc.node +0 -0
  18. package/prebuilds/linux-x64/@photostructure+sqlite.musl.node +0 -0
  19. package/prebuilds/test_extension.so +0 -0
  20. package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
  21. package/src/aggregate_function.cpp +503 -235
  22. package/src/aggregate_function.h +57 -42
  23. package/src/binding.cpp +117 -14
  24. package/src/dirname.ts +1 -1
  25. package/src/index.ts +122 -332
  26. package/src/lru-cache.ts +84 -0
  27. package/src/shims/env-inl.h +6 -15
  28. package/src/shims/node_errors.h +4 -0
  29. package/src/shims/sqlite_errors.h +162 -0
  30. package/src/shims/util.h +29 -4
  31. package/src/sql-tag-store.ts +140 -0
  32. package/src/sqlite_exception.h +49 -0
  33. package/src/sqlite_impl.cpp +711 -127
  34. package/src/sqlite_impl.h +84 -6
  35. package/src/{stack_path.ts → stack-path.ts} +7 -1
  36. package/src/types/aggregate-options.ts +22 -0
  37. package/src/types/changeset-apply-options.ts +18 -0
  38. package/src/types/database-sync-instance.ts +203 -0
  39. package/src/types/database-sync-options.ts +69 -0
  40. package/src/types/session-options.ts +10 -0
  41. package/src/types/sql-tag-store-instance.ts +51 -0
  42. package/src/types/sqlite-authorization-actions.ts +77 -0
  43. package/src/types/sqlite-authorization-results.ts +15 -0
  44. package/src/types/sqlite-changeset-conflict-types.ts +19 -0
  45. package/src/types/sqlite-changeset-resolution.ts +15 -0
  46. package/src/types/sqlite-open-flags.ts +50 -0
  47. package/src/types/statement-sync-instance.ts +73 -0
  48. package/src/types/user-functions-options.ts +14 -0
  49. package/src/upstream/node_sqlite.cc +960 -259
  50. package/src/upstream/node_sqlite.h +127 -2
  51. package/src/upstream/sqlite.js +1 -14
  52. package/src/upstream/sqlite3.c +4510 -1411
  53. package/src/upstream/sqlite3.h +390 -195
  54. package/src/upstream/sqlite3ext.h +7 -0
  55. package/src/user_function.cpp +88 -36
  56. package/src/user_function.h +2 -1
@@ -2,25 +2,67 @@
2
2
 
3
3
  #include <cmath>
4
4
  #include <cstring>
5
+ #include <limits>
6
+ #include <unordered_map>
7
+ #include <vector>
5
8
 
6
- #include "shims/node_errors.h"
7
9
  #include "sqlite_impl.h"
8
10
 
9
- namespace photostructure {
10
- namespace sqlite {
11
+ namespace photostructure::sqlite {
11
12
 
12
- CustomAggregate::CustomAggregate(Napi::Env env, DatabaseSync *db,
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};
18
+
19
+ // 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
25
+ try {
26
+ storage_[id] = Napi::Reference<Napi::Value>::New(value, 1);
27
+ } catch (...) {
28
+ // If Reference creation fails, throw to let caller handle
29
+ throw;
30
+ }
31
+
32
+ return id;
33
+ }
34
+
35
+ 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();
39
+ }
40
+
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);
47
+ }
48
+ }
49
+
50
+ CustomAggregate::CustomAggregate(Napi::Env env,
51
+ [[maybe_unused]] DatabaseSync *db,
13
52
  bool use_bigint_args, Napi::Value start,
14
53
  Napi::Function step_fn,
15
54
  Napi::Function inverse_fn,
16
55
  Napi::Function result_fn)
17
- : env_(env), db_(db), use_bigint_args_(use_bigint_args),
18
- async_context_(nullptr) {
56
+ : env_(env), use_bigint_args_(use_bigint_args), async_context_(nullptr) {
19
57
  // Handle start value based on type
20
58
  if (start.IsNull()) {
21
59
  start_type_ = PRIMITIVE_NULL;
22
60
  } else if (start.IsUndefined()) {
23
61
  start_type_ = PRIMITIVE_UNDEFINED;
62
+ } else if (start.IsFunction()) {
63
+ start_type_ = FUNCTION;
64
+ start_fn_ =
65
+ Napi::Reference<Napi::Function>::New(start.As<Napi::Function>(), 1);
24
66
  } else if (start.IsNumber()) {
25
67
  start_type_ = PRIMITIVE_NUMBER;
26
68
  number_value_ = start.As<Napi::Number>().DoubleValue();
@@ -49,40 +91,57 @@ CustomAggregate::CustomAggregate(Napi::Env env, DatabaseSync *db,
49
91
  result_fn_ = Napi::Reference<Napi::Function>::New(result_fn, 1);
50
92
  }
51
93
 
52
- // Create async context for callbacks
53
- napi_status status = napi_async_init(
54
- env, nullptr, Napi::String::New(env, "SQLiteAggregate"), &async_context_);
55
- if (status != napi_ok) {
56
- Napi::Error::New(env, "Failed to create async context")
57
- .ThrowAsJavaScriptException();
58
- }
94
+ // Don't create async context immediately - we'll create it lazily if needed
95
+ async_context_ = nullptr;
59
96
  }
60
97
 
61
98
  CustomAggregate::~CustomAggregate() {
62
- if (start_type_ == OBJECT && !object_ref_.IsEmpty()) {
63
- object_ref_.Reset();
64
- }
65
- if (!step_fn_.IsEmpty())
66
- step_fn_.Reset();
67
- if (!inverse_fn_.IsEmpty())
68
- inverse_fn_.Reset();
69
- if (!result_fn_.IsEmpty())
70
- result_fn_.Reset();
99
+ // 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
+ napi_handle_scope scope;
103
+ napi_status status = napi_open_handle_scope(env_, &scope);
104
+
105
+ 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
+ }
71
124
 
72
- // Cleanup async context
73
- if (async_context_ != nullptr) {
74
- napi_async_destroy(env_, async_context_);
125
+ // Clean up async context if it was created
126
+ if (async_context_ != nullptr) {
127
+ napi_async_destroy(env_, async_context_);
128
+ async_context_ = nullptr;
129
+ }
130
+
131
+ napi_close_handle_scope(env_, scope);
75
132
  }
133
+ // If status != napi_ok, env is invalid - skip cleanup.
134
+ // References will be leaked, but that's better than crashing.
76
135
  }
77
136
 
78
137
  void CustomAggregate::xStep(sqlite3_context *ctx, int argc,
79
138
  sqlite3_value **argv) {
80
- xStepBase(ctx, argc, argv, false);
139
+ xStepBase(ctx, argc, argv, &CustomAggregate::step_fn_);
81
140
  }
82
141
 
83
142
  void CustomAggregate::xInverse(sqlite3_context *ctx, int argc,
84
143
  sqlite3_value **argv) {
85
- xStepBase(ctx, argc, argv, true);
144
+ xStepBase(ctx, argc, argv, &CustomAggregate::inverse_fn_);
86
145
  }
87
146
 
88
147
  void CustomAggregate::xFinal(sqlite3_context *ctx) { xValueBase(ctx, true); }
@@ -95,139 +154,309 @@ void CustomAggregate::xDestroy(void *self) {
95
154
  }
96
155
  }
97
156
 
98
- void CustomAggregate::xStepBase(sqlite3_context *ctx, int argc,
99
- sqlite3_value **argv, bool use_inverse) {
100
- void *user_data = sqlite3_user_data(ctx);
101
- if (!user_data) {
102
- sqlite3_result_error(ctx, "Invalid user data in aggregate function", -1);
103
- return;
104
- }
105
-
106
- CustomAggregate *self = static_cast<CustomAggregate *>(user_data);
157
+ void CustomAggregate::xStepBase(
158
+ sqlite3_context *ctx, int argc, sqlite3_value **argv,
159
+ Napi::Reference<Napi::Function> CustomAggregate::*mptr) {
160
+ CustomAggregate *self =
161
+ static_cast<CustomAggregate *>(sqlite3_user_data(ctx));
107
162
  if (!self) {
108
163
  sqlite3_result_error(ctx, "No user data", -1);
109
164
  return;
110
165
  }
111
166
 
112
- // Create HandleScope and CallbackScope for this operation
167
+ // Create HandleScope for N-API operations
113
168
  Napi::HandleScope scope(self->env_);
114
- Napi::CallbackScope callback_scope(self->env_, self->async_context_);
115
169
 
116
- try {
117
- auto agg = self->GetAggregate(ctx);
118
- if (!agg) {
119
- sqlite3_result_error(ctx, "Failed to get aggregate context", -1);
120
- return;
121
- }
170
+ AggregateValue *state = static_cast<AggregateValue *>(
171
+ sqlite3_aggregate_context(ctx, sizeof(AggregateValue)));
122
172
 
123
- // Choose the right function
124
- Napi::Function func;
125
- if (use_inverse) {
126
- if (self->inverse_fn_.IsEmpty()) {
127
- sqlite3_result_error(ctx, "Inverse function not provided", -1);
128
- return;
173
+ if (!state) {
174
+ sqlite3_result_error(ctx, "Failed to get aggregate state", -1);
175
+ return;
176
+ }
177
+
178
+ if (!state->is_initialized) {
179
+ // Initialize with the proper start value
180
+ Napi::Value start_val = self->GetStartValue();
181
+
182
+ // Store the start value in the appropriate type
183
+ if (start_val.IsNumber()) {
184
+ state->type = AggregateValue::NUMBER;
185
+ state->number_value = start_val.As<Napi::Number>().DoubleValue();
186
+ } else if (start_val.IsString()) {
187
+ state->type = AggregateValue::STRING;
188
+ std::string str_val = start_val.As<Napi::String>().Utf8Value();
189
+ size_t copy_len =
190
+ std::min(str_val.length(), sizeof(state->string_buffer) - 1);
191
+ memcpy(state->string_buffer, str_val.c_str(), copy_len);
192
+ state->string_buffer[copy_len] = '\0';
193
+ state->string_length = copy_len;
194
+ } else if (start_val.IsBigInt()) {
195
+ state->type = AggregateValue::BIGINT;
196
+ bool lossless;
197
+ state->bigint_value = start_val.As<Napi::BigInt>().Int64Value(&lossless);
198
+ } else if (start_val.IsBoolean()) {
199
+ state->type = AggregateValue::BOOLEAN;
200
+ state->bool_value = start_val.As<Napi::Boolean>().Value();
201
+ } else if (start_val.IsBuffer()) {
202
+ state->type = AggregateValue::BUFFER;
203
+ Napi::Buffer<uint8_t> buffer = start_val.As<Napi::Buffer<uint8_t>>();
204
+ size_t copy_len =
205
+ std::min(buffer.Length(), sizeof(state->string_buffer) - 1);
206
+ memcpy(state->string_buffer, buffer.Data(), copy_len);
207
+ state->string_length = copy_len;
208
+ } else if (start_val.IsObject() && !start_val.IsArray() &&
209
+ !start_val.IsBuffer()) {
210
+ // Store objects as JSON strings
211
+ state->type = AggregateValue::OBJECT_JSON;
212
+ // Use JSON.stringify to serialize the object
213
+ std::string json_str = SafeJsonStringify(self->env_, start_val);
214
+
215
+ // If JSON is too long, use a simpler representation
216
+ if (json_str.length() >= sizeof(state->string_buffer) - 1) {
217
+ const char *fallback = "{\"_truncated\":true}";
218
+ size_t fallback_len = strlen(fallback);
219
+ memcpy(state->string_buffer, fallback, fallback_len);
220
+ state->string_buffer[fallback_len] = '\0';
221
+ state->string_length = fallback_len;
222
+ } else {
223
+ memcpy(state->string_buffer, json_str.c_str(), json_str.length());
224
+ state->string_buffer[json_str.length()] = '\0';
225
+ state->string_length = json_str.length();
129
226
  }
130
- func = self->inverse_fn_.Value();
131
227
  } else {
132
- if (self->step_fn_.IsEmpty()) {
133
- sqlite3_result_error(ctx, "Step function is empty", -1);
134
- return;
135
- }
136
- func = self->step_fn_.Value();
228
+ state->type = AggregateValue::NULL_VAL;
137
229
  }
138
230
 
139
- // Prepare arguments for JavaScript function call
140
- std::vector<Napi::Value> js_argv;
231
+ state->is_initialized = true;
232
+ }
141
233
 
142
- // First argument is the current aggregate value
143
- Napi::Value agg_val;
144
- if (agg->first_call) {
145
- agg_val = self->GetStartValue();
146
- // Store the start value as raw data for future reference
147
- self->StoreJSValueAsRaw(agg, agg_val);
148
- agg->first_call = false;
149
- } else {
150
- agg_val = self->RawValueToJS(agg);
151
- }
152
- js_argv.push_back(agg_val);
234
+ // Get the JavaScript function
235
+ if ((self->*mptr).IsEmpty()) {
236
+ sqlite3_result_error(ctx, "Function not defined", -1);
237
+ return;
238
+ }
239
+ Napi::Function func = (self->*mptr).Value();
153
240
 
154
- // Add the SQLite arguments
155
- for (int i = 0; i < argc; ++i) {
156
- Napi::Value js_val = self->SqliteValueToJS(argv[i]);
157
- js_argv.push_back(js_val);
158
- }
241
+ // Build arguments for the JavaScript function
242
+ std::vector<Napi::Value> js_args;
243
+ js_args.reserve(argc + 1);
159
244
 
160
- // Call the JavaScript function
161
- Napi::Value result;
245
+ // First argument: current aggregate value (convert from stored type)
246
+ Napi::Value current_value;
247
+ switch (state->type) {
248
+ case AggregateValue::NUMBER:
249
+ current_value = Napi::Number::New(self->env_, state->number_value);
250
+ break;
251
+ case AggregateValue::STRING:
252
+ current_value = Napi::String::New(self->env_, state->string_buffer,
253
+ state->string_length);
254
+ break;
255
+ case AggregateValue::BIGINT:
256
+ current_value = Napi::BigInt::New(self->env_, state->bigint_value);
257
+ break;
258
+ case AggregateValue::BOOLEAN:
259
+ current_value = Napi::Boolean::New(self->env_, state->bool_value);
260
+ 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);
265
+ break;
266
+ case AggregateValue::OBJECT_JSON: {
267
+ // Parse JSON back to object using JSON.parse
162
268
  try {
163
- // Debug: Log the call
164
- result = func.Call(js_argv);
165
- if (result.IsEmpty() || result.IsUndefined()) {
166
- sqlite3_result_error(ctx, "Step function returned empty/undefined", -1);
167
- return;
168
- }
169
- } catch (const std::exception &e) {
170
- throw;
269
+ Napi::Object global = self->env_.Global();
270
+ Napi::Object json = global.Get("JSON").As<Napi::Object>();
271
+ Napi::Function parse = json.Get("parse").As<Napi::Function>();
272
+ Napi::String json_str = Napi::String::New(
273
+ self->env_, state->string_buffer, state->string_length);
274
+ current_value = parse.Call({json_str});
275
+ } catch (...) {
276
+ // If JSON parsing fails, return the string instead
277
+ current_value = Napi::String::New(self->env_, state->string_buffer,
278
+ state->string_length);
171
279
  }
280
+ break;
281
+ }
282
+ case AggregateValue::NULL_VAL:
283
+ default:
284
+ current_value = self->env_.Null();
285
+ break;
286
+ }
287
+ js_args.push_back(current_value);
172
288
 
173
- // Update the aggregate value
174
- self->StoreJSValueAsRaw(agg, result);
289
+ // Convert SQLite values to JavaScript
290
+ for (int i = 0; i < argc; ++i) {
291
+ js_args.push_back(self->SqliteValueToJS(argv[i]));
292
+ }
175
293
 
176
- } catch (const Napi::Error &e) {
177
- // More detailed error message
178
- std::string error_msg = "Aggregate step error: ";
179
- error_msg += e.what();
180
- sqlite3_result_error(ctx, error_msg.c_str(), -1);
181
- } catch (const std::exception &e) {
182
- // Catch any other exceptions
183
- std::string error_msg = "Aggregate step exception: ";
184
- error_msg += e.what();
185
- sqlite3_result_error(ctx, error_msg.c_str(), -1);
294
+ // Convert to napi_value array
295
+ std::vector<napi_value> raw_args;
296
+ for (const auto &arg : js_args) {
297
+ raw_args.push_back(arg);
186
298
  }
187
- }
188
299
 
189
- void CustomAggregate::xValueBase(sqlite3_context *ctx, bool finalize) {
190
- void *user_data = sqlite3_user_data(ctx);
191
- if (!user_data) {
192
- sqlite3_result_error(ctx, "Invalid user data in aggregate value function",
193
- -1);
300
+ // Call the JavaScript function
301
+ napi_value result;
302
+ napi_status status =
303
+ napi_call_function(self->env_, self->env_.Undefined(), func,
304
+ raw_args.size(), raw_args.data(), &result);
305
+
306
+ if (status != napi_ok) {
307
+ sqlite3_result_error(ctx, "Error calling aggregate step function", -1);
194
308
  return;
195
309
  }
196
310
 
197
- CustomAggregate *self = static_cast<CustomAggregate *>(user_data);
311
+ // Convert result back and store in appropriate type
312
+ Napi::Value result_val(self->env_, result);
313
+
314
+ // Check for Promise (from async functions) first
315
+ if (result_val.IsObject() && !result_val.IsArray() &&
316
+ !result_val.IsBuffer()) {
317
+ Napi::Object obj = result_val.As<Napi::Object>();
318
+ // Check if it's a Promise by looking for 'then' method
319
+ if (obj.Has("then") && obj.Get("then").IsFunction()) {
320
+ sqlite3_result_error(ctx, "User-defined function returned invalid type",
321
+ -1);
322
+ return;
323
+ }
324
+ }
325
+
326
+ if (result_val.IsNumber()) {
327
+ state->type = AggregateValue::NUMBER;
328
+ state->number_value = result_val.As<Napi::Number>().DoubleValue();
329
+ } else if (result_val.IsString()) {
330
+ state->type = AggregateValue::STRING;
331
+ std::string str_val = result_val.As<Napi::String>().Utf8Value();
332
+ size_t copy_len =
333
+ std::min(str_val.length(), sizeof(state->string_buffer) - 1);
334
+ memcpy(state->string_buffer, str_val.c_str(), copy_len);
335
+ state->string_buffer[copy_len] = '\0';
336
+ state->string_length = copy_len;
337
+ } else if (result_val.IsBigInt()) {
338
+ state->type = AggregateValue::BIGINT;
339
+ bool lossless;
340
+ state->bigint_value = result_val.As<Napi::BigInt>().Int64Value(&lossless);
341
+ } else if (result_val.IsBoolean()) {
342
+ state->type = AggregateValue::BOOLEAN;
343
+ state->bool_value = result_val.As<Napi::Boolean>().Value();
344
+ } else if (result_val.IsBuffer()) {
345
+ state->type = AggregateValue::BUFFER;
346
+ Napi::Buffer<uint8_t> buffer = result_val.As<Napi::Buffer<uint8_t>>();
347
+ size_t copy_len =
348
+ std::min(buffer.Length(), sizeof(state->string_buffer) - 1);
349
+ memcpy(state->string_buffer, buffer.Data(), copy_len);
350
+ state->string_length = copy_len;
351
+ } else if (result_val.IsObject() && !result_val.IsArray() &&
352
+ !result_val.IsBuffer()) {
353
+ // Store objects as JSON strings
354
+ state->type = AggregateValue::OBJECT_JSON;
355
+ // Use JSON.stringify to serialize the object
356
+ std::string json_str = SafeJsonStringify(self->env_, result_val);
357
+
358
+ // If JSON is too long, use a simpler representation
359
+ if (json_str.length() >= sizeof(state->string_buffer) - 1) {
360
+ const char *fallback = "{\"_truncated\":true}";
361
+ size_t fallback_len = strlen(fallback);
362
+ memcpy(state->string_buffer, fallback, fallback_len);
363
+ state->string_buffer[fallback_len] = '\0';
364
+ state->string_length = fallback_len;
365
+ } else {
366
+ memcpy(state->string_buffer, json_str.c_str(), json_str.length());
367
+ state->string_buffer[json_str.length()] = '\0';
368
+ state->string_length = json_str.length();
369
+ }
370
+ } else if (result_val.IsNull() || result_val.IsUndefined()) {
371
+ state->type = AggregateValue::NULL_VAL;
372
+ }
373
+ }
374
+
375
+ void CustomAggregate::xValueBase(sqlite3_context *ctx, bool is_final) {
376
+ CustomAggregate *self =
377
+ static_cast<CustomAggregate *>(sqlite3_user_data(ctx));
378
+ if (!self) {
379
+ sqlite3_result_error(ctx, "No user data", -1);
380
+ return;
381
+ }
198
382
 
199
- // Create HandleScope and CallbackScope for this operation
200
383
  Napi::HandleScope scope(self->env_);
201
- Napi::CallbackScope callback_scope(self->env_, self->async_context_);
202
384
 
203
- try {
204
- auto agg = self->GetAggregate(ctx);
205
- if (!agg) {
206
- sqlite3_result_null(ctx);
207
- return;
208
- }
385
+ // Get the same AggregateValue struct used in xStepBase
386
+ AggregateValue *state = static_cast<AggregateValue *>(
387
+ sqlite3_aggregate_context(ctx, sizeof(AggregateValue)));
209
388
 
210
- Napi::Value final_value = self->RawValueToJS(agg);
389
+ if (!state || !state->is_initialized) {
390
+ // No rows processed, return null
391
+ sqlite3_result_null(ctx);
392
+ return;
393
+ }
211
394
 
212
- // If we have a result function, call it
213
- if (!self->result_fn_.IsEmpty()) {
214
- Napi::Function result_func = self->result_fn_.Value();
215
- final_value = result_func.Call({final_value});
395
+ // Convert the stored value to JavaScript first
396
+ Napi::Value current_value;
397
+ switch (state->type) {
398
+ case AggregateValue::NUMBER:
399
+ current_value = Napi::Number::New(self->env_, state->number_value);
400
+ break;
401
+ case AggregateValue::STRING:
402
+ current_value = Napi::String::New(self->env_, state->string_buffer,
403
+ state->string_length);
404
+ break;
405
+ case AggregateValue::BIGINT:
406
+ current_value = Napi::BigInt::New(self->env_, state->bigint_value);
407
+ break;
408
+ case AggregateValue::BOOLEAN:
409
+ current_value = Napi::Boolean::New(self->env_, state->bool_value);
410
+ 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);
415
+ break;
416
+ case AggregateValue::OBJECT_JSON: {
417
+ // Parse JSON back to object using JSON.parse
418
+ try {
419
+ Napi::Object global = self->env_.Global();
420
+ Napi::Object json = global.Get("JSON").As<Napi::Object>();
421
+ Napi::Function parse = json.Get("parse").As<Napi::Function>();
422
+ Napi::String json_str = Napi::String::New(
423
+ self->env_, state->string_buffer, state->string_length);
424
+ current_value = parse.Call({json_str});
425
+ } catch (...) {
426
+ // If JSON parsing fails, return the string instead
427
+ current_value = Napi::String::New(self->env_, state->string_buffer,
428
+ state->string_length);
216
429
  }
430
+ break;
431
+ }
432
+ case AggregateValue::NULL_VAL:
433
+ default:
434
+ current_value = self->env_.Null();
435
+ break;
436
+ }
437
+
438
+ // Apply result function if provided
439
+ Napi::Value final_value = current_value;
440
+ if (!self->result_fn_.IsEmpty()) {
441
+ Napi::Function result_func = self->result_fn_.Value();
217
442
 
218
- // Convert to SQLite result
219
- self->JSValueToSqliteResult(ctx, final_value);
443
+ std::vector<napi_value> args = {current_value};
444
+ napi_value result;
220
445
 
221
- // Clean up if this is finalization
222
- if (finalize) {
223
- // Properly destroy the C++ object constructed with placement new
224
- // This will call the destructor for Napi::Reference members
225
- agg->~AggregateData();
446
+ napi_status status =
447
+ napi_call_function(self->env_, self->env_.Undefined(), result_func, 1,
448
+ args.data(), &result);
449
+
450
+ if (status != napi_ok) {
451
+ sqlite3_result_error(ctx, "Error calling aggregate result function", -1);
452
+ return;
226
453
  }
227
454
 
228
- } catch (const Napi::Error &e) {
229
- sqlite3_result_error(ctx, e.what(), -1);
455
+ final_value = Napi::Value(self->env_, result);
230
456
  }
457
+
458
+ // Convert the final JavaScript value to SQLite result
459
+ self->JSValueToSqliteResult(ctx, final_value);
231
460
  }
232
461
 
233
462
  CustomAggregate::AggregateData *
@@ -235,110 +464,152 @@ CustomAggregate::GetAggregate(sqlite3_context *ctx) {
235
464
  AggregateData *agg = static_cast<AggregateData *>(
236
465
  sqlite3_aggregate_context(ctx, sizeof(AggregateData)));
237
466
 
238
- // sqlite3_aggregate_context only returns NULL if size is 0 or memory
239
- // allocation fails
240
467
  if (!agg) {
468
+ sqlite3_result_error(ctx, "Failed to allocate aggregate context", -1);
241
469
  return nullptr;
242
470
  }
243
471
 
244
- // Check if this is uninitialized memory by testing if initialized flag is
245
- // false or garbage We need to be careful because the memory might contain
246
- // random values Check if this memory has been initialized as a C++ object
247
- if (agg->initialized != true) {
248
- // First call - use placement new to properly construct the C++ object
249
- new (agg) AggregateData();
250
- agg->value_type = AggregateData::TYPE_NULL;
251
- agg->number_val = 0.0;
252
- agg->boolean_val = false;
253
- agg->bigint_val = 0;
472
+ if (!agg->initialized) {
473
+ Napi::Value start_value;
474
+
475
+ if (start_type_ == FUNCTION) {
476
+ // Call start function
477
+ Napi::Function start_func = start_fn_.Value();
478
+ napi_value result;
479
+ napi_async_context async_ctx = GetAsyncContext();
480
+
481
+ napi_status status = napi_make_callback(env_, async_ctx, env_.Undefined(),
482
+ start_func, 0, nullptr, &result);
483
+
484
+ if (status != napi_ok) {
485
+ sqlite3_result_error(ctx, "Error calling aggregate start function", -1);
486
+ return nullptr;
487
+ }
488
+
489
+ start_value = Napi::Value(env_, result);
490
+ } else {
491
+ start_value = GetStartValue();
492
+ }
493
+
494
+ agg->value_id = ValueStorage::Store(env_, start_value);
254
495
  agg->initialized = true;
255
496
  agg->is_window = false;
256
- agg->first_call = true; // Mark that we need to initialize with start value
257
497
  }
258
498
 
259
499
  return agg;
260
500
  }
261
501
 
262
- Napi::Value CustomAggregate::SqliteValueToJS(sqlite3_value *value) {
263
- // Don't create HandleScope here - let the caller manage it
502
+ void CustomAggregate::DestroyAggregateData(sqlite3_context *ctx) {
503
+ AggregateData *agg = static_cast<AggregateData *>(
504
+ sqlite3_aggregate_context(ctx, sizeof(AggregateData)));
505
+
506
+ if (agg && agg->initialized) {
507
+ ValueStorage::Remove(agg->value_id);
508
+ agg->initialized = false;
509
+ }
510
+ }
264
511
 
265
- int type = sqlite3_value_type(value);
512
+ Napi::Value CustomAggregate::SqliteValueToJS(sqlite3_value *value) {
513
+ switch (sqlite3_value_type(value)) {
514
+ case SQLITE_NULL:
515
+ return env_.Null();
266
516
 
267
- switch (type) {
268
- case SQLITE_INTEGER: {
269
- sqlite3_int64 int_val = sqlite3_value_int64(value);
517
+ case SQLITE_INTEGER:
270
518
  if (use_bigint_args_) {
271
- return Napi::BigInt::New(env_, static_cast<int64_t>(int_val));
519
+ return Napi::BigInt::New(
520
+ env_, static_cast<int64_t>(sqlite3_value_int64(value)));
272
521
  } else {
273
- return Napi::Number::New(env_, static_cast<double>(int_val));
522
+ return Napi::Number::New(env_, sqlite3_value_int64(value));
274
523
  }
275
- break;
276
- }
277
- case SQLITE_FLOAT: {
278
- double float_val = sqlite3_value_double(value);
279
- return Napi::Number::New(env_, float_val);
280
- }
524
+
525
+ case SQLITE_FLOAT:
526
+ return Napi::Number::New(env_, sqlite3_value_double(value));
527
+
281
528
  case SQLITE_TEXT: {
282
- const char *text_val =
529
+ const char *text =
283
530
  reinterpret_cast<const char *>(sqlite3_value_text(value));
284
- return Napi::String::New(env_, text_val);
531
+ return Napi::String::New(env_, text ? text : "");
285
532
  }
533
+
286
534
  case SQLITE_BLOB: {
287
- const void *blob_data = sqlite3_value_blob(value);
288
- int blob_size = sqlite3_value_bytes(value);
289
- return Napi::Buffer<uint8_t>::Copy(
290
- env_, static_cast<const uint8_t *>(blob_data), blob_size);
535
+ const void *blob = sqlite3_value_blob(value);
536
+ int bytes = sqlite3_value_bytes(value);
537
+ if (blob && bytes > 0) {
538
+ return Napi::Buffer<uint8_t>::Copy(
539
+ env_, static_cast<const uint8_t *>(blob), static_cast<size_t>(bytes));
540
+ } else {
541
+ return Napi::Buffer<uint8_t>::New(env_, 0);
542
+ }
291
543
  }
292
- case SQLITE_NULL:
544
+
293
545
  default:
294
- return env_.Null();
546
+ return env_.Undefined();
295
547
  }
296
548
  }
297
549
 
298
550
  void CustomAggregate::JSValueToSqliteResult(sqlite3_context *ctx,
299
551
  Napi::Value value) {
300
- if (value.IsNull() || value.IsUndefined()) {
552
+ if (value.IsNull()) {
301
553
  sqlite3_result_null(ctx);
554
+ } else if (value.IsUndefined()) {
555
+ 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
+ }
302
567
  } else if (value.IsBoolean()) {
303
- bool bool_val = value.As<Napi::Boolean>().Value();
304
- sqlite3_result_int(ctx, bool_val ? 1 : 0);
568
+ sqlite3_result_int(ctx, value.As<Napi::Boolean>().Value() ? 1 : 0);
305
569
  } else if (value.IsBigInt()) {
306
570
  bool lossless;
307
- int64_t bigint_val = value.As<Napi::BigInt>().Int64Value(&lossless);
571
+ int64_t val = value.As<Napi::BigInt>().Int64Value(&lossless);
308
572
  if (lossless) {
309
- sqlite3_result_int64(ctx, static_cast<sqlite3_int64>(bigint_val));
573
+ sqlite3_result_int64(ctx, val);
310
574
  } else {
311
- sqlite3_result_error(ctx, "BigInt value too large for SQLite", -1);
312
- }
313
- } else if (value.IsNumber()) {
314
- double num_val = value.As<Napi::Number>().DoubleValue();
315
- // Note: We cast INT64_MIN/MAX to double to avoid implicit conversion
316
- // warnings
317
- if (floor(num_val) == num_val &&
318
- num_val >= static_cast<double>(INT64_MIN) &&
319
- num_val <= static_cast<double>(INT64_MAX)) {
320
- sqlite3_result_int64(ctx, static_cast<sqlite3_int64>(num_val));
321
- } else {
322
- sqlite3_result_double(ctx, num_val);
575
+ sqlite3_result_error(ctx, "BigInt value is too large for SQLite", -1);
323
576
  }
324
577
  } else if (value.IsString()) {
325
- std::string str_val = value.As<Napi::String>().Utf8Value();
326
- sqlite3_result_text(ctx, str_val.c_str(), str_val.length(),
578
+ std::string str = value.As<Napi::String>().Utf8Value();
579
+ sqlite3_result_text(ctx, str.c_str(), SafeCastToInt(str.length()),
327
580
  SQLITE_TRANSIENT);
581
+ } else if (value.IsDataView()) {
582
+ // IMPORTANT: Check DataView BEFORE IsBuffer() because N-API's IsBuffer()
583
+ // returns true for ALL ArrayBufferViews (including DataView), but
584
+ // Buffer::As() doesn't work correctly for DataView (returns length=0).
585
+ // See: https://github.com/nodejs/node/pull/56227
586
+ Napi::DataView dataView = value.As<Napi::DataView>();
587
+ Napi::ArrayBuffer arrayBuffer = dataView.ArrayBuffer();
588
+ size_t byteOffset = dataView.ByteOffset();
589
+ size_t byteLength = dataView.ByteLength();
590
+
591
+ if (arrayBuffer.Data() != nullptr && byteLength > 0) {
592
+ const uint8_t *data =
593
+ static_cast<const uint8_t *>(arrayBuffer.Data()) + byteOffset;
594
+ sqlite3_result_blob(ctx, data, SafeCastToInt(byteLength),
595
+ SQLITE_TRANSIENT);
596
+ } else {
597
+ sqlite3_result_zeroblob(ctx, 0);
598
+ }
328
599
  } else if (value.IsBuffer()) {
600
+ // Handles both Node.js Buffer and TypedArrays (Uint8Array, etc.)
329
601
  Napi::Buffer<uint8_t> buffer = value.As<Napi::Buffer<uint8_t>>();
330
- sqlite3_result_blob(ctx, buffer.Data(), buffer.Length(), SQLITE_TRANSIENT);
602
+ sqlite3_result_blob(ctx, buffer.Data(), SafeCastToInt(buffer.Length()),
603
+ SQLITE_TRANSIENT);
331
604
  } else {
332
- // Convert to string as fallback
333
- std::string str_val = value.ToString().Utf8Value();
334
- sqlite3_result_text(ctx, str_val.c_str(), str_val.length(),
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()),
335
608
  SQLITE_TRANSIENT);
336
609
  }
337
610
  }
338
611
 
339
612
  Napi::Value CustomAggregate::GetStartValue() {
340
- // Don't create HandleScope here - let the caller manage it
341
-
342
613
  switch (start_type_) {
343
614
  case PRIMITIVE_NULL:
344
615
  return env_.Null();
@@ -354,64 +625,61 @@ Napi::Value CustomAggregate::GetStartValue() {
354
625
  return Napi::BigInt::New(env_, bigint_value_);
355
626
  case OBJECT:
356
627
  return object_ref_.Value();
628
+ case FUNCTION:
629
+ // This shouldn't be called for FUNCTION type - it's handled separately
630
+ return env_.Undefined();
357
631
  default:
358
- return env_.Null();
632
+ return env_.Undefined();
359
633
  }
360
634
  }
361
635
 
362
- void CustomAggregate::StoreJSValueAsRaw(AggregateData *agg, Napi::Value value) {
363
- // Always clean up previous object reference if it exists
364
- if (agg->value_type == AggregateData::TYPE_OBJECT &&
365
- !agg->object_ref.IsEmpty()) {
366
- agg->object_ref.Reset();
367
- }
368
-
369
- if (value.IsNull()) {
370
- agg->value_type = AggregateData::TYPE_NULL;
371
- } else if (value.IsUndefined()) {
372
- agg->value_type = AggregateData::TYPE_UNDEFINED;
373
- } else if (value.IsNumber()) {
374
- agg->value_type = AggregateData::TYPE_NUMBER;
375
- agg->number_val = value.As<Napi::Number>().DoubleValue();
376
- } else if (value.IsString()) {
377
- agg->value_type = AggregateData::TYPE_STRING;
378
- agg->string_val = value.As<Napi::String>().Utf8Value();
379
- } else if (value.IsBoolean()) {
380
- agg->value_type = AggregateData::TYPE_BOOLEAN;
381
- agg->boolean_val = value.As<Napi::Boolean>().Value();
382
- } else if (value.IsBigInt()) {
383
- agg->value_type = AggregateData::TYPE_BIGINT;
384
- bool lossless;
385
- agg->bigint_val = value.As<Napi::BigInt>().Int64Value(&lossless);
386
- } else {
387
- // Complex object - this still requires Persistent reference
388
- agg->value_type = AggregateData::TYPE_OBJECT;
389
- agg->object_ref = Napi::Reference<Napi::Value>::New(value, 1);
636
+ napi_async_context CustomAggregate::GetAsyncContext() {
637
+ if (async_context_ == nullptr) {
638
+ napi_async_context context;
639
+ napi_status status =
640
+ napi_async_init(env_, env_.Null(),
641
+ Napi::String::New(env_, "sqlite_aggregate"), &context);
642
+ if (status == napi_ok) {
643
+ async_context_ = context;
644
+ }
390
645
  }
646
+ return async_context_;
391
647
  }
392
648
 
393
- Napi::Value CustomAggregate::RawValueToJS(AggregateData *agg) {
394
- // Don't create HandleScope here - it should be managed by the caller
649
+ // Helper method for safe JSON serialization with circular reference handling
650
+ std::string CustomAggregate::SafeJsonStringify(Napi::Env env,
651
+ Napi::Value value) {
652
+ try {
653
+ Napi::Object global = env.Global();
654
+ Napi::Object json = global.Get("JSON").As<Napi::Object>();
655
+ Napi::Function stringify = json.Get("stringify").As<Napi::Function>();
656
+ Napi::Value json_result = stringify.Call({value});
657
+ return json_result.As<Napi::String>().Utf8Value();
658
+ } catch (...) {
659
+ // Handle circular references by creating a simplified object
660
+ // Try to preserve key properties while breaking circularity
661
+ try {
662
+ if (!value.IsObject()) {
663
+ return "{\"_error\":\"non_object\"}";
664
+ }
395
665
 
396
- switch (agg->value_type) {
397
- case AggregateData::TYPE_NULL:
398
- return env_.Null();
399
- case AggregateData::TYPE_UNDEFINED:
400
- return env_.Undefined();
401
- case AggregateData::TYPE_NUMBER:
402
- return Napi::Number::New(env_, agg->number_val);
403
- case AggregateData::TYPE_STRING:
404
- return Napi::String::New(env_, agg->string_val);
405
- case AggregateData::TYPE_BOOLEAN:
406
- return Napi::Boolean::New(env_, agg->boolean_val);
407
- case AggregateData::TYPE_BIGINT:
408
- return Napi::BigInt::New(env_, agg->bigint_val);
409
- case AggregateData::TYPE_OBJECT:
410
- return agg->object_ref.Value();
411
- default:
412
- return env_.Null();
666
+ Napi::Object obj = value.As<Napi::Object>();
667
+ // For objects with circular refs, try to extract simple properties
668
+ if (obj.Has("value")) {
669
+ Napi::Value value_prop = obj.Get("value");
670
+ if (value_prop.IsNumber()) {
671
+ double val = value_prop.As<Napi::Number>().DoubleValue();
672
+ return "{\"value\":" + std::to_string(val) + "}";
673
+ } else {
674
+ return "{\"value\":0}";
675
+ }
676
+ } else {
677
+ return "{\"_error\":\"circular_reference\"}";
678
+ }
679
+ } catch (...) {
680
+ return "{\"_error\":\"circular_reference\"}";
681
+ }
413
682
  }
414
683
  }
415
684
 
416
- } // namespace sqlite
417
- } // namespace photostructure
685
+ } // namespace photostructure::sqlite