@photostructure/sqlite 0.0.1
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 +43 -0
- package/LICENSE +21 -0
- package/README.md +522 -0
- package/SECURITY.md +114 -0
- package/binding.gyp +94 -0
- package/dist/index.cjs +134 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +408 -0
- package/dist/index.d.mts +408 -0
- package/dist/index.d.ts +408 -0
- package/dist/index.mjs +103 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +144 -0
- package/prebuilds/darwin-arm64/@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/win32-x64/@photostructure+sqlite.glibc.node +0 -0
- package/scripts/post-build.mjs +21 -0
- package/scripts/prebuild-linux-glibc.sh +108 -0
- package/src/aggregate_function.cpp +417 -0
- package/src/aggregate_function.h +116 -0
- package/src/binding.cpp +160 -0
- package/src/dirname.ts +13 -0
- package/src/index.ts +465 -0
- package/src/shims/base_object-inl.h +8 -0
- package/src/shims/base_object.h +50 -0
- package/src/shims/debug_utils-inl.h +23 -0
- package/src/shims/env-inl.h +19 -0
- package/src/shims/memory_tracker-inl.h +17 -0
- package/src/shims/napi_extensions.h +73 -0
- package/src/shims/node.h +16 -0
- package/src/shims/node_errors.h +66 -0
- package/src/shims/node_mem-inl.h +8 -0
- package/src/shims/node_mem.h +31 -0
- package/src/shims/node_url.h +23 -0
- package/src/shims/promise_resolver.h +31 -0
- package/src/shims/util-inl.h +18 -0
- package/src/shims/util.h +101 -0
- package/src/sqlite_impl.cpp +2440 -0
- package/src/sqlite_impl.h +314 -0
- package/src/stack_path.ts +64 -0
- package/src/types/node-gyp-build.d.ts +4 -0
- package/src/upstream/node_sqlite.cc +2706 -0
- package/src/upstream/node_sqlite.h +234 -0
- package/src/upstream/sqlite.gyp +38 -0
- package/src/upstream/sqlite.js +19 -0
- package/src/upstream/sqlite3.c +262809 -0
- package/src/upstream/sqlite3.h +13773 -0
- package/src/upstream/sqlite3ext.h +723 -0
- package/src/user_function.cpp +225 -0
- package/src/user_function.h +40 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#include "aggregate_function.h"
|
|
2
|
+
|
|
3
|
+
#include <cmath>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
|
|
6
|
+
#include "shims/node_errors.h"
|
|
7
|
+
#include "sqlite_impl.h"
|
|
8
|
+
|
|
9
|
+
namespace photostructure {
|
|
10
|
+
namespace sqlite {
|
|
11
|
+
|
|
12
|
+
CustomAggregate::CustomAggregate(Napi::Env env, DatabaseSync *db,
|
|
13
|
+
bool use_bigint_args, Napi::Value start,
|
|
14
|
+
Napi::Function step_fn,
|
|
15
|
+
Napi::Function inverse_fn,
|
|
16
|
+
Napi::Function result_fn)
|
|
17
|
+
: env_(env), db_(db), use_bigint_args_(use_bigint_args),
|
|
18
|
+
async_context_(nullptr) {
|
|
19
|
+
// Handle start value based on type
|
|
20
|
+
if (start.IsNull()) {
|
|
21
|
+
start_type_ = PRIMITIVE_NULL;
|
|
22
|
+
} else if (start.IsUndefined()) {
|
|
23
|
+
start_type_ = PRIMITIVE_UNDEFINED;
|
|
24
|
+
} else if (start.IsNumber()) {
|
|
25
|
+
start_type_ = PRIMITIVE_NUMBER;
|
|
26
|
+
number_value_ = start.As<Napi::Number>().DoubleValue();
|
|
27
|
+
} else if (start.IsString()) {
|
|
28
|
+
start_type_ = PRIMITIVE_STRING;
|
|
29
|
+
string_value_ = start.As<Napi::String>().Utf8Value();
|
|
30
|
+
} else if (start.IsBoolean()) {
|
|
31
|
+
start_type_ = PRIMITIVE_BOOLEAN;
|
|
32
|
+
boolean_value_ = start.As<Napi::Boolean>().Value();
|
|
33
|
+
} else if (start.IsBigInt()) {
|
|
34
|
+
start_type_ = PRIMITIVE_BIGINT;
|
|
35
|
+
bool lossless;
|
|
36
|
+
bigint_value_ = start.As<Napi::BigInt>().Int64Value(&lossless);
|
|
37
|
+
} else {
|
|
38
|
+
// Object, Array, or other complex type
|
|
39
|
+
start_type_ = OBJECT;
|
|
40
|
+
object_ref_ = Napi::Reference<Napi::Value>::New(start, 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
step_fn_ = Napi::Reference<Napi::Function>::New(step_fn, 1);
|
|
44
|
+
|
|
45
|
+
if (!inverse_fn.IsEmpty()) {
|
|
46
|
+
inverse_fn_ = Napi::Reference<Napi::Function>::New(inverse_fn, 1);
|
|
47
|
+
}
|
|
48
|
+
if (!result_fn.IsEmpty()) {
|
|
49
|
+
result_fn_ = Napi::Reference<Napi::Function>::New(result_fn, 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
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
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
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();
|
|
71
|
+
|
|
72
|
+
// Cleanup async context
|
|
73
|
+
if (async_context_ != nullptr) {
|
|
74
|
+
napi_async_destroy(env_, async_context_);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
void CustomAggregate::xStep(sqlite3_context *ctx, int argc,
|
|
79
|
+
sqlite3_value **argv) {
|
|
80
|
+
xStepBase(ctx, argc, argv, false);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void CustomAggregate::xInverse(sqlite3_context *ctx, int argc,
|
|
84
|
+
sqlite3_value **argv) {
|
|
85
|
+
xStepBase(ctx, argc, argv, true);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
void CustomAggregate::xFinal(sqlite3_context *ctx) { xValueBase(ctx, true); }
|
|
89
|
+
|
|
90
|
+
void CustomAggregate::xValue(sqlite3_context *ctx) { xValueBase(ctx, false); }
|
|
91
|
+
|
|
92
|
+
void CustomAggregate::xDestroy(void *self) {
|
|
93
|
+
if (self) {
|
|
94
|
+
delete static_cast<CustomAggregate *>(self);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
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);
|
|
107
|
+
if (!self) {
|
|
108
|
+
sqlite3_result_error(ctx, "No user data", -1);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Create HandleScope and CallbackScope for this operation
|
|
113
|
+
Napi::HandleScope scope(self->env_);
|
|
114
|
+
Napi::CallbackScope callback_scope(self->env_, self->async_context_);
|
|
115
|
+
|
|
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
|
+
}
|
|
122
|
+
|
|
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;
|
|
129
|
+
}
|
|
130
|
+
func = self->inverse_fn_.Value();
|
|
131
|
+
} 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();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Prepare arguments for JavaScript function call
|
|
140
|
+
std::vector<Napi::Value> js_argv;
|
|
141
|
+
|
|
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);
|
|
153
|
+
|
|
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
|
+
}
|
|
159
|
+
|
|
160
|
+
// Call the JavaScript function
|
|
161
|
+
Napi::Value result;
|
|
162
|
+
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;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Update the aggregate value
|
|
174
|
+
self->StoreJSValueAsRaw(agg, result);
|
|
175
|
+
|
|
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);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
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);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
CustomAggregate *self = static_cast<CustomAggregate *>(user_data);
|
|
198
|
+
|
|
199
|
+
// Create HandleScope and CallbackScope for this operation
|
|
200
|
+
Napi::HandleScope scope(self->env_);
|
|
201
|
+
Napi::CallbackScope callback_scope(self->env_, self->async_context_);
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
auto agg = self->GetAggregate(ctx);
|
|
205
|
+
if (!agg) {
|
|
206
|
+
sqlite3_result_null(ctx);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
Napi::Value final_value = self->RawValueToJS(agg);
|
|
211
|
+
|
|
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});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Convert to SQLite result
|
|
219
|
+
self->JSValueToSqliteResult(ctx, final_value);
|
|
220
|
+
|
|
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();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
} catch (const Napi::Error &e) {
|
|
229
|
+
sqlite3_result_error(ctx, e.what(), -1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
CustomAggregate::AggregateData *
|
|
234
|
+
CustomAggregate::GetAggregate(sqlite3_context *ctx) {
|
|
235
|
+
AggregateData *agg = static_cast<AggregateData *>(
|
|
236
|
+
sqlite3_aggregate_context(ctx, sizeof(AggregateData)));
|
|
237
|
+
|
|
238
|
+
// sqlite3_aggregate_context only returns NULL if size is 0 or memory
|
|
239
|
+
// allocation fails
|
|
240
|
+
if (!agg) {
|
|
241
|
+
return nullptr;
|
|
242
|
+
}
|
|
243
|
+
|
|
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;
|
|
254
|
+
agg->initialized = true;
|
|
255
|
+
agg->is_window = false;
|
|
256
|
+
agg->first_call = true; // Mark that we need to initialize with start value
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return agg;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
Napi::Value CustomAggregate::SqliteValueToJS(sqlite3_value *value) {
|
|
263
|
+
// Don't create HandleScope here - let the caller manage it
|
|
264
|
+
|
|
265
|
+
int type = sqlite3_value_type(value);
|
|
266
|
+
|
|
267
|
+
switch (type) {
|
|
268
|
+
case SQLITE_INTEGER: {
|
|
269
|
+
sqlite3_int64 int_val = sqlite3_value_int64(value);
|
|
270
|
+
if (use_bigint_args_) {
|
|
271
|
+
return Napi::BigInt::New(env_, static_cast<int64_t>(int_val));
|
|
272
|
+
} else {
|
|
273
|
+
return Napi::Number::New(env_, static_cast<double>(int_val));
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case SQLITE_FLOAT: {
|
|
278
|
+
double float_val = sqlite3_value_double(value);
|
|
279
|
+
return Napi::Number::New(env_, float_val);
|
|
280
|
+
}
|
|
281
|
+
case SQLITE_TEXT: {
|
|
282
|
+
const char *text_val =
|
|
283
|
+
reinterpret_cast<const char *>(sqlite3_value_text(value));
|
|
284
|
+
return Napi::String::New(env_, text_val);
|
|
285
|
+
}
|
|
286
|
+
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);
|
|
291
|
+
}
|
|
292
|
+
case SQLITE_NULL:
|
|
293
|
+
default:
|
|
294
|
+
return env_.Null();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
void CustomAggregate::JSValueToSqliteResult(sqlite3_context *ctx,
|
|
299
|
+
Napi::Value value) {
|
|
300
|
+
if (value.IsNull() || value.IsUndefined()) {
|
|
301
|
+
sqlite3_result_null(ctx);
|
|
302
|
+
} else if (value.IsBoolean()) {
|
|
303
|
+
bool bool_val = value.As<Napi::Boolean>().Value();
|
|
304
|
+
sqlite3_result_int(ctx, bool_val ? 1 : 0);
|
|
305
|
+
} else if (value.IsBigInt()) {
|
|
306
|
+
bool lossless;
|
|
307
|
+
int64_t bigint_val = value.As<Napi::BigInt>().Int64Value(&lossless);
|
|
308
|
+
if (lossless) {
|
|
309
|
+
sqlite3_result_int64(ctx, static_cast<sqlite3_int64>(bigint_val));
|
|
310
|
+
} 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);
|
|
323
|
+
}
|
|
324
|
+
} 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(),
|
|
327
|
+
SQLITE_TRANSIENT);
|
|
328
|
+
} else if (value.IsBuffer()) {
|
|
329
|
+
Napi::Buffer<uint8_t> buffer = value.As<Napi::Buffer<uint8_t>>();
|
|
330
|
+
sqlite3_result_blob(ctx, buffer.Data(), buffer.Length(), SQLITE_TRANSIENT);
|
|
331
|
+
} 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(),
|
|
335
|
+
SQLITE_TRANSIENT);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
Napi::Value CustomAggregate::GetStartValue() {
|
|
340
|
+
// Don't create HandleScope here - let the caller manage it
|
|
341
|
+
|
|
342
|
+
switch (start_type_) {
|
|
343
|
+
case PRIMITIVE_NULL:
|
|
344
|
+
return env_.Null();
|
|
345
|
+
case PRIMITIVE_UNDEFINED:
|
|
346
|
+
return env_.Undefined();
|
|
347
|
+
case PRIMITIVE_NUMBER:
|
|
348
|
+
return Napi::Number::New(env_, number_value_);
|
|
349
|
+
case PRIMITIVE_STRING:
|
|
350
|
+
return Napi::String::New(env_, string_value_);
|
|
351
|
+
case PRIMITIVE_BOOLEAN:
|
|
352
|
+
return Napi::Boolean::New(env_, boolean_value_);
|
|
353
|
+
case PRIMITIVE_BIGINT:
|
|
354
|
+
return Napi::BigInt::New(env_, bigint_value_);
|
|
355
|
+
case OBJECT:
|
|
356
|
+
return object_ref_.Value();
|
|
357
|
+
default:
|
|
358
|
+
return env_.Null();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
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);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
Napi::Value CustomAggregate::RawValueToJS(AggregateData *agg) {
|
|
394
|
+
// Don't create HandleScope here - it should be managed by the caller
|
|
395
|
+
|
|
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();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
} // namespace sqlite
|
|
417
|
+
} // namespace photostructure
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#ifndef SRC_AGGREGATE_FUNCTION_H_
|
|
2
|
+
#define SRC_AGGREGATE_FUNCTION_H_
|
|
3
|
+
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
#include <sqlite3.h>
|
|
6
|
+
|
|
7
|
+
#include <memory>
|
|
8
|
+
#include <string>
|
|
9
|
+
|
|
10
|
+
namespace photostructure {
|
|
11
|
+
namespace sqlite {
|
|
12
|
+
|
|
13
|
+
// Forward declarations
|
|
14
|
+
class DatabaseSync;
|
|
15
|
+
|
|
16
|
+
class CustomAggregate {
|
|
17
|
+
public:
|
|
18
|
+
explicit CustomAggregate(Napi::Env env, DatabaseSync *db,
|
|
19
|
+
bool use_bigint_args, Napi::Value start,
|
|
20
|
+
Napi::Function step_fn, Napi::Function inverse_fn,
|
|
21
|
+
Napi::Function result_fn);
|
|
22
|
+
~CustomAggregate();
|
|
23
|
+
|
|
24
|
+
// SQLite aggregate function callbacks
|
|
25
|
+
static void xStep(sqlite3_context *ctx, int argc, sqlite3_value **argv);
|
|
26
|
+
static void xInverse(sqlite3_context *ctx, int argc, sqlite3_value **argv);
|
|
27
|
+
static void xFinal(sqlite3_context *ctx);
|
|
28
|
+
static void xValue(sqlite3_context *ctx);
|
|
29
|
+
static void xDestroy(void *self);
|
|
30
|
+
|
|
31
|
+
private:
|
|
32
|
+
struct AggregateData {
|
|
33
|
+
// Store value as raw C++ data instead of JavaScript objects
|
|
34
|
+
enum ValueType {
|
|
35
|
+
TYPE_NULL,
|
|
36
|
+
TYPE_UNDEFINED,
|
|
37
|
+
TYPE_NUMBER,
|
|
38
|
+
TYPE_STRING,
|
|
39
|
+
TYPE_BOOLEAN,
|
|
40
|
+
TYPE_BIGINT,
|
|
41
|
+
TYPE_OBJECT // For complex objects, we'll need special handling
|
|
42
|
+
} value_type;
|
|
43
|
+
|
|
44
|
+
union {
|
|
45
|
+
double number_val;
|
|
46
|
+
bool boolean_val;
|
|
47
|
+
int64_t bigint_val;
|
|
48
|
+
};
|
|
49
|
+
std::string string_val; // For strings
|
|
50
|
+
Napi::Reference<Napi::Value> object_ref; // For complex objects (fallback)
|
|
51
|
+
|
|
52
|
+
bool initialized;
|
|
53
|
+
bool is_window;
|
|
54
|
+
bool first_call; // True if this is the first call and we need to
|
|
55
|
+
// initialize with start value
|
|
56
|
+
|
|
57
|
+
// Default constructor
|
|
58
|
+
AggregateData()
|
|
59
|
+
: value_type(TYPE_NULL), number_val(0.0), initialized(false),
|
|
60
|
+
is_window(false), first_call(false) {}
|
|
61
|
+
|
|
62
|
+
// Destructor to properly clean up Napi::Reference
|
|
63
|
+
~AggregateData() {
|
|
64
|
+
if (value_type == TYPE_OBJECT && !object_ref.IsEmpty()) {
|
|
65
|
+
object_ref.Reset();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Helper methods
|
|
71
|
+
static void xStepBase(sqlite3_context *ctx, int argc, sqlite3_value **argv,
|
|
72
|
+
bool use_inverse);
|
|
73
|
+
static void xValueBase(sqlite3_context *ctx, bool finalize);
|
|
74
|
+
|
|
75
|
+
AggregateData *GetAggregate(sqlite3_context *ctx);
|
|
76
|
+
Napi::Value SqliteValueToJS(sqlite3_value *value);
|
|
77
|
+
void JSValueToSqliteResult(sqlite3_context *ctx, Napi::Value value);
|
|
78
|
+
Napi::Value GetStartValue();
|
|
79
|
+
|
|
80
|
+
// New methods for raw C++ value handling
|
|
81
|
+
void StoreJSValueAsRaw(AggregateData *agg, Napi::Value value);
|
|
82
|
+
Napi::Value RawValueToJS(AggregateData *agg);
|
|
83
|
+
|
|
84
|
+
Napi::Env env_;
|
|
85
|
+
DatabaseSync *db_;
|
|
86
|
+
bool use_bigint_args_;
|
|
87
|
+
|
|
88
|
+
// Storage for start value - handle primitives differently
|
|
89
|
+
enum StartValueType {
|
|
90
|
+
PRIMITIVE_NULL,
|
|
91
|
+
PRIMITIVE_UNDEFINED,
|
|
92
|
+
PRIMITIVE_NUMBER,
|
|
93
|
+
PRIMITIVE_STRING,
|
|
94
|
+
PRIMITIVE_BOOLEAN,
|
|
95
|
+
PRIMITIVE_BIGINT,
|
|
96
|
+
OBJECT
|
|
97
|
+
};
|
|
98
|
+
StartValueType start_type_;
|
|
99
|
+
double number_value_;
|
|
100
|
+
std::string string_value_;
|
|
101
|
+
bool boolean_value_;
|
|
102
|
+
int64_t bigint_value_;
|
|
103
|
+
Napi::Reference<Napi::Value> object_ref_;
|
|
104
|
+
|
|
105
|
+
Napi::Reference<Napi::Function> step_fn_;
|
|
106
|
+
Napi::Reference<Napi::Function> inverse_fn_;
|
|
107
|
+
Napi::Reference<Napi::Function> result_fn_;
|
|
108
|
+
|
|
109
|
+
// Async context for callbacks
|
|
110
|
+
napi_async_context async_context_;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
} // namespace sqlite
|
|
114
|
+
} // namespace photostructure
|
|
115
|
+
|
|
116
|
+
#endif // SRC_AGGREGATE_FUNCTION_H_
|
package/src/binding.cpp
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#include <memory>
|
|
2
|
+
#include <mutex>
|
|
3
|
+
#include <napi.h>
|
|
4
|
+
#include <set>
|
|
5
|
+
|
|
6
|
+
#include "sqlite_impl.h"
|
|
7
|
+
|
|
8
|
+
namespace photostructure {
|
|
9
|
+
namespace sqlite {
|
|
10
|
+
|
|
11
|
+
// Cleanup function for worker termination
|
|
12
|
+
void CleanupAddonData(napi_env env, void *finalize_data, void *finalize_hint) {
|
|
13
|
+
AddonData *addon_data = static_cast<AddonData *>(finalize_data);
|
|
14
|
+
|
|
15
|
+
// Clean up any remaining database connections
|
|
16
|
+
{
|
|
17
|
+
std::lock_guard<std::mutex> lock(addon_data->mutex);
|
|
18
|
+
addon_data->databases.clear();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Clean up persistent references to prevent memory leaks
|
|
22
|
+
if (!addon_data->databaseSyncConstructor.IsEmpty()) {
|
|
23
|
+
addon_data->databaseSyncConstructor.Reset();
|
|
24
|
+
}
|
|
25
|
+
if (!addon_data->statementSyncConstructor.IsEmpty()) {
|
|
26
|
+
addon_data->statementSyncConstructor.Reset();
|
|
27
|
+
}
|
|
28
|
+
if (!addon_data->statementSyncIteratorConstructor.IsEmpty()) {
|
|
29
|
+
addon_data->statementSyncIteratorConstructor.Reset();
|
|
30
|
+
}
|
|
31
|
+
if (!addon_data->sessionConstructor.IsEmpty()) {
|
|
32
|
+
addon_data->sessionConstructor.Reset();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
delete addon_data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Helper to get addon data for current environment
|
|
39
|
+
AddonData *GetAddonData(napi_env env) {
|
|
40
|
+
void *data = nullptr;
|
|
41
|
+
napi_status status = napi_get_instance_data(env, &data);
|
|
42
|
+
if (status != napi_ok || data == nullptr) {
|
|
43
|
+
return nullptr;
|
|
44
|
+
}
|
|
45
|
+
return static_cast<AddonData *>(data);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Register a database instance for cleanup tracking
|
|
49
|
+
void RegisterDatabaseInstance(Napi::Env env, DatabaseSync *database) {
|
|
50
|
+
AddonData *addon_data = GetAddonData(env);
|
|
51
|
+
if (addon_data) {
|
|
52
|
+
std::lock_guard<std::mutex> lock(addon_data->mutex);
|
|
53
|
+
addon_data->databases.insert(database);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Unregister a database instance
|
|
58
|
+
void UnregisterDatabaseInstance(Napi::Env env, DatabaseSync *database) {
|
|
59
|
+
AddonData *addon_data = GetAddonData(env);
|
|
60
|
+
if (addon_data) {
|
|
61
|
+
std::lock_guard<std::mutex> lock(addon_data->mutex);
|
|
62
|
+
addon_data->databases.erase(database);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialize the SQLite module
|
|
67
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
68
|
+
// Set up per-worker instance data
|
|
69
|
+
AddonData *addon_data = new AddonData();
|
|
70
|
+
napi_status status =
|
|
71
|
+
napi_set_instance_data(env, addon_data, CleanupAddonData, nullptr);
|
|
72
|
+
if (status != napi_ok) {
|
|
73
|
+
delete addon_data;
|
|
74
|
+
Napi::Error::New(env, "Failed to set instance data")
|
|
75
|
+
.ThrowAsJavaScriptException();
|
|
76
|
+
return exports;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
DatabaseSync::Init(env, exports);
|
|
80
|
+
StatementSync::Init(env, exports);
|
|
81
|
+
StatementSyncIterator::Init(env, exports);
|
|
82
|
+
Session::Init(env, exports);
|
|
83
|
+
|
|
84
|
+
// Add SQLite constants
|
|
85
|
+
Napi::Object constants = Napi::Object::New(env);
|
|
86
|
+
constants.Set("SQLITE_OPEN_READONLY",
|
|
87
|
+
Napi::Number::New(env, SQLITE_OPEN_READONLY));
|
|
88
|
+
constants.Set("SQLITE_OPEN_READWRITE",
|
|
89
|
+
Napi::Number::New(env, SQLITE_OPEN_READWRITE));
|
|
90
|
+
constants.Set("SQLITE_OPEN_CREATE",
|
|
91
|
+
Napi::Number::New(env, SQLITE_OPEN_CREATE));
|
|
92
|
+
constants.Set("SQLITE_OPEN_DELETEONCLOSE",
|
|
93
|
+
Napi::Number::New(env, SQLITE_OPEN_DELETEONCLOSE));
|
|
94
|
+
constants.Set("SQLITE_OPEN_EXCLUSIVE",
|
|
95
|
+
Napi::Number::New(env, SQLITE_OPEN_EXCLUSIVE));
|
|
96
|
+
constants.Set("SQLITE_OPEN_AUTOPROXY",
|
|
97
|
+
Napi::Number::New(env, SQLITE_OPEN_AUTOPROXY));
|
|
98
|
+
constants.Set("SQLITE_OPEN_URI", Napi::Number::New(env, SQLITE_OPEN_URI));
|
|
99
|
+
constants.Set("SQLITE_OPEN_MEMORY",
|
|
100
|
+
Napi::Number::New(env, SQLITE_OPEN_MEMORY));
|
|
101
|
+
constants.Set("SQLITE_OPEN_MAIN_DB",
|
|
102
|
+
Napi::Number::New(env, SQLITE_OPEN_MAIN_DB));
|
|
103
|
+
constants.Set("SQLITE_OPEN_TEMP_DB",
|
|
104
|
+
Napi::Number::New(env, SQLITE_OPEN_TEMP_DB));
|
|
105
|
+
constants.Set("SQLITE_OPEN_TRANSIENT_DB",
|
|
106
|
+
Napi::Number::New(env, SQLITE_OPEN_TRANSIENT_DB));
|
|
107
|
+
constants.Set("SQLITE_OPEN_MAIN_JOURNAL",
|
|
108
|
+
Napi::Number::New(env, SQLITE_OPEN_MAIN_JOURNAL));
|
|
109
|
+
constants.Set("SQLITE_OPEN_TEMP_JOURNAL",
|
|
110
|
+
Napi::Number::New(env, SQLITE_OPEN_TEMP_JOURNAL));
|
|
111
|
+
constants.Set("SQLITE_OPEN_SUBJOURNAL",
|
|
112
|
+
Napi::Number::New(env, SQLITE_OPEN_SUBJOURNAL));
|
|
113
|
+
constants.Set("SQLITE_OPEN_SUPER_JOURNAL",
|
|
114
|
+
Napi::Number::New(env, SQLITE_OPEN_SUPER_JOURNAL));
|
|
115
|
+
constants.Set("SQLITE_OPEN_NOMUTEX",
|
|
116
|
+
Napi::Number::New(env, SQLITE_OPEN_NOMUTEX));
|
|
117
|
+
constants.Set("SQLITE_OPEN_FULLMUTEX",
|
|
118
|
+
Napi::Number::New(env, SQLITE_OPEN_FULLMUTEX));
|
|
119
|
+
constants.Set("SQLITE_OPEN_SHAREDCACHE",
|
|
120
|
+
Napi::Number::New(env, SQLITE_OPEN_SHAREDCACHE));
|
|
121
|
+
constants.Set("SQLITE_OPEN_PRIVATECACHE",
|
|
122
|
+
Napi::Number::New(env, SQLITE_OPEN_PRIVATECACHE));
|
|
123
|
+
constants.Set("SQLITE_OPEN_WAL", Napi::Number::New(env, SQLITE_OPEN_WAL));
|
|
124
|
+
|
|
125
|
+
// Changeset/session constants
|
|
126
|
+
constants.Set("SQLITE_CHANGESET_OMIT",
|
|
127
|
+
Napi::Number::New(env, SQLITE_CHANGESET_OMIT));
|
|
128
|
+
constants.Set("SQLITE_CHANGESET_REPLACE",
|
|
129
|
+
Napi::Number::New(env, SQLITE_CHANGESET_REPLACE));
|
|
130
|
+
constants.Set("SQLITE_CHANGESET_ABORT",
|
|
131
|
+
Napi::Number::New(env, SQLITE_CHANGESET_ABORT));
|
|
132
|
+
|
|
133
|
+
constants.Set("SQLITE_CHANGESET_DATA",
|
|
134
|
+
Napi::Number::New(env, SQLITE_CHANGESET_DATA));
|
|
135
|
+
constants.Set("SQLITE_CHANGESET_NOTFOUND",
|
|
136
|
+
Napi::Number::New(env, SQLITE_CHANGESET_NOTFOUND));
|
|
137
|
+
constants.Set("SQLITE_CHANGESET_CONFLICT",
|
|
138
|
+
Napi::Number::New(env, SQLITE_CHANGESET_CONFLICT));
|
|
139
|
+
constants.Set("SQLITE_CHANGESET_CONSTRAINT",
|
|
140
|
+
Napi::Number::New(env, SQLITE_CHANGESET_CONSTRAINT));
|
|
141
|
+
constants.Set("SQLITE_CHANGESET_FOREIGN_KEY",
|
|
142
|
+
Napi::Number::New(env, SQLITE_CHANGESET_FOREIGN_KEY));
|
|
143
|
+
|
|
144
|
+
exports.Set("constants", constants);
|
|
145
|
+
|
|
146
|
+
// TODO: Add backup function
|
|
147
|
+
|
|
148
|
+
return exports;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
} // namespace sqlite
|
|
152
|
+
} // namespace photostructure
|
|
153
|
+
|
|
154
|
+
// Module initialization function
|
|
155
|
+
Napi::Object InitSqlite(Napi::Env env, Napi::Object exports) {
|
|
156
|
+
return photostructure::sqlite::Init(env, exports);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Register the module
|
|
160
|
+
NODE_API_MODULE(phstr_sqlite, InitSqlite)
|
package/src/dirname.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getCallerDirname } from "./stack_path";
|
|
2
|
+
|
|
3
|
+
// Thanks to tsup shims, __dirname should always be defined except when run by
|
|
4
|
+
// jest (which will use the stack_path shim)
|
|
5
|
+
export function _dirname() {
|
|
6
|
+
try {
|
|
7
|
+
if (typeof __dirname !== "undefined") return __dirname;
|
|
8
|
+
} catch {
|
|
9
|
+
// ignore
|
|
10
|
+
}
|
|
11
|
+
// we must be in jest. Use the stack_path ~~hack~~ shim:
|
|
12
|
+
return getCallerDirname();
|
|
13
|
+
}
|