@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.
- package/CHANGELOG.md +52 -16
- package/README.md +5 -4
- package/binding.gyp +2 -2
- package/dist/index.cjs +158 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +284 -89
- package/dist/index.d.mts +284 -89
- package/dist/index.d.ts +284 -89
- package/dist/index.mjs +155 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +71 -62
- package/prebuilds/darwin-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/darwin-x64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+sqlite.musl.node +0 -0
- package/prebuilds/linux-x64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-x64/@photostructure+sqlite.musl.node +0 -0
- package/prebuilds/test_extension.so +0 -0
- package/prebuilds/win32-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
- package/src/aggregate_function.cpp +222 -114
- package/src/aggregate_function.h +5 -6
- package/src/binding.cpp +30 -21
- package/src/enhance.ts +228 -0
- package/src/index.ts +83 -9
- package/src/shims/node_errors.h +34 -15
- package/src/shims/sqlite_errors.h +34 -8
- package/src/sql-tag-store.ts +6 -9
- package/src/sqlite_impl.cpp +1044 -394
- package/src/sqlite_impl.h +46 -7
- package/src/transaction.ts +178 -0
- package/src/types/database-sync-instance.ts +6 -40
- package/src/types/pragma-options.ts +23 -0
- package/src/types/statement-sync-instance.ts +38 -12
- package/src/types/transaction.ts +72 -0
- package/src/upstream/node_sqlite.cc +143 -43
- package/src/upstream/node_sqlite.h +15 -11
- package/src/upstream/sqlite3.c +102 -58
- package/src/upstream/sqlite3.h +5 -5
- package/src/user_function.cpp +138 -141
- 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
|
-
|
|
13
|
+
// Maximum safe integer for JavaScript numbers (2^53 - 1)
|
|
14
|
+
static constexpr int64_t kMaxSafeJsInteger = 9007199254740991LL;
|
|
12
15
|
|
|
13
|
-
|
|
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(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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::
|
|
51
|
-
|
|
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),
|
|
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) {
|
|
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
|
|
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()
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
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
|
-
|
|
413
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
539
|
-
|
|
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
|
-
|
|
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.
|
|
570
|
-
|
|
571
|
-
|
|
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(),
|
|
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,
|
|
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.
|
|
600
|
-
// Handles
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
|
package/src/aggregate_function.h
CHANGED
|
@@ -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
|
|
21
|
-
|
|
22
|
-
addon_data->
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
addon_data->
|
|
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
|
|
222
|
-
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
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;
|