@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,2440 @@
|
|
|
1
|
+
#include "sqlite_impl.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cctype>
|
|
5
|
+
#include <climits>
|
|
6
|
+
#include <cmath>
|
|
7
|
+
#include <iostream>
|
|
8
|
+
|
|
9
|
+
#include "aggregate_function.h"
|
|
10
|
+
#include "user_function.h"
|
|
11
|
+
|
|
12
|
+
namespace photostructure {
|
|
13
|
+
namespace sqlite {
|
|
14
|
+
|
|
15
|
+
// JavaScript safe integer limits (2^53 - 1)
|
|
16
|
+
constexpr int64_t JS_MAX_SAFE_INTEGER = 9007199254740991LL;
|
|
17
|
+
constexpr int64_t JS_MIN_SAFE_INTEGER = -9007199254740991LL;
|
|
18
|
+
|
|
19
|
+
// Path validation function implementation
|
|
20
|
+
std::optional<std::string> ValidateDatabasePath(Napi::Env env, Napi::Value path,
|
|
21
|
+
const std::string &field_name) {
|
|
22
|
+
auto has_null_bytes = [](const std::string &str) {
|
|
23
|
+
return str.find('\0') != std::string::npos;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (path.IsString()) {
|
|
27
|
+
std::string location = path.As<Napi::String>().Utf8Value();
|
|
28
|
+
if (!has_null_bytes(location)) {
|
|
29
|
+
return location;
|
|
30
|
+
}
|
|
31
|
+
} else if (path.IsBuffer()) {
|
|
32
|
+
Napi::Buffer<uint8_t> buffer = path.As<Napi::Buffer<uint8_t>>();
|
|
33
|
+
size_t length = buffer.Length();
|
|
34
|
+
const uint8_t *data = buffer.Data();
|
|
35
|
+
|
|
36
|
+
// Check for null bytes in buffer
|
|
37
|
+
if (std::find(data, data + length, 0) == data + length) {
|
|
38
|
+
return std::string(reinterpret_cast<const char *>(data), length);
|
|
39
|
+
}
|
|
40
|
+
} else if (path.IsObject()) {
|
|
41
|
+
Napi::Object url = path.As<Napi::Object>();
|
|
42
|
+
|
|
43
|
+
// Check if it's a URL object with href property
|
|
44
|
+
if (url.Has("href")) {
|
|
45
|
+
Napi::Value href = url.Get("href");
|
|
46
|
+
if (href.IsString()) {
|
|
47
|
+
std::string location = href.As<Napi::String>().Utf8Value();
|
|
48
|
+
if (!has_null_bytes(location)) {
|
|
49
|
+
// Check if it's a file:// URL
|
|
50
|
+
if (location.compare(0, 7, "file://") == 0) {
|
|
51
|
+
// Convert file:// URL to file path with proper validation
|
|
52
|
+
std::string file_path = location.substr(7);
|
|
53
|
+
|
|
54
|
+
// Enhanced URL decoding with security checks
|
|
55
|
+
std::string decoded_path;
|
|
56
|
+
decoded_path.reserve(file_path.length());
|
|
57
|
+
|
|
58
|
+
// Maximum path length check (platform-specific, but 4096 is
|
|
59
|
+
// reasonable)
|
|
60
|
+
const size_t MAX_PATH_LENGTH = 4096;
|
|
61
|
+
if (file_path.length() > MAX_PATH_LENGTH) {
|
|
62
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
63
|
+
env,
|
|
64
|
+
("The \"" + field_name + "\" path is too long.").c_str());
|
|
65
|
+
return std::nullopt;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// URL decode with multiple passes to prevent double-encoding bypass
|
|
69
|
+
int decode_passes = 0;
|
|
70
|
+
const int MAX_DECODE_PASSES = 5; // Prevent infinite decoding loops
|
|
71
|
+
std::string current_path = file_path;
|
|
72
|
+
std::string next_path;
|
|
73
|
+
|
|
74
|
+
while (decode_passes < MAX_DECODE_PASSES) {
|
|
75
|
+
bool found_encoding = false;
|
|
76
|
+
next_path.clear();
|
|
77
|
+
next_path.reserve(current_path.length());
|
|
78
|
+
|
|
79
|
+
for (size_t i = 0; i < current_path.length(); ++i) {
|
|
80
|
+
if (current_path[i] == '%' && i + 2 < current_path.length()) {
|
|
81
|
+
// Validate hex characters
|
|
82
|
+
if (std::isxdigit(current_path[i + 1]) &&
|
|
83
|
+
std::isxdigit(current_path[i + 2])) {
|
|
84
|
+
char hex_str[3] = {current_path[i + 1], current_path[i + 2],
|
|
85
|
+
'\0'};
|
|
86
|
+
long val = std::strtol(hex_str, nullptr, 16);
|
|
87
|
+
|
|
88
|
+
// Special handling for control characters and dangerous
|
|
89
|
+
// sequences
|
|
90
|
+
if (val == 0) {
|
|
91
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
92
|
+
env, ("The \"" + field_name +
|
|
93
|
+
"\" contains encoded null bytes.")
|
|
94
|
+
.c_str());
|
|
95
|
+
return std::nullopt;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
next_path += static_cast<char>(val);
|
|
99
|
+
i += 2;
|
|
100
|
+
found_encoding = true;
|
|
101
|
+
} else {
|
|
102
|
+
// Invalid hex sequence, reject
|
|
103
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
104
|
+
env, ("The \"" + field_name +
|
|
105
|
+
"\" contains invalid percent encoding.")
|
|
106
|
+
.c_str());
|
|
107
|
+
return std::nullopt;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
next_path += current_path[i];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!found_encoding) {
|
|
115
|
+
decoded_path = current_path;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
current_path = next_path;
|
|
120
|
+
decode_passes++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (decode_passes >= MAX_DECODE_PASSES) {
|
|
124
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
125
|
+
env, ("The \"" + field_name +
|
|
126
|
+
"\" contains too many levels of percent encoding.")
|
|
127
|
+
.c_str());
|
|
128
|
+
return std::nullopt;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Security: Check for null bytes after all decoding
|
|
132
|
+
if (has_null_bytes(decoded_path)) {
|
|
133
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
134
|
+
env, ("The \"" + field_name +
|
|
135
|
+
"\" argument contains null bytes after URL decoding.")
|
|
136
|
+
.c_str());
|
|
137
|
+
return std::nullopt;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Security: Normalize path components to detect traversal attempts
|
|
141
|
+
// This includes various representations of ".."
|
|
142
|
+
std::vector<std::string> dangerous_patterns = {
|
|
143
|
+
"..", "/..", "../", "\\..", "..\\",
|
|
144
|
+
// Windows alternate stream syntax (but allow single colon for
|
|
145
|
+
// drive letters)
|
|
146
|
+
"::", "::$",
|
|
147
|
+
// Zero-width characters that might hide dangerous sequences
|
|
148
|
+
"\u200B", "\u200C", "\u200D", "\uFEFF"};
|
|
149
|
+
|
|
150
|
+
// Check each component after splitting by directory separators
|
|
151
|
+
std::string normalized_path = decoded_path;
|
|
152
|
+
std::replace(normalized_path.begin(), normalized_path.end(), '\\',
|
|
153
|
+
'/');
|
|
154
|
+
|
|
155
|
+
// Split path and check each component
|
|
156
|
+
size_t start = 0;
|
|
157
|
+
size_t end = normalized_path.find('/');
|
|
158
|
+
|
|
159
|
+
while (end != std::string::npos) {
|
|
160
|
+
std::string component =
|
|
161
|
+
normalized_path.substr(start, end - start);
|
|
162
|
+
|
|
163
|
+
// Check for dangerous patterns in component
|
|
164
|
+
if (component == "..") {
|
|
165
|
+
// Always reject ..
|
|
166
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
167
|
+
env, ("The \"" + field_name +
|
|
168
|
+
"\" argument contains path traversal sequences.")
|
|
169
|
+
.c_str());
|
|
170
|
+
return std::nullopt;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check for other dangerous patterns
|
|
174
|
+
for (const auto &pattern : dangerous_patterns) {
|
|
175
|
+
if (component.find(pattern) != std::string::npos) {
|
|
176
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
177
|
+
env, ("The \"" + field_name +
|
|
178
|
+
"\" argument contains dangerous sequences.")
|
|
179
|
+
.c_str());
|
|
180
|
+
return std::nullopt;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
start = end + 1;
|
|
185
|
+
end = normalized_path.find('/', start);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check last component
|
|
189
|
+
if (start < normalized_path.length()) {
|
|
190
|
+
std::string component = normalized_path.substr(start);
|
|
191
|
+
if (component == "..") {
|
|
192
|
+
// Always reject ..
|
|
193
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
194
|
+
env, ("The \"" + field_name +
|
|
195
|
+
"\" argument contains path traversal sequences.")
|
|
196
|
+
.c_str());
|
|
197
|
+
return std::nullopt;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check for other dangerous patterns
|
|
201
|
+
for (const auto &pattern : dangerous_patterns) {
|
|
202
|
+
if (component.find(pattern) != std::string::npos) {
|
|
203
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
204
|
+
env, ("The \"" + field_name +
|
|
205
|
+
"\" argument contains dangerous sequences.")
|
|
206
|
+
.c_str());
|
|
207
|
+
return std::nullopt;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return decoded_path;
|
|
213
|
+
} else {
|
|
214
|
+
node::THROW_ERR_INVALID_URL_SCHEME(env);
|
|
215
|
+
return std::nullopt;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env, ("The \"" + field_name +
|
|
223
|
+
"\" argument must be a string, "
|
|
224
|
+
"Buffer, or URL without null bytes.")
|
|
225
|
+
.c_str());
|
|
226
|
+
return std::nullopt;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Note: Static constructors removed to fix worker thread issues
|
|
230
|
+
// Constructors are now stored in per-instance AddonData
|
|
231
|
+
|
|
232
|
+
// Forward declarations for addon data access
|
|
233
|
+
extern AddonData *GetAddonData(napi_env env);
|
|
234
|
+
|
|
235
|
+
// DatabaseSync Implementation
|
|
236
|
+
Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
|
|
237
|
+
Napi::Function func = DefineClass(
|
|
238
|
+
env, "DatabaseSync",
|
|
239
|
+
{InstanceMethod("open", &DatabaseSync::Open),
|
|
240
|
+
InstanceMethod("close", &DatabaseSync::Close),
|
|
241
|
+
InstanceMethod("prepare", &DatabaseSync::Prepare),
|
|
242
|
+
InstanceMethod("exec", &DatabaseSync::Exec),
|
|
243
|
+
InstanceMethod("function", &DatabaseSync::CustomFunction),
|
|
244
|
+
InstanceMethod("aggregate", &DatabaseSync::AggregateFunction),
|
|
245
|
+
InstanceMethod("enableLoadExtension",
|
|
246
|
+
&DatabaseSync::EnableLoadExtension),
|
|
247
|
+
InstanceMethod("loadExtension", &DatabaseSync::LoadExtension),
|
|
248
|
+
InstanceMethod("createSession", &DatabaseSync::CreateSession),
|
|
249
|
+
InstanceMethod("applyChangeset", &DatabaseSync::ApplyChangeset),
|
|
250
|
+
InstanceMethod("backup", &DatabaseSync::Backup),
|
|
251
|
+
InstanceMethod("location", &DatabaseSync::LocationMethod),
|
|
252
|
+
InstanceAccessor("isOpen", &DatabaseSync::IsOpenGetter, nullptr),
|
|
253
|
+
InstanceAccessor("isTransaction", &DatabaseSync::IsTransactionGetter,
|
|
254
|
+
nullptr)});
|
|
255
|
+
|
|
256
|
+
// Store constructor in per-instance addon data instead of static variable
|
|
257
|
+
AddonData *addon_data = GetAddonData(env);
|
|
258
|
+
if (addon_data) {
|
|
259
|
+
addon_data->databaseSyncConstructor =
|
|
260
|
+
Napi::Reference<Napi::Function>::New(func);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
exports.Set("DatabaseSync", func);
|
|
264
|
+
return exports;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
|
|
268
|
+
: Napi::ObjectWrap<DatabaseSync>(info),
|
|
269
|
+
creation_thread_(std::this_thread::get_id()), env_(info.Env()) {
|
|
270
|
+
// Register this instance for cleanup tracking
|
|
271
|
+
RegisterDatabaseInstance(info.Env(), this);
|
|
272
|
+
|
|
273
|
+
// If no arguments, create but don't open (for manual open() call)
|
|
274
|
+
if (info.Length() == 0) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Validate and extract the database path
|
|
279
|
+
std::optional<std::string> location =
|
|
280
|
+
ValidateDatabasePath(info.Env(), info[0], "path");
|
|
281
|
+
if (!location.has_value()) {
|
|
282
|
+
return; // Error already thrown by ValidateDatabasePath
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
DatabaseOpenConfiguration config(std::move(location.value()));
|
|
287
|
+
|
|
288
|
+
// Handle options object if provided as second argument
|
|
289
|
+
if (info.Length() > 1 && info[1].IsObject()) {
|
|
290
|
+
Napi::Object options = info[1].As<Napi::Object>();
|
|
291
|
+
|
|
292
|
+
if (options.Has("readOnly") && options.Get("readOnly").IsBoolean()) {
|
|
293
|
+
config.set_read_only(
|
|
294
|
+
options.Get("readOnly").As<Napi::Boolean>().Value());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Support both old and new naming for backwards compatibility
|
|
298
|
+
if (options.Has("enableForeignKeyConstraints") &&
|
|
299
|
+
options.Get("enableForeignKeyConstraints").IsBoolean()) {
|
|
300
|
+
config.set_enable_foreign_keys(
|
|
301
|
+
options.Get("enableForeignKeyConstraints")
|
|
302
|
+
.As<Napi::Boolean>()
|
|
303
|
+
.Value());
|
|
304
|
+
} else if (options.Has("enableForeignKeys") &&
|
|
305
|
+
options.Get("enableForeignKeys").IsBoolean()) {
|
|
306
|
+
config.set_enable_foreign_keys(
|
|
307
|
+
options.Get("enableForeignKeys").As<Napi::Boolean>().Value());
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (options.Has("timeout") && options.Get("timeout").IsNumber()) {
|
|
311
|
+
config.set_timeout(
|
|
312
|
+
options.Get("timeout").As<Napi::Number>().Int32Value());
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (options.Has("enableDoubleQuotedStringLiterals") &&
|
|
316
|
+
options.Get("enableDoubleQuotedStringLiterals").IsBoolean()) {
|
|
317
|
+
config.set_enable_dqs(options.Get("enableDoubleQuotedStringLiterals")
|
|
318
|
+
.As<Napi::Boolean>()
|
|
319
|
+
.Value());
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (options.Has("allowExtension") &&
|
|
323
|
+
options.Get("allowExtension").IsBoolean()) {
|
|
324
|
+
allow_load_extension_ =
|
|
325
|
+
options.Get("allowExtension").As<Napi::Boolean>().Value();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
InternalOpen(config);
|
|
330
|
+
} catch (const std::exception &e) {
|
|
331
|
+
node::THROW_ERR_SQLITE_ERROR(info.Env(), e.what());
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
DatabaseSync::~DatabaseSync() {
|
|
336
|
+
// Unregister this instance
|
|
337
|
+
UnregisterDatabaseInstance(env_, this);
|
|
338
|
+
|
|
339
|
+
if (connection_) {
|
|
340
|
+
InternalClose();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
Napi::Value DatabaseSync::Open(const Napi::CallbackInfo &info) {
|
|
345
|
+
Napi::Env env = info.Env();
|
|
346
|
+
|
|
347
|
+
if (IsOpen()) {
|
|
348
|
+
node::THROW_ERR_INVALID_STATE(env, "Database is already open");
|
|
349
|
+
return env.Undefined();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
353
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env, "Expected configuration object");
|
|
354
|
+
return env.Undefined();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
Napi::Object config_obj = info[0].As<Napi::Object>();
|
|
358
|
+
|
|
359
|
+
if (!config_obj.Has("location") || !config_obj.Get("location").IsString()) {
|
|
360
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env,
|
|
361
|
+
"Configuration must have location string");
|
|
362
|
+
return env.Undefined();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
std::string location =
|
|
366
|
+
config_obj.Get("location").As<Napi::String>().Utf8Value();
|
|
367
|
+
DatabaseOpenConfiguration config(std::move(location));
|
|
368
|
+
|
|
369
|
+
// Parse other options
|
|
370
|
+
if (config_obj.Has("readOnly") && config_obj.Get("readOnly").IsBoolean()) {
|
|
371
|
+
config.set_read_only(
|
|
372
|
+
config_obj.Get("readOnly").As<Napi::Boolean>().Value());
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Support both old and new naming for backwards compatibility
|
|
376
|
+
if (config_obj.Has("enableForeignKeyConstraints") &&
|
|
377
|
+
config_obj.Get("enableForeignKeyConstraints").IsBoolean()) {
|
|
378
|
+
config.set_enable_foreign_keys(config_obj.Get("enableForeignKeyConstraints")
|
|
379
|
+
.As<Napi::Boolean>()
|
|
380
|
+
.Value());
|
|
381
|
+
} else if (config_obj.Has("enableForeignKeys") &&
|
|
382
|
+
config_obj.Get("enableForeignKeys").IsBoolean()) {
|
|
383
|
+
config.set_enable_foreign_keys(
|
|
384
|
+
config_obj.Get("enableForeignKeys").As<Napi::Boolean>().Value());
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (config_obj.Has("timeout") && config_obj.Get("timeout").IsNumber()) {
|
|
388
|
+
config.set_timeout(
|
|
389
|
+
config_obj.Get("timeout").As<Napi::Number>().Int32Value());
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (config_obj.Has("enableDoubleQuotedStringLiterals") &&
|
|
393
|
+
config_obj.Get("enableDoubleQuotedStringLiterals").IsBoolean()) {
|
|
394
|
+
config.set_enable_dqs(config_obj.Get("enableDoubleQuotedStringLiterals")
|
|
395
|
+
.As<Napi::Boolean>()
|
|
396
|
+
.Value());
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (config_obj.Has("allowExtension") &&
|
|
400
|
+
config_obj.Get("allowExtension").IsBoolean()) {
|
|
401
|
+
allow_load_extension_ =
|
|
402
|
+
config_obj.Get("allowExtension").As<Napi::Boolean>().Value();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
InternalOpen(config);
|
|
407
|
+
} catch (const std::exception &e) {
|
|
408
|
+
node::THROW_ERR_SQLITE_ERROR(env, e.what());
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return env.Undefined();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
Napi::Value DatabaseSync::Close(const Napi::CallbackInfo &info) {
|
|
415
|
+
Napi::Env env = info.Env();
|
|
416
|
+
|
|
417
|
+
if (!ValidateThread(env)) {
|
|
418
|
+
return env.Undefined();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!IsOpen()) {
|
|
422
|
+
node::THROW_ERR_INVALID_STATE(env, "Database is not open");
|
|
423
|
+
return env.Undefined();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
InternalClose();
|
|
428
|
+
} catch (const std::exception &e) {
|
|
429
|
+
node::THROW_ERR_SQLITE_ERROR(env, e.what());
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return env.Undefined();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
|
|
436
|
+
Napi::Env env = info.Env();
|
|
437
|
+
|
|
438
|
+
if (!ValidateThread(env)) {
|
|
439
|
+
return env.Undefined();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!IsOpen()) {
|
|
443
|
+
node::THROW_ERR_INVALID_STATE(env, "Database is not open");
|
|
444
|
+
return env.Undefined();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
448
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env, "Expected SQL string");
|
|
449
|
+
return env.Undefined();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
std::string sql = info[0].As<Napi::String>().Utf8Value();
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
// Create new StatementSync instance using addon data constructor
|
|
456
|
+
AddonData *addon_data = GetAddonData(env);
|
|
457
|
+
if (!addon_data || addon_data->statementSyncConstructor.IsEmpty()) {
|
|
458
|
+
node::THROW_ERR_INVALID_STATE(
|
|
459
|
+
env, "StatementSync constructor not initialized");
|
|
460
|
+
return env.Undefined();
|
|
461
|
+
}
|
|
462
|
+
Napi::Object stmt_obj =
|
|
463
|
+
addon_data->statementSyncConstructor.New({}).As<Napi::Object>();
|
|
464
|
+
|
|
465
|
+
// Initialize the statement
|
|
466
|
+
StatementSync *stmt = StatementSync::Unwrap(stmt_obj);
|
|
467
|
+
stmt->InitStatement(this, sql);
|
|
468
|
+
|
|
469
|
+
return stmt_obj;
|
|
470
|
+
} catch (const std::exception &e) {
|
|
471
|
+
node::THROW_ERR_SQLITE_ERROR(env, e.what());
|
|
472
|
+
return env.Undefined();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
Napi::Value DatabaseSync::Exec(const Napi::CallbackInfo &info) {
|
|
477
|
+
Napi::Env env = info.Env();
|
|
478
|
+
|
|
479
|
+
if (!ValidateThread(env)) {
|
|
480
|
+
return env.Undefined();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!IsOpen()) {
|
|
484
|
+
node::THROW_ERR_INVALID_STATE(env, "Database is not open");
|
|
485
|
+
return env.Undefined();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
489
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env, "Expected SQL string");
|
|
490
|
+
return env.Undefined();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
std::string sql = info[0].As<Napi::String>().Utf8Value();
|
|
494
|
+
|
|
495
|
+
char *error_msg = nullptr;
|
|
496
|
+
int result =
|
|
497
|
+
sqlite3_exec(connection(), sql.c_str(), nullptr, nullptr, &error_msg);
|
|
498
|
+
|
|
499
|
+
if (result != SQLITE_OK) {
|
|
500
|
+
std::string error = error_msg ? error_msg : "Unknown SQLite error";
|
|
501
|
+
if (error_msg)
|
|
502
|
+
sqlite3_free(error_msg);
|
|
503
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return env.Undefined();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
Napi::Value DatabaseSync::LocationMethod(const Napi::CallbackInfo &info) {
|
|
510
|
+
Napi::Env env = info.Env();
|
|
511
|
+
|
|
512
|
+
if (!IsOpen()) {
|
|
513
|
+
node::THROW_ERR_INVALID_STATE(env, "Database is not open");
|
|
514
|
+
return env.Undefined();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Default to "main" if no dbName provided
|
|
518
|
+
std::string db_name = "main";
|
|
519
|
+
if (info.Length() > 0 && info[0].IsString()) {
|
|
520
|
+
db_name = info[0].As<Napi::String>().Utf8Value();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Use sqlite3_db_filename() to get the database file path
|
|
524
|
+
const char *filename = sqlite3_db_filename(connection(), db_name.c_str());
|
|
525
|
+
|
|
526
|
+
// Return null for in-memory databases, non-existent databases, or if database
|
|
527
|
+
// not found
|
|
528
|
+
if (filename == nullptr || strlen(filename) == 0) {
|
|
529
|
+
return env.Null();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return Napi::String::New(env, filename);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
Napi::Value DatabaseSync::IsOpenGetter(const Napi::CallbackInfo &info) {
|
|
536
|
+
return Napi::Boolean::New(info.Env(), IsOpen());
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
Napi::Value DatabaseSync::IsTransactionGetter(const Napi::CallbackInfo &info) {
|
|
540
|
+
// Check if we're in a transaction
|
|
541
|
+
bool in_transaction = IsOpen() && !sqlite3_get_autocommit(connection());
|
|
542
|
+
return Napi::Boolean::New(info.Env(), in_transaction);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
|
|
546
|
+
location_ = config.location();
|
|
547
|
+
read_only_ = config.get_read_only();
|
|
548
|
+
|
|
549
|
+
int flags = SQLITE_OPEN_CREATE;
|
|
550
|
+
if (read_only_) {
|
|
551
|
+
flags = SQLITE_OPEN_READONLY;
|
|
552
|
+
} else {
|
|
553
|
+
flags |= SQLITE_OPEN_READWRITE;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
int result = sqlite3_open_v2(location_.c_str(), &connection_, flags, nullptr);
|
|
557
|
+
|
|
558
|
+
if (result != SQLITE_OK) {
|
|
559
|
+
std::string error = sqlite3_errmsg(connection_);
|
|
560
|
+
if (connection_) {
|
|
561
|
+
sqlite3_close(connection_);
|
|
562
|
+
connection_ = nullptr;
|
|
563
|
+
}
|
|
564
|
+
throw std::runtime_error("Failed to open database: " + error);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Configure database
|
|
568
|
+
if (config.get_enable_foreign_keys()) {
|
|
569
|
+
sqlite3_exec(connection(), "PRAGMA foreign_keys = ON", nullptr, nullptr,
|
|
570
|
+
nullptr);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (config.get_timeout() > 0) {
|
|
574
|
+
sqlite3_busy_timeout(connection(), config.get_timeout());
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Configure double-quoted string literals
|
|
578
|
+
if (config.get_enable_dqs()) {
|
|
579
|
+
int dqs_enable = 1;
|
|
580
|
+
result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DQS_DML,
|
|
581
|
+
dqs_enable, nullptr);
|
|
582
|
+
if (result != SQLITE_OK) {
|
|
583
|
+
std::string error = sqlite3_errmsg(connection());
|
|
584
|
+
sqlite3_close(connection_);
|
|
585
|
+
connection_ = nullptr;
|
|
586
|
+
throw std::runtime_error("Failed to configure DQS_DML: " + error);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DQS_DDL,
|
|
590
|
+
dqs_enable, nullptr);
|
|
591
|
+
if (result != SQLITE_OK) {
|
|
592
|
+
std::string error = sqlite3_errmsg(connection());
|
|
593
|
+
sqlite3_close(connection_);
|
|
594
|
+
connection_ = nullptr;
|
|
595
|
+
throw std::runtime_error("Failed to configure DQS_DDL: " + error);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
void DatabaseSync::InternalClose() {
|
|
601
|
+
if (connection_) {
|
|
602
|
+
// Finalize all prepared statements
|
|
603
|
+
prepared_statements_.clear();
|
|
604
|
+
|
|
605
|
+
// Delete all sessions before closing the database
|
|
606
|
+
// This is required by SQLite to avoid undefined behavior
|
|
607
|
+
DeleteAllSessions();
|
|
608
|
+
|
|
609
|
+
// Close the database connection
|
|
610
|
+
int result = sqlite3_close(connection_);
|
|
611
|
+
if (result != SQLITE_OK) {
|
|
612
|
+
// Force close even if there are outstanding statements
|
|
613
|
+
sqlite3_close_v2(connection_);
|
|
614
|
+
}
|
|
615
|
+
connection_ = nullptr;
|
|
616
|
+
}
|
|
617
|
+
location_.clear();
|
|
618
|
+
enable_load_extension_ = false;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
Napi::Value DatabaseSync::CustomFunction(const Napi::CallbackInfo &info) {
|
|
622
|
+
Napi::Env env = info.Env();
|
|
623
|
+
|
|
624
|
+
if (!IsOpen()) {
|
|
625
|
+
node::THROW_ERR_INVALID_STATE(env, "Database is not open");
|
|
626
|
+
return env.Undefined();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (info.Length() < 2) {
|
|
630
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
631
|
+
env, "Expected at least 2 arguments: name and function");
|
|
632
|
+
return env.Undefined();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!info[0].IsString()) {
|
|
636
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env, "Function name must be a string");
|
|
637
|
+
return env.Undefined();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Parse arguments: function(name, [options], callback)
|
|
641
|
+
int fn_index = info.Length() < 3 ? 1 : 2;
|
|
642
|
+
bool use_bigint_args = false;
|
|
643
|
+
bool varargs = false;
|
|
644
|
+
bool deterministic = false;
|
|
645
|
+
bool direct_only = false;
|
|
646
|
+
|
|
647
|
+
// Parse options object if provided
|
|
648
|
+
if (fn_index > 1 && info[1].IsObject()) {
|
|
649
|
+
Napi::Object options = info[1].As<Napi::Object>();
|
|
650
|
+
|
|
651
|
+
if (options.Has("useBigIntArguments") &&
|
|
652
|
+
options.Get("useBigIntArguments").IsBoolean()) {
|
|
653
|
+
use_bigint_args =
|
|
654
|
+
options.Get("useBigIntArguments").As<Napi::Boolean>().Value();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (options.Has("varargs") && options.Get("varargs").IsBoolean()) {
|
|
658
|
+
varargs = options.Get("varargs").As<Napi::Boolean>().Value();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (options.Has("deterministic") &&
|
|
662
|
+
options.Get("deterministic").IsBoolean()) {
|
|
663
|
+
deterministic = options.Get("deterministic").As<Napi::Boolean>().Value();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (options.Has("directOnly") && options.Get("directOnly").IsBoolean()) {
|
|
667
|
+
direct_only = options.Get("directOnly").As<Napi::Boolean>().Value();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (!info[fn_index].IsFunction()) {
|
|
672
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env, "Callback must be a function");
|
|
673
|
+
return env.Undefined();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
std::string name = info[0].As<Napi::String>().Utf8Value();
|
|
677
|
+
Napi::Function fn = info[fn_index].As<Napi::Function>();
|
|
678
|
+
|
|
679
|
+
// Determine argument count
|
|
680
|
+
int argc = -1; // Default to varargs
|
|
681
|
+
if (!varargs) {
|
|
682
|
+
// Try to get function.length
|
|
683
|
+
Napi::Value length_prop = fn.Get("length");
|
|
684
|
+
if (length_prop.IsNumber()) {
|
|
685
|
+
argc = length_prop.As<Napi::Number>().Int32Value();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Create UserDefinedFunction wrapper
|
|
690
|
+
UserDefinedFunction *user_data =
|
|
691
|
+
new UserDefinedFunction(env, fn, this, use_bigint_args);
|
|
692
|
+
|
|
693
|
+
// Set SQLite flags
|
|
694
|
+
int flags = SQLITE_UTF8;
|
|
695
|
+
if (deterministic) {
|
|
696
|
+
flags |= SQLITE_DETERMINISTIC;
|
|
697
|
+
}
|
|
698
|
+
if (direct_only) {
|
|
699
|
+
flags |= SQLITE_DIRECTONLY;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Register with SQLite
|
|
703
|
+
int result =
|
|
704
|
+
sqlite3_create_function_v2(connection(), name.c_str(), argc, flags,
|
|
705
|
+
user_data, UserDefinedFunction::xFunc,
|
|
706
|
+
nullptr, // No aggregate step
|
|
707
|
+
nullptr, // No aggregate final
|
|
708
|
+
UserDefinedFunction::xDestroy);
|
|
709
|
+
|
|
710
|
+
if (result != SQLITE_OK) {
|
|
711
|
+
delete user_data; // Clean up on failure
|
|
712
|
+
std::string error = "Failed to create function: ";
|
|
713
|
+
error += sqlite3_errmsg(connection());
|
|
714
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return env.Undefined();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
Napi::Value DatabaseSync::AggregateFunction(const Napi::CallbackInfo &info) {
|
|
721
|
+
Napi::Env env = info.Env();
|
|
722
|
+
|
|
723
|
+
if (!IsOpen()) {
|
|
724
|
+
node::THROW_ERR_INVALID_STATE(env, "Database is not open");
|
|
725
|
+
return env.Undefined();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (info.Length() < 2) {
|
|
729
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
730
|
+
env, "Expected at least 2 arguments: name and options");
|
|
731
|
+
return env.Undefined();
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (!info[0].IsString()) {
|
|
735
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env, "Function name must be a string");
|
|
736
|
+
return env.Undefined();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (!info[1].IsObject()) {
|
|
740
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env, "Options must be an object");
|
|
741
|
+
return env.Undefined();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
std::string name = info[0].As<Napi::String>().Utf8Value();
|
|
745
|
+
Napi::Object options = info[1].As<Napi::Object>();
|
|
746
|
+
|
|
747
|
+
// Parse required options - start can be undefined, will default to null
|
|
748
|
+
Napi::Value start = env.Null();
|
|
749
|
+
if (options.Has("start") && !options.Get("start").IsUndefined()) {
|
|
750
|
+
start = options.Get("start");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (!options.Has("step") || !options.Get("step").IsFunction()) {
|
|
754
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env, "options.step must be a function");
|
|
755
|
+
return env.Undefined();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
Napi::Function step_fn = options.Get("step").As<Napi::Function>();
|
|
759
|
+
|
|
760
|
+
// Parse optional options
|
|
761
|
+
Napi::Function inverse_fn;
|
|
762
|
+
if (options.Has("inverse") && options.Get("inverse").IsFunction()) {
|
|
763
|
+
inverse_fn = options.Get("inverse").As<Napi::Function>();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
Napi::Function result_fn;
|
|
767
|
+
if (options.Has("result") && options.Get("result").IsFunction()) {
|
|
768
|
+
result_fn = options.Get("result").As<Napi::Function>();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
bool use_bigint_args = false;
|
|
772
|
+
if (options.Has("useBigIntArguments") &&
|
|
773
|
+
options.Get("useBigIntArguments").IsBoolean()) {
|
|
774
|
+
use_bigint_args =
|
|
775
|
+
options.Get("useBigIntArguments").As<Napi::Boolean>().Value();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
bool varargs = false;
|
|
779
|
+
if (options.Has("varargs") && options.Get("varargs").IsBoolean()) {
|
|
780
|
+
varargs = options.Get("varargs").As<Napi::Boolean>().Value();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
bool deterministic = false;
|
|
784
|
+
if (options.Has("deterministic") &&
|
|
785
|
+
options.Get("deterministic").IsBoolean()) {
|
|
786
|
+
deterministic = options.Get("deterministic").As<Napi::Boolean>().Value();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
bool direct_only = false;
|
|
790
|
+
if (options.Has("directOnly") && options.Get("directOnly").IsBoolean()) {
|
|
791
|
+
direct_only = options.Get("directOnly").As<Napi::Boolean>().Value();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Determine argument count
|
|
795
|
+
int argc = -1; // Default to varargs
|
|
796
|
+
if (!varargs) {
|
|
797
|
+
Napi::Value length_prop = step_fn.Get("length");
|
|
798
|
+
if (length_prop.IsNumber()) {
|
|
799
|
+
// Subtract 1 because the first argument is the aggregate value
|
|
800
|
+
argc = length_prop.As<Napi::Number>().Int32Value() - 1;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Also check inverse function length if provided
|
|
804
|
+
if (!inverse_fn.IsEmpty()) {
|
|
805
|
+
Napi::Value inverse_length = inverse_fn.Get("length");
|
|
806
|
+
if (inverse_length.IsNumber()) {
|
|
807
|
+
int inverse_argc = inverse_length.As<Napi::Number>().Int32Value() - 1;
|
|
808
|
+
argc = std::max({argc, inverse_argc, 0});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Ensure argc is non-negative
|
|
813
|
+
argc = std::max(argc, 0);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Set SQLite flags
|
|
817
|
+
int flags = SQLITE_UTF8;
|
|
818
|
+
if (deterministic) {
|
|
819
|
+
flags |= SQLITE_DETERMINISTIC;
|
|
820
|
+
}
|
|
821
|
+
if (direct_only) {
|
|
822
|
+
flags |= SQLITE_DIRECTONLY;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Create CustomAggregate wrapper
|
|
826
|
+
CustomAggregate *user_data;
|
|
827
|
+
try {
|
|
828
|
+
user_data = new CustomAggregate(env, this, use_bigint_args, start, step_fn,
|
|
829
|
+
inverse_fn, result_fn);
|
|
830
|
+
} catch (const std::exception &e) {
|
|
831
|
+
std::string error = "Failed to create CustomAggregate: ";
|
|
832
|
+
error += e.what();
|
|
833
|
+
node::THROW_ERR_INVALID_ARG_VALUE(env, error.c_str());
|
|
834
|
+
return env.Undefined();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Register with SQLite - Node.js always uses sqlite3_create_window_function
|
|
838
|
+
// for aggregates
|
|
839
|
+
auto xInverse = !inverse_fn.IsEmpty() ? CustomAggregate::xInverse : nullptr;
|
|
840
|
+
auto xValue = xInverse ? CustomAggregate::xValue : nullptr;
|
|
841
|
+
int result = sqlite3_create_window_function(
|
|
842
|
+
connection(), name.c_str(), argc, flags, user_data,
|
|
843
|
+
CustomAggregate::xStep, CustomAggregate::xFinal, xValue, xInverse,
|
|
844
|
+
CustomAggregate::xDestroy);
|
|
845
|
+
|
|
846
|
+
if (result != SQLITE_OK) {
|
|
847
|
+
delete user_data; // Clean up on failure
|
|
848
|
+
std::string error = "Failed to create aggregate function '";
|
|
849
|
+
error += name;
|
|
850
|
+
error += "': ";
|
|
851
|
+
error += sqlite3_errmsg(connection());
|
|
852
|
+
error += " (SQLite error code: ";
|
|
853
|
+
error += std::to_string(result);
|
|
854
|
+
error += ")";
|
|
855
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return env.Undefined();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
Napi::Value DatabaseSync::EnableLoadExtension(const Napi::CallbackInfo &info) {
|
|
862
|
+
Napi::Env env = info.Env();
|
|
863
|
+
|
|
864
|
+
if (!IsOpen()) {
|
|
865
|
+
node::THROW_ERR_INVALID_STATE(env, "Database is not open");
|
|
866
|
+
return env.Undefined();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (info.Length() < 1 || !info[0].IsBoolean()) {
|
|
870
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
871
|
+
env, "The \"allow\" argument must be a boolean.");
|
|
872
|
+
return env.Undefined();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
bool enable = info[0].As<Napi::Boolean>().Value();
|
|
876
|
+
|
|
877
|
+
// Check if extension loading was disallowed at database creation
|
|
878
|
+
if (!allow_load_extension_ && enable) {
|
|
879
|
+
node::THROW_ERR_INVALID_STATE(env,
|
|
880
|
+
"Cannot enable extension loading because it "
|
|
881
|
+
"was disabled at database creation.");
|
|
882
|
+
return env.Undefined();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
enable_load_extension_ = enable;
|
|
886
|
+
|
|
887
|
+
// Configure SQLite to enable/disable extension loading
|
|
888
|
+
int result =
|
|
889
|
+
sqlite3_db_config(connection(), SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION,
|
|
890
|
+
enable ? 1 : 0, nullptr);
|
|
891
|
+
|
|
892
|
+
if (result != SQLITE_OK) {
|
|
893
|
+
std::string error = "Failed to configure extension loading: ";
|
|
894
|
+
error += sqlite3_errmsg(connection());
|
|
895
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return env.Undefined();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
Napi::Value DatabaseSync::LoadExtension(const Napi::CallbackInfo &info) {
|
|
902
|
+
Napi::Env env = info.Env();
|
|
903
|
+
|
|
904
|
+
if (!IsOpen()) {
|
|
905
|
+
node::THROW_ERR_INVALID_STATE(env, "Database is not open");
|
|
906
|
+
return env.Undefined();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (!allow_load_extension_) {
|
|
910
|
+
node::THROW_ERR_INVALID_STATE(env, "Extension loading is not allowed");
|
|
911
|
+
return env.Undefined();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (!enable_load_extension_) {
|
|
915
|
+
node::THROW_ERR_INVALID_STATE(env, "Extension loading is not enabled");
|
|
916
|
+
return env.Undefined();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
920
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env,
|
|
921
|
+
"The \"path\" argument must be a string.");
|
|
922
|
+
return env.Undefined();
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
std::string path = info[0].As<Napi::String>().Utf8Value();
|
|
926
|
+
|
|
927
|
+
// Optional entry point parameter
|
|
928
|
+
const char *entry_point = nullptr;
|
|
929
|
+
std::string entry_point_str;
|
|
930
|
+
if (info.Length() > 1 && info[1].IsString()) {
|
|
931
|
+
entry_point_str = info[1].As<Napi::String>().Utf8Value();
|
|
932
|
+
entry_point = entry_point_str.c_str();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Load the extension
|
|
936
|
+
char *errmsg = nullptr;
|
|
937
|
+
int result =
|
|
938
|
+
sqlite3_load_extension(connection(), path.c_str(), entry_point, &errmsg);
|
|
939
|
+
|
|
940
|
+
if (result != SQLITE_OK) {
|
|
941
|
+
std::string error = "Failed to load extension '";
|
|
942
|
+
error += path;
|
|
943
|
+
error += "': ";
|
|
944
|
+
if (errmsg) {
|
|
945
|
+
error += errmsg;
|
|
946
|
+
sqlite3_free(errmsg);
|
|
947
|
+
} else {
|
|
948
|
+
error += sqlite3_errmsg(connection());
|
|
949
|
+
}
|
|
950
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
951
|
+
return env.Undefined();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return env.Undefined();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
Napi::Value DatabaseSync::CreateSession(const Napi::CallbackInfo &info) {
|
|
958
|
+
Napi::Env env = info.Env();
|
|
959
|
+
|
|
960
|
+
if (!IsOpen()) {
|
|
961
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
962
|
+
return env.Undefined();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
std::string table;
|
|
966
|
+
std::string db_name = "main";
|
|
967
|
+
|
|
968
|
+
// Parse options if provided
|
|
969
|
+
if (info.Length() > 0) {
|
|
970
|
+
if (!info[0].IsObject()) {
|
|
971
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
972
|
+
env, "The \"options\" argument must be an object.");
|
|
973
|
+
return env.Undefined();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
Napi::Object options = info[0].As<Napi::Object>();
|
|
977
|
+
|
|
978
|
+
// Get table option
|
|
979
|
+
if (options.Has("table")) {
|
|
980
|
+
Napi::Value table_value = options.Get("table");
|
|
981
|
+
if (table_value.IsString()) {
|
|
982
|
+
table = table_value.As<Napi::String>().Utf8Value();
|
|
983
|
+
} else {
|
|
984
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
985
|
+
env, "The \"options.table\" argument must be a string.");
|
|
986
|
+
return env.Undefined();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Get db option
|
|
991
|
+
if (options.Has("db")) {
|
|
992
|
+
Napi::Value db_value = options.Get("db");
|
|
993
|
+
if (db_value.IsString()) {
|
|
994
|
+
db_name = db_value.As<Napi::String>().Utf8Value();
|
|
995
|
+
} else {
|
|
996
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
997
|
+
env, "The \"options.db\" argument must be a string.");
|
|
998
|
+
return env.Undefined();
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Create the session
|
|
1004
|
+
sqlite3_session *pSession;
|
|
1005
|
+
int r = sqlite3session_create(connection(), db_name.c_str(), &pSession);
|
|
1006
|
+
|
|
1007
|
+
if (r != SQLITE_OK) {
|
|
1008
|
+
std::string error = "Failed to create session: ";
|
|
1009
|
+
error += sqlite3_errmsg(connection());
|
|
1010
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
1011
|
+
return env.Undefined();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Attach table if specified
|
|
1015
|
+
r = sqlite3session_attach(pSession, table.empty() ? nullptr : table.c_str());
|
|
1016
|
+
|
|
1017
|
+
if (r != SQLITE_OK) {
|
|
1018
|
+
sqlite3session_delete(pSession);
|
|
1019
|
+
std::string error = "Failed to attach table to session: ";
|
|
1020
|
+
error += sqlite3_errmsg(connection());
|
|
1021
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
1022
|
+
return env.Undefined();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Create and return the Session object
|
|
1026
|
+
return Session::Create(env, this, pSession);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
void DatabaseSync::AddSession(Session *session) {
|
|
1030
|
+
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
|
1031
|
+
sessions_.insert(session);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
void DatabaseSync::RemoveSession(Session *session) {
|
|
1035
|
+
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
|
1036
|
+
sessions_.erase(session);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
void DatabaseSync::DeleteAllSessions() {
|
|
1040
|
+
std::lock_guard<std::mutex> lock(sessions_mutex_);
|
|
1041
|
+
// Copy the set to avoid iterator invalidation
|
|
1042
|
+
std::set<Session *> sessions_copy = sessions_;
|
|
1043
|
+
sessions_.clear(); // Clear first to prevent re-entrance
|
|
1044
|
+
|
|
1045
|
+
// Now delete each session
|
|
1046
|
+
for (auto *session : sessions_copy) {
|
|
1047
|
+
// Direct SQLite cleanup since we're in database destruction
|
|
1048
|
+
if (session->GetSession()) {
|
|
1049
|
+
sqlite3session_delete(session->GetSession());
|
|
1050
|
+
// Clear the session's internal pointers
|
|
1051
|
+
session->session_ = nullptr;
|
|
1052
|
+
session->database_ = nullptr;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Context structure for changeset callbacks to avoid global state
|
|
1058
|
+
struct ChangesetCallbacks {
|
|
1059
|
+
std::function<int(int)> conflictCallback;
|
|
1060
|
+
std::function<bool(std::string)> filterCallback;
|
|
1061
|
+
Napi::Env env;
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
static int xConflict(void *pCtx, int eConflict, sqlite3_changeset_iter *pIter) {
|
|
1065
|
+
if (!pCtx)
|
|
1066
|
+
return SQLITE_CHANGESET_OMIT;
|
|
1067
|
+
ChangesetCallbacks *callbacks = static_cast<ChangesetCallbacks *>(pCtx);
|
|
1068
|
+
if (!callbacks->conflictCallback)
|
|
1069
|
+
return SQLITE_CHANGESET_OMIT;
|
|
1070
|
+
return callbacks->conflictCallback(eConflict);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
static int xFilter(void *pCtx, const char *zTab) {
|
|
1074
|
+
if (!pCtx)
|
|
1075
|
+
return 1;
|
|
1076
|
+
ChangesetCallbacks *callbacks = static_cast<ChangesetCallbacks *>(pCtx);
|
|
1077
|
+
if (!callbacks->filterCallback)
|
|
1078
|
+
return 1;
|
|
1079
|
+
return callbacks->filterCallback(zTab) ? 1 : 0;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
Napi::Value DatabaseSync::ApplyChangeset(const Napi::CallbackInfo &info) {
|
|
1083
|
+
Napi::Env env = info.Env();
|
|
1084
|
+
|
|
1085
|
+
if (!IsOpen()) {
|
|
1086
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
1087
|
+
return env.Undefined();
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (info.Length() < 1 || !info[0].IsBuffer()) {
|
|
1091
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1092
|
+
env, "The \"changeset\" argument must be a Buffer.");
|
|
1093
|
+
return env.Undefined();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Create callback context to avoid global state
|
|
1097
|
+
ChangesetCallbacks callbacks{nullptr, nullptr, env};
|
|
1098
|
+
|
|
1099
|
+
// Parse options if provided
|
|
1100
|
+
if (info.Length() > 1 && !info[1].IsUndefined()) {
|
|
1101
|
+
if (!info[1].IsObject()) {
|
|
1102
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1103
|
+
env, "The \"options\" argument must be an object.");
|
|
1104
|
+
return env.Undefined();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
Napi::Object options = info[1].As<Napi::Object>();
|
|
1108
|
+
|
|
1109
|
+
// Handle onConflict callback
|
|
1110
|
+
if (options.Has("onConflict")) {
|
|
1111
|
+
Napi::Value conflictValue = options.Get("onConflict");
|
|
1112
|
+
if (!conflictValue.IsUndefined()) {
|
|
1113
|
+
if (!conflictValue.IsFunction()) {
|
|
1114
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1115
|
+
env, "The \"options.onConflict\" argument must be a function.");
|
|
1116
|
+
return env.Undefined();
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
Napi::Function conflictFunc = conflictValue.As<Napi::Function>();
|
|
1120
|
+
callbacks.conflictCallback = [env,
|
|
1121
|
+
conflictFunc](int conflictType) -> int {
|
|
1122
|
+
Napi::HandleScope scope(env);
|
|
1123
|
+
Napi::Value result =
|
|
1124
|
+
conflictFunc.Call({Napi::Number::New(env, conflictType)});
|
|
1125
|
+
|
|
1126
|
+
if (env.IsExceptionPending()) {
|
|
1127
|
+
// If callback threw, abort the changeset apply
|
|
1128
|
+
return SQLITE_CHANGESET_ABORT;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
if (!result.IsNumber()) {
|
|
1132
|
+
return -1; // Invalid value
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return result.As<Napi::Number>().Int32Value();
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Handle filter callback
|
|
1141
|
+
if (options.Has("filter")) {
|
|
1142
|
+
Napi::Value filterValue = options.Get("filter");
|
|
1143
|
+
if (!filterValue.IsFunction()) {
|
|
1144
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1145
|
+
env, "The \"options.filter\" argument must be a function.");
|
|
1146
|
+
return env.Undefined();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
Napi::Function filterFunc = filterValue.As<Napi::Function>();
|
|
1150
|
+
callbacks.filterCallback = [env,
|
|
1151
|
+
filterFunc](std::string tableName) -> bool {
|
|
1152
|
+
Napi::HandleScope scope(env);
|
|
1153
|
+
Napi::Value result =
|
|
1154
|
+
filterFunc.Call({Napi::String::New(env, tableName)});
|
|
1155
|
+
|
|
1156
|
+
if (env.IsExceptionPending()) {
|
|
1157
|
+
// If callback threw, exclude the table
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
return result.ToBoolean().Value();
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Get the changeset buffer
|
|
1167
|
+
Napi::Buffer<uint8_t> buffer = info[0].As<Napi::Buffer<uint8_t>>();
|
|
1168
|
+
|
|
1169
|
+
// Apply the changeset with context instead of global state
|
|
1170
|
+
int r = sqlite3changeset_apply(connection(), buffer.Length(), buffer.Data(),
|
|
1171
|
+
xFilter, xConflict, &callbacks);
|
|
1172
|
+
|
|
1173
|
+
if (r == SQLITE_OK) {
|
|
1174
|
+
return Napi::Boolean::New(env, true);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (r == SQLITE_ABORT) {
|
|
1178
|
+
// Not an error, just means the operation was aborted
|
|
1179
|
+
return Napi::Boolean::New(env, false);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Other errors
|
|
1183
|
+
std::string error = "Failed to apply changeset: ";
|
|
1184
|
+
error += sqlite3_errmsg(connection());
|
|
1185
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
1186
|
+
return env.Undefined();
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// StatementSync Implementation
|
|
1190
|
+
Napi::Object StatementSync::Init(Napi::Env env, Napi::Object exports) {
|
|
1191
|
+
Napi::Function func = DefineClass(
|
|
1192
|
+
env, "StatementSync",
|
|
1193
|
+
{InstanceMethod("run", &StatementSync::Run),
|
|
1194
|
+
InstanceMethod("get", &StatementSync::Get),
|
|
1195
|
+
InstanceMethod("all", &StatementSync::All),
|
|
1196
|
+
InstanceMethod("iterate", &StatementSync::Iterate),
|
|
1197
|
+
InstanceMethod("finalize", &StatementSync::FinalizeStatement),
|
|
1198
|
+
InstanceMethod("setReadBigInts", &StatementSync::SetReadBigInts),
|
|
1199
|
+
InstanceMethod("setReturnArrays", &StatementSync::SetReturnArrays),
|
|
1200
|
+
InstanceMethod("setAllowBareNamedParameters",
|
|
1201
|
+
&StatementSync::SetAllowBareNamedParameters),
|
|
1202
|
+
InstanceMethod("columns", &StatementSync::Columns),
|
|
1203
|
+
InstanceAccessor("sourceSQL", &StatementSync::SourceSQLGetter, nullptr),
|
|
1204
|
+
InstanceAccessor("expandedSQL", &StatementSync::ExpandedSQLGetter,
|
|
1205
|
+
nullptr)});
|
|
1206
|
+
|
|
1207
|
+
// Store constructor in per-instance addon data instead of static variable
|
|
1208
|
+
AddonData *addon_data = GetAddonData(env);
|
|
1209
|
+
if (addon_data) {
|
|
1210
|
+
addon_data->statementSyncConstructor =
|
|
1211
|
+
Napi::Reference<Napi::Function>::New(func);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
exports.Set("StatementSync", func);
|
|
1215
|
+
return exports;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
StatementSync::StatementSync(const Napi::CallbackInfo &info)
|
|
1219
|
+
: Napi::ObjectWrap<StatementSync>(info),
|
|
1220
|
+
creation_thread_(std::this_thread::get_id()) {
|
|
1221
|
+
// Constructor - initialization happens in InitStatement
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
void StatementSync::InitStatement(DatabaseSync *database,
|
|
1225
|
+
const std::string &sql) {
|
|
1226
|
+
if (!database || !database->IsOpen()) {
|
|
1227
|
+
throw std::runtime_error("Database is not open");
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
database_ = database;
|
|
1231
|
+
source_sql_ = sql;
|
|
1232
|
+
|
|
1233
|
+
// Prepare the statement
|
|
1234
|
+
const char *tail = nullptr;
|
|
1235
|
+
int result = sqlite3_prepare_v2(database->connection(), sql.c_str(), -1,
|
|
1236
|
+
&statement_, &tail);
|
|
1237
|
+
|
|
1238
|
+
if (result != SQLITE_OK) {
|
|
1239
|
+
std::string error = sqlite3_errmsg(database->connection());
|
|
1240
|
+
throw std::runtime_error("Failed to prepare statement: " + error);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
StatementSync::~StatementSync() {
|
|
1245
|
+
if (statement_ && !finalized_) {
|
|
1246
|
+
sqlite3_finalize(statement_);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
|
|
1251
|
+
Napi::Env env = info.Env();
|
|
1252
|
+
|
|
1253
|
+
if (!ValidateThread(env)) {
|
|
1254
|
+
return env.Undefined();
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (finalized_) {
|
|
1258
|
+
node::THROW_ERR_INVALID_STATE(env, "Statement has been finalized");
|
|
1259
|
+
return env.Undefined();
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1263
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
1264
|
+
return env.Undefined();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (!statement_) {
|
|
1268
|
+
node::THROW_ERR_INVALID_STATE(env, "Statement is not properly initialized");
|
|
1269
|
+
return env.Undefined();
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
try {
|
|
1273
|
+
Reset();
|
|
1274
|
+
BindParameters(info);
|
|
1275
|
+
|
|
1276
|
+
int result = sqlite3_step(statement_);
|
|
1277
|
+
|
|
1278
|
+
if (result != SQLITE_DONE && result != SQLITE_ROW) {
|
|
1279
|
+
std::string error = sqlite3_errmsg(database_->connection());
|
|
1280
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
1281
|
+
return env.Undefined();
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Create result object
|
|
1285
|
+
Napi::Object result_obj = Napi::Object::New(env);
|
|
1286
|
+
result_obj.Set(
|
|
1287
|
+
"changes",
|
|
1288
|
+
Napi::Number::New(env, sqlite3_changes(database_->connection())));
|
|
1289
|
+
|
|
1290
|
+
sqlite3_int64 last_rowid =
|
|
1291
|
+
sqlite3_last_insert_rowid(database_->connection());
|
|
1292
|
+
// Use JavaScript's safe integer limits (2^53 - 1)
|
|
1293
|
+
if (last_rowid > JS_MAX_SAFE_INTEGER || last_rowid < JS_MIN_SAFE_INTEGER) {
|
|
1294
|
+
result_obj.Set("lastInsertRowid",
|
|
1295
|
+
Napi::BigInt::New(env, static_cast<int64_t>(last_rowid)));
|
|
1296
|
+
} else {
|
|
1297
|
+
result_obj.Set("lastInsertRowid",
|
|
1298
|
+
Napi::Number::New(env, static_cast<double>(last_rowid)));
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return result_obj;
|
|
1302
|
+
} catch (const std::exception &e) {
|
|
1303
|
+
node::THROW_ERR_SQLITE_ERROR(env, e.what());
|
|
1304
|
+
return env.Undefined();
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
Napi::Value StatementSync::Get(const Napi::CallbackInfo &info) {
|
|
1309
|
+
Napi::Env env = info.Env();
|
|
1310
|
+
|
|
1311
|
+
if (!ValidateThread(env)) {
|
|
1312
|
+
return env.Undefined();
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (finalized_) {
|
|
1316
|
+
node::THROW_ERR_INVALID_STATE(env, "Statement has been finalized");
|
|
1317
|
+
return env.Undefined();
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1321
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
1322
|
+
return env.Undefined();
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (!statement_) {
|
|
1326
|
+
node::THROW_ERR_INVALID_STATE(env, "Statement is not properly initialized");
|
|
1327
|
+
return env.Undefined();
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
try {
|
|
1331
|
+
Reset();
|
|
1332
|
+
BindParameters(info);
|
|
1333
|
+
|
|
1334
|
+
int result = sqlite3_step(statement_);
|
|
1335
|
+
|
|
1336
|
+
if (result == SQLITE_ROW) {
|
|
1337
|
+
return CreateResult();
|
|
1338
|
+
} else if (result == SQLITE_DONE) {
|
|
1339
|
+
return env.Undefined();
|
|
1340
|
+
} else {
|
|
1341
|
+
std::string error = sqlite3_errmsg(database_->connection());
|
|
1342
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
1343
|
+
return env.Undefined();
|
|
1344
|
+
}
|
|
1345
|
+
} catch (const std::exception &e) {
|
|
1346
|
+
node::THROW_ERR_SQLITE_ERROR(env, e.what());
|
|
1347
|
+
return env.Undefined();
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
Napi::Value StatementSync::All(const Napi::CallbackInfo &info) {
|
|
1352
|
+
Napi::Env env = info.Env();
|
|
1353
|
+
|
|
1354
|
+
if (finalized_) {
|
|
1355
|
+
node::THROW_ERR_INVALID_STATE(env, "Statement has been finalized");
|
|
1356
|
+
return env.Undefined();
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1360
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
1361
|
+
return env.Undefined();
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
if (!statement_) {
|
|
1365
|
+
node::THROW_ERR_INVALID_STATE(env, "Statement is not properly initialized");
|
|
1366
|
+
return env.Undefined();
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
try {
|
|
1370
|
+
Reset();
|
|
1371
|
+
BindParameters(info);
|
|
1372
|
+
|
|
1373
|
+
Napi::Array results = Napi::Array::New(env);
|
|
1374
|
+
uint32_t index = 0;
|
|
1375
|
+
|
|
1376
|
+
while (true) {
|
|
1377
|
+
int result = sqlite3_step(statement_);
|
|
1378
|
+
|
|
1379
|
+
if (result == SQLITE_ROW) {
|
|
1380
|
+
results.Set(index++, CreateResult());
|
|
1381
|
+
} else if (result == SQLITE_DONE) {
|
|
1382
|
+
break;
|
|
1383
|
+
} else {
|
|
1384
|
+
std::string error = sqlite3_errmsg(database_->connection());
|
|
1385
|
+
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
1386
|
+
return env.Undefined();
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
return results;
|
|
1391
|
+
} catch (const std::exception &e) {
|
|
1392
|
+
node::THROW_ERR_SQLITE_ERROR(env, e.what());
|
|
1393
|
+
return env.Undefined();
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
Napi::Value StatementSync::Iterate(const Napi::CallbackInfo &info) {
|
|
1398
|
+
if (finalized_) {
|
|
1399
|
+
node::THROW_ERR_INVALID_STATE(info.Env(), "statement has been finalized");
|
|
1400
|
+
return info.Env().Undefined();
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1404
|
+
node::THROW_ERR_INVALID_STATE(info.Env(), "Database connection is closed");
|
|
1405
|
+
return info.Env().Undefined();
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (!statement_) {
|
|
1409
|
+
node::THROW_ERR_INVALID_STATE(info.Env(),
|
|
1410
|
+
"Statement is not properly initialized");
|
|
1411
|
+
return info.Env().Undefined();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// Reset the statement first
|
|
1415
|
+
int r = sqlite3_reset(statement_);
|
|
1416
|
+
if (r != SQLITE_OK) {
|
|
1417
|
+
node::THROW_ERR_SQLITE_ERROR(info.Env(),
|
|
1418
|
+
sqlite3_errmsg(database_->connection()));
|
|
1419
|
+
return info.Env().Undefined();
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Bind parameters if provided
|
|
1423
|
+
BindParameters(info, 0);
|
|
1424
|
+
|
|
1425
|
+
// Create and return iterator
|
|
1426
|
+
return StatementSyncIterator::Create(info.Env(), this);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
Napi::Value StatementSync::FinalizeStatement(const Napi::CallbackInfo &info) {
|
|
1430
|
+
if (statement_ && !finalized_) {
|
|
1431
|
+
// It's safe to finalize even if database is closed
|
|
1432
|
+
// SQLite handles this gracefully
|
|
1433
|
+
sqlite3_finalize(statement_);
|
|
1434
|
+
statement_ = nullptr;
|
|
1435
|
+
finalized_ = true;
|
|
1436
|
+
}
|
|
1437
|
+
return info.Env().Undefined();
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
Napi::Value StatementSync::SourceSQLGetter(const Napi::CallbackInfo &info) {
|
|
1441
|
+
return Napi::String::New(info.Env(), source_sql_);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
Napi::Value StatementSync::ExpandedSQLGetter(const Napi::CallbackInfo &info) {
|
|
1445
|
+
if (finalized_) {
|
|
1446
|
+
node::THROW_ERR_INVALID_STATE(info.Env(), "Statement has been finalized");
|
|
1447
|
+
return info.Env().Undefined();
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1451
|
+
node::THROW_ERR_INVALID_STATE(info.Env(), "Database connection is closed");
|
|
1452
|
+
return info.Env().Undefined();
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (statement_) {
|
|
1456
|
+
char *expanded = sqlite3_expanded_sql(statement_);
|
|
1457
|
+
if (expanded) {
|
|
1458
|
+
Napi::String result = Napi::String::New(info.Env(), expanded);
|
|
1459
|
+
sqlite3_free(expanded);
|
|
1460
|
+
return result;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return info.Env().Undefined();
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
Napi::Value StatementSync::SetReadBigInts(const Napi::CallbackInfo &info) {
|
|
1467
|
+
Napi::Env env = info.Env();
|
|
1468
|
+
|
|
1469
|
+
if (finalized_) {
|
|
1470
|
+
node::THROW_ERR_INVALID_STATE(env, "The statement has been finalized");
|
|
1471
|
+
return env.Undefined();
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1475
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
1476
|
+
return env.Undefined();
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if (info.Length() < 1 || !info[0].IsBoolean()) {
|
|
1480
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1481
|
+
env, "The \"readBigInts\" argument must be a boolean.");
|
|
1482
|
+
return env.Undefined();
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
use_big_ints_ = info[0].As<Napi::Boolean>().Value();
|
|
1486
|
+
return env.Undefined();
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
Napi::Value StatementSync::SetReturnArrays(const Napi::CallbackInfo &info) {
|
|
1490
|
+
Napi::Env env = info.Env();
|
|
1491
|
+
|
|
1492
|
+
if (finalized_) {
|
|
1493
|
+
node::THROW_ERR_INVALID_STATE(env, "The statement has been finalized");
|
|
1494
|
+
return env.Undefined();
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1498
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
1499
|
+
return env.Undefined();
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (info.Length() < 1 || !info[0].IsBoolean()) {
|
|
1503
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1504
|
+
env, "The \"returnArrays\" argument must be a boolean.");
|
|
1505
|
+
return env.Undefined();
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
return_arrays_ = info[0].As<Napi::Boolean>().Value();
|
|
1509
|
+
return env.Undefined();
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
Napi::Value
|
|
1513
|
+
StatementSync::SetAllowBareNamedParameters(const Napi::CallbackInfo &info) {
|
|
1514
|
+
Napi::Env env = info.Env();
|
|
1515
|
+
|
|
1516
|
+
if (finalized_) {
|
|
1517
|
+
node::THROW_ERR_INVALID_STATE(env, "The statement has been finalized");
|
|
1518
|
+
return env.Undefined();
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1522
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
1523
|
+
return env.Undefined();
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if (info.Length() < 1 || !info[0].IsBoolean()) {
|
|
1527
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1528
|
+
env, "The \"allowBareNamedParameters\" argument must be a boolean.");
|
|
1529
|
+
return env.Undefined();
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
allow_bare_named_params_ = info[0].As<Napi::Boolean>().Value();
|
|
1533
|
+
return env.Undefined();
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
Napi::Value StatementSync::Columns(const Napi::CallbackInfo &info) {
|
|
1537
|
+
Napi::Env env = info.Env();
|
|
1538
|
+
|
|
1539
|
+
if (finalized_) {
|
|
1540
|
+
node::THROW_ERR_INVALID_STATE(env, "The statement has been finalized");
|
|
1541
|
+
return env.Undefined();
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1545
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
1546
|
+
return env.Undefined();
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
if (!statement_) {
|
|
1550
|
+
node::THROW_ERR_INVALID_STATE(env, "Statement is not properly initialized");
|
|
1551
|
+
return env.Undefined();
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
int column_count = sqlite3_column_count(statement_);
|
|
1555
|
+
Napi::Array columns = Napi::Array::New(env, column_count);
|
|
1556
|
+
|
|
1557
|
+
for (int i = 0; i < column_count; i++) {
|
|
1558
|
+
Napi::Object column_info = Napi::Object::New(env);
|
|
1559
|
+
|
|
1560
|
+
// column: The original column name (sqlite3_column_origin_name)
|
|
1561
|
+
const char *origin_name = sqlite3_column_origin_name(statement_, i);
|
|
1562
|
+
if (origin_name) {
|
|
1563
|
+
column_info.Set("column", Napi::String::New(env, origin_name));
|
|
1564
|
+
} else {
|
|
1565
|
+
column_info.Set("column", env.Null());
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// database: The database name (sqlite3_column_database_name)
|
|
1569
|
+
const char *database_name = sqlite3_column_database_name(statement_, i);
|
|
1570
|
+
if (database_name) {
|
|
1571
|
+
column_info.Set("database", Napi::String::New(env, database_name));
|
|
1572
|
+
} else {
|
|
1573
|
+
column_info.Set("database", env.Null());
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// name: The column name/alias (sqlite3_column_name)
|
|
1577
|
+
const char *column_name = sqlite3_column_name(statement_, i);
|
|
1578
|
+
if (column_name) {
|
|
1579
|
+
column_info.Set("name", Napi::String::New(env, column_name));
|
|
1580
|
+
} else {
|
|
1581
|
+
column_info.Set("name", env.Null());
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// table: The table name (sqlite3_column_table_name)
|
|
1585
|
+
const char *table_name = sqlite3_column_table_name(statement_, i);
|
|
1586
|
+
if (table_name) {
|
|
1587
|
+
column_info.Set("table", Napi::String::New(env, table_name));
|
|
1588
|
+
} else {
|
|
1589
|
+
column_info.Set("table", env.Null());
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// type: The declared type (sqlite3_column_decltype)
|
|
1593
|
+
const char *decl_type = sqlite3_column_decltype(statement_, i);
|
|
1594
|
+
if (decl_type) {
|
|
1595
|
+
column_info.Set("type", Napi::String::New(env, decl_type));
|
|
1596
|
+
} else {
|
|
1597
|
+
column_info.Set("type", env.Null());
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
columns.Set(i, column_info);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
return columns;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
void StatementSync::BindParameters(const Napi::CallbackInfo &info,
|
|
1607
|
+
size_t start_index) {
|
|
1608
|
+
Napi::Env env = info.Env();
|
|
1609
|
+
|
|
1610
|
+
// Safety checks
|
|
1611
|
+
if (finalized_) {
|
|
1612
|
+
node::THROW_ERR_INVALID_STATE(env, "Statement has been finalized");
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1617
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (!statement_) {
|
|
1622
|
+
node::THROW_ERR_INVALID_STATE(env, "Statement is not properly initialized");
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Check if we have a single object for named parameters
|
|
1627
|
+
if (info.Length() == start_index + 1 && info[start_index].IsObject() &&
|
|
1628
|
+
!info[start_index].IsBuffer() && !info[start_index].IsArray()) {
|
|
1629
|
+
// Named parameters binding
|
|
1630
|
+
Napi::Object obj = info[start_index].As<Napi::Object>();
|
|
1631
|
+
|
|
1632
|
+
// Build bare named params map if needed
|
|
1633
|
+
if (allow_bare_named_params_ && !bare_named_params_.has_value()) {
|
|
1634
|
+
bare_named_params_.emplace();
|
|
1635
|
+
int param_count = sqlite3_bind_parameter_count(statement_);
|
|
1636
|
+
|
|
1637
|
+
// Parameter indexing starts at one
|
|
1638
|
+
for (int i = 1; i <= param_count; ++i) {
|
|
1639
|
+
const char *name = sqlite3_bind_parameter_name(statement_, i);
|
|
1640
|
+
if (name == nullptr) {
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
std::string bare_name = std::string(name + 1); // Skip the : or $ prefix
|
|
1645
|
+
std::string full_name = std::string(name);
|
|
1646
|
+
auto insertion = bare_named_params_->insert({bare_name, full_name});
|
|
1647
|
+
|
|
1648
|
+
if (!insertion.second) {
|
|
1649
|
+
// Check if the existing mapping is the same
|
|
1650
|
+
auto existing_full_name = insertion.first->second;
|
|
1651
|
+
if (full_name != existing_full_name) {
|
|
1652
|
+
std::string error_msg =
|
|
1653
|
+
"Cannot create bare named parameter '" + bare_name +
|
|
1654
|
+
"' because of conflicting names '" + existing_full_name +
|
|
1655
|
+
"' and '" + full_name + "'.";
|
|
1656
|
+
node::THROW_ERR_INVALID_STATE(env, error_msg.c_str());
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Bind named parameters
|
|
1664
|
+
Napi::Array keys = obj.GetPropertyNames();
|
|
1665
|
+
for (uint32_t j = 0; j < keys.Length(); j++) {
|
|
1666
|
+
Napi::Value key = keys[j];
|
|
1667
|
+
std::string key_str = key.As<Napi::String>().Utf8Value();
|
|
1668
|
+
|
|
1669
|
+
int param_index =
|
|
1670
|
+
sqlite3_bind_parameter_index(statement_, key_str.c_str());
|
|
1671
|
+
if (param_index == 0 && allow_bare_named_params_ &&
|
|
1672
|
+
bare_named_params_.has_value()) {
|
|
1673
|
+
// Try to find bare named parameter
|
|
1674
|
+
auto lookup = bare_named_params_->find(key_str);
|
|
1675
|
+
if (lookup != bare_named_params_->end()) {
|
|
1676
|
+
param_index =
|
|
1677
|
+
sqlite3_bind_parameter_index(statement_, lookup->second.c_str());
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (param_index > 0) {
|
|
1682
|
+
Napi::Value value = obj.Get(key_str);
|
|
1683
|
+
try {
|
|
1684
|
+
BindSingleParameter(param_index, value);
|
|
1685
|
+
} catch (const Napi::Error &e) {
|
|
1686
|
+
// Re-throw with parameter info
|
|
1687
|
+
std::string msg =
|
|
1688
|
+
"Error binding parameter '" + key_str + "': " + e.Message();
|
|
1689
|
+
node::THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str());
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
} else {
|
|
1695
|
+
// Positional parameters binding
|
|
1696
|
+
for (size_t i = start_index; i < info.Length(); i++) {
|
|
1697
|
+
int param_index = static_cast<int>(i - start_index + 1);
|
|
1698
|
+
try {
|
|
1699
|
+
BindSingleParameter(param_index, info[i]);
|
|
1700
|
+
} catch (const Napi::Error &e) {
|
|
1701
|
+
// Re-throw with parameter info
|
|
1702
|
+
std::string msg = "Error binding parameter " +
|
|
1703
|
+
std::to_string(param_index) + ": " + e.Message();
|
|
1704
|
+
node::THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str());
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
void StatementSync::BindSingleParameter(int param_index, Napi::Value param) {
|
|
1712
|
+
// Safety check - statement_ should be valid if we got here
|
|
1713
|
+
if (!statement_ || finalized_) {
|
|
1714
|
+
return; // Silent return since error was already thrown by caller
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
try {
|
|
1718
|
+
if (param.IsNull() || param.IsUndefined()) {
|
|
1719
|
+
sqlite3_bind_null(statement_, param_index);
|
|
1720
|
+
} else if (param.IsBigInt()) {
|
|
1721
|
+
// Handle BigInt before IsNumber since BigInt values should bind as int64
|
|
1722
|
+
bool lossless;
|
|
1723
|
+
int64_t bigint_val = param.As<Napi::BigInt>().Int64Value(&lossless);
|
|
1724
|
+
if (lossless) {
|
|
1725
|
+
sqlite3_bind_int64(statement_, param_index,
|
|
1726
|
+
static_cast<sqlite3_int64>(bigint_val));
|
|
1727
|
+
} else {
|
|
1728
|
+
// BigInt too large, convert to text
|
|
1729
|
+
std::string bigint_str =
|
|
1730
|
+
param.As<Napi::BigInt>().ToString().Utf8Value();
|
|
1731
|
+
sqlite3_bind_text(statement_, param_index, bigint_str.c_str(), -1,
|
|
1732
|
+
SQLITE_TRANSIENT);
|
|
1733
|
+
}
|
|
1734
|
+
} else if (param.IsNumber()) {
|
|
1735
|
+
double val = param.As<Napi::Number>().DoubleValue();
|
|
1736
|
+
if (val == std::floor(val) && val >= INT32_MIN && val <= INT32_MAX) {
|
|
1737
|
+
sqlite3_bind_int(statement_, param_index,
|
|
1738
|
+
param.As<Napi::Number>().Int32Value());
|
|
1739
|
+
} else {
|
|
1740
|
+
sqlite3_bind_double(statement_, param_index,
|
|
1741
|
+
param.As<Napi::Number>().DoubleValue());
|
|
1742
|
+
}
|
|
1743
|
+
} else if (param.IsString()) {
|
|
1744
|
+
std::string str = param.As<Napi::String>().Utf8Value();
|
|
1745
|
+
sqlite3_bind_text(statement_, param_index, str.c_str(), -1,
|
|
1746
|
+
SQLITE_TRANSIENT);
|
|
1747
|
+
} else if (param.IsBoolean()) {
|
|
1748
|
+
sqlite3_bind_int(statement_, param_index,
|
|
1749
|
+
param.As<Napi::Boolean>().Value() ? 1 : 0);
|
|
1750
|
+
} else if (param.IsBuffer()) {
|
|
1751
|
+
Napi::Buffer<uint8_t> buffer = param.As<Napi::Buffer<uint8_t>>();
|
|
1752
|
+
sqlite3_bind_blob(statement_, param_index, buffer.Data(),
|
|
1753
|
+
static_cast<int>(buffer.Length()), SQLITE_TRANSIENT);
|
|
1754
|
+
} else if (param.IsFunction()) {
|
|
1755
|
+
// Functions cannot be stored in SQLite - bind as NULL
|
|
1756
|
+
sqlite3_bind_null(statement_, param_index);
|
|
1757
|
+
} else if (param.IsObject()) {
|
|
1758
|
+
// Try to convert object to string
|
|
1759
|
+
Napi::String str_value = param.ToString();
|
|
1760
|
+
std::string str = str_value.Utf8Value();
|
|
1761
|
+
sqlite3_bind_text(statement_, param_index, str.c_str(), -1,
|
|
1762
|
+
SQLITE_TRANSIENT);
|
|
1763
|
+
} else {
|
|
1764
|
+
// For any other type, bind as NULL
|
|
1765
|
+
sqlite3_bind_null(statement_, param_index);
|
|
1766
|
+
}
|
|
1767
|
+
} catch (const Napi::Error &e) {
|
|
1768
|
+
// Re-throw Napi errors
|
|
1769
|
+
throw;
|
|
1770
|
+
} catch (const std::exception &e) {
|
|
1771
|
+
// Convert standard exceptions to Napi errors
|
|
1772
|
+
throw Napi::Error::New(Env(), e.what());
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
Napi::Value StatementSync::CreateResult() {
|
|
1777
|
+
Napi::Env env = Env();
|
|
1778
|
+
|
|
1779
|
+
// Safety checks
|
|
1780
|
+
if (!statement_ || finalized_) {
|
|
1781
|
+
node::THROW_ERR_INVALID_STATE(env, "Statement has been finalized");
|
|
1782
|
+
return env.Undefined();
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
if (!database_ || !database_->IsOpen()) {
|
|
1786
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
1787
|
+
return env.Undefined();
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
int column_count = sqlite3_column_count(statement_);
|
|
1791
|
+
|
|
1792
|
+
if (return_arrays_) {
|
|
1793
|
+
// Return result as array when returnArrays is true
|
|
1794
|
+
Napi::Array result = Napi::Array::New(env, column_count);
|
|
1795
|
+
|
|
1796
|
+
for (int i = 0; i < column_count; i++) {
|
|
1797
|
+
int column_type = sqlite3_column_type(statement_, i);
|
|
1798
|
+
Napi::Value value;
|
|
1799
|
+
|
|
1800
|
+
switch (column_type) {
|
|
1801
|
+
case SQLITE_NULL:
|
|
1802
|
+
value = env.Null();
|
|
1803
|
+
break;
|
|
1804
|
+
case SQLITE_INTEGER: {
|
|
1805
|
+
sqlite3_int64 int_val = sqlite3_column_int64(statement_, i);
|
|
1806
|
+
if (use_big_ints_) {
|
|
1807
|
+
// Always return BigInt when readBigInts is true
|
|
1808
|
+
value = Napi::BigInt::New(env, static_cast<int64_t>(int_val));
|
|
1809
|
+
} else if (int_val > JS_MAX_SAFE_INTEGER ||
|
|
1810
|
+
int_val < JS_MIN_SAFE_INTEGER) {
|
|
1811
|
+
// Return BigInt for values outside JavaScript's safe integer range
|
|
1812
|
+
value = Napi::BigInt::New(env, static_cast<int64_t>(int_val));
|
|
1813
|
+
} else {
|
|
1814
|
+
value = Napi::Number::New(env, static_cast<double>(int_val));
|
|
1815
|
+
}
|
|
1816
|
+
break;
|
|
1817
|
+
}
|
|
1818
|
+
case SQLITE_FLOAT:
|
|
1819
|
+
value = Napi::Number::New(env, sqlite3_column_double(statement_, i));
|
|
1820
|
+
break;
|
|
1821
|
+
case SQLITE_TEXT: {
|
|
1822
|
+
const unsigned char *text = sqlite3_column_text(statement_, i);
|
|
1823
|
+
value = Napi::String::New(env, reinterpret_cast<const char *>(text));
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
case SQLITE_BLOB: {
|
|
1827
|
+
const void *blob_data = sqlite3_column_blob(statement_, i);
|
|
1828
|
+
int blob_size = sqlite3_column_bytes(statement_, i);
|
|
1829
|
+
value = Napi::Buffer<uint8_t>::Copy(
|
|
1830
|
+
env, static_cast<const uint8_t *>(blob_data), blob_size);
|
|
1831
|
+
break;
|
|
1832
|
+
}
|
|
1833
|
+
default:
|
|
1834
|
+
value = env.Null();
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
result.Set(i, value);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
return result;
|
|
1842
|
+
} else {
|
|
1843
|
+
// Return result as object (default behavior)
|
|
1844
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1845
|
+
|
|
1846
|
+
for (int i = 0; i < column_count; i++) {
|
|
1847
|
+
const char *column_name = sqlite3_column_name(statement_, i);
|
|
1848
|
+
int column_type = sqlite3_column_type(statement_, i);
|
|
1849
|
+
|
|
1850
|
+
Napi::Value value;
|
|
1851
|
+
|
|
1852
|
+
switch (column_type) {
|
|
1853
|
+
case SQLITE_NULL:
|
|
1854
|
+
value = env.Null();
|
|
1855
|
+
break;
|
|
1856
|
+
case SQLITE_INTEGER: {
|
|
1857
|
+
sqlite3_int64 int_val = sqlite3_column_int64(statement_, i);
|
|
1858
|
+
if (use_big_ints_) {
|
|
1859
|
+
// Always return BigInt when readBigInts is true
|
|
1860
|
+
value = Napi::BigInt::New(env, static_cast<int64_t>(int_val));
|
|
1861
|
+
} else if (int_val > JS_MAX_SAFE_INTEGER ||
|
|
1862
|
+
int_val < JS_MIN_SAFE_INTEGER) {
|
|
1863
|
+
// Return BigInt for values outside JavaScript's safe integer range
|
|
1864
|
+
value = Napi::BigInt::New(env, static_cast<int64_t>(int_val));
|
|
1865
|
+
} else {
|
|
1866
|
+
value = Napi::Number::New(env, static_cast<double>(int_val));
|
|
1867
|
+
}
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
case SQLITE_FLOAT:
|
|
1871
|
+
value = Napi::Number::New(env, sqlite3_column_double(statement_, i));
|
|
1872
|
+
break;
|
|
1873
|
+
case SQLITE_TEXT: {
|
|
1874
|
+
const unsigned char *text = sqlite3_column_text(statement_, i);
|
|
1875
|
+
value = Napi::String::New(env, reinterpret_cast<const char *>(text));
|
|
1876
|
+
break;
|
|
1877
|
+
}
|
|
1878
|
+
case SQLITE_BLOB: {
|
|
1879
|
+
const void *blob_data = sqlite3_column_blob(statement_, i);
|
|
1880
|
+
int blob_size = sqlite3_column_bytes(statement_, i);
|
|
1881
|
+
value = Napi::Buffer<uint8_t>::Copy(
|
|
1882
|
+
env, static_cast<const uint8_t *>(blob_data), blob_size);
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
default:
|
|
1886
|
+
value = env.Null();
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
result.Set(column_name, value);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
return result;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
void StatementSync::Reset() {
|
|
1898
|
+
// Safety check
|
|
1899
|
+
if (!statement_ || finalized_) {
|
|
1900
|
+
return; // Silent return, error should have been caught earlier
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
sqlite3_reset(statement_);
|
|
1904
|
+
sqlite3_clear_bindings(statement_);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// ================================
|
|
1908
|
+
// StatementSyncIterator Implementation
|
|
1909
|
+
// ================================
|
|
1910
|
+
|
|
1911
|
+
Napi::Object StatementSyncIterator::Init(Napi::Env env, Napi::Object exports) {
|
|
1912
|
+
Napi::Function func =
|
|
1913
|
+
DefineClass(env, "StatementSyncIterator",
|
|
1914
|
+
{InstanceMethod("next", &StatementSyncIterator::Next),
|
|
1915
|
+
InstanceMethod("return", &StatementSyncIterator::Return)});
|
|
1916
|
+
|
|
1917
|
+
// Set up Symbol.iterator on the prototype to make it properly iterable
|
|
1918
|
+
Napi::Object prototype = func.Get("prototype").As<Napi::Object>();
|
|
1919
|
+
Napi::Symbol iteratorSymbol = Napi::Symbol::WellKnown(env, "iterator");
|
|
1920
|
+
|
|
1921
|
+
// Add [Symbol.iterator]() { return this; } to make it iterable
|
|
1922
|
+
prototype.Set(iteratorSymbol,
|
|
1923
|
+
Napi::Function::New(env, [](const Napi::CallbackInfo &info) {
|
|
1924
|
+
return info.This();
|
|
1925
|
+
}));
|
|
1926
|
+
|
|
1927
|
+
// Store constructor in per-instance addon data instead of static variable
|
|
1928
|
+
AddonData *addon_data = GetAddonData(env);
|
|
1929
|
+
if (addon_data) {
|
|
1930
|
+
addon_data->statementSyncIteratorConstructor =
|
|
1931
|
+
Napi::Reference<Napi::Function>::New(func);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
exports.Set("StatementSyncIterator", func);
|
|
1935
|
+
return exports;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
Napi::Object StatementSyncIterator::Create(Napi::Env env, StatementSync *stmt) {
|
|
1939
|
+
AddonData *addon_data = GetAddonData(env);
|
|
1940
|
+
if (!addon_data || addon_data->statementSyncIteratorConstructor.IsEmpty()) {
|
|
1941
|
+
Napi::Error::New(env, "StatementSyncIterator constructor not initialized")
|
|
1942
|
+
.ThrowAsJavaScriptException();
|
|
1943
|
+
return Napi::Object::New(env);
|
|
1944
|
+
}
|
|
1945
|
+
Napi::Object obj = addon_data->statementSyncIteratorConstructor.New({});
|
|
1946
|
+
StatementSyncIterator *iter =
|
|
1947
|
+
Napi::ObjectWrap<StatementSyncIterator>::Unwrap(obj);
|
|
1948
|
+
iter->SetStatement(stmt);
|
|
1949
|
+
return obj;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
StatementSyncIterator::StatementSyncIterator(const Napi::CallbackInfo &info)
|
|
1953
|
+
: Napi::ObjectWrap<StatementSyncIterator>(info), stmt_(nullptr),
|
|
1954
|
+
done_(false) {}
|
|
1955
|
+
|
|
1956
|
+
StatementSyncIterator::~StatementSyncIterator() {}
|
|
1957
|
+
|
|
1958
|
+
void StatementSyncIterator::SetStatement(StatementSync *stmt) {
|
|
1959
|
+
stmt_ = stmt;
|
|
1960
|
+
done_ = false;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
Napi::Value StatementSyncIterator::Next(const Napi::CallbackInfo &info) {
|
|
1964
|
+
Napi::Env env = info.Env();
|
|
1965
|
+
|
|
1966
|
+
if (!stmt_ || stmt_->finalized_) {
|
|
1967
|
+
node::THROW_ERR_INVALID_STATE(env, "statement has been finalized");
|
|
1968
|
+
return env.Undefined();
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
if (!stmt_->database_ || !stmt_->database_->IsOpen()) {
|
|
1972
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
1973
|
+
return env.Undefined();
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (done_) {
|
|
1977
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1978
|
+
result.Set("done", true);
|
|
1979
|
+
result.Set("value", env.Null());
|
|
1980
|
+
return result;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
int r = sqlite3_step(stmt_->statement_);
|
|
1984
|
+
|
|
1985
|
+
if (r != SQLITE_ROW) {
|
|
1986
|
+
if (r != SQLITE_DONE) {
|
|
1987
|
+
node::THROW_ERR_SQLITE_ERROR(
|
|
1988
|
+
env, sqlite3_errmsg(stmt_->database_->connection()));
|
|
1989
|
+
return env.Undefined();
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// End of results
|
|
1993
|
+
sqlite3_reset(stmt_->statement_);
|
|
1994
|
+
done_ = true;
|
|
1995
|
+
|
|
1996
|
+
Napi::Object result = Napi::Object::New(env);
|
|
1997
|
+
result.Set("done", true);
|
|
1998
|
+
result.Set("value", env.Null());
|
|
1999
|
+
return result;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// Create row object using existing CreateResult method
|
|
2003
|
+
Napi::Value row_value = stmt_->CreateResult();
|
|
2004
|
+
|
|
2005
|
+
Napi::Object result = Napi::Object::New(env);
|
|
2006
|
+
result.Set("done", false);
|
|
2007
|
+
result.Set("value", row_value);
|
|
2008
|
+
return result;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
Napi::Value StatementSyncIterator::Return(const Napi::CallbackInfo &info) {
|
|
2012
|
+
Napi::Env env = info.Env();
|
|
2013
|
+
|
|
2014
|
+
if (!stmt_ || stmt_->finalized_) {
|
|
2015
|
+
node::THROW_ERR_INVALID_STATE(env, "statement has been finalized");
|
|
2016
|
+
return env.Undefined();
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
if (!stmt_->database_ || !stmt_->database_->IsOpen()) {
|
|
2020
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
2021
|
+
return env.Undefined();
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// Reset the statement and mark as done
|
|
2025
|
+
sqlite3_reset(stmt_->statement_);
|
|
2026
|
+
done_ = true;
|
|
2027
|
+
|
|
2028
|
+
Napi::Object result = Napi::Object::New(env);
|
|
2029
|
+
result.Set("done", true);
|
|
2030
|
+
result.Set("value", env.Null());
|
|
2031
|
+
return result;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Session Implementation
|
|
2035
|
+
Napi::Object Session::Init(Napi::Env env, Napi::Object exports) {
|
|
2036
|
+
Napi::Function func =
|
|
2037
|
+
DefineClass(env, "Session",
|
|
2038
|
+
{InstanceMethod("changeset", &Session::Changeset),
|
|
2039
|
+
InstanceMethod("patchset", &Session::Patchset),
|
|
2040
|
+
InstanceMethod("close", &Session::Close)});
|
|
2041
|
+
|
|
2042
|
+
// Store constructor in per-instance addon data instead of static variable
|
|
2043
|
+
AddonData *addon_data = GetAddonData(env);
|
|
2044
|
+
if (addon_data) {
|
|
2045
|
+
addon_data->sessionConstructor = Napi::Reference<Napi::Function>::New(func);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
exports.Set("Session", func);
|
|
2049
|
+
return exports;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
Napi::Object Session::Create(Napi::Env env, DatabaseSync *database,
|
|
2053
|
+
sqlite3_session *session) {
|
|
2054
|
+
AddonData *addon_data = GetAddonData(env);
|
|
2055
|
+
if (!addon_data || addon_data->sessionConstructor.IsEmpty()) {
|
|
2056
|
+
Napi::Error::New(env, "Session constructor not initialized")
|
|
2057
|
+
.ThrowAsJavaScriptException();
|
|
2058
|
+
return Napi::Object::New(env);
|
|
2059
|
+
}
|
|
2060
|
+
Napi::Object obj = addon_data->sessionConstructor.New({});
|
|
2061
|
+
Session *sess = Napi::ObjectWrap<Session>::Unwrap(obj);
|
|
2062
|
+
sess->SetSession(database, session);
|
|
2063
|
+
return obj;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
Session::Session(const Napi::CallbackInfo &info)
|
|
2067
|
+
: Napi::ObjectWrap<Session>(info), session_(nullptr) {}
|
|
2068
|
+
|
|
2069
|
+
Session::~Session() { Delete(); }
|
|
2070
|
+
|
|
2071
|
+
void Session::SetSession(DatabaseSync *database, sqlite3_session *session) {
|
|
2072
|
+
database_ = database;
|
|
2073
|
+
session_ = session;
|
|
2074
|
+
if (database_) {
|
|
2075
|
+
database_->AddSession(this);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
void Session::Delete() {
|
|
2080
|
+
if (session_ == nullptr)
|
|
2081
|
+
return;
|
|
2082
|
+
|
|
2083
|
+
// Store the session pointer and clear our member immediately
|
|
2084
|
+
// to prevent double-delete
|
|
2085
|
+
sqlite3_session *session_to_delete = session_;
|
|
2086
|
+
session_ = nullptr;
|
|
2087
|
+
|
|
2088
|
+
// Remove ourselves from the database's session list BEFORE deleting
|
|
2089
|
+
// to avoid any potential issues with the database trying to access us
|
|
2090
|
+
DatabaseSync *database = database_;
|
|
2091
|
+
database_ = nullptr;
|
|
2092
|
+
|
|
2093
|
+
if (database) {
|
|
2094
|
+
database->RemoveSession(this);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// Now it's safe to delete the SQLite session
|
|
2098
|
+
sqlite3session_delete(session_to_delete);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
template <int (*sqliteChangesetFunc)(sqlite3_session *, int *, void **)>
|
|
2102
|
+
Napi::Value Session::GenericChangeset(const Napi::CallbackInfo &info) {
|
|
2103
|
+
Napi::Env env = info.Env();
|
|
2104
|
+
|
|
2105
|
+
if (session_ == nullptr) {
|
|
2106
|
+
node::THROW_ERR_INVALID_STATE(env, "session is not open");
|
|
2107
|
+
return env.Undefined();
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
if (!database_ || !database_->IsOpen()) {
|
|
2111
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
2112
|
+
return env.Undefined();
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
int nChangeset;
|
|
2116
|
+
void *pChangeset;
|
|
2117
|
+
int r = sqliteChangesetFunc(session_, &nChangeset, &pChangeset);
|
|
2118
|
+
|
|
2119
|
+
if (r != SQLITE_OK) {
|
|
2120
|
+
const char *errMsg = sqlite3_errmsg(database_->connection());
|
|
2121
|
+
Napi::Error::New(env,
|
|
2122
|
+
std::string("Failed to generate changeset: ") + errMsg)
|
|
2123
|
+
.ThrowAsJavaScriptException();
|
|
2124
|
+
return env.Undefined();
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// Create a Buffer from the changeset data
|
|
2128
|
+
Napi::Buffer<uint8_t> buffer = Napi::Buffer<uint8_t>::New(env, nChangeset);
|
|
2129
|
+
std::memcpy(buffer.Data(), pChangeset, nChangeset);
|
|
2130
|
+
|
|
2131
|
+
// Free the changeset allocated by SQLite
|
|
2132
|
+
sqlite3_free(pChangeset);
|
|
2133
|
+
|
|
2134
|
+
return buffer;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
Napi::Value Session::Changeset(const Napi::CallbackInfo &info) {
|
|
2138
|
+
return GenericChangeset<sqlite3session_changeset>(info);
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
Napi::Value Session::Patchset(const Napi::CallbackInfo &info) {
|
|
2142
|
+
return GenericChangeset<sqlite3session_patchset>(info);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
Napi::Value Session::Close(const Napi::CallbackInfo &info) {
|
|
2146
|
+
Napi::Env env = info.Env();
|
|
2147
|
+
|
|
2148
|
+
if (session_ == nullptr) {
|
|
2149
|
+
node::THROW_ERR_INVALID_STATE(env, "session is not open");
|
|
2150
|
+
return env.Undefined();
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
Delete();
|
|
2154
|
+
return env.Undefined();
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// Static members for tracking active jobs
|
|
2158
|
+
std::atomic<int> BackupJob::active_jobs_(0);
|
|
2159
|
+
std::mutex BackupJob::active_jobs_mutex_;
|
|
2160
|
+
std::set<BackupJob *> BackupJob::active_job_instances_;
|
|
2161
|
+
|
|
2162
|
+
// BackupJob Implementation
|
|
2163
|
+
BackupJob::BackupJob(Napi::Env env, DatabaseSync *source,
|
|
2164
|
+
const std::string &destination_path,
|
|
2165
|
+
const std::string &source_db, const std::string &dest_db,
|
|
2166
|
+
int pages, Napi::Function progress_func,
|
|
2167
|
+
Napi::Promise::Deferred deferred)
|
|
2168
|
+
: Napi::AsyncProgressWorker<BackupProgress>(
|
|
2169
|
+
!progress_func.IsEmpty() && !progress_func.IsUndefined()
|
|
2170
|
+
? progress_func
|
|
2171
|
+
: Napi::Function::New(env, [](const Napi::CallbackInfo &) {})),
|
|
2172
|
+
source_(source), destination_path_(destination_path),
|
|
2173
|
+
source_db_(source_db), dest_db_(dest_db), pages_(pages),
|
|
2174
|
+
deferred_(deferred) {
|
|
2175
|
+
if (!progress_func.IsEmpty() && !progress_func.IsUndefined()) {
|
|
2176
|
+
progress_func_ = Napi::Reference<Napi::Function>::New(progress_func);
|
|
2177
|
+
}
|
|
2178
|
+
active_jobs_++;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
BackupJob::~BackupJob() { active_jobs_--; }
|
|
2182
|
+
|
|
2183
|
+
void BackupJob::Execute(const ExecutionProgress &progress) {
|
|
2184
|
+
// This method is executed on a worker thread, not the main thread
|
|
2185
|
+
// Note: SQLite backup operations are thread-safe when the source database
|
|
2186
|
+
// is only being read. The backup API creates its own read transaction
|
|
2187
|
+
// and can safely operate across threads.
|
|
2188
|
+
|
|
2189
|
+
backup_status_ = sqlite3_open_v2(
|
|
2190
|
+
destination_path_.c_str(), &dest_,
|
|
2191
|
+
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI, nullptr);
|
|
2192
|
+
|
|
2193
|
+
if (backup_status_ != SQLITE_OK) {
|
|
2194
|
+
SetError("Failed to open destination database");
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// Initialize backup
|
|
2199
|
+
backup_ = sqlite3_backup_init(dest_, dest_db_.c_str(), source_->connection(),
|
|
2200
|
+
source_db_.c_str());
|
|
2201
|
+
|
|
2202
|
+
if (!backup_) {
|
|
2203
|
+
SetError("Failed to initialize backup");
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// Initial page count may be 0 until first step
|
|
2208
|
+
int remaining_pages = sqlite3_backup_remaining(backup_);
|
|
2209
|
+
total_pages_ = 0; // Will be updated after first step
|
|
2210
|
+
|
|
2211
|
+
while ((remaining_pages > 0 || total_pages_ == 0) &&
|
|
2212
|
+
backup_status_ == SQLITE_OK) {
|
|
2213
|
+
// If pages_ is negative, use -1 to copy all remaining pages
|
|
2214
|
+
int pages_to_copy = pages_ < 0 ? -1 : pages_;
|
|
2215
|
+
backup_status_ = sqlite3_backup_step(backup_, pages_to_copy);
|
|
2216
|
+
|
|
2217
|
+
// Update total pages after first step (when SQLite knows the actual count)
|
|
2218
|
+
if (total_pages_ == 0) {
|
|
2219
|
+
total_pages_ = sqlite3_backup_pagecount(backup_);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
if (backup_status_ == SQLITE_OK || backup_status_ == SQLITE_DONE) {
|
|
2223
|
+
remaining_pages = sqlite3_backup_remaining(backup_);
|
|
2224
|
+
int current_page = total_pages_ - remaining_pages;
|
|
2225
|
+
|
|
2226
|
+
// Send progress update to main thread
|
|
2227
|
+
if (!progress_func_.IsEmpty() && total_pages_ > 0) {
|
|
2228
|
+
BackupProgress prog = {current_page, total_pages_};
|
|
2229
|
+
progress.Send(&prog, 1);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// Check if we're done
|
|
2233
|
+
if (backup_status_ == SQLITE_DONE) {
|
|
2234
|
+
break;
|
|
2235
|
+
}
|
|
2236
|
+
} else if (backup_status_ == SQLITE_BUSY ||
|
|
2237
|
+
backup_status_ == SQLITE_LOCKED) {
|
|
2238
|
+
// These are retryable errors - continue
|
|
2239
|
+
backup_status_ = SQLITE_OK;
|
|
2240
|
+
} else {
|
|
2241
|
+
// Fatal error
|
|
2242
|
+
break;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// Store final status for use in OnOK/OnError
|
|
2247
|
+
if (backup_status_ != SQLITE_DONE) {
|
|
2248
|
+
std::string error = "Backup failed with SQLite error: ";
|
|
2249
|
+
error += sqlite3_errmsg(dest_);
|
|
2250
|
+
SetError(error);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
void BackupJob::OnProgress(const BackupProgress *data, size_t count) {
|
|
2255
|
+
// This runs on the main thread
|
|
2256
|
+
if (!progress_func_.IsEmpty() && count > 0) {
|
|
2257
|
+
Napi::HandleScope scope(Env());
|
|
2258
|
+
Napi::Function progress_fn = progress_func_.Value();
|
|
2259
|
+
Napi::Object progress_info = Napi::Object::New(Env());
|
|
2260
|
+
progress_info.Set("totalPages", Napi::Number::New(Env(), data->total));
|
|
2261
|
+
progress_info.Set("remainingPages",
|
|
2262
|
+
Napi::Number::New(Env(), data->total - data->current));
|
|
2263
|
+
|
|
2264
|
+
try {
|
|
2265
|
+
progress_fn.Call(Env().Null(), {progress_info});
|
|
2266
|
+
} catch (...) {
|
|
2267
|
+
// Ignore errors in progress callback
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
void BackupJob::OnOK() {
|
|
2273
|
+
// This runs on the main thread after Execute completes successfully
|
|
2274
|
+
Napi::HandleScope scope(Env());
|
|
2275
|
+
|
|
2276
|
+
// Cleanup SQLite resources
|
|
2277
|
+
Cleanup();
|
|
2278
|
+
|
|
2279
|
+
// Resolve the promise with the total number of pages
|
|
2280
|
+
deferred_.Resolve(Napi::Number::New(Env(), total_pages_));
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
void BackupJob::OnError(const Napi::Error &error) {
|
|
2284
|
+
// This runs on the main thread if Execute encounters an error
|
|
2285
|
+
Napi::HandleScope scope(Env());
|
|
2286
|
+
|
|
2287
|
+
// Cleanup SQLite resources
|
|
2288
|
+
Cleanup();
|
|
2289
|
+
|
|
2290
|
+
// Create a more detailed error if we have SQLite error info
|
|
2291
|
+
if (dest_ && backup_status_ != SQLITE_OK) {
|
|
2292
|
+
Napi::Error detailed_error = Napi::Error::New(Env(), error.Message());
|
|
2293
|
+
detailed_error.Set(
|
|
2294
|
+
"code", Napi::String::New(Env(), sqlite3_errstr(backup_status_)));
|
|
2295
|
+
detailed_error.Set("errno", Napi::Number::New(Env(), backup_status_));
|
|
2296
|
+
deferred_.Reject(detailed_error.Value());
|
|
2297
|
+
} else {
|
|
2298
|
+
deferred_.Reject(error.Value());
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// HandleBackupError method removed - error handling now done in OnError
|
|
2303
|
+
|
|
2304
|
+
void BackupJob::Cleanup() {
|
|
2305
|
+
if (backup_) {
|
|
2306
|
+
sqlite3_backup_finish(backup_);
|
|
2307
|
+
backup_ = nullptr;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
if (dest_) {
|
|
2311
|
+
backup_status_ = sqlite3_errcode(dest_);
|
|
2312
|
+
sqlite3_close_v2(dest_);
|
|
2313
|
+
dest_ = nullptr;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// DatabaseSync::Backup implementation
|
|
2318
|
+
Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
|
|
2319
|
+
Napi::Env env = info.Env();
|
|
2320
|
+
|
|
2321
|
+
// Create a promise early for error handling
|
|
2322
|
+
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
|
|
2323
|
+
|
|
2324
|
+
if (!IsOpen()) {
|
|
2325
|
+
deferred.Reject(Napi::Error::New(env, "database is not open").Value());
|
|
2326
|
+
return deferred.Promise();
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
if (info.Length() < 1) {
|
|
2330
|
+
deferred.Reject(
|
|
2331
|
+
Napi::TypeError::New(env, "The \"destination\" argument is required")
|
|
2332
|
+
.Value());
|
|
2333
|
+
return deferred.Promise();
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
std::optional<std::string> destination_path =
|
|
2337
|
+
ValidateDatabasePath(env, info[0], "destination");
|
|
2338
|
+
if (!destination_path.has_value()) {
|
|
2339
|
+
deferred.Reject(Napi::Error::New(env, "Invalid destination path").Value());
|
|
2340
|
+
return deferred.Promise();
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// Default options matching Node.js API
|
|
2344
|
+
int rate = 100;
|
|
2345
|
+
std::string source_db = "main";
|
|
2346
|
+
std::string target_db = "main";
|
|
2347
|
+
Napi::Function progress_func;
|
|
2348
|
+
|
|
2349
|
+
// Parse options if provided
|
|
2350
|
+
if (info.Length() > 1) {
|
|
2351
|
+
if (!info[1].IsObject()) {
|
|
2352
|
+
deferred.Reject(Napi::TypeError::New(
|
|
2353
|
+
env, "The \"options\" argument must be an object")
|
|
2354
|
+
.Value());
|
|
2355
|
+
return deferred.Promise();
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
Napi::Object options = info[1].As<Napi::Object>();
|
|
2359
|
+
|
|
2360
|
+
// Get rate option (number of pages per step)
|
|
2361
|
+
Napi::Value rate_value = options.Get("rate");
|
|
2362
|
+
if (!rate_value.IsUndefined()) {
|
|
2363
|
+
if (!rate_value.IsNumber()) {
|
|
2364
|
+
deferred.Reject(
|
|
2365
|
+
Napi::TypeError::New(env, "The \"options.rate\" must be a number")
|
|
2366
|
+
.Value());
|
|
2367
|
+
return deferred.Promise();
|
|
2368
|
+
}
|
|
2369
|
+
rate = rate_value.As<Napi::Number>().Int32Value();
|
|
2370
|
+
// Note: Node.js allows negative values for rate
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// Get source database option
|
|
2374
|
+
Napi::Value source_value = options.Get("source");
|
|
2375
|
+
if (!source_value.IsUndefined()) {
|
|
2376
|
+
if (!source_value.IsString()) {
|
|
2377
|
+
deferred.Reject(
|
|
2378
|
+
Napi::TypeError::New(env, "The \"options.source\" must be a string")
|
|
2379
|
+
.Value());
|
|
2380
|
+
return deferred.Promise();
|
|
2381
|
+
}
|
|
2382
|
+
source_db = source_value.As<Napi::String>().Utf8Value();
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// Get target database option
|
|
2386
|
+
Napi::Value target_value = options.Get("target");
|
|
2387
|
+
if (!target_value.IsUndefined()) {
|
|
2388
|
+
if (!target_value.IsString()) {
|
|
2389
|
+
deferred.Reject(
|
|
2390
|
+
Napi::TypeError::New(env, "The \"options.target\" must be a string")
|
|
2391
|
+
.Value());
|
|
2392
|
+
return deferred.Promise();
|
|
2393
|
+
}
|
|
2394
|
+
target_db = target_value.As<Napi::String>().Utf8Value();
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// Get progress callback
|
|
2398
|
+
Napi::Value progress_value = options.Get("progress");
|
|
2399
|
+
if (!progress_value.IsUndefined()) {
|
|
2400
|
+
if (!progress_value.IsFunction()) {
|
|
2401
|
+
deferred.Reject(Napi::TypeError::New(
|
|
2402
|
+
env, "The \"options.progress\" must be a function")
|
|
2403
|
+
.Value());
|
|
2404
|
+
return deferred.Promise();
|
|
2405
|
+
}
|
|
2406
|
+
progress_func = progress_value.As<Napi::Function>();
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// Create and schedule backup job
|
|
2411
|
+
BackupJob *job = new BackupJob(env, this, destination_path.value(), source_db,
|
|
2412
|
+
target_db, rate, progress_func, deferred);
|
|
2413
|
+
|
|
2414
|
+
// Queue the async work - AsyncWorker will delete itself when complete
|
|
2415
|
+
job->Queue();
|
|
2416
|
+
|
|
2417
|
+
return deferred.Promise();
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// Thread validation implementations
|
|
2421
|
+
bool DatabaseSync::ValidateThread(Napi::Env env) const {
|
|
2422
|
+
if (std::this_thread::get_id() != creation_thread_) {
|
|
2423
|
+
node::THROW_ERR_INVALID_STATE(
|
|
2424
|
+
env, "Database connection cannot be used from different thread");
|
|
2425
|
+
return false;
|
|
2426
|
+
}
|
|
2427
|
+
return true;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
bool StatementSync::ValidateThread(Napi::Env env) const {
|
|
2431
|
+
if (std::this_thread::get_id() != creation_thread_) {
|
|
2432
|
+
node::THROW_ERR_INVALID_STATE(
|
|
2433
|
+
env, "Statement cannot be used from different thread");
|
|
2434
|
+
return false;
|
|
2435
|
+
}
|
|
2436
|
+
return true;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
} // namespace sqlite
|
|
2440
|
+
} // namespace photostructure
|