-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuser_function.cpp
More file actions
274 lines (236 loc) · 9.47 KB
/
user_function.cpp
File metadata and controls
274 lines (236 loc) · 9.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#include "user_function.h"
#include <cinttypes>
#include <climits>
#include <cmath>
#include <limits>
#include <stdexcept>
#include "shims/node_errors.h"
#include "sqlite_impl.h"
// Maximum safe integer for JavaScript numbers (2^53 - 1)
static constexpr int64_t kMaxSafeJsInteger = 9007199254740991LL;
namespace photostructure::sqlite {
UserDefinedFunction::UserDefinedFunction(Napi::Env env, Napi::Function fn,
DatabaseSync *db, bool use_bigint_args)
: env_(env), fn_(Napi::Reference<Napi::Function>::New(fn, 1)), db_(db),
use_bigint_args_(use_bigint_args), async_context_(nullptr) {
// Register cleanup hook to Reset() reference before environment teardown.
// This is required for worker thread support per Node-API best practices.
// See:
// https://nodejs.github.io/node-addon-examples/special-topics/context-awareness/
napi_add_env_cleanup_hook(env_, CleanupHook, this);
// Create async context for callbacks
const napi_status status = napi_async_init(
env, nullptr, Napi::String::New(env, "SQLiteUserFunction"),
&async_context_);
if (status != napi_ok) {
Napi::Error::New(env, "Failed to create async context")
.ThrowAsJavaScriptException();
}
}
UserDefinedFunction::~UserDefinedFunction() noexcept {
// Remove cleanup hook if still registered
napi_remove_env_cleanup_hook(env_, CleanupHook, this);
// Don't call fn_.Reset() here - CleanupHook already handled it,
// or the environment is being torn down and Reset() would be unsafe.
// Clean up async context if environment is still valid
napi_handle_scope scope;
napi_status status = napi_open_handle_scope(env_, &scope);
if (status == napi_ok) {
if (async_context_ != nullptr) {
napi_async_destroy(env_, async_context_);
async_context_ = nullptr;
}
napi_close_handle_scope(env_, scope);
}
}
void UserDefinedFunction::CleanupHook(void *arg) {
// Called before environment teardown - safe to Reset() here
auto *self = static_cast<UserDefinedFunction *>(arg);
if (!self->fn_.IsEmpty()) {
self->fn_.Reset();
}
}
void UserDefinedFunction::xFunc(sqlite3_context *ctx, int argc,
sqlite3_value **argv) {
void *user_data = sqlite3_user_data(ctx);
if (!user_data) {
sqlite3_result_error(ctx, "Invalid user data in function callback", -1);
return;
}
UserDefinedFunction *self = static_cast<UserDefinedFunction *>(user_data);
Napi::HandleScope scope(self->env_);
Napi::CallbackScope callback_scope(self->env_, self->async_context_);
// Check if function reference is still valid
if (self->fn_.IsEmpty()) {
sqlite3_result_error(ctx, "Function reference is no longer valid", -1);
return;
}
Napi::Value fn_value;
try {
fn_value = self->fn_.Value();
} catch (const Napi::Error &e) {
sqlite3_result_error(ctx, "Failed to retrieve function reference", -1);
return;
}
// Additional check for function validity
if (!fn_value.IsFunction()) {
sqlite3_result_error(ctx, "Invalid function reference - not a function",
-1);
return;
}
Napi::Function fn = fn_value.As<Napi::Function>();
// Convert SQLite arguments to JavaScript values
std::vector<napi_value> js_args;
js_args.reserve(argc);
for (int i = 0; i < argc; i++) {
Napi::Value js_val = self->SqliteValueToJS(argv[i]);
// Check if SqliteValueToJS threw an exception (e.g., ERR_OUT_OF_RANGE)
if (self->env_.IsExceptionPending()) {
// Ignore the SQLite error because a JavaScript exception is pending
self->db_->SetIgnoreNextSQLiteError(true);
sqlite3_result_error(ctx, "", 0);
return;
}
js_args.push_back(js_val);
}
// Call the JavaScript function
napi_value js_result;
napi_value js_func = fn;
napi_value this_arg = self->env_.Undefined();
napi_status status =
napi_call_function(self->env_, this_arg, js_func, js_args.size(),
js_args.data(), &js_result);
if (status != napi_ok || self->env_.IsExceptionPending()) {
// JavaScript exception is pending - let it propagate
// Ignore the SQLite error because the JavaScript exception takes precedence
self->db_->SetIgnoreNextSQLiteError(true);
sqlite3_result_error(ctx, "", 0);
return;
}
Napi::Value result(self->env_, js_result);
// Convert result back to SQLite
self->JSValueToSqliteResult(ctx, result);
// Check if JSValueToSqliteResult threw an exception (e.g., ERR_OUT_OF_RANGE)
if (self->env_.IsExceptionPending()) {
// Ignore the SQLite error because a JavaScript exception is pending
self->db_->SetIgnoreNextSQLiteError(true);
sqlite3_result_error(ctx, "", 0);
return;
}
}
void UserDefinedFunction::xDestroy(void *self) {
if (self) {
delete static_cast<UserDefinedFunction *>(self);
}
}
Napi::Value UserDefinedFunction::SqliteValueToJS(sqlite3_value *value) {
switch (sqlite3_value_type(value)) {
case SQLITE_INTEGER: {
sqlite3_int64 int_val = sqlite3_value_int64(value);
if (use_bigint_args_) {
return Napi::BigInt::New(env_, static_cast<int64_t>(int_val));
} else if (std::abs(int_val) <= kMaxSafeJsInteger) {
return Napi::Number::New(env_, static_cast<double>(int_val));
} else {
// Value is outside safe integer range for JavaScript numbers
// Throw ERR_OUT_OF_RANGE directly - we're in a valid N-API context
char error_msg[128];
snprintf(error_msg, sizeof(error_msg),
"Value is too large to be represented as a JavaScript number: "
"%" PRId64,
static_cast<int64_t>(int_val));
node::THROW_ERR_OUT_OF_RANGE(env_, error_msg);
return env_.Undefined(); // Return undefined, exception is pending
}
}
case SQLITE_FLOAT: {
double double_val = sqlite3_value_double(value);
return Napi::Number::New(env_, double_val);
}
case SQLITE_TEXT: {
const char *text =
reinterpret_cast<const char *>(sqlite3_value_text(value));
return Napi::String::New(env_, text ? text : "");
}
case SQLITE_BLOB: {
const void *blob = sqlite3_value_blob(value);
int bytes = sqlite3_value_bytes(value);
// Return Uint8Array to match Node.js node:sqlite behavior
if (blob && bytes > 0) {
auto array_buffer = Napi::ArrayBuffer::New(env_, bytes);
memcpy(array_buffer.Data(), blob, bytes);
return Napi::Uint8Array::New(env_, bytes, array_buffer, 0);
} else {
auto array_buffer = Napi::ArrayBuffer::New(env_, 0);
return Napi::Uint8Array::New(env_, 0, array_buffer, 0);
}
}
case SQLITE_NULL:
default:
return env_.Null();
}
}
void UserDefinedFunction::JSValueToSqliteResult(sqlite3_context *ctx,
Napi::Value value) {
if (value.IsNull() || value.IsUndefined()) {
sqlite3_result_null(ctx);
} else if (value.IsBoolean()) {
// Extension over Node.js: Convert booleans to 0/1
sqlite3_result_int(ctx, value.As<Napi::Boolean>().Value() ? 1 : 0);
} else if (value.IsNumber()) {
// Match Node.js: numbers are stored as doubles
sqlite3_result_double(ctx, value.As<Napi::Number>().DoubleValue());
} else if (value.IsString()) {
std::string str_val = value.As<Napi::String>().Utf8Value();
sqlite3_result_text(ctx, str_val.c_str(),
static_cast<int>(str_val.length()), SQLITE_TRANSIENT);
} else if (value.IsDataView()) {
// IMPORTANT: Check DataView BEFORE IsBuffer() because N-API's IsBuffer()
// returns true for ALL ArrayBufferViews (including DataView), but
// Buffer::As() doesn't work correctly for DataView (returns length=0).
// See: https://github.com/nodejs/node/pull/56227
Napi::DataView dataView = value.As<Napi::DataView>();
Napi::ArrayBuffer arrayBuffer = dataView.ArrayBuffer();
size_t byteOffset = dataView.ByteOffset();
size_t byteLength = dataView.ByteLength();
if (arrayBuffer.Data() != nullptr && byteLength > 0) {
const uint8_t *data =
static_cast<const uint8_t *>(arrayBuffer.Data()) + byteOffset;
sqlite3_result_blob(ctx, data, static_cast<int>(byteLength),
SQLITE_TRANSIENT);
} else {
sqlite3_result_zeroblob(ctx, 0);
}
} else if (value.IsTypedArray()) {
// Handles Uint8Array and other TypedArrays (but not DataView, handled
// above)
Napi::TypedArray arr = value.As<Napi::TypedArray>();
Napi::ArrayBuffer buf = arr.ArrayBuffer();
sqlite3_result_blob(
ctx, static_cast<const uint8_t *>(buf.Data()) + arr.ByteOffset(),
static_cast<int>(arr.ByteLength()), SQLITE_TRANSIENT);
} else if (value.IsBigInt()) {
// Check BigInt - must fit in int64
bool lossless;
int64_t bigint_val = value.As<Napi::BigInt>().Int64Value(&lossless);
if (!lossless) {
// BigInt too large for SQLite - throw ERR_OUT_OF_RANGE
node::THROW_ERR_OUT_OF_RANGE(
env_,
"BigInt value is too large to be represented as a SQLite integer");
return;
}
sqlite3_result_int64(ctx, static_cast<sqlite3_int64>(bigint_val));
} else if (value.IsPromise()) {
// Promises are not supported - must use sqlite3_result_error for this one
// because it's an ERR_SQLITE_ERROR per the test expectations
sqlite3_result_error(
ctx, "Asynchronous user-defined functions are not supported", -1);
} else {
// Unsupported type - must use sqlite3_result_error
sqlite3_result_error(
ctx, "Returned JavaScript value cannot be converted to a SQLite value",
-1);
}
}
} // namespace photostructure::sqlite