@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.
- package/CHANGELOG.md +36 -2
- package/README.md +45 -484
- package/SECURITY.md +27 -84
- package/binding.gyp +69 -22
- package/dist/index.cjs +185 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +552 -100
- package/dist/index.d.mts +552 -100
- package/dist/index.d.ts +552 -100
- package/dist/index.mjs +183 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +51 -41
- 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-x64/@photostructure+sqlite.glibc.node +0 -0
- package/src/aggregate_function.cpp +503 -235
- package/src/aggregate_function.h +57 -42
- package/src/binding.cpp +117 -14
- package/src/dirname.ts +1 -1
- package/src/index.ts +122 -332
- package/src/lru-cache.ts +84 -0
- package/src/shims/env-inl.h +6 -15
- package/src/shims/node_errors.h +4 -0
- package/src/shims/sqlite_errors.h +162 -0
- package/src/shims/util.h +29 -4
- package/src/sql-tag-store.ts +140 -0
- package/src/sqlite_exception.h +49 -0
- package/src/sqlite_impl.cpp +711 -127
- package/src/sqlite_impl.h +84 -6
- package/src/{stack_path.ts → stack-path.ts} +7 -1
- package/src/types/aggregate-options.ts +22 -0
- package/src/types/changeset-apply-options.ts +18 -0
- package/src/types/database-sync-instance.ts +203 -0
- package/src/types/database-sync-options.ts +69 -0
- package/src/types/session-options.ts +10 -0
- package/src/types/sql-tag-store-instance.ts +51 -0
- package/src/types/sqlite-authorization-actions.ts +77 -0
- package/src/types/sqlite-authorization-results.ts +15 -0
- package/src/types/sqlite-changeset-conflict-types.ts +19 -0
- package/src/types/sqlite-changeset-resolution.ts +15 -0
- package/src/types/sqlite-open-flags.ts +50 -0
- package/src/types/statement-sync-instance.ts +73 -0
- package/src/types/user-functions-options.ts +14 -0
- package/src/upstream/node_sqlite.cc +960 -259
- package/src/upstream/node_sqlite.h +127 -2
- package/src/upstream/sqlite.js +1 -14
- package/src/upstream/sqlite3.c +4510 -1411
- package/src/upstream/sqlite3.h +390 -195
- package/src/upstream/sqlite3ext.h +7 -0
- package/src/user_function.cpp +88 -36
- 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
|
-
|
|
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),
|
|
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
|
-
//
|
|
53
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
231
|
+
state->is_initialized = true;
|
|
232
|
+
}
|
|
141
233
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
} catch (
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
389
|
+
if (!state || !state->is_initialized) {
|
|
390
|
+
// No rows processed, return null
|
|
391
|
+
sqlite3_result_null(ctx);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
211
394
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
443
|
+
std::vector<napi_value> args = {current_value};
|
|
444
|
+
napi_value result;
|
|
220
445
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
519
|
+
return Napi::BigInt::New(
|
|
520
|
+
env_, static_cast<int64_t>(sqlite3_value_int64(value)));
|
|
272
521
|
} else {
|
|
273
|
-
return Napi::Number::New(env_,
|
|
522
|
+
return Napi::Number::New(env_, sqlite3_value_int64(value));
|
|
274
523
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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 *
|
|
529
|
+
const char *text =
|
|
283
530
|
reinterpret_cast<const char *>(sqlite3_value_text(value));
|
|
284
|
-
return Napi::String::New(env_,
|
|
531
|
+
return Napi::String::New(env_, text ? text : "");
|
|
285
532
|
}
|
|
533
|
+
|
|
286
534
|
case SQLITE_BLOB: {
|
|
287
|
-
const void *
|
|
288
|
-
int
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
544
|
+
|
|
293
545
|
default:
|
|
294
|
-
return env_.
|
|
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()
|
|
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
|
-
|
|
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
|
|
571
|
+
int64_t val = value.As<Napi::BigInt>().Int64Value(&lossless);
|
|
308
572
|
if (lossless) {
|
|
309
|
-
sqlite3_result_int64(ctx,
|
|
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
|
|
326
|
-
sqlite3_result_text(ctx,
|
|
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(),
|
|
602
|
+
sqlite3_result_blob(ctx, buffer.Data(), SafeCastToInt(buffer.Length()),
|
|
603
|
+
SQLITE_TRANSIENT);
|
|
331
604
|
} else {
|
|
332
|
-
//
|
|
333
|
-
std::string
|
|
334
|
-
sqlite3_result_text(ctx,
|
|
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_.
|
|
632
|
+
return env_.Undefined();
|
|
359
633
|
}
|
|
360
634
|
}
|
|
361
635
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|