@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +522 -0
  4. package/SECURITY.md +114 -0
  5. package/binding.gyp +94 -0
  6. package/dist/index.cjs +134 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.cts +408 -0
  9. package/dist/index.d.mts +408 -0
  10. package/dist/index.d.ts +408 -0
  11. package/dist/index.mjs +103 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/package.json +144 -0
  14. package/prebuilds/darwin-arm64/@photostructure+sqlite.glibc.node +0 -0
  15. package/prebuilds/linux-arm64/@photostructure+sqlite.glibc.node +0 -0
  16. package/prebuilds/linux-arm64/@photostructure+sqlite.musl.node +0 -0
  17. package/prebuilds/linux-x64/@photostructure+sqlite.glibc.node +0 -0
  18. package/prebuilds/linux-x64/@photostructure+sqlite.musl.node +0 -0
  19. package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
  20. package/scripts/post-build.mjs +21 -0
  21. package/scripts/prebuild-linux-glibc.sh +108 -0
  22. package/src/aggregate_function.cpp +417 -0
  23. package/src/aggregate_function.h +116 -0
  24. package/src/binding.cpp +160 -0
  25. package/src/dirname.ts +13 -0
  26. package/src/index.ts +465 -0
  27. package/src/shims/base_object-inl.h +8 -0
  28. package/src/shims/base_object.h +50 -0
  29. package/src/shims/debug_utils-inl.h +23 -0
  30. package/src/shims/env-inl.h +19 -0
  31. package/src/shims/memory_tracker-inl.h +17 -0
  32. package/src/shims/napi_extensions.h +73 -0
  33. package/src/shims/node.h +16 -0
  34. package/src/shims/node_errors.h +66 -0
  35. package/src/shims/node_mem-inl.h +8 -0
  36. package/src/shims/node_mem.h +31 -0
  37. package/src/shims/node_url.h +23 -0
  38. package/src/shims/promise_resolver.h +31 -0
  39. package/src/shims/util-inl.h +18 -0
  40. package/src/shims/util.h +101 -0
  41. package/src/sqlite_impl.cpp +2440 -0
  42. package/src/sqlite_impl.h +314 -0
  43. package/src/stack_path.ts +64 -0
  44. package/src/types/node-gyp-build.d.ts +4 -0
  45. package/src/upstream/node_sqlite.cc +2706 -0
  46. package/src/upstream/node_sqlite.h +234 -0
  47. package/src/upstream/sqlite.gyp +38 -0
  48. package/src/upstream/sqlite.js +19 -0
  49. package/src/upstream/sqlite3.c +262809 -0
  50. package/src/upstream/sqlite3.h +13773 -0
  51. package/src/upstream/sqlite3ext.h +723 -0
  52. package/src/user_function.cpp +225 -0
  53. 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