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