@photostructure/sqlite 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -15
- package/README.md +5 -4
- package/binding.gyp +2 -2
- package/dist/index.cjs +159 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +286 -91
- package/dist/index.d.mts +286 -91
- package/dist/index.d.ts +286 -91
- package/dist/index.mjs +156 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +74 -65
- package/prebuilds/darwin-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/darwin-x64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+sqlite.musl.node +0 -0
- package/prebuilds/linux-x64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-x64/@photostructure+sqlite.musl.node +0 -0
- package/prebuilds/test_extension.so +0 -0
- package/prebuilds/win32-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
- package/scripts/prebuild-linux-glibc.sh +6 -4
- package/src/aggregate_function.cpp +222 -114
- package/src/aggregate_function.h +5 -6
- package/src/binding.cpp +30 -21
- package/src/enhance.ts +228 -0
- package/src/index.ts +83 -9
- package/src/shims/node_errors.h +34 -15
- package/src/shims/sqlite_errors.h +34 -8
- package/src/sql-tag-store.ts +7 -10
- package/src/sqlite_impl.cpp +1044 -394
- package/src/sqlite_impl.h +46 -7
- package/src/transaction.ts +178 -0
- package/src/types/database-sync-instance.ts +6 -40
- package/src/types/pragma-options.ts +23 -0
- package/src/types/sql-tag-store-instance.ts +1 -1
- package/src/types/statement-sync-instance.ts +38 -12
- package/src/types/transaction.ts +72 -0
- package/src/upstream/node_sqlite.cc +143 -43
- package/src/upstream/node_sqlite.h +15 -11
- package/src/upstream/sqlite3.c +102 -58
- package/src/upstream/sqlite3.h +5 -5
- package/src/user_function.cpp +138 -141
- package/src/user_function.h +3 -0
package/src/sqlite_impl.cpp
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include <algorithm>
|
|
4
4
|
#include <cctype>
|
|
5
|
+
#include <cinttypes>
|
|
5
6
|
#include <climits>
|
|
6
7
|
#include <cmath>
|
|
7
8
|
#include <limits>
|
|
@@ -34,7 +35,7 @@ inline void ThrowErrSqliteErrorWithDb(Napi::Env env,
|
|
|
34
35
|
|
|
35
36
|
inline void ThrowEnhancedSqliteErrorWithDB(
|
|
36
37
|
Napi::Env env, photostructure::sqlite::DatabaseSync *db_sync, sqlite3 *db,
|
|
37
|
-
int sqlite_code
|
|
38
|
+
int /*sqlite_code*/, const std::string &message) {
|
|
38
39
|
// Check if we should ignore this SQLite error due to pending JavaScript
|
|
39
40
|
// exception (e.g., from authorizer callback)
|
|
40
41
|
if (db_sync != nullptr && db_sync->ShouldIgnoreSQLiteError()) {
|
|
@@ -49,8 +50,11 @@ inline void ThrowEnhancedSqliteErrorWithDB(
|
|
|
49
50
|
return; // Don't throw SQLite error, JavaScript exception takes precedence
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
//
|
|
53
|
-
|
|
53
|
+
// Use extended error code from db handle (e.g., 1555 for
|
|
54
|
+
// SQLITE_CONSTRAINT_PRIMARYKEY) instead of basic code (e.g., 19 for
|
|
55
|
+
// SQLITE_CONSTRAINT) to match Node.js behavior
|
|
56
|
+
int extended_code = db ? sqlite3_extended_errcode(db) : SQLITE_ERROR;
|
|
57
|
+
node::ThrowEnhancedSqliteError(env, db, extended_code, message);
|
|
54
58
|
}
|
|
55
59
|
} // namespace
|
|
56
60
|
#include "sqlite_exception.h"
|
|
@@ -94,6 +98,14 @@ std::optional<std::string> ValidateDatabasePath(Napi::Env env, Napi::Value path,
|
|
|
94
98
|
if (!has_null_bytes(location)) {
|
|
95
99
|
// Check if it's a file:// URL
|
|
96
100
|
if (location.compare(0, 7, "file://") == 0) {
|
|
101
|
+
// Check if URL has query parameters - if so, return full URI
|
|
102
|
+
// for SQLite URI mode (e.g., file:///path/to/db?mode=ro)
|
|
103
|
+
size_t query_pos = location.find('?');
|
|
104
|
+
if (query_pos != std::string::npos) {
|
|
105
|
+
// Return full URI for SQLite to parse with SQLITE_OPEN_URI
|
|
106
|
+
return location;
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
// Convert file:// URL to file path with proper validation
|
|
98
110
|
std::string file_path = location.substr(7);
|
|
99
111
|
|
|
@@ -265,10 +277,11 @@ std::optional<std::string> ValidateDatabasePath(Napi::Env env, Napi::Value path,
|
|
|
265
277
|
}
|
|
266
278
|
}
|
|
267
279
|
|
|
268
|
-
node::THROW_ERR_INVALID_ARG_TYPE(env,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
280
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env,
|
|
281
|
+
("The \"" + field_name +
|
|
282
|
+
"\" argument must be a string, "
|
|
283
|
+
"Uint8Array, or URL without null bytes.")
|
|
284
|
+
.c_str());
|
|
272
285
|
return std::nullopt;
|
|
273
286
|
}
|
|
274
287
|
|
|
@@ -278,6 +291,21 @@ std::optional<std::string> ValidateDatabasePath(Napi::Env env, Napi::Value path,
|
|
|
278
291
|
// Forward declarations for addon data access
|
|
279
292
|
extern AddonData *GetAddonData(napi_env env);
|
|
280
293
|
|
|
294
|
+
// Helper to create an object with null prototype (matches Node.js behavior)
|
|
295
|
+
// Node.js uses Object::New(isolate, Null(isolate), ...) but N-API doesn't have
|
|
296
|
+
// this capability, so we use cached Object.create(null) instead.
|
|
297
|
+
Napi::Object CreateObjectWithNullPrototype(Napi::Env env) {
|
|
298
|
+
AddonData *addon_data = GetAddonData(env);
|
|
299
|
+
if (addon_data && !addon_data->objectCreateFn.IsEmpty()) {
|
|
300
|
+
// Call Object.create(null) to create object with null prototype
|
|
301
|
+
return addon_data->objectCreateFn.Value()
|
|
302
|
+
.Call({env.Null()})
|
|
303
|
+
.As<Napi::Object>();
|
|
304
|
+
}
|
|
305
|
+
// Fallback to regular object if Object.create not available
|
|
306
|
+
return Napi::Object::New(env);
|
|
307
|
+
}
|
|
308
|
+
|
|
281
309
|
// DatabaseSync Implementation
|
|
282
310
|
Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
|
|
283
311
|
Napi::Function func = DefineClass(
|
|
@@ -295,8 +323,8 @@ Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
|
|
|
295
323
|
InstanceMethod("enableDefensive", &DatabaseSync::EnableDefensive),
|
|
296
324
|
InstanceMethod("createSession", &DatabaseSync::CreateSession),
|
|
297
325
|
InstanceMethod("applyChangeset", &DatabaseSync::ApplyChangeset),
|
|
298
|
-
InstanceMethod("backup", &DatabaseSync::Backup),
|
|
299
326
|
InstanceMethod("setAuthorizer", &DatabaseSync::SetAuthorizer),
|
|
327
|
+
InstanceMethod("backup", &DatabaseSync::Backup),
|
|
300
328
|
InstanceMethod("location", &DatabaseSync::LocationMethod),
|
|
301
329
|
InstanceAccessor("isOpen", &DatabaseSync::IsOpenGetter, nullptr),
|
|
302
330
|
InstanceAccessor("isTransaction", &DatabaseSync::IsTransactionGetter,
|
|
@@ -346,8 +374,15 @@ DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
|
|
|
346
374
|
// Register this instance for cleanup tracking
|
|
347
375
|
RegisterDatabaseInstance(info.Env(), this);
|
|
348
376
|
|
|
349
|
-
//
|
|
350
|
-
|
|
377
|
+
// Register cleanup hook to clean up references before environment teardown
|
|
378
|
+
napi_add_env_cleanup_hook(env_, CleanupHook, this);
|
|
379
|
+
|
|
380
|
+
// Node.js requires a path argument - throw if missing
|
|
381
|
+
if (info.Length() == 0 || info[0].IsUndefined()) {
|
|
382
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
383
|
+
info.Env(),
|
|
384
|
+
"The \"path\" argument must be a string, Uint8Array, or URL without "
|
|
385
|
+
"null bytes.");
|
|
351
386
|
return;
|
|
352
387
|
}
|
|
353
388
|
|
|
@@ -359,100 +394,172 @@ DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
|
|
|
359
394
|
}
|
|
360
395
|
|
|
361
396
|
try {
|
|
362
|
-
|
|
397
|
+
std::string loc = std::move(*location);
|
|
398
|
+
// Check if this is a file:// URI (for SQLite URI mode)
|
|
399
|
+
bool is_uri = (loc.compare(0, 7, "file://") == 0);
|
|
400
|
+
DatabaseOpenConfiguration config(std::move(loc));
|
|
401
|
+
if (is_uri) {
|
|
402
|
+
config.set_open_uri(true);
|
|
403
|
+
}
|
|
363
404
|
|
|
364
405
|
// Track whether to open immediately (default: true)
|
|
365
406
|
bool should_open = true;
|
|
366
407
|
|
|
367
408
|
// Handle options object if provided as second argument
|
|
368
|
-
if (info.Length() > 1
|
|
409
|
+
if (info.Length() > 1) {
|
|
410
|
+
if (!info[1].IsObject()) {
|
|
411
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
412
|
+
info.Env(), "The \"options\" argument must be an object.");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
369
416
|
Napi::Object options = info[1].As<Napi::Object>();
|
|
370
417
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
418
|
+
// Validate and parse 'open' option
|
|
419
|
+
Napi::Value open_val = options.Get("open");
|
|
420
|
+
if (!open_val.IsUndefined()) {
|
|
421
|
+
if (!open_val.IsBoolean()) {
|
|
422
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
423
|
+
info.Env(), "The \"options.open\" argument must be a boolean.");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
should_open = open_val.As<Napi::Boolean>().Value();
|
|
374
427
|
}
|
|
375
428
|
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
config.
|
|
386
|
-
options.Get("enableForeignKeys").As<Napi::Boolean>().Value());
|
|
429
|
+
// Validate and parse 'readOnly' option
|
|
430
|
+
Napi::Value read_only_val = options.Get("readOnly");
|
|
431
|
+
if (!read_only_val.IsUndefined()) {
|
|
432
|
+
if (!read_only_val.IsBoolean()) {
|
|
433
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
434
|
+
info.Env(),
|
|
435
|
+
"The \"options.readOnly\" argument must be a boolean.");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
config.set_read_only(read_only_val.As<Napi::Boolean>().Value());
|
|
387
439
|
}
|
|
388
440
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
441
|
+
// Validate and parse 'enableForeignKeyConstraints' option
|
|
442
|
+
Napi::Value enable_fk_val = options.Get("enableForeignKeyConstraints");
|
|
443
|
+
if (!enable_fk_val.IsUndefined()) {
|
|
444
|
+
if (!enable_fk_val.IsBoolean()) {
|
|
445
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
446
|
+
info.Env(),
|
|
447
|
+
"The \"options.enableForeignKeyConstraints\" argument must be a "
|
|
448
|
+
"boolean.");
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
config.set_enable_foreign_keys(
|
|
452
|
+
enable_fk_val.As<Napi::Boolean>().Value());
|
|
392
453
|
}
|
|
393
454
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
455
|
+
// Validate and parse 'enableDoubleQuotedStringLiterals' option
|
|
456
|
+
Napi::Value enable_dqs_val =
|
|
457
|
+
options.Get("enableDoubleQuotedStringLiterals");
|
|
458
|
+
if (!enable_dqs_val.IsUndefined()) {
|
|
459
|
+
if (!enable_dqs_val.IsBoolean()) {
|
|
460
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
461
|
+
info.Env(),
|
|
462
|
+
"The \"options.enableDoubleQuotedStringLiterals\" argument must "
|
|
463
|
+
"be a boolean.");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
config.set_enable_dqs(enable_dqs_val.As<Napi::Boolean>().Value());
|
|
399
467
|
}
|
|
400
468
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
469
|
+
// Validate and parse 'allowExtension' option
|
|
470
|
+
Napi::Value allow_ext_val = options.Get("allowExtension");
|
|
471
|
+
if (!allow_ext_val.IsUndefined()) {
|
|
472
|
+
if (!allow_ext_val.IsBoolean()) {
|
|
473
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
474
|
+
info.Env(),
|
|
475
|
+
"The \"options.allowExtension\" argument must be a boolean.");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
allow_load_extension_ = allow_ext_val.As<Napi::Boolean>().Value();
|
|
405
479
|
}
|
|
406
480
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
481
|
+
// Validate and parse 'timeout' option
|
|
482
|
+
Napi::Value timeout_val = options.Get("timeout");
|
|
483
|
+
if (!timeout_val.IsUndefined()) {
|
|
484
|
+
if (!timeout_val.IsNumber()) {
|
|
485
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
486
|
+
info.Env(),
|
|
487
|
+
"The \"options.timeout\" argument must be an integer.");
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
double timeout_double = timeout_val.As<Napi::Number>().DoubleValue();
|
|
491
|
+
if (timeout_double != std::trunc(timeout_double)) {
|
|
492
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
493
|
+
info.Env(),
|
|
494
|
+
"The \"options.timeout\" argument must be an integer.");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
config.set_timeout(timeout_val.As<Napi::Number>().Int32Value());
|
|
411
498
|
}
|
|
412
499
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
500
|
+
// Validate and parse 'readBigInts' option
|
|
501
|
+
Napi::Value read_bigints_val = options.Get("readBigInts");
|
|
502
|
+
if (!read_bigints_val.IsUndefined()) {
|
|
503
|
+
if (!read_bigints_val.IsBoolean()) {
|
|
504
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
505
|
+
info.Env(),
|
|
506
|
+
"The \"options.readBigInts\" argument must be a boolean.");
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
config.set_read_big_ints(read_bigints_val.As<Napi::Boolean>().Value());
|
|
417
510
|
}
|
|
418
511
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
512
|
+
// Validate and parse 'returnArrays' option
|
|
513
|
+
Napi::Value return_arrays_val = options.Get("returnArrays");
|
|
514
|
+
if (!return_arrays_val.IsUndefined()) {
|
|
515
|
+
if (!return_arrays_val.IsBoolean()) {
|
|
516
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
517
|
+
info.Env(),
|
|
518
|
+
"The \"options.returnArrays\" argument must be a boolean.");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
config.set_return_arrays(return_arrays_val.As<Napi::Boolean>().Value());
|
|
425
522
|
}
|
|
426
523
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
524
|
+
// Validate and parse 'allowBareNamedParameters' option
|
|
525
|
+
Napi::Value allow_bare_val = options.Get("allowBareNamedParameters");
|
|
526
|
+
if (!allow_bare_val.IsUndefined()) {
|
|
527
|
+
if (!allow_bare_val.IsBoolean()) {
|
|
528
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
529
|
+
info.Env(),
|
|
530
|
+
"The \"options.allowBareNamedParameters\" argument must be a "
|
|
531
|
+
"boolean.");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
config.set_allow_bare_named_params(
|
|
535
|
+
allow_bare_val.As<Napi::Boolean>().Value());
|
|
433
536
|
}
|
|
434
537
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
defensive_val.As<Napi::Boolean>().Value());
|
|
538
|
+
// Validate and parse 'allowUnknownNamedParameters' option
|
|
539
|
+
Napi::Value allow_unknown_val =
|
|
540
|
+
options.Get("allowUnknownNamedParameters");
|
|
541
|
+
if (!allow_unknown_val.IsUndefined()) {
|
|
542
|
+
if (!allow_unknown_val.IsBoolean()) {
|
|
543
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
544
|
+
info.Env(),
|
|
545
|
+
"The \"options.allowUnknownNamedParameters\" argument must be a "
|
|
546
|
+
"boolean.");
|
|
547
|
+
return;
|
|
446
548
|
}
|
|
549
|
+
config.set_allow_unknown_named_params(
|
|
550
|
+
allow_unknown_val.As<Napi::Boolean>().Value());
|
|
447
551
|
}
|
|
448
552
|
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
|
|
553
|
+
// Validate and parse 'defensive' option
|
|
554
|
+
Napi::Value defensive_val = options.Get("defensive");
|
|
555
|
+
if (!defensive_val.IsUndefined()) {
|
|
556
|
+
if (!defensive_val.IsBoolean()) {
|
|
557
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
558
|
+
info.Env(),
|
|
559
|
+
"The \"options.defensive\" argument must be a boolean.");
|
|
560
|
+
return;
|
|
454
561
|
}
|
|
455
|
-
|
|
562
|
+
config.set_enable_defensive(defensive_val.As<Napi::Boolean>().Value());
|
|
456
563
|
}
|
|
457
564
|
}
|
|
458
565
|
|
|
@@ -471,6 +578,9 @@ DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
|
|
|
471
578
|
}
|
|
472
579
|
|
|
473
580
|
DatabaseSync::~DatabaseSync() {
|
|
581
|
+
// Remove cleanup hook if still registered
|
|
582
|
+
napi_remove_env_cleanup_hook(env_, CleanupHook, this);
|
|
583
|
+
|
|
474
584
|
// Unregister this instance
|
|
475
585
|
UnregisterDatabaseInstance(env_, this);
|
|
476
586
|
|
|
@@ -479,6 +589,16 @@ DatabaseSync::~DatabaseSync() {
|
|
|
479
589
|
}
|
|
480
590
|
}
|
|
481
591
|
|
|
592
|
+
void DatabaseSync::CleanupHook(void *arg) {
|
|
593
|
+
// Called before environment teardown - safe to Reset() references here
|
|
594
|
+
auto *self = static_cast<DatabaseSync *>(arg);
|
|
595
|
+
|
|
596
|
+
// Clean up authorizer callback if it exists
|
|
597
|
+
if (self->authorizer_callback_) {
|
|
598
|
+
self->authorizer_callback_->Reset();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
482
602
|
Napi::Value DatabaseSync::Open(const Napi::CallbackInfo &info) {
|
|
483
603
|
Napi::Env env = info.Env();
|
|
484
604
|
|
|
@@ -506,7 +626,7 @@ Napi::Value DatabaseSync::Close(const Napi::CallbackInfo &info) {
|
|
|
506
626
|
}
|
|
507
627
|
|
|
508
628
|
if (!IsOpen()) {
|
|
509
|
-
node::THROW_ERR_INVALID_STATE(env, "
|
|
629
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
510
630
|
return env.Undefined();
|
|
511
631
|
}
|
|
512
632
|
|
|
@@ -547,17 +667,82 @@ Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
|
|
|
547
667
|
}
|
|
548
668
|
|
|
549
669
|
if (!IsOpen()) {
|
|
550
|
-
node::THROW_ERR_INVALID_STATE(env, "
|
|
670
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
551
671
|
return env.Undefined();
|
|
552
672
|
}
|
|
553
673
|
|
|
554
674
|
if (info.Length() < 1 || !info[0].IsString()) {
|
|
555
|
-
node::THROW_ERR_INVALID_ARG_TYPE(env,
|
|
675
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env,
|
|
676
|
+
"The \"sql\" argument must be a string.");
|
|
556
677
|
return env.Undefined();
|
|
557
678
|
}
|
|
558
679
|
|
|
559
680
|
std::string sql = info[0].As<Napi::String>().Utf8Value();
|
|
560
681
|
|
|
682
|
+
// Parse optional second argument (options object)
|
|
683
|
+
// Node.js v25+ supports per-statement options that override database defaults
|
|
684
|
+
std::optional<bool> opt_read_big_ints;
|
|
685
|
+
std::optional<bool> opt_return_arrays;
|
|
686
|
+
std::optional<bool> opt_allow_bare_named_params;
|
|
687
|
+
std::optional<bool> opt_allow_unknown_named_params;
|
|
688
|
+
|
|
689
|
+
if (info.Length() > 1 && !info[1].IsUndefined()) {
|
|
690
|
+
if (!info[1].IsObject()) {
|
|
691
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
692
|
+
env, "The \"options\" argument must be an object.");
|
|
693
|
+
return env.Undefined();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
Napi::Object options = info[1].As<Napi::Object>();
|
|
697
|
+
|
|
698
|
+
// Parse readBigInts option
|
|
699
|
+
Napi::Value read_big_ints_val = options.Get("readBigInts");
|
|
700
|
+
if (!read_big_ints_val.IsUndefined()) {
|
|
701
|
+
if (!read_big_ints_val.IsBoolean()) {
|
|
702
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
703
|
+
env, "The \"options.readBigInts\" argument must be a boolean.");
|
|
704
|
+
return env.Undefined();
|
|
705
|
+
}
|
|
706
|
+
opt_read_big_ints = read_big_ints_val.As<Napi::Boolean>().Value();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Parse returnArrays option
|
|
710
|
+
Napi::Value return_arrays_val = options.Get("returnArrays");
|
|
711
|
+
if (!return_arrays_val.IsUndefined()) {
|
|
712
|
+
if (!return_arrays_val.IsBoolean()) {
|
|
713
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
714
|
+
env, "The \"options.returnArrays\" argument must be a boolean.");
|
|
715
|
+
return env.Undefined();
|
|
716
|
+
}
|
|
717
|
+
opt_return_arrays = return_arrays_val.As<Napi::Boolean>().Value();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Parse allowBareNamedParameters option
|
|
721
|
+
Napi::Value allow_bare_val = options.Get("allowBareNamedParameters");
|
|
722
|
+
if (!allow_bare_val.IsUndefined()) {
|
|
723
|
+
if (!allow_bare_val.IsBoolean()) {
|
|
724
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
725
|
+
env, "The \"options.allowBareNamedParameters\" argument must be a "
|
|
726
|
+
"boolean.");
|
|
727
|
+
return env.Undefined();
|
|
728
|
+
}
|
|
729
|
+
opt_allow_bare_named_params = allow_bare_val.As<Napi::Boolean>().Value();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Parse allowUnknownNamedParameters option
|
|
733
|
+
Napi::Value allow_unknown_val = options.Get("allowUnknownNamedParameters");
|
|
734
|
+
if (!allow_unknown_val.IsUndefined()) {
|
|
735
|
+
if (!allow_unknown_val.IsBoolean()) {
|
|
736
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
737
|
+
env, "The \"options.allowUnknownNamedParameters\" argument must be "
|
|
738
|
+
"a boolean.");
|
|
739
|
+
return env.Undefined();
|
|
740
|
+
}
|
|
741
|
+
opt_allow_unknown_named_params =
|
|
742
|
+
allow_unknown_val.As<Napi::Boolean>().Value();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
561
746
|
// Clear any stale deferred exception from a previous operation
|
|
562
747
|
ClearDeferredAuthorizerException();
|
|
563
748
|
SetIgnoreNextSQLiteError(false);
|
|
@@ -573,10 +758,24 @@ Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
|
|
|
573
758
|
Napi::Object stmt_obj =
|
|
574
759
|
addon_data->statementSyncConstructor.New({}).As<Napi::Object>();
|
|
575
760
|
|
|
576
|
-
// Initialize the statement
|
|
761
|
+
// Initialize the statement (applies database-level defaults)
|
|
577
762
|
StatementSync *stmt = StatementSync::Unwrap(stmt_obj);
|
|
578
763
|
stmt->InitStatement(this, sql);
|
|
579
764
|
|
|
765
|
+
// Apply per-statement option overrides (if explicitly provided)
|
|
766
|
+
if (opt_read_big_ints.has_value()) {
|
|
767
|
+
stmt->use_big_ints_ = *opt_read_big_ints;
|
|
768
|
+
}
|
|
769
|
+
if (opt_return_arrays.has_value()) {
|
|
770
|
+
stmt->return_arrays_ = *opt_return_arrays;
|
|
771
|
+
}
|
|
772
|
+
if (opt_allow_bare_named_params.has_value()) {
|
|
773
|
+
stmt->allow_bare_named_params_ = *opt_allow_bare_named_params;
|
|
774
|
+
}
|
|
775
|
+
if (opt_allow_unknown_named_params.has_value()) {
|
|
776
|
+
stmt->allow_unknown_named_params_ = *opt_allow_unknown_named_params;
|
|
777
|
+
}
|
|
778
|
+
|
|
580
779
|
return stmt_obj;
|
|
581
780
|
} catch (const SqliteException &e) {
|
|
582
781
|
// SqliteException stores message in std::string, avoiding Windows ARM ABI
|
|
@@ -627,12 +826,13 @@ Napi::Value DatabaseSync::Exec(const Napi::CallbackInfo &info) {
|
|
|
627
826
|
}
|
|
628
827
|
|
|
629
828
|
if (!IsOpen()) {
|
|
630
|
-
node::THROW_ERR_INVALID_STATE(env, "
|
|
829
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
631
830
|
return env.Undefined();
|
|
632
831
|
}
|
|
633
832
|
|
|
634
833
|
if (info.Length() < 1 || !info[0].IsString()) {
|
|
635
|
-
node::THROW_ERR_INVALID_ARG_TYPE(env,
|
|
834
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env,
|
|
835
|
+
"The \"sql\" argument must be a string.");
|
|
636
836
|
return env.Undefined();
|
|
637
837
|
}
|
|
638
838
|
|
|
@@ -672,13 +872,18 @@ Napi::Value DatabaseSync::LocationMethod(const Napi::CallbackInfo &info) {
|
|
|
672
872
|
Napi::Env env = info.Env();
|
|
673
873
|
|
|
674
874
|
if (!IsOpen()) {
|
|
675
|
-
node::THROW_ERR_INVALID_STATE(env, "
|
|
875
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
676
876
|
return env.Undefined();
|
|
677
877
|
}
|
|
678
878
|
|
|
679
879
|
// Default to "main" if no dbName provided
|
|
680
880
|
std::string db_name = "main";
|
|
681
|
-
if (info.Length() > 0 && info[0].
|
|
881
|
+
if (info.Length() > 0 && !info[0].IsUndefined()) {
|
|
882
|
+
if (!info[0].IsString()) {
|
|
883
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
884
|
+
env, "The \"dbName\" argument must be a string.");
|
|
885
|
+
return env.Undefined();
|
|
886
|
+
}
|
|
682
887
|
db_name = info[0].As<Napi::String>().Utf8Value();
|
|
683
888
|
}
|
|
684
889
|
|
|
@@ -699,9 +904,16 @@ Napi::Value DatabaseSync::IsOpenGetter(const Napi::CallbackInfo &info) {
|
|
|
699
904
|
}
|
|
700
905
|
|
|
701
906
|
Napi::Value DatabaseSync::IsTransactionGetter(const Napi::CallbackInfo &info) {
|
|
907
|
+
Napi::Env env = info.Env();
|
|
908
|
+
|
|
909
|
+
if (!IsOpen()) {
|
|
910
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
911
|
+
return env.Undefined();
|
|
912
|
+
}
|
|
913
|
+
|
|
702
914
|
// Check if we're in a transaction
|
|
703
|
-
bool in_transaction =
|
|
704
|
-
return Napi::Boolean::New(
|
|
915
|
+
bool in_transaction = !sqlite3_get_autocommit(connection());
|
|
916
|
+
return Napi::Boolean::New(env, in_transaction);
|
|
705
917
|
}
|
|
706
918
|
|
|
707
919
|
void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
|
|
@@ -717,6 +929,11 @@ void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
|
|
|
717
929
|
flags |= SQLITE_OPEN_READWRITE;
|
|
718
930
|
}
|
|
719
931
|
|
|
932
|
+
// Add URI flag when location contains URI parameters
|
|
933
|
+
if (config_.get_open_uri()) {
|
|
934
|
+
flags |= SQLITE_OPEN_URI;
|
|
935
|
+
}
|
|
936
|
+
|
|
720
937
|
int result = sqlite3_open_v2(location_.c_str(), &connection_, flags, nullptr);
|
|
721
938
|
|
|
722
939
|
if (result != SQLITE_OK) {
|
|
@@ -731,10 +948,19 @@ void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
|
|
|
731
948
|
throw ex;
|
|
732
949
|
}
|
|
733
950
|
|
|
734
|
-
// Configure
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
951
|
+
// Configure foreign keys using sqlite3_db_config (matches Node.js behavior)
|
|
952
|
+
// This properly handles both enabling and disabling FK constraints
|
|
953
|
+
int fk_enabled;
|
|
954
|
+
result =
|
|
955
|
+
sqlite3_db_config(connection(), SQLITE_DBCONFIG_ENABLE_FKEY,
|
|
956
|
+
config_.get_enable_foreign_keys() ? 1 : 0, &fk_enabled);
|
|
957
|
+
if (result != SQLITE_OK) {
|
|
958
|
+
std::string error = sqlite3_errmsg(connection());
|
|
959
|
+
SqliteException ex(connection_, result,
|
|
960
|
+
"Failed to configure foreign keys: " + error);
|
|
961
|
+
sqlite3_close(connection_);
|
|
962
|
+
connection_ = nullptr;
|
|
963
|
+
throw ex;
|
|
738
964
|
}
|
|
739
965
|
|
|
740
966
|
if (config_.get_timeout() > 0) {
|
|
@@ -785,6 +1011,10 @@ void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
|
|
|
785
1011
|
|
|
786
1012
|
void DatabaseSync::InternalClose() {
|
|
787
1013
|
if (connection_) {
|
|
1014
|
+
// Finalize any active backup jobs first
|
|
1015
|
+
// This prevents use-after-free if backup is running on worker thread
|
|
1016
|
+
FinalizeBackups();
|
|
1017
|
+
|
|
788
1018
|
// Finalize all prepared statements
|
|
789
1019
|
prepared_statements_.clear();
|
|
790
1020
|
|
|
@@ -808,18 +1038,13 @@ Napi::Value DatabaseSync::CustomFunction(const Napi::CallbackInfo &info) {
|
|
|
808
1038
|
Napi::Env env = info.Env();
|
|
809
1039
|
|
|
810
1040
|
if (!IsOpen()) {
|
|
811
|
-
node::THROW_ERR_INVALID_STATE(env, "
|
|
812
|
-
return env.Undefined();
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
if (info.Length() < 2) {
|
|
816
|
-
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
817
|
-
env, "Expected at least 2 arguments: name and function");
|
|
1041
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
818
1042
|
return env.Undefined();
|
|
819
1043
|
}
|
|
820
1044
|
|
|
821
1045
|
if (!info[0].IsString()) {
|
|
822
|
-
node::THROW_ERR_INVALID_ARG_TYPE(env,
|
|
1046
|
+
node::THROW_ERR_INVALID_ARG_TYPE(env,
|
|
1047
|
+
"The \"name\" argument must be a string.");
|
|
823
1048
|
return env.Undefined();
|
|
824
1049
|
}
|
|
825
1050
|
|
|
@@ -831,31 +1056,60 @@ Napi::Value DatabaseSync::CustomFunction(const Napi::CallbackInfo &info) {
|
|
|
831
1056
|
bool direct_only = false;
|
|
832
1057
|
|
|
833
1058
|
// Parse options object if provided
|
|
834
|
-
if (fn_index > 1
|
|
1059
|
+
if (fn_index > 1) {
|
|
1060
|
+
if (!info[1].IsObject()) {
|
|
1061
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1062
|
+
env, "The \"options\" argument must be an object.");
|
|
1063
|
+
return env.Undefined();
|
|
1064
|
+
}
|
|
1065
|
+
|
|
835
1066
|
Napi::Object options = info[1].As<Napi::Object>();
|
|
836
1067
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1068
|
+
Napi::Value use_bigint_args_v = options.Get("useBigIntArguments");
|
|
1069
|
+
if (!use_bigint_args_v.IsUndefined()) {
|
|
1070
|
+
if (!use_bigint_args_v.IsBoolean()) {
|
|
1071
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1072
|
+
env,
|
|
1073
|
+
"The \"options.useBigIntArguments\" argument must be a boolean.");
|
|
1074
|
+
return env.Undefined();
|
|
1075
|
+
}
|
|
1076
|
+
use_bigint_args = use_bigint_args_v.As<Napi::Boolean>().Value();
|
|
841
1077
|
}
|
|
842
1078
|
|
|
843
|
-
|
|
844
|
-
|
|
1079
|
+
Napi::Value varargs_v = options.Get("varargs");
|
|
1080
|
+
if (!varargs_v.IsUndefined()) {
|
|
1081
|
+
if (!varargs_v.IsBoolean()) {
|
|
1082
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1083
|
+
env, "The \"options.varargs\" argument must be a boolean.");
|
|
1084
|
+
return env.Undefined();
|
|
1085
|
+
}
|
|
1086
|
+
varargs = varargs_v.As<Napi::Boolean>().Value();
|
|
845
1087
|
}
|
|
846
1088
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1089
|
+
Napi::Value deterministic_v = options.Get("deterministic");
|
|
1090
|
+
if (!deterministic_v.IsUndefined()) {
|
|
1091
|
+
if (!deterministic_v.IsBoolean()) {
|
|
1092
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1093
|
+
env, "The \"options.deterministic\" argument must be a boolean.");
|
|
1094
|
+
return env.Undefined();
|
|
1095
|
+
}
|
|
1096
|
+
deterministic = deterministic_v.As<Napi::Boolean>().Value();
|
|
850
1097
|
}
|
|
851
1098
|
|
|
852
|
-
|
|
853
|
-
|
|
1099
|
+
Napi::Value direct_only_v = options.Get("directOnly");
|
|
1100
|
+
if (!direct_only_v.IsUndefined()) {
|
|
1101
|
+
if (!direct_only_v.IsBoolean()) {
|
|
1102
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1103
|
+
env, "The \"options.directOnly\" argument must be a boolean.");
|
|
1104
|
+
return env.Undefined();
|
|
1105
|
+
}
|
|
1106
|
+
direct_only = direct_only_v.As<Napi::Boolean>().Value();
|
|
854
1107
|
}
|
|
855
1108
|
}
|
|
856
1109
|
|
|
857
1110
|
if (!info[fn_index].IsFunction()) {
|
|
858
|
-
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1111
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1112
|
+
env, "The \"function\" argument must be a function.");
|
|
859
1113
|
return env.Undefined();
|
|
860
1114
|
}
|
|
861
1115
|
|
|
@@ -907,74 +1161,96 @@ Napi::Value DatabaseSync::AggregateFunction(const Napi::CallbackInfo &info) {
|
|
|
907
1161
|
Napi::Env env = info.Env();
|
|
908
1162
|
|
|
909
1163
|
if (!IsOpen()) {
|
|
910
|
-
node::THROW_ERR_INVALID_STATE(env, "
|
|
1164
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
911
1165
|
return env.Undefined();
|
|
912
1166
|
}
|
|
913
1167
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1168
|
+
// Node.js doesn't check argument count - it just accesses args directly
|
|
1169
|
+
std::string name = info[0].IsString() ? info[0].As<Napi::String>().Utf8Value()
|
|
1170
|
+
: std::string();
|
|
1171
|
+
Napi::Object options =
|
|
1172
|
+
info[1].IsObject() ? info[1].As<Napi::Object>() : Napi::Object();
|
|
919
1173
|
|
|
920
|
-
if (
|
|
921
|
-
|
|
922
|
-
|
|
1174
|
+
if (options.IsEmpty() || options.IsNull() || options.IsUndefined()) {
|
|
1175
|
+
// If options isn't an object, trying to access its properties will fail
|
|
1176
|
+
// Node.js would throw from the property access, we mimic that
|
|
1177
|
+
options = Napi::Object::New(env);
|
|
923
1178
|
}
|
|
924
1179
|
|
|
925
|
-
|
|
926
|
-
|
|
1180
|
+
// Parse start value
|
|
1181
|
+
Napi::Value start = options.Get("start");
|
|
1182
|
+
if (start.IsUndefined()) {
|
|
1183
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1184
|
+
env, "The \"options.start\" argument must be a function or a primitive "
|
|
1185
|
+
"value.");
|
|
927
1186
|
return env.Undefined();
|
|
928
1187
|
}
|
|
929
1188
|
|
|
930
|
-
|
|
931
|
-
Napi::
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
if (options.Has("start") && !options.Get("start").IsUndefined()) {
|
|
936
|
-
start = options.Get("start");
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
if (!options.Has("step") || !options.Get("step").IsFunction()) {
|
|
940
|
-
node::THROW_ERR_INVALID_ARG_TYPE(env, "options.step must be a function");
|
|
1189
|
+
// Parse step function
|
|
1190
|
+
Napi::Value step_v = options.Get("step");
|
|
1191
|
+
if (!step_v.IsFunction()) {
|
|
1192
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1193
|
+
env, "The \"options.step\" argument must be a function.");
|
|
941
1194
|
return env.Undefined();
|
|
942
1195
|
}
|
|
1196
|
+
Napi::Function step_fn = step_v.As<Napi::Function>();
|
|
943
1197
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
// Parse optional options
|
|
947
|
-
Napi::Function inverse_fn;
|
|
948
|
-
if (options.Has("inverse") && options.Get("inverse").IsFunction()) {
|
|
949
|
-
inverse_fn = options.Get("inverse").As<Napi::Function>();
|
|
950
|
-
}
|
|
951
|
-
|
|
1198
|
+
// Parse result function (optional)
|
|
952
1199
|
Napi::Function result_fn;
|
|
953
|
-
|
|
954
|
-
|
|
1200
|
+
Napi::Value result_v = options.Get("result");
|
|
1201
|
+
if (!result_v.IsUndefined()) {
|
|
1202
|
+
result_fn = result_v.As<Napi::Function>();
|
|
955
1203
|
}
|
|
956
1204
|
|
|
1205
|
+
// Parse boolean options with validation
|
|
957
1206
|
bool use_bigint_args = false;
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1207
|
+
Napi::Value use_bigint_args_v = options.Get("useBigIntArguments");
|
|
1208
|
+
if (!use_bigint_args_v.IsUndefined()) {
|
|
1209
|
+
if (!use_bigint_args_v.IsBoolean()) {
|
|
1210
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1211
|
+
env,
|
|
1212
|
+
"The \"options.useBigIntArguments\" argument must be a boolean.");
|
|
1213
|
+
return env.Undefined();
|
|
1214
|
+
}
|
|
1215
|
+
use_bigint_args = use_bigint_args_v.As<Napi::Boolean>().Value();
|
|
962
1216
|
}
|
|
963
1217
|
|
|
964
1218
|
bool varargs = false;
|
|
965
|
-
|
|
966
|
-
|
|
1219
|
+
Napi::Value varargs_v = options.Get("varargs");
|
|
1220
|
+
if (!varargs_v.IsUndefined()) {
|
|
1221
|
+
if (!varargs_v.IsBoolean()) {
|
|
1222
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1223
|
+
env, "The \"options.varargs\" argument must be a boolean.");
|
|
1224
|
+
return env.Undefined();
|
|
1225
|
+
}
|
|
1226
|
+
varargs = varargs_v.As<Napi::Boolean>().Value();
|
|
967
1227
|
}
|
|
968
1228
|
|
|
969
1229
|
bool deterministic = false;
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
deterministic = options.Get("deterministic").As<Napi::Boolean>().Value();
|
|
973
|
-
}
|
|
1230
|
+
// Note: deterministic is handled via sqlite flags but Node.js
|
|
1231
|
+
// doesn't seem to validate it separately for aggregates
|
|
974
1232
|
|
|
975
1233
|
bool direct_only = false;
|
|
976
|
-
|
|
977
|
-
|
|
1234
|
+
Napi::Value direct_only_v = options.Get("directOnly");
|
|
1235
|
+
if (!direct_only_v.IsUndefined()) {
|
|
1236
|
+
if (!direct_only_v.IsBoolean()) {
|
|
1237
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1238
|
+
env, "The \"options.directOnly\" argument must be a boolean.");
|
|
1239
|
+
return env.Undefined();
|
|
1240
|
+
}
|
|
1241
|
+
direct_only = direct_only_v.As<Napi::Boolean>().Value();
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Parse inverse function (optional)
|
|
1245
|
+
Napi::Function inverse_fn;
|
|
1246
|
+
Napi::Value inverse_v = options.Get("inverse");
|
|
1247
|
+
if (!inverse_v.IsUndefined()) {
|
|
1248
|
+
if (!inverse_v.IsFunction()) {
|
|
1249
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1250
|
+
env, "The \"options.inverse\" argument must be a function.");
|
|
1251
|
+
return env.Undefined();
|
|
1252
|
+
}
|
|
1253
|
+
inverse_fn = inverse_v.As<Napi::Function>();
|
|
978
1254
|
}
|
|
979
1255
|
|
|
980
1256
|
// Determine argument count
|
|
@@ -1048,7 +1324,7 @@ Napi::Value DatabaseSync::EnableLoadExtension(const Napi::CallbackInfo &info) {
|
|
|
1048
1324
|
Napi::Env env = info.Env();
|
|
1049
1325
|
|
|
1050
1326
|
if (!IsOpen()) {
|
|
1051
|
-
node::THROW_ERR_INVALID_STATE(env, "
|
|
1327
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
1052
1328
|
return env.Undefined();
|
|
1053
1329
|
}
|
|
1054
1330
|
|
|
@@ -1088,7 +1364,7 @@ Napi::Value DatabaseSync::LoadExtension(const Napi::CallbackInfo &info) {
|
|
|
1088
1364
|
Napi::Env env = info.Env();
|
|
1089
1365
|
|
|
1090
1366
|
if (!IsOpen()) {
|
|
1091
|
-
node::THROW_ERR_INVALID_STATE(env, "
|
|
1367
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
1092
1368
|
return env.Undefined();
|
|
1093
1369
|
}
|
|
1094
1370
|
|
|
@@ -1263,26 +1539,108 @@ void DatabaseSync::DeleteAllSessions() {
|
|
|
1263
1539
|
// Direct SQLite cleanup since we're in database destruction
|
|
1264
1540
|
if (session->GetSession()) {
|
|
1265
1541
|
sqlite3session_delete(session->GetSession());
|
|
1266
|
-
// Clear the session
|
|
1542
|
+
// Clear the session pointer but KEEP database_ so we can detect
|
|
1543
|
+
// "database closed" vs "session closed" in Session methods
|
|
1267
1544
|
session->session_ = nullptr;
|
|
1268
|
-
|
|
1545
|
+
// Note: Don't null database_ - we need it to check IsOpen()
|
|
1269
1546
|
}
|
|
1270
1547
|
}
|
|
1271
1548
|
}
|
|
1272
1549
|
|
|
1550
|
+
void DatabaseSync::AddBackup(BackupJob *backup) {
|
|
1551
|
+
std::lock_guard<std::mutex> lock(backups_mutex_);
|
|
1552
|
+
backups_.insert(backup);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
void DatabaseSync::RemoveBackup(BackupJob *backup) {
|
|
1556
|
+
std::lock_guard<std::mutex> lock(backups_mutex_);
|
|
1557
|
+
backups_.erase(backup);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
void DatabaseSync::FinalizeBackups() {
|
|
1561
|
+
// Copy the set while holding the lock, then release before cleanup
|
|
1562
|
+
// This prevents deadlock if destructor calls RemoveBackup
|
|
1563
|
+
std::set<BackupJob *> backups_copy;
|
|
1564
|
+
{
|
|
1565
|
+
std::lock_guard<std::mutex> lock(backups_mutex_);
|
|
1566
|
+
backups_copy = backups_;
|
|
1567
|
+
backups_.clear(); // Clear now to prevent destructor issues
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Clean up each active backup without holding the lock
|
|
1571
|
+
// We clear source_ to prevent the destructor from trying to
|
|
1572
|
+
// RemoveBackup (the set is already empty anyway)
|
|
1573
|
+
for (auto *backup : backups_copy) {
|
|
1574
|
+
backup->ClearSource(); // Prevent RemoveBackup call in destructor
|
|
1575
|
+
backup->Cleanup();
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1273
1579
|
// Context structure for changeset callbacks to avoid global state
|
|
1274
1580
|
struct ChangesetCallbacks {
|
|
1275
1581
|
std::function<int(int)> conflictCallback;
|
|
1276
1582
|
std::function<bool(std::string)> filterCallback;
|
|
1277
1583
|
Napi::Env env;
|
|
1584
|
+
// Store pending exception for re-throwing after SQLite call
|
|
1585
|
+
std::string pendingExceptionMessage;
|
|
1586
|
+
bool hasPendingException = false;
|
|
1278
1587
|
};
|
|
1279
1588
|
|
|
1589
|
+
// Helper to extract error message from Napi::Error
|
|
1590
|
+
// Handles both Error objects (use .message property) and primitive throws.
|
|
1591
|
+
// For GetAndClearPendingException(), err.Value() returns the ORIGINAL thrown
|
|
1592
|
+
// value - if JS throws "string", Value() IS the string, not a wrapper.
|
|
1593
|
+
// See: node-addon-api/test/error.cc line 306-310 (CatchError function)
|
|
1594
|
+
static std::string GetErrorMessage(const Napi::Error &err,
|
|
1595
|
+
const char *fallback) {
|
|
1596
|
+
// Try 1: Message() works for Error objects with .message property
|
|
1597
|
+
try {
|
|
1598
|
+
std::string msg = err.Message();
|
|
1599
|
+
if (!msg.empty()) {
|
|
1600
|
+
return msg;
|
|
1601
|
+
}
|
|
1602
|
+
} catch (...) {
|
|
1603
|
+
// Message() failed, continue trying
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// Try 2: For primitives or when Message() is empty, err.Value() returns
|
|
1607
|
+
// the original thrown value. Just convert it to string directly.
|
|
1608
|
+
// This works for: throw "string", throw 42, throw new Error("msg")
|
|
1609
|
+
try {
|
|
1610
|
+
Napi::Value val = err.Value();
|
|
1611
|
+
if (!val.IsEmpty() && !val.IsUndefined() && !val.IsNull()) {
|
|
1612
|
+
return val.ToString().Utf8Value();
|
|
1613
|
+
}
|
|
1614
|
+
} catch (...) {
|
|
1615
|
+
// Value() or ToString() failed, continue
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// Try 3: For C++ catch blocks, check ERROR_WRAP_VALUE property
|
|
1619
|
+
// (only relevant when catching Napi::Error as C++ exception)
|
|
1620
|
+
try {
|
|
1621
|
+
Napi::Value val = err.Value();
|
|
1622
|
+
if (val.IsObject()) {
|
|
1623
|
+
Napi::Object errObj = val.As<Napi::Object>();
|
|
1624
|
+
static const char *ERROR_WRAP_VALUE =
|
|
1625
|
+
"4bda9e7e-4913-4dbc-95de-891cbf66598e-errorVal";
|
|
1626
|
+
Napi::Value wrapped = errObj.Get(ERROR_WRAP_VALUE);
|
|
1627
|
+
if (!wrapped.IsUndefined()) {
|
|
1628
|
+
return wrapped.ToString().Utf8Value();
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
} catch (...) {
|
|
1632
|
+
// Property access failed, continue
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
return fallback;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1280
1638
|
static int xConflict(void *pCtx, int eConflict, sqlite3_changeset_iter *pIter) {
|
|
1281
1639
|
if (!pCtx)
|
|
1282
|
-
return
|
|
1640
|
+
return SQLITE_CHANGESET_ABORT;
|
|
1283
1641
|
ChangesetCallbacks *callbacks = static_cast<ChangesetCallbacks *>(pCtx);
|
|
1284
1642
|
if (!callbacks->conflictCallback)
|
|
1285
|
-
return
|
|
1643
|
+
return SQLITE_CHANGESET_ABORT;
|
|
1286
1644
|
return callbacks->conflictCallback(eConflict);
|
|
1287
1645
|
}
|
|
1288
1646
|
|
|
@@ -1290,6 +1648,9 @@ static int xFilter(void *pCtx, const char *zTab) {
|
|
|
1290
1648
|
if (!pCtx)
|
|
1291
1649
|
return 1;
|
|
1292
1650
|
ChangesetCallbacks *callbacks = static_cast<ChangesetCallbacks *>(pCtx);
|
|
1651
|
+
// Skip filter callback if we already have a pending exception
|
|
1652
|
+
if (callbacks->hasPendingException)
|
|
1653
|
+
return 0;
|
|
1293
1654
|
if (!callbacks->filterCallback)
|
|
1294
1655
|
return 1;
|
|
1295
1656
|
return callbacks->filterCallback(zTab) ? 1 : 0;
|
|
@@ -1303,14 +1664,14 @@ Napi::Value DatabaseSync::ApplyChangeset(const Napi::CallbackInfo &info) {
|
|
|
1303
1664
|
return env.Undefined();
|
|
1304
1665
|
}
|
|
1305
1666
|
|
|
1306
|
-
if (info.Length() < 1 || !info[0].
|
|
1667
|
+
if (info.Length() < 1 || !info[0].IsTypedArray()) {
|
|
1307
1668
|
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
1308
|
-
env, "The \"changeset\" argument must be a
|
|
1669
|
+
env, "The \"changeset\" argument must be a Uint8Array.");
|
|
1309
1670
|
return env.Undefined();
|
|
1310
1671
|
}
|
|
1311
1672
|
|
|
1312
1673
|
// Create callback context to avoid global state
|
|
1313
|
-
ChangesetCallbacks callbacks{nullptr, nullptr, env};
|
|
1674
|
+
ChangesetCallbacks callbacks{nullptr, nullptr, env, "", false};
|
|
1314
1675
|
|
|
1315
1676
|
// Parse options if provided
|
|
1316
1677
|
if (info.Length() > 1 && !info[1].IsUndefined()) {
|
|
@@ -1333,31 +1694,65 @@ Napi::Value DatabaseSync::ApplyChangeset(const Napi::CallbackInfo &info) {
|
|
|
1333
1694
|
}
|
|
1334
1695
|
|
|
1335
1696
|
Napi::Function conflictFunc = conflictValue.As<Napi::Function>();
|
|
1336
|
-
callbacks.conflictCallback = [env,
|
|
1697
|
+
callbacks.conflictCallback = [&callbacks, env,
|
|
1337
1698
|
conflictFunc](int conflictType) -> int {
|
|
1338
|
-
|
|
1699
|
+
// Wrap in try-catch to prevent C++ exceptions from propagating
|
|
1700
|
+
// through C callback boundary into SQLite (causes SIGSEGV)
|
|
1339
1701
|
try {
|
|
1702
|
+
// Skip callback if we already have a pending exception
|
|
1703
|
+
if (callbacks.hasPendingException)
|
|
1704
|
+
return SQLITE_CHANGESET_ABORT;
|
|
1705
|
+
|
|
1706
|
+
Napi::HandleScope scope(env);
|
|
1340
1707
|
Napi::Value result =
|
|
1341
1708
|
conflictFunc.Call({Napi::Number::New(env, conflictType)});
|
|
1342
1709
|
|
|
1710
|
+
// Check for exception - Call() may have thrown
|
|
1343
1711
|
if (env.IsExceptionPending()) {
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1712
|
+
Napi::Error err = env.GetAndClearPendingException();
|
|
1713
|
+
callbacks.pendingExceptionMessage = GetErrorMessage(
|
|
1714
|
+
err, "onConflict callback threw an exception");
|
|
1715
|
+
callbacks.hasPendingException = true;
|
|
1347
1716
|
return SQLITE_CHANGESET_ABORT;
|
|
1348
1717
|
}
|
|
1349
1718
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1719
|
+
// Check for empty result (another exception indicator)
|
|
1720
|
+
if (result.IsEmpty()) {
|
|
1721
|
+
callbacks.pendingExceptionMessage = "Callback threw an exception";
|
|
1722
|
+
callbacks.hasPendingException = true;
|
|
1352
1723
|
return SQLITE_CHANGESET_ABORT;
|
|
1353
1724
|
}
|
|
1354
1725
|
|
|
1726
|
+
// Return -1 (invalid value) for non-integer results
|
|
1727
|
+
// This makes SQLite return SQLITE_MISUSE
|
|
1728
|
+
if (!result.IsNumber()) {
|
|
1729
|
+
return -1;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1355
1732
|
return result.As<Napi::Number>().Int32Value();
|
|
1733
|
+
} catch (const Napi::Error &e) {
|
|
1734
|
+
// Catch Napi::Error specifically (inherits from std::exception)
|
|
1735
|
+
callbacks.pendingExceptionMessage =
|
|
1736
|
+
GetErrorMessage(e, "onConflict callback threw an exception");
|
|
1737
|
+
callbacks.hasPendingException = true;
|
|
1738
|
+
return SQLITE_CHANGESET_ABORT;
|
|
1739
|
+
} catch (const std::exception &e) {
|
|
1740
|
+
// Catch non-Napi C++ exceptions (e.g., SqliteException)
|
|
1741
|
+
callbacks.pendingExceptionMessage =
|
|
1742
|
+
std::string("C++ exception in onConflict: ") + e.what();
|
|
1743
|
+
callbacks.hasPendingException = true;
|
|
1744
|
+
return SQLITE_CHANGESET_ABORT;
|
|
1356
1745
|
} catch (...) {
|
|
1357
|
-
// Catch
|
|
1746
|
+
// Catch all other exceptions
|
|
1358
1747
|
if (env.IsExceptionPending()) {
|
|
1359
|
-
env.GetAndClearPendingException();
|
|
1748
|
+
Napi::Error err = env.GetAndClearPendingException();
|
|
1749
|
+
callbacks.pendingExceptionMessage =
|
|
1750
|
+
GetErrorMessage(err, "Exception in onConflict callback");
|
|
1751
|
+
} else {
|
|
1752
|
+
callbacks.pendingExceptionMessage =
|
|
1753
|
+
"Unknown exception in onConflict callback";
|
|
1360
1754
|
}
|
|
1755
|
+
callbacks.hasPendingException = true;
|
|
1361
1756
|
return SQLITE_CHANGESET_ABORT;
|
|
1362
1757
|
}
|
|
1363
1758
|
};
|
|
@@ -1374,38 +1769,83 @@ Napi::Value DatabaseSync::ApplyChangeset(const Napi::CallbackInfo &info) {
|
|
|
1374
1769
|
}
|
|
1375
1770
|
|
|
1376
1771
|
Napi::Function filterFunc = filterValue.As<Napi::Function>();
|
|
1377
|
-
callbacks.filterCallback = [env,
|
|
1772
|
+
callbacks.filterCallback = [&callbacks, env,
|
|
1378
1773
|
filterFunc](std::string tableName) -> bool {
|
|
1379
|
-
|
|
1774
|
+
// Wrap in try-catch to prevent C++ exceptions from propagating
|
|
1775
|
+
// through C callback boundary into SQLite (causes SIGSEGV)
|
|
1380
1776
|
try {
|
|
1777
|
+
// Skip callback if we already have a pending exception
|
|
1778
|
+
if (callbacks.hasPendingException)
|
|
1779
|
+
return false;
|
|
1780
|
+
|
|
1781
|
+
Napi::HandleScope scope(env);
|
|
1381
1782
|
Napi::Value result =
|
|
1382
1783
|
filterFunc.Call({Napi::String::New(env, tableName)});
|
|
1383
1784
|
|
|
1785
|
+
// Check for exception - Call() may have thrown
|
|
1384
1786
|
if (env.IsExceptionPending()) {
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1787
|
+
Napi::Error err = env.GetAndClearPendingException();
|
|
1788
|
+
callbacks.pendingExceptionMessage =
|
|
1789
|
+
GetErrorMessage(err, "Filter callback threw an exception");
|
|
1790
|
+
callbacks.hasPendingException = true;
|
|
1791
|
+
return false;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Check for empty result (another exception indicator)
|
|
1795
|
+
if (result.IsEmpty()) {
|
|
1796
|
+
callbacks.pendingExceptionMessage =
|
|
1797
|
+
"Filter callback threw an exception";
|
|
1798
|
+
callbacks.hasPendingException = true;
|
|
1388
1799
|
return false;
|
|
1389
1800
|
}
|
|
1390
1801
|
|
|
1391
1802
|
return result.ToBoolean().Value();
|
|
1803
|
+
} catch (const Napi::Error &e) {
|
|
1804
|
+
// Catch Napi::Error specifically (inherits from std::exception)
|
|
1805
|
+
callbacks.pendingExceptionMessage =
|
|
1806
|
+
GetErrorMessage(e, "Filter callback threw an exception");
|
|
1807
|
+
callbacks.hasPendingException = true;
|
|
1808
|
+
return false;
|
|
1809
|
+
} catch (const std::exception &e) {
|
|
1810
|
+
// Catch non-Napi C++ exceptions (e.g., SqliteException)
|
|
1811
|
+
callbacks.pendingExceptionMessage =
|
|
1812
|
+
std::string("C++ exception in filter: ") + e.what();
|
|
1813
|
+
callbacks.hasPendingException = true;
|
|
1814
|
+
return false;
|
|
1392
1815
|
} catch (...) {
|
|
1393
|
-
// Catch
|
|
1816
|
+
// Catch all other exceptions
|
|
1394
1817
|
if (env.IsExceptionPending()) {
|
|
1395
|
-
env.GetAndClearPendingException();
|
|
1818
|
+
Napi::Error err = env.GetAndClearPendingException();
|
|
1819
|
+
callbacks.pendingExceptionMessage =
|
|
1820
|
+
GetErrorMessage(err, "Exception in filter callback");
|
|
1821
|
+
} else {
|
|
1822
|
+
callbacks.pendingExceptionMessage =
|
|
1823
|
+
"Unknown exception in filter callback";
|
|
1396
1824
|
}
|
|
1825
|
+
callbacks.hasPendingException = true;
|
|
1397
1826
|
return false;
|
|
1398
1827
|
}
|
|
1399
1828
|
};
|
|
1400
1829
|
}
|
|
1401
1830
|
}
|
|
1402
1831
|
|
|
1403
|
-
// Get the changeset
|
|
1404
|
-
Napi::
|
|
1832
|
+
// Get the changeset data from TypedArray (Uint8Array or Buffer)
|
|
1833
|
+
Napi::TypedArray typed_array = info[0].As<Napi::TypedArray>();
|
|
1834
|
+
Napi::ArrayBuffer array_buffer = typed_array.ArrayBuffer();
|
|
1835
|
+
size_t byte_offset = typed_array.ByteOffset();
|
|
1836
|
+
size_t byte_length = typed_array.ByteLength();
|
|
1837
|
+
uint8_t *data = static_cast<uint8_t *>(array_buffer.Data()) + byte_offset;
|
|
1405
1838
|
|
|
1406
1839
|
// Apply the changeset with context instead of global state
|
|
1407
|
-
int r = sqlite3changeset_apply(connection(),
|
|
1408
|
-
xFilter, xConflict, &callbacks);
|
|
1840
|
+
int r = sqlite3changeset_apply(connection(), static_cast<int>(byte_length),
|
|
1841
|
+
data, xFilter, xConflict, &callbacks);
|
|
1842
|
+
|
|
1843
|
+
// Check for pending exception from callbacks - re-throw it
|
|
1844
|
+
if (callbacks.hasPendingException) {
|
|
1845
|
+
Napi::Error::New(env, callbacks.pendingExceptionMessage)
|
|
1846
|
+
.ThrowAsJavaScriptException();
|
|
1847
|
+
return env.Undefined();
|
|
1848
|
+
}
|
|
1409
1849
|
|
|
1410
1850
|
if (r == SQLITE_OK) {
|
|
1411
1851
|
return Napi::Boolean::New(env, true);
|
|
@@ -1416,10 +1856,9 @@ Napi::Value DatabaseSync::ApplyChangeset(const Napi::CallbackInfo &info) {
|
|
|
1416
1856
|
return Napi::Boolean::New(env, false);
|
|
1417
1857
|
}
|
|
1418
1858
|
|
|
1419
|
-
// Other errors
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
1859
|
+
// Other errors - use enhanced error with errcode property
|
|
1860
|
+
const char *errMsg = sqlite3_errmsg(connection());
|
|
1861
|
+
node::ThrowEnhancedSqliteError(env, connection(), r, errMsg);
|
|
1423
1862
|
return env.Undefined();
|
|
1424
1863
|
}
|
|
1425
1864
|
|
|
@@ -1431,8 +1870,6 @@ Napi::Object StatementSync::Init(Napi::Env env, Napi::Object exports) {
|
|
|
1431
1870
|
InstanceMethod("get", &StatementSync::Get),
|
|
1432
1871
|
InstanceMethod("all", &StatementSync::All),
|
|
1433
1872
|
InstanceMethod("iterate", &StatementSync::Iterate),
|
|
1434
|
-
InstanceMethod("finalize", &StatementSync::FinalizeStatement),
|
|
1435
|
-
InstanceMethod("dispose", &StatementSync::Dispose),
|
|
1436
1873
|
InstanceMethod("setReadBigInts", &StatementSync::SetReadBigInts),
|
|
1437
1874
|
InstanceMethod("setReturnArrays", &StatementSync::SetReturnArrays),
|
|
1438
1875
|
InstanceMethod("setAllowBareNamedParameters",
|
|
@@ -1442,8 +1879,6 @@ Napi::Object StatementSync::Init(Napi::Env env, Napi::Object exports) {
|
|
|
1442
1879
|
InstanceMethod("columns", &StatementSync::Columns),
|
|
1443
1880
|
InstanceAccessor("sourceSQL", &StatementSync::SourceSQLGetter, nullptr),
|
|
1444
1881
|
InstanceAccessor("expandedSQL", &StatementSync::ExpandedSQLGetter,
|
|
1445
|
-
nullptr),
|
|
1446
|
-
InstanceAccessor("finalized", &StatementSync::FinalizedGetter,
|
|
1447
1882
|
nullptr)});
|
|
1448
1883
|
|
|
1449
1884
|
// Store constructor in per-instance addon data instead of static variable
|
|
@@ -1453,21 +1888,6 @@ Napi::Object StatementSync::Init(Napi::Env env, Napi::Object exports) {
|
|
|
1453
1888
|
Napi::Reference<Napi::Function>::New(func);
|
|
1454
1889
|
}
|
|
1455
1890
|
|
|
1456
|
-
// Add Symbol.dispose to the prototype (Node.js v25+ compatibility)
|
|
1457
|
-
Napi::Value symbolDispose =
|
|
1458
|
-
env.Global().Get("Symbol").As<Napi::Object>().Get("dispose");
|
|
1459
|
-
if (!symbolDispose.IsUndefined()) {
|
|
1460
|
-
func.Get("prototype")
|
|
1461
|
-
.As<Napi::Object>()
|
|
1462
|
-
.Set(symbolDispose,
|
|
1463
|
-
Napi::Function::New(
|
|
1464
|
-
env, [](const Napi::CallbackInfo &info) -> Napi::Value {
|
|
1465
|
-
StatementSync *stmt =
|
|
1466
|
-
StatementSync::Unwrap(info.This().As<Napi::Object>());
|
|
1467
|
-
return stmt->Dispose(info);
|
|
1468
|
-
}));
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
1891
|
exports.Set("StatementSync", func);
|
|
1472
1892
|
return exports;
|
|
1473
1893
|
}
|
|
@@ -1481,15 +1901,10 @@ StatementSync::StatementSync(const Napi::CallbackInfo &info)
|
|
|
1481
1901
|
void StatementSync::InitStatement(DatabaseSync *database,
|
|
1482
1902
|
const std::string &sql) {
|
|
1483
1903
|
if (!database || !database->IsOpen()) {
|
|
1484
|
-
throw std::runtime_error("
|
|
1904
|
+
throw std::runtime_error("database is not open");
|
|
1485
1905
|
}
|
|
1486
1906
|
|
|
1487
1907
|
database_ = database;
|
|
1488
|
-
// Create a strong reference to the database object to prevent it from being
|
|
1489
|
-
// garbage collected while this statement exists. This fixes use-after-free
|
|
1490
|
-
// when the database is GC'd before its statements.
|
|
1491
|
-
// See: https://github.com/nodejs/node/pull/56840
|
|
1492
|
-
database_ref_ = Napi::Persistent(database->Value());
|
|
1493
1908
|
source_sql_ = sql;
|
|
1494
1909
|
|
|
1495
1910
|
// Apply database-level defaults
|
|
@@ -1524,8 +1939,8 @@ void StatementSync::InitStatement(DatabaseSync *database,
|
|
|
1524
1939
|
// object and will be retrieved by the caller.
|
|
1525
1940
|
throw std::runtime_error("");
|
|
1526
1941
|
}
|
|
1527
|
-
|
|
1528
|
-
error
|
|
1942
|
+
// Use sqlite3_errmsg directly without prefix - matches Node.js error format
|
|
1943
|
+
std::string error = sqlite3_errmsg(database->connection());
|
|
1529
1944
|
// Use SqliteException to capture error info - avoids Windows ARM ABI issues
|
|
1530
1945
|
// with std::runtime_error::what() returning corrupted strings
|
|
1531
1946
|
throw SqliteException(database->connection(), result, error);
|
|
@@ -1536,10 +1951,10 @@ StatementSync::~StatementSync() {
|
|
|
1536
1951
|
if (statement_ && !finalized_) {
|
|
1537
1952
|
sqlite3_finalize(statement_);
|
|
1538
1953
|
}
|
|
1539
|
-
//
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1954
|
+
// Raw pointer to database is managed by DatabaseSync::FinalizeStatements()
|
|
1955
|
+
// which is called before DatabaseSync destructor. This avoids N-API
|
|
1956
|
+
// Reset() calls during GC which cause JIT corruption on Alpine/musl.
|
|
1957
|
+
// See: commit 4da0638, nodejs/node-addon-api#660
|
|
1543
1958
|
}
|
|
1544
1959
|
|
|
1545
1960
|
Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
|
|
@@ -1590,17 +2005,26 @@ Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
|
|
|
1590
2005
|
|
|
1591
2006
|
// Create result object
|
|
1592
2007
|
Napi::Object result_obj = Napi::Object::New(env);
|
|
1593
|
-
result_obj.Set(
|
|
1594
|
-
"changes",
|
|
1595
|
-
Napi::Number::New(env, sqlite3_changes(database_->connection())));
|
|
1596
2008
|
|
|
2009
|
+
// Get changes and lastInsertRowid
|
|
2010
|
+
int changes = sqlite3_changes(database_->connection());
|
|
1597
2011
|
sqlite3_int64 last_rowid =
|
|
1598
2012
|
sqlite3_last_insert_rowid(database_->connection());
|
|
1599
|
-
|
|
1600
|
-
|
|
2013
|
+
|
|
2014
|
+
// When readBigInts is true, return BigInt for both (matches Node.js)
|
|
2015
|
+
if (use_big_ints_) {
|
|
2016
|
+
result_obj.Set("changes",
|
|
2017
|
+
Napi::BigInt::New(env, static_cast<int64_t>(changes)));
|
|
2018
|
+
result_obj.Set("lastInsertRowid",
|
|
2019
|
+
Napi::BigInt::New(env, static_cast<int64_t>(last_rowid)));
|
|
2020
|
+
} else if (last_rowid > JS_MAX_SAFE_INTEGER ||
|
|
2021
|
+
last_rowid < JS_MIN_SAFE_INTEGER) {
|
|
2022
|
+
// Use JavaScript's safe integer limits (2^53 - 1)
|
|
2023
|
+
result_obj.Set("changes", Napi::Number::New(env, changes));
|
|
1601
2024
|
result_obj.Set("lastInsertRowid",
|
|
1602
2025
|
Napi::BigInt::New(env, static_cast<int64_t>(last_rowid)));
|
|
1603
2026
|
} else {
|
|
2027
|
+
result_obj.Set("changes", Napi::Number::New(env, changes));
|
|
1604
2028
|
result_obj.Set("lastInsertRowid",
|
|
1605
2029
|
Napi::Number::New(env, static_cast<double>(last_rowid)));
|
|
1606
2030
|
}
|
|
@@ -1646,16 +2070,26 @@ Napi::Value StatementSync::Get(const Napi::CallbackInfo &info) {
|
|
|
1646
2070
|
int result = sqlite3_step(statement_);
|
|
1647
2071
|
|
|
1648
2072
|
if (result == SQLITE_ROW) {
|
|
1649
|
-
|
|
2073
|
+
Napi::Value value = CreateResult();
|
|
2074
|
+
// Reset statement after fetching result to release locks (like Node.js
|
|
2075
|
+
// OnScopeLeave)
|
|
2076
|
+
sqlite3_reset(statement_);
|
|
2077
|
+
return value;
|
|
1650
2078
|
} else if (result == SQLITE_DONE) {
|
|
2079
|
+
// Reset statement to release locks even when no rows returned
|
|
2080
|
+
sqlite3_reset(statement_);
|
|
1651
2081
|
return env.Undefined();
|
|
1652
2082
|
} else {
|
|
2083
|
+
// Reset statement before throwing to release locks
|
|
2084
|
+
sqlite3_reset(statement_);
|
|
1653
2085
|
std::string error = sqlite3_errmsg(database_->connection());
|
|
1654
2086
|
ThrowEnhancedSqliteErrorWithDB(env, database_, database_->connection(),
|
|
1655
2087
|
result, error);
|
|
1656
2088
|
return env.Undefined();
|
|
1657
2089
|
}
|
|
1658
2090
|
} catch (const std::exception &e) {
|
|
2091
|
+
// Reset statement on exception to release locks
|
|
2092
|
+
sqlite3_reset(statement_);
|
|
1659
2093
|
ThrowErrSqliteErrorWithDb(env, database_, e.what());
|
|
1660
2094
|
return env.Undefined();
|
|
1661
2095
|
}
|
|
@@ -1697,8 +2131,12 @@ Napi::Value StatementSync::All(const Napi::CallbackInfo &info) {
|
|
|
1697
2131
|
if (result == SQLITE_ROW) {
|
|
1698
2132
|
results.Set(index++, CreateResult());
|
|
1699
2133
|
} else if (result == SQLITE_DONE) {
|
|
2134
|
+
// Reset statement to release locks (like Node.js OnScopeLeave)
|
|
2135
|
+
sqlite3_reset(statement_);
|
|
1700
2136
|
break;
|
|
1701
2137
|
} else {
|
|
2138
|
+
// Reset statement before throwing to release locks
|
|
2139
|
+
sqlite3_reset(statement_);
|
|
1702
2140
|
std::string error = sqlite3_errmsg(database_->connection());
|
|
1703
2141
|
node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
|
|
1704
2142
|
return env.Undefined();
|
|
@@ -1707,6 +2145,8 @@ Napi::Value StatementSync::All(const Napi::CallbackInfo &info) {
|
|
|
1707
2145
|
|
|
1708
2146
|
return results;
|
|
1709
2147
|
} catch (const std::exception &e) {
|
|
2148
|
+
// Reset statement on exception to release locks
|
|
2149
|
+
sqlite3_reset(statement_);
|
|
1710
2150
|
node::THROW_ERR_SQLITE_ERROR(env, e.what());
|
|
1711
2151
|
return env.Undefined();
|
|
1712
2152
|
}
|
|
@@ -1902,13 +2342,9 @@ StatementSync::SetAllowUnknownNamedParameters(const Napi::CallbackInfo &info) {
|
|
|
1902
2342
|
Napi::Value StatementSync::Columns(const Napi::CallbackInfo &info) {
|
|
1903
2343
|
Napi::Env env = info.Env();
|
|
1904
2344
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
if (!database_ || !database_->IsOpen()) {
|
|
1911
|
-
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
2345
|
+
// When database is closed, statement is implicitly finalized by SQLite
|
|
2346
|
+
if (finalized_ || !database_ || !database_->IsOpen()) {
|
|
2347
|
+
node::THROW_ERR_INVALID_STATE(env, "statement has been finalized");
|
|
1912
2348
|
return env.Undefined();
|
|
1913
2349
|
}
|
|
1914
2350
|
|
|
@@ -1921,7 +2357,7 @@ Napi::Value StatementSync::Columns(const Napi::CallbackInfo &info) {
|
|
|
1921
2357
|
Napi::Array columns = Napi::Array::New(env, column_count);
|
|
1922
2358
|
|
|
1923
2359
|
for (int i = 0; i < column_count; i++) {
|
|
1924
|
-
Napi::Object column_info =
|
|
2360
|
+
Napi::Object column_info = CreateObjectWithNullPrototype(env);
|
|
1925
2361
|
|
|
1926
2362
|
// column: The original column name (sqlite3_column_origin_name)
|
|
1927
2363
|
const char *origin_name = sqlite3_column_origin_name(statement_, i);
|
|
@@ -1989,11 +2425,18 @@ void StatementSync::BindParameters(const Napi::CallbackInfo &info,
|
|
|
1989
2425
|
return;
|
|
1990
2426
|
}
|
|
1991
2427
|
|
|
1992
|
-
//
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2428
|
+
// Track where positional parameters start
|
|
2429
|
+
size_t positional_start = start_index;
|
|
2430
|
+
|
|
2431
|
+
// Check if first argument is an object for named parameters
|
|
2432
|
+
// (not a Buffer, TypedArray, or Array - those are positional values)
|
|
2433
|
+
if (info.Length() > start_index && info[start_index].IsObject() &&
|
|
2434
|
+
!info[start_index].IsBuffer() && !info[start_index].IsArray() &&
|
|
2435
|
+
!info[start_index].IsTypedArray()) {
|
|
2436
|
+
// Named parameters binding from the object
|
|
1996
2437
|
Napi::Object obj = info[start_index].As<Napi::Object>();
|
|
2438
|
+
positional_start =
|
|
2439
|
+
start_index + 1; // Positional args start after the object
|
|
1997
2440
|
|
|
1998
2441
|
// Build bare named params map if needed
|
|
1999
2442
|
if (allow_bare_named_params_ && !bare_named_params_.has_value()) {
|
|
@@ -2046,13 +2489,11 @@ void StatementSync::BindParameters(const Napi::CallbackInfo &info,
|
|
|
2046
2489
|
|
|
2047
2490
|
if (param_index > 0) {
|
|
2048
2491
|
Napi::Value value = obj.Get(key_str);
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
"Error binding parameter '" + key_str + "': " + e.Message();
|
|
2055
|
-
node::THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str());
|
|
2492
|
+
BindSingleParameter(param_index, value);
|
|
2493
|
+
// Check for pending exceptions set by THROW_ERR_* macros
|
|
2494
|
+
// (they use ThrowAsJavaScriptException which doesn't throw C++
|
|
2495
|
+
// exceptions)
|
|
2496
|
+
if (env.IsExceptionPending()) {
|
|
2056
2497
|
return;
|
|
2057
2498
|
}
|
|
2058
2499
|
} else {
|
|
@@ -2063,24 +2504,41 @@ void StatementSync::BindParameters(const Napi::CallbackInfo &info,
|
|
|
2063
2504
|
} else {
|
|
2064
2505
|
// Throw error when not allowed (default behavior)
|
|
2065
2506
|
std::string msg = "Unknown named parameter '" + key_str + "'";
|
|
2066
|
-
node::
|
|
2507
|
+
node::THROW_ERR_INVALID_STATE(env, msg.c_str());
|
|
2067
2508
|
return;
|
|
2068
2509
|
}
|
|
2069
2510
|
}
|
|
2070
2511
|
}
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// Bind remaining positional parameters to anonymous placeholders (?)
|
|
2515
|
+
// This handles both: (a) all args are positional, (b) first arg was named
|
|
2516
|
+
// params object and remaining are positional
|
|
2517
|
+
if (positional_start < info.Length()) {
|
|
2518
|
+
int anon_idx = 1;
|
|
2519
|
+
int param_count = sqlite3_bind_parameter_count(statement_);
|
|
2520
|
+
|
|
2521
|
+
for (size_t i = positional_start; i < info.Length(); i++) {
|
|
2522
|
+
// Skip to the next anonymous placeholder (unnamed or ?NNN)
|
|
2523
|
+
while (anon_idx <= param_count) {
|
|
2524
|
+
const char *param_name =
|
|
2525
|
+
sqlite3_bind_parameter_name(statement_, anon_idx);
|
|
2526
|
+
// Anonymous placeholders have nullptr name or start with '?'
|
|
2527
|
+
if (param_name == nullptr || param_name[0] == '?') {
|
|
2528
|
+
break;
|
|
2529
|
+
}
|
|
2530
|
+
anon_idx++;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// When anon_idx > param_count, SQLite will return SQLITE_RANGE
|
|
2534
|
+
// and we'll throw an appropriate ERR_SQLITE_ERROR
|
|
2535
|
+
|
|
2536
|
+
BindSingleParameter(anon_idx, info[i]);
|
|
2537
|
+
// Check for pending exceptions set by THROW_ERR_* macros
|
|
2538
|
+
if (env.IsExceptionPending()) {
|
|
2082
2539
|
return;
|
|
2083
2540
|
}
|
|
2541
|
+
anon_idx++;
|
|
2084
2542
|
}
|
|
2085
2543
|
}
|
|
2086
2544
|
}
|
|
@@ -2091,41 +2549,49 @@ void StatementSync::BindSingleParameter(int param_index, Napi::Value param) {
|
|
|
2091
2549
|
return; // Silent return since error was already thrown by caller
|
|
2092
2550
|
}
|
|
2093
2551
|
|
|
2552
|
+
int rc = SQLITE_OK;
|
|
2553
|
+
|
|
2094
2554
|
try {
|
|
2095
|
-
if (param.IsNull()
|
|
2096
|
-
sqlite3_bind_null(statement_, param_index);
|
|
2555
|
+
if (param.IsNull()) {
|
|
2556
|
+
rc = sqlite3_bind_null(statement_, param_index);
|
|
2557
|
+
} else if (param.IsUndefined()) {
|
|
2558
|
+
// Node.js throws for undefined (unlike null which binds as SQL NULL)
|
|
2559
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
2560
|
+
Env(), ("Provided value cannot be bound to SQLite parameter " +
|
|
2561
|
+
std::to_string(param_index) + ".")
|
|
2562
|
+
.c_str());
|
|
2563
|
+
return;
|
|
2097
2564
|
} else if (param.IsBigInt()) {
|
|
2098
2565
|
// Handle BigInt before IsNumber since BigInt values should bind as int64
|
|
2099
2566
|
bool lossless;
|
|
2100
2567
|
int64_t bigint_val = param.As<Napi::BigInt>().Int64Value(&lossless);
|
|
2101
2568
|
if (lossless) {
|
|
2102
|
-
sqlite3_bind_int64(statement_, param_index,
|
|
2103
|
-
|
|
2569
|
+
rc = sqlite3_bind_int64(statement_, param_index,
|
|
2570
|
+
static_cast<sqlite3_int64>(bigint_val));
|
|
2104
2571
|
} else {
|
|
2105
|
-
// BigInt too large
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
SQLITE_TRANSIENT);
|
|
2572
|
+
// BigInt too large for SQLite int64 - throw error (matches Node.js)
|
|
2573
|
+
node::THROW_ERR_INVALID_ARG_VALUE(Env(),
|
|
2574
|
+
"BigInt value is too large to bind.");
|
|
2575
|
+
return;
|
|
2110
2576
|
}
|
|
2111
2577
|
} else if (param.IsNumber()) {
|
|
2112
2578
|
double val = param.As<Napi::Number>().DoubleValue();
|
|
2113
2579
|
if (std::abs(val - std::floor(val)) <
|
|
2114
2580
|
std::numeric_limits<double>::epsilon() &&
|
|
2115
2581
|
val >= INT32_MIN && val <= INT32_MAX) {
|
|
2116
|
-
sqlite3_bind_int(statement_, param_index,
|
|
2117
|
-
|
|
2582
|
+
rc = sqlite3_bind_int(statement_, param_index,
|
|
2583
|
+
param.As<Napi::Number>().Int32Value());
|
|
2118
2584
|
} else {
|
|
2119
|
-
sqlite3_bind_double(statement_, param_index,
|
|
2120
|
-
|
|
2585
|
+
rc = sqlite3_bind_double(statement_, param_index,
|
|
2586
|
+
param.As<Napi::Number>().DoubleValue());
|
|
2121
2587
|
}
|
|
2122
2588
|
} else if (param.IsString()) {
|
|
2123
2589
|
std::string str = param.As<Napi::String>().Utf8Value();
|
|
2124
|
-
sqlite3_bind_text(statement_, param_index, str.c_str(), -1,
|
|
2125
|
-
|
|
2590
|
+
rc = sqlite3_bind_text(statement_, param_index, str.c_str(), -1,
|
|
2591
|
+
SQLITE_TRANSIENT);
|
|
2126
2592
|
} else if (param.IsBoolean()) {
|
|
2127
|
-
sqlite3_bind_int(statement_, param_index,
|
|
2128
|
-
|
|
2593
|
+
rc = sqlite3_bind_int(statement_, param_index,
|
|
2594
|
+
param.As<Napi::Boolean>().Value() ? 1 : 0);
|
|
2129
2595
|
} else if (param.IsDataView()) {
|
|
2130
2596
|
// IMPORTANT: Check DataView BEFORE IsBuffer() because N-API's IsBuffer()
|
|
2131
2597
|
// returns true for ALL ArrayBufferViews (including DataView), but
|
|
@@ -2134,47 +2600,66 @@ void StatementSync::BindSingleParameter(int param_index, Napi::Value param) {
|
|
|
2134
2600
|
Napi::ArrayBuffer arrayBuffer = dataView.ArrayBuffer();
|
|
2135
2601
|
size_t byteOffset = dataView.ByteOffset();
|
|
2136
2602
|
size_t byteLength = dataView.ByteLength();
|
|
2137
|
-
|
|
2603
|
+
// Use non-NULL pointer for zero-length blobs to preserve BLOB vs NULL
|
|
2604
|
+
// distinction. See: https://sqlite.org/c3ref/bind_blob.html
|
|
2605
|
+
const void *data = nullptr;
|
|
2138
2606
|
if (arrayBuffer.Data() != nullptr && byteLength > 0) {
|
|
2139
|
-
const uint8_t
|
|
2140
|
-
static_cast<const uint8_t *>(arrayBuffer.Data()) + byteOffset;
|
|
2141
|
-
sqlite3_bind_blob(statement_, param_index, data,
|
|
2142
|
-
SafeCastToInt(byteLength), SQLITE_TRANSIENT);
|
|
2143
|
-
} else {
|
|
2144
|
-
sqlite3_bind_null(statement_, param_index);
|
|
2607
|
+
data = static_cast<const uint8_t *>(arrayBuffer.Data()) + byteOffset;
|
|
2145
2608
|
}
|
|
2609
|
+
rc = sqlite3_bind_blob(statement_, param_index, data ? data : "",
|
|
2610
|
+
SafeCastToInt(byteLength), SQLITE_TRANSIENT);
|
|
2146
2611
|
} else if (param.IsBuffer()) {
|
|
2147
2612
|
// Handles Buffer and TypedArray (both are ArrayBufferViews that work
|
|
2148
2613
|
// correctly with Buffer cast - Buffer::Data() handles byte offsets
|
|
2149
2614
|
// internally)
|
|
2150
2615
|
Napi::Buffer<uint8_t> buffer = param.As<Napi::Buffer<uint8_t>>();
|
|
2151
|
-
|
|
2152
|
-
|
|
2616
|
+
// Use non-NULL pointer for zero-length blobs to preserve BLOB vs NULL
|
|
2617
|
+
// distinction. See: https://sqlite.org/c3ref/bind_blob.html
|
|
2618
|
+
const void *data = buffer.Data();
|
|
2619
|
+
rc = sqlite3_bind_blob(statement_, param_index, data ? data : "",
|
|
2620
|
+
SafeCastToInt(buffer.Length()), SQLITE_TRANSIENT);
|
|
2153
2621
|
} else if (param.IsFunction()) {
|
|
2154
|
-
// Functions cannot be
|
|
2155
|
-
|
|
2622
|
+
// Functions cannot be bound to SQLite parameters - throw error
|
|
2623
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
2624
|
+
Env(), ("Provided value cannot be bound to SQLite parameter " +
|
|
2625
|
+
std::to_string(param_index) + ".")
|
|
2626
|
+
.c_str());
|
|
2627
|
+
return;
|
|
2156
2628
|
} else if (param.IsArrayBuffer()) {
|
|
2157
2629
|
// Handle ArrayBuffer as binary data
|
|
2158
2630
|
Napi::ArrayBuffer arrayBuffer = param.As<Napi::ArrayBuffer>();
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
}
|
|
2631
|
+
// Use non-NULL pointer for zero-length blobs to preserve BLOB vs NULL
|
|
2632
|
+
// distinction. See: https://sqlite.org/c3ref/bind_blob.html
|
|
2633
|
+
const void *data = arrayBuffer.Data();
|
|
2634
|
+
rc = sqlite3_bind_blob(statement_, param_index, data ? data : "",
|
|
2635
|
+
SafeCastToInt(arrayBuffer.ByteLength()),
|
|
2636
|
+
SQLITE_TRANSIENT);
|
|
2166
2637
|
} else if (param.IsObject()) {
|
|
2167
2638
|
// Objects and arrays cannot be bound to SQLite parameters (same as
|
|
2168
2639
|
// Node.js behavior). Note: DataView, Buffer, TypedArray, and ArrayBuffer
|
|
2169
2640
|
// are handled above and don't reach this branch.
|
|
2170
|
-
|
|
2171
|
-
Env(), "Provided value cannot be bound to SQLite parameter " +
|
|
2172
|
-
|
|
2641
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
2642
|
+
Env(), ("Provided value cannot be bound to SQLite parameter " +
|
|
2643
|
+
std::to_string(param_index) + ".")
|
|
2644
|
+
.c_str());
|
|
2645
|
+
return;
|
|
2173
2646
|
} else {
|
|
2174
|
-
// For any other type, throw error like Node.js does
|
|
2175
|
-
|
|
2176
|
-
Env(), "Provided value cannot be bound to SQLite parameter " +
|
|
2177
|
-
|
|
2647
|
+
// For any other type (Symbol, etc.), throw error like Node.js does
|
|
2648
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
2649
|
+
Env(), ("Provided value cannot be bound to SQLite parameter " +
|
|
2650
|
+
std::to_string(param_index) + ".")
|
|
2651
|
+
.c_str());
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// Check the result of sqlite3_bind_*
|
|
2656
|
+
if (rc != SQLITE_OK) {
|
|
2657
|
+
sqlite3 *db_handle = database_ ? database_->connection() : nullptr;
|
|
2658
|
+
// Get the error message from SQLite
|
|
2659
|
+
const char *err_msg = sqlite3_errstr(rc);
|
|
2660
|
+
node::ThrowEnhancedSqliteError(Env(), db_handle, rc,
|
|
2661
|
+
err_msg ? err_msg : "SQLite error");
|
|
2662
|
+
return;
|
|
2178
2663
|
}
|
|
2179
2664
|
} catch (const Napi::Error &e) {
|
|
2180
2665
|
// Re-throw Napi errors
|
|
@@ -2220,8 +2705,15 @@ Napi::Value StatementSync::CreateResult() {
|
|
|
2220
2705
|
value = Napi::BigInt::New(env, static_cast<int64_t>(int_val));
|
|
2221
2706
|
} else if (int_val > JS_MAX_SAFE_INTEGER ||
|
|
2222
2707
|
int_val < JS_MIN_SAFE_INTEGER) {
|
|
2223
|
-
//
|
|
2224
|
-
|
|
2708
|
+
// Throw ERR_OUT_OF_RANGE for values outside safe integer range
|
|
2709
|
+
// (matches Node.js behavior)
|
|
2710
|
+
char error_msg[128];
|
|
2711
|
+
snprintf(error_msg, sizeof(error_msg),
|
|
2712
|
+
"Value is too large to be represented as a JavaScript "
|
|
2713
|
+
"number: %" PRId64,
|
|
2714
|
+
static_cast<int64_t>(int_val));
|
|
2715
|
+
node::THROW_ERR_OUT_OF_RANGE(env, error_msg);
|
|
2716
|
+
return env.Undefined();
|
|
2225
2717
|
} else {
|
|
2226
2718
|
value = Napi::Number::New(env, static_cast<double>(int_val));
|
|
2227
2719
|
}
|
|
@@ -2244,12 +2736,15 @@ Napi::Value StatementSync::CreateResult() {
|
|
|
2244
2736
|
const void *blob_data = sqlite3_column_blob(statement_, i);
|
|
2245
2737
|
int blob_size = sqlite3_column_bytes(statement_, i);
|
|
2246
2738
|
// sqlite3_column_blob() can return NULL for zero-length BLOBs or on OOM
|
|
2739
|
+
// Return Uint8Array to match Node.js node:sqlite behavior
|
|
2247
2740
|
if (!blob_data || blob_size == 0) {
|
|
2248
|
-
// Handle empty/NULL blob - create empty
|
|
2249
|
-
|
|
2741
|
+
// Handle empty/NULL blob - create empty Uint8Array
|
|
2742
|
+
auto array_buffer = Napi::ArrayBuffer::New(env, 0);
|
|
2743
|
+
value = Napi::Uint8Array::New(env, 0, array_buffer, 0);
|
|
2250
2744
|
} else {
|
|
2251
|
-
|
|
2252
|
-
|
|
2745
|
+
auto array_buffer = Napi::ArrayBuffer::New(env, blob_size);
|
|
2746
|
+
memcpy(array_buffer.Data(), blob_data, blob_size);
|
|
2747
|
+
value = Napi::Uint8Array::New(env, blob_size, array_buffer, 0);
|
|
2253
2748
|
}
|
|
2254
2749
|
break;
|
|
2255
2750
|
}
|
|
@@ -2263,8 +2758,8 @@ Napi::Value StatementSync::CreateResult() {
|
|
|
2263
2758
|
|
|
2264
2759
|
return result;
|
|
2265
2760
|
} else {
|
|
2266
|
-
// Return result as object (
|
|
2267
|
-
Napi::Object result =
|
|
2761
|
+
// Return result as object with null prototype (matches Node.js behavior)
|
|
2762
|
+
Napi::Object result = CreateObjectWithNullPrototype(env);
|
|
2268
2763
|
|
|
2269
2764
|
for (int i = 0; i < column_count; i++) {
|
|
2270
2765
|
const char *column_name = sqlite3_column_name(statement_, i);
|
|
@@ -2283,8 +2778,15 @@ Napi::Value StatementSync::CreateResult() {
|
|
|
2283
2778
|
value = Napi::BigInt::New(env, static_cast<int64_t>(int_val));
|
|
2284
2779
|
} else if (int_val > JS_MAX_SAFE_INTEGER ||
|
|
2285
2780
|
int_val < JS_MIN_SAFE_INTEGER) {
|
|
2286
|
-
//
|
|
2287
|
-
|
|
2781
|
+
// Throw ERR_OUT_OF_RANGE for values outside safe integer range
|
|
2782
|
+
// (matches Node.js behavior)
|
|
2783
|
+
char error_msg[128];
|
|
2784
|
+
snprintf(error_msg, sizeof(error_msg),
|
|
2785
|
+
"Value is too large to be represented as a JavaScript "
|
|
2786
|
+
"number: %" PRId64,
|
|
2787
|
+
static_cast<int64_t>(int_val));
|
|
2788
|
+
node::THROW_ERR_OUT_OF_RANGE(env, error_msg);
|
|
2789
|
+
return env.Undefined();
|
|
2288
2790
|
} else {
|
|
2289
2791
|
value = Napi::Number::New(env, static_cast<double>(int_val));
|
|
2290
2792
|
}
|
|
@@ -2307,12 +2809,15 @@ Napi::Value StatementSync::CreateResult() {
|
|
|
2307
2809
|
const void *blob_data = sqlite3_column_blob(statement_, i);
|
|
2308
2810
|
int blob_size = sqlite3_column_bytes(statement_, i);
|
|
2309
2811
|
// sqlite3_column_blob() can return NULL for zero-length BLOBs or on OOM
|
|
2812
|
+
// Return Uint8Array to match Node.js node:sqlite behavior
|
|
2310
2813
|
if (!blob_data || blob_size == 0) {
|
|
2311
|
-
// Handle empty/NULL blob - create empty
|
|
2312
|
-
|
|
2814
|
+
// Handle empty/NULL blob - create empty Uint8Array
|
|
2815
|
+
auto array_buffer = Napi::ArrayBuffer::New(env, 0);
|
|
2816
|
+
value = Napi::Uint8Array::New(env, 0, array_buffer, 0);
|
|
2313
2817
|
} else {
|
|
2314
|
-
|
|
2315
|
-
|
|
2818
|
+
auto array_buffer = Napi::ArrayBuffer::New(env, blob_size);
|
|
2819
|
+
memcpy(array_buffer.Data(), blob_data, blob_size);
|
|
2820
|
+
value = Napi::Uint8Array::New(env, blob_size, array_buffer, 0);
|
|
2316
2821
|
}
|
|
2317
2822
|
break;
|
|
2318
2823
|
}
|
|
@@ -2346,7 +2851,8 @@ Napi::Object StatementSyncIterator::Init(Napi::Env env, Napi::Object exports) {
|
|
|
2346
2851
|
Napi::Function func =
|
|
2347
2852
|
DefineClass(env, "StatementSyncIterator",
|
|
2348
2853
|
{InstanceMethod("next", &StatementSyncIterator::Next),
|
|
2349
|
-
InstanceMethod("return", &StatementSyncIterator::Return)
|
|
2854
|
+
InstanceMethod("return", &StatementSyncIterator::Return),
|
|
2855
|
+
InstanceMethod("toArray", &StatementSyncIterator::ToArray)});
|
|
2350
2856
|
|
|
2351
2857
|
// Set up Symbol.iterator on the prototype to make it properly iterable
|
|
2352
2858
|
Napi::Object prototype = func.Get("prototype").As<Napi::Object>();
|
|
@@ -2358,6 +2864,20 @@ Napi::Object StatementSyncIterator::Init(Napi::Env env, Napi::Object exports) {
|
|
|
2358
2864
|
return info.This();
|
|
2359
2865
|
}));
|
|
2360
2866
|
|
|
2867
|
+
// Make our iterator inherit from Iterator.prototype so it's an instanceof
|
|
2868
|
+
// Iterator and gets Iterator Helper methods (map, filter, etc.)
|
|
2869
|
+
Napi::Object global = env.Global();
|
|
2870
|
+
Napi::Value iteratorValue = global.Get("Iterator");
|
|
2871
|
+
if (iteratorValue.IsFunction()) {
|
|
2872
|
+
Napi::Object iteratorProto =
|
|
2873
|
+
iteratorValue.As<Napi::Function>().Get("prototype").As<Napi::Object>();
|
|
2874
|
+
// Use Object.setPrototypeOf to set Iterator.prototype as prototype
|
|
2875
|
+
Napi::Object objectCtor = global.Get("Object").As<Napi::Object>();
|
|
2876
|
+
Napi::Function setPrototypeOf =
|
|
2877
|
+
objectCtor.Get("setPrototypeOf").As<Napi::Function>();
|
|
2878
|
+
setPrototypeOf.Call({prototype, iteratorProto});
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2361
2881
|
// Store constructor in per-instance addon data instead of static variable
|
|
2362
2882
|
AddonData *addon_data = GetAddonData(env);
|
|
2363
2883
|
if (addon_data) {
|
|
@@ -2408,7 +2928,7 @@ Napi::Value StatementSyncIterator::Next(const Napi::CallbackInfo &info) {
|
|
|
2408
2928
|
}
|
|
2409
2929
|
|
|
2410
2930
|
if (done_) {
|
|
2411
|
-
Napi::Object result =
|
|
2931
|
+
Napi::Object result = CreateObjectWithNullPrototype(env);
|
|
2412
2932
|
result.Set("done", true);
|
|
2413
2933
|
result.Set("value", env.Null());
|
|
2414
2934
|
return result;
|
|
@@ -2427,7 +2947,7 @@ Napi::Value StatementSyncIterator::Next(const Napi::CallbackInfo &info) {
|
|
|
2427
2947
|
sqlite3_reset(stmt_->statement_);
|
|
2428
2948
|
done_ = true;
|
|
2429
2949
|
|
|
2430
|
-
Napi::Object result =
|
|
2950
|
+
Napi::Object result = CreateObjectWithNullPrototype(env);
|
|
2431
2951
|
result.Set("done", true);
|
|
2432
2952
|
result.Set("value", env.Null());
|
|
2433
2953
|
return result;
|
|
@@ -2436,7 +2956,7 @@ Napi::Value StatementSyncIterator::Next(const Napi::CallbackInfo &info) {
|
|
|
2436
2956
|
// Create row object using existing CreateResult method
|
|
2437
2957
|
Napi::Value row_value = stmt_->CreateResult();
|
|
2438
2958
|
|
|
2439
|
-
Napi::Object result =
|
|
2959
|
+
Napi::Object result = CreateObjectWithNullPrototype(env);
|
|
2440
2960
|
result.Set("done", false);
|
|
2441
2961
|
result.Set("value", row_value);
|
|
2442
2962
|
return result;
|
|
@@ -2459,12 +2979,58 @@ Napi::Value StatementSyncIterator::Return(const Napi::CallbackInfo &info) {
|
|
|
2459
2979
|
sqlite3_reset(stmt_->statement_);
|
|
2460
2980
|
done_ = true;
|
|
2461
2981
|
|
|
2462
|
-
Napi::Object result =
|
|
2982
|
+
Napi::Object result = CreateObjectWithNullPrototype(env);
|
|
2463
2983
|
result.Set("done", true);
|
|
2464
2984
|
result.Set("value", env.Null());
|
|
2465
2985
|
return result;
|
|
2466
2986
|
}
|
|
2467
2987
|
|
|
2988
|
+
Napi::Value StatementSyncIterator::ToArray(const Napi::CallbackInfo &info) {
|
|
2989
|
+
Napi::Env env = info.Env();
|
|
2990
|
+
|
|
2991
|
+
if (!stmt_ || stmt_->finalized_) {
|
|
2992
|
+
node::THROW_ERR_INVALID_STATE(env, "statement has been finalized");
|
|
2993
|
+
return env.Undefined();
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
if (!stmt_->database_ || !stmt_->database_->IsOpen()) {
|
|
2997
|
+
node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
|
|
2998
|
+
return env.Undefined();
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
Napi::Array arr = Napi::Array::New(env);
|
|
3002
|
+
uint32_t idx = 0;
|
|
3003
|
+
|
|
3004
|
+
while (!done_) {
|
|
3005
|
+
int r = sqlite3_step(stmt_->statement_);
|
|
3006
|
+
|
|
3007
|
+
if (r != SQLITE_ROW) {
|
|
3008
|
+
if (r != SQLITE_DONE) {
|
|
3009
|
+
node::THROW_ERR_SQLITE_ERROR(
|
|
3010
|
+
env, sqlite3_errmsg(stmt_->database_->connection()));
|
|
3011
|
+
return env.Undefined();
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// End of results
|
|
3015
|
+
sqlite3_reset(stmt_->statement_);
|
|
3016
|
+
done_ = true;
|
|
3017
|
+
break;
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// Create row object using existing CreateResult method
|
|
3021
|
+
Napi::Value row_value = stmt_->CreateResult();
|
|
3022
|
+
|
|
3023
|
+
// Check if CreateResult threw an error
|
|
3024
|
+
if (env.IsExceptionPending()) {
|
|
3025
|
+
return env.Undefined();
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
arr.Set(idx++, row_value);
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
return arr;
|
|
3032
|
+
}
|
|
3033
|
+
|
|
2468
3034
|
// Session Implementation
|
|
2469
3035
|
Napi::Object Session::Init(Napi::Env env, Napi::Object exports) {
|
|
2470
3036
|
Napi::Function func =
|
|
@@ -2537,11 +3103,10 @@ void Session::Delete() {
|
|
|
2537
3103
|
|
|
2538
3104
|
// Remove ourselves from the database's session list BEFORE deleting
|
|
2539
3105
|
// to avoid any potential issues with the database trying to access us
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
database->RemoveSession(this);
|
|
3106
|
+
// Note: Keep database_ non-null so we can check if db is open vs session
|
|
3107
|
+
// closed
|
|
3108
|
+
if (database_) {
|
|
3109
|
+
database_->RemoveSession(this);
|
|
2545
3110
|
}
|
|
2546
3111
|
|
|
2547
3112
|
// Now it's safe to delete the SQLite session
|
|
@@ -2552,12 +3117,20 @@ template <int (*sqliteChangesetFunc)(sqlite3_session *, int *, void **)>
|
|
|
2552
3117
|
Napi::Value Session::GenericChangeset(const Napi::CallbackInfo &info) {
|
|
2553
3118
|
Napi::Env env = info.Env();
|
|
2554
3119
|
|
|
3120
|
+
// Check database first - if db was closed, that's the primary error
|
|
3121
|
+
// Note: database_ is preserved in Delete(), so we can check IsOpen()
|
|
3122
|
+
if (database_ && !database_->IsOpen()) {
|
|
3123
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
3124
|
+
return env.Undefined();
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// Then check if session was explicitly closed
|
|
2555
3128
|
if (session_ == nullptr) {
|
|
2556
3129
|
node::THROW_ERR_INVALID_STATE(env, "session is not open");
|
|
2557
3130
|
return env.Undefined();
|
|
2558
3131
|
}
|
|
2559
3132
|
|
|
2560
|
-
if (!database_
|
|
3133
|
+
if (!database_) {
|
|
2561
3134
|
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
2562
3135
|
return env.Undefined();
|
|
2563
3136
|
}
|
|
@@ -2567,21 +3140,24 @@ Napi::Value Session::GenericChangeset(const Napi::CallbackInfo &info) {
|
|
|
2567
3140
|
int r = sqliteChangesetFunc(session_, &nChangeset, &pChangeset);
|
|
2568
3141
|
|
|
2569
3142
|
if (r != SQLITE_OK) {
|
|
2570
|
-
|
|
3143
|
+
// Use sqlite3_errstr(r) to get a description of the error code,
|
|
3144
|
+
// rather than sqlite3_errmsg() which returns the last error on the
|
|
3145
|
+
// connection (which may not be related to this session operation)
|
|
3146
|
+
const char *errStr = sqlite3_errstr(r);
|
|
2571
3147
|
Napi::Error::New(env,
|
|
2572
|
-
std::string("Failed to generate changeset: ") +
|
|
3148
|
+
std::string("Failed to generate changeset: ") + errStr)
|
|
2573
3149
|
.ThrowAsJavaScriptException();
|
|
2574
3150
|
return env.Undefined();
|
|
2575
3151
|
}
|
|
2576
3152
|
|
|
2577
|
-
// Create a
|
|
2578
|
-
Napi::
|
|
2579
|
-
std::memcpy(
|
|
3153
|
+
// Create a Uint8Array from the changeset data (matches node:sqlite API)
|
|
3154
|
+
Napi::ArrayBuffer arrayBuffer = Napi::ArrayBuffer::New(env, nChangeset);
|
|
3155
|
+
std::memcpy(arrayBuffer.Data(), pChangeset, nChangeset);
|
|
2580
3156
|
|
|
2581
3157
|
// Free the changeset allocated by SQLite
|
|
2582
3158
|
sqlite3_free(pChangeset);
|
|
2583
3159
|
|
|
2584
|
-
return
|
|
3160
|
+
return Napi::Uint8Array::New(env, nChangeset, arrayBuffer, 0);
|
|
2585
3161
|
}
|
|
2586
3162
|
|
|
2587
3163
|
Napi::Value Session::Changeset(const Napi::CallbackInfo &info) {
|
|
@@ -2595,11 +3171,24 @@ Napi::Value Session::Patchset(const Napi::CallbackInfo &info) {
|
|
|
2595
3171
|
Napi::Value Session::Close(const Napi::CallbackInfo &info) {
|
|
2596
3172
|
Napi::Env env = info.Env();
|
|
2597
3173
|
|
|
3174
|
+
// Check database first - if db was closed, that's the primary error
|
|
3175
|
+
// Note: database_ is preserved in Delete(), so we can check IsOpen()
|
|
3176
|
+
if (database_ && !database_->IsOpen()) {
|
|
3177
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
3178
|
+
return env.Undefined();
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
// Then check if session was explicitly closed
|
|
2598
3182
|
if (session_ == nullptr) {
|
|
2599
3183
|
node::THROW_ERR_INVALID_STATE(env, "session is not open");
|
|
2600
3184
|
return env.Undefined();
|
|
2601
3185
|
}
|
|
2602
3186
|
|
|
3187
|
+
if (!database_) {
|
|
3188
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
3189
|
+
return env.Undefined();
|
|
3190
|
+
}
|
|
3191
|
+
|
|
2603
3192
|
Delete();
|
|
2604
3193
|
return env.Undefined();
|
|
2605
3194
|
}
|
|
@@ -2622,6 +3211,14 @@ std::atomic<int> BackupJob::active_jobs_(0);
|
|
|
2622
3211
|
std::mutex BackupJob::active_jobs_mutex_;
|
|
2623
3212
|
std::set<BackupJob *> BackupJob::active_job_instances_;
|
|
2624
3213
|
|
|
3214
|
+
void BackupJob::CleanupHook(void *arg) {
|
|
3215
|
+
// Called before environment teardown - safe to Reset() references here
|
|
3216
|
+
auto *self = static_cast<BackupJob *>(arg);
|
|
3217
|
+
if (!self->progress_func_.IsEmpty()) {
|
|
3218
|
+
self->progress_func_.Reset();
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
|
|
2625
3222
|
// BackupJob Implementation
|
|
2626
3223
|
BackupJob::BackupJob(Napi::Env env, DatabaseSync *source,
|
|
2627
3224
|
std::string destination_path, std::string source_db,
|
|
@@ -2632,16 +3229,42 @@ BackupJob::BackupJob(Napi::Env env, DatabaseSync *source,
|
|
|
2632
3229
|
!progress_func.IsEmpty() && !progress_func.IsUndefined()
|
|
2633
3230
|
? progress_func
|
|
2634
3231
|
: Napi::Function::New(env, [](const Napi::CallbackInfo &) {})),
|
|
2635
|
-
source_(source),
|
|
3232
|
+
source_(source),
|
|
3233
|
+
// Capture connection pointer now while we know it's valid
|
|
3234
|
+
// This prevents use-after-free if database is closed during backup
|
|
3235
|
+
source_connection_(source->connection()),
|
|
3236
|
+
destination_path_(std::move(destination_path)),
|
|
2636
3237
|
source_db_(std::move(source_db)), dest_db_(std::move(dest_db)),
|
|
2637
|
-
pages_(pages), deferred_(deferred) {
|
|
3238
|
+
pages_(pages), deferred_(deferred), env_(env) {
|
|
2638
3239
|
if (!progress_func.IsEmpty() && !progress_func.IsUndefined()) {
|
|
2639
3240
|
progress_func_ = Napi::Reference<Napi::Function>::New(progress_func);
|
|
2640
3241
|
}
|
|
3242
|
+
|
|
3243
|
+
// Register cleanup hook to Reset() reference before environment teardown.
|
|
3244
|
+
// This is required for worker thread support per Node-API best practices.
|
|
3245
|
+
// See:
|
|
3246
|
+
// https://nodejs.github.io/node-addon-examples/special-topics/context-awareness/
|
|
3247
|
+
napi_add_env_cleanup_hook(env_, CleanupHook, this);
|
|
3248
|
+
|
|
2641
3249
|
active_jobs_++;
|
|
3250
|
+
// Register with database for proper cleanup coordination
|
|
3251
|
+
source_->AddBackup(this);
|
|
2642
3252
|
}
|
|
2643
3253
|
|
|
2644
|
-
BackupJob::~BackupJob() {
|
|
3254
|
+
BackupJob::~BackupJob() {
|
|
3255
|
+
// Remove cleanup hook if still registered
|
|
3256
|
+
napi_remove_env_cleanup_hook(env_, CleanupHook, this);
|
|
3257
|
+
|
|
3258
|
+
// Don't call progress_func_.Reset() here - CleanupHook already handled it,
|
|
3259
|
+
// or the environment is being torn down and Reset() would be unsafe.
|
|
3260
|
+
|
|
3261
|
+
active_jobs_--;
|
|
3262
|
+
// Unregister from database
|
|
3263
|
+
// Note: source_ may be null if FinalizeBackups was called
|
|
3264
|
+
if (source_) {
|
|
3265
|
+
source_->RemoveBackup(this);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
2645
3268
|
|
|
2646
3269
|
void BackupJob::Execute(const ExecutionProgress &progress) {
|
|
2647
3270
|
// This method is executed on a worker thread, not the main thread
|
|
@@ -2654,48 +3277,53 @@ void BackupJob::Execute(const ExecutionProgress &progress) {
|
|
|
2654
3277
|
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI, nullptr);
|
|
2655
3278
|
|
|
2656
3279
|
if (backup_status_ != SQLITE_OK) {
|
|
2657
|
-
|
|
3280
|
+
// Use SQLite's actual error message
|
|
3281
|
+
SetError(sqlite3_errmsg(dest_));
|
|
2658
3282
|
return;
|
|
2659
3283
|
}
|
|
2660
3284
|
|
|
2661
|
-
// Initialize backup
|
|
2662
|
-
|
|
3285
|
+
// Initialize backup using the connection pointer captured at construction
|
|
3286
|
+
// This prevents use-after-free if database is closed during backup
|
|
3287
|
+
backup_ = sqlite3_backup_init(dest_, dest_db_.c_str(), source_connection_,
|
|
2663
3288
|
source_db_.c_str());
|
|
2664
3289
|
|
|
2665
3290
|
if (!backup_) {
|
|
2666
|
-
|
|
3291
|
+
// Use SQLite's actual error message from destination db
|
|
3292
|
+
// (sqlite3_backup_init errors are stored in the dest db handle)
|
|
3293
|
+
SetError(sqlite3_errmsg(dest_));
|
|
2667
3294
|
return;
|
|
2668
3295
|
}
|
|
2669
3296
|
|
|
2670
3297
|
// Initial page count may be 0 until first step
|
|
2671
|
-
int remaining_pages = sqlite3_backup_remaining(backup_);
|
|
2672
3298
|
total_pages_ = 0; // Will be updated after first step
|
|
3299
|
+
bool is_first_step = true;
|
|
2673
3300
|
|
|
2674
|
-
while (
|
|
2675
|
-
backup_status_ == SQLITE_OK) {
|
|
3301
|
+
while (backup_status_ == SQLITE_OK) {
|
|
2676
3302
|
// If pages_ is negative, use -1 to copy all remaining pages
|
|
2677
3303
|
int pages_to_copy = pages_ < 0 ? -1 : pages_;
|
|
2678
3304
|
backup_status_ = sqlite3_backup_step(backup_, pages_to_copy);
|
|
2679
3305
|
|
|
2680
3306
|
// Update total pages after first step (when SQLite knows the actual count)
|
|
2681
|
-
if (
|
|
3307
|
+
if (is_first_step) {
|
|
2682
3308
|
total_pages_ = sqlite3_backup_pagecount(backup_);
|
|
3309
|
+
is_first_step = false;
|
|
2683
3310
|
}
|
|
2684
3311
|
|
|
2685
|
-
if (backup_status_ == SQLITE_OK
|
|
2686
|
-
|
|
3312
|
+
if (backup_status_ == SQLITE_OK) {
|
|
3313
|
+
// More steps remaining - send progress update
|
|
3314
|
+
int remaining_pages = sqlite3_backup_remaining(backup_);
|
|
2687
3315
|
int current_page = total_pages_ - remaining_pages;
|
|
2688
3316
|
|
|
2689
3317
|
// Send progress update to main thread
|
|
2690
|
-
|
|
3318
|
+
// Node.js only calls progress when there are still pages remaining
|
|
3319
|
+
if (!progress_func_.IsEmpty() && total_pages_ > 0 &&
|
|
3320
|
+
remaining_pages > 0) {
|
|
2691
3321
|
BackupProgress prog = {current_page, total_pages_};
|
|
2692
3322
|
progress.Send(&prog, 1);
|
|
2693
3323
|
}
|
|
2694
|
-
|
|
2695
|
-
//
|
|
2696
|
-
|
|
2697
|
-
break;
|
|
2698
|
-
}
|
|
3324
|
+
} else if (backup_status_ == SQLITE_DONE) {
|
|
3325
|
+
// Backup complete - don't send progress for remaining:0
|
|
3326
|
+
break;
|
|
2699
3327
|
} else if (backup_status_ == SQLITE_BUSY ||
|
|
2700
3328
|
backup_status_ == SQLITE_LOCKED) {
|
|
2701
3329
|
// These are retryable errors - continue
|
|
@@ -2708,15 +3336,14 @@ void BackupJob::Execute(const ExecutionProgress &progress) {
|
|
|
2708
3336
|
|
|
2709
3337
|
// Store final status for use in OnOK/OnError
|
|
2710
3338
|
if (backup_status_ != SQLITE_DONE) {
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
SetError(error);
|
|
3339
|
+
// Use SQLite's actual error message (matches Node.js behavior)
|
|
3340
|
+
SetError(sqlite3_errmsg(dest_));
|
|
2714
3341
|
}
|
|
2715
3342
|
}
|
|
2716
3343
|
|
|
2717
3344
|
void BackupJob::OnProgress(const BackupProgress *data, size_t count) {
|
|
2718
3345
|
// This runs on the main thread
|
|
2719
|
-
if (!progress_func_.IsEmpty() && count > 0) {
|
|
3346
|
+
if (!progress_func_.IsEmpty() && count > 0 && !progress_error_.has_value()) {
|
|
2720
3347
|
Napi::HandleScope scope(Env());
|
|
2721
3348
|
Napi::Function progress_fn = progress_func_.Value();
|
|
2722
3349
|
Napi::Object progress_info = Napi::Object::New(Env());
|
|
@@ -2726,8 +3353,12 @@ void BackupJob::OnProgress(const BackupProgress *data, size_t count) {
|
|
|
2726
3353
|
|
|
2727
3354
|
try {
|
|
2728
3355
|
progress_fn.Call(Env().Null(), {progress_info});
|
|
3356
|
+
} catch (const Napi::Error &e) {
|
|
3357
|
+
// Capture error from progress callback - backup should fail with this
|
|
3358
|
+
progress_error_ = e.Message();
|
|
2729
3359
|
} catch (...) {
|
|
2730
|
-
//
|
|
3360
|
+
// Unknown error
|
|
3361
|
+
progress_error_ = "Unknown error in progress callback";
|
|
2731
3362
|
}
|
|
2732
3363
|
}
|
|
2733
3364
|
}
|
|
@@ -2739,6 +3370,13 @@ void BackupJob::OnOK() {
|
|
|
2739
3370
|
// Cleanup SQLite resources
|
|
2740
3371
|
Cleanup();
|
|
2741
3372
|
|
|
3373
|
+
// If progress callback threw an error, reject with that error
|
|
3374
|
+
if (progress_error_.has_value()) {
|
|
3375
|
+
Napi::Error error = Napi::Error::New(Env(), *progress_error_);
|
|
3376
|
+
deferred_.Reject(error.Value());
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
|
|
2742
3380
|
// Resolve the promise with the total number of pages
|
|
2743
3381
|
deferred_.Resolve(Napi::Number::New(Env(), total_pages_));
|
|
2744
3382
|
}
|
|
@@ -2764,7 +3402,19 @@ void BackupJob::OnError(const Napi::Error &error) {
|
|
|
2764
3402
|
|
|
2765
3403
|
// Use saved values for error details (matching node:sqlite property names)
|
|
2766
3404
|
if (saved_status != SQLITE_OK && saved_status != SQLITE_DONE) {
|
|
2767
|
-
|
|
3405
|
+
// Prefer the detailed error message from sqlite3_errmsg(dest_) if it's
|
|
3406
|
+
// useful, otherwise fall back to sqlite3_errstr(status) which is the
|
|
3407
|
+
// generic message. sqlite3_errmsg can return "not an error" if the error
|
|
3408
|
+
// wasn't stored in dest.
|
|
3409
|
+
std::string err_message;
|
|
3410
|
+
if (!saved_errmsg.empty() && saved_errmsg != "not an error") {
|
|
3411
|
+
err_message = saved_errmsg;
|
|
3412
|
+
} else {
|
|
3413
|
+
err_message = sqlite3_errstr(saved_status);
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
Napi::Error detailed_error = Napi::Error::New(Env(), err_message);
|
|
3417
|
+
detailed_error.Set("code", Napi::String::New(Env(), "ERR_SQLITE_ERROR"));
|
|
2768
3418
|
detailed_error.Set("errcode", Napi::Number::New(Env(), saved_status));
|
|
2769
3419
|
detailed_error.Set("errstr",
|
|
2770
3420
|
Napi::String::New(Env(), sqlite3_errstr(saved_status)));
|
|
@@ -2790,29 +3440,24 @@ void BackupJob::Cleanup() {
|
|
|
2790
3440
|
}
|
|
2791
3441
|
|
|
2792
3442
|
// DatabaseSync::Backup implementation
|
|
3443
|
+
// Note: Validation errors are thrown synchronously (matching Node.js behavior).
|
|
3444
|
+
// Only the actual backup operation returns a promise.
|
|
2793
3445
|
Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
|
|
2794
3446
|
Napi::Env env = info.Env();
|
|
2795
3447
|
|
|
2796
|
-
//
|
|
2797
|
-
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
|
|
2798
|
-
|
|
3448
|
+
// Validation errors throw synchronously (matching Node.js behavior)
|
|
2799
3449
|
if (!IsOpen()) {
|
|
2800
|
-
|
|
2801
|
-
return
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
if (info.Length() < 1) {
|
|
2805
|
-
deferred.Reject(
|
|
2806
|
-
Napi::TypeError::New(env, "The \"destination\" argument is required")
|
|
2807
|
-
.Value());
|
|
2808
|
-
return deferred.Promise();
|
|
3450
|
+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
|
|
3451
|
+
return env.Undefined();
|
|
2809
3452
|
}
|
|
2810
3453
|
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3454
|
+
// ValidateDatabasePath throws synchronously with ERR_INVALID_ARG_TYPE
|
|
3455
|
+
// Use "path" as argument name to match Node.js
|
|
3456
|
+
std::optional<std::string> dest_path =
|
|
3457
|
+
ValidateDatabasePath(env, info[0], "path");
|
|
3458
|
+
if (!dest_path.has_value()) {
|
|
3459
|
+
// Exception already thrown by ValidateDatabasePath
|
|
3460
|
+
return env.Undefined();
|
|
2816
3461
|
}
|
|
2817
3462
|
|
|
2818
3463
|
// Default options matching Node.js API
|
|
@@ -2824,10 +3469,9 @@ Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
|
|
|
2824
3469
|
// Parse options if provided
|
|
2825
3470
|
if (info.Length() > 1) {
|
|
2826
3471
|
if (!info[1].IsObject()) {
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
return deferred.Promise();
|
|
3472
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
3473
|
+
env, "The \"options\" argument must be an object.");
|
|
3474
|
+
return env.Undefined();
|
|
2831
3475
|
}
|
|
2832
3476
|
|
|
2833
3477
|
Napi::Object options = info[1].As<Napi::Object>();
|
|
@@ -2835,11 +3479,17 @@ Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
|
|
|
2835
3479
|
// Get rate option (number of pages per step)
|
|
2836
3480
|
Napi::Value rate_value = options.Get("rate");
|
|
2837
3481
|
if (!rate_value.IsUndefined()) {
|
|
3482
|
+
// Check if it's a number and an integer (not fractional)
|
|
2838
3483
|
if (!rate_value.IsNumber()) {
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
3484
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
3485
|
+
env, "The \"options.rate\" argument must be an integer.");
|
|
3486
|
+
return env.Undefined();
|
|
3487
|
+
}
|
|
3488
|
+
double rate_double = rate_value.As<Napi::Number>().DoubleValue();
|
|
3489
|
+
if (rate_double != std::trunc(rate_double)) {
|
|
3490
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
3491
|
+
env, "The \"options.rate\" argument must be an integer.");
|
|
3492
|
+
return env.Undefined();
|
|
2843
3493
|
}
|
|
2844
3494
|
rate = rate_value.As<Napi::Number>().Int32Value();
|
|
2845
3495
|
// Note: Node.js allows negative values for rate
|
|
@@ -2849,10 +3499,9 @@ Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
|
|
|
2849
3499
|
Napi::Value source_value = options.Get("source");
|
|
2850
3500
|
if (!source_value.IsUndefined()) {
|
|
2851
3501
|
if (!source_value.IsString()) {
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
return deferred.Promise();
|
|
3502
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
3503
|
+
env, "The \"options.source\" argument must be a string.");
|
|
3504
|
+
return env.Undefined();
|
|
2856
3505
|
}
|
|
2857
3506
|
source_db = source_value.As<Napi::String>().Utf8Value();
|
|
2858
3507
|
}
|
|
@@ -2861,10 +3510,9 @@ Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
|
|
|
2861
3510
|
Napi::Value target_value = options.Get("target");
|
|
2862
3511
|
if (!target_value.IsUndefined()) {
|
|
2863
3512
|
if (!target_value.IsString()) {
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
return deferred.Promise();
|
|
3513
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
3514
|
+
env, "The \"options.target\" argument must be a string.");
|
|
3515
|
+
return env.Undefined();
|
|
2868
3516
|
}
|
|
2869
3517
|
target_db = target_value.As<Napi::String>().Utf8Value();
|
|
2870
3518
|
}
|
|
@@ -2873,19 +3521,21 @@ Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
|
|
|
2873
3521
|
Napi::Value progress_value = options.Get("progress");
|
|
2874
3522
|
if (!progress_value.IsUndefined()) {
|
|
2875
3523
|
if (!progress_value.IsFunction()) {
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
return deferred.Promise();
|
|
3524
|
+
node::THROW_ERR_INVALID_ARG_TYPE(
|
|
3525
|
+
env, "The \"options.progress\" argument must be a function.");
|
|
3526
|
+
return env.Undefined();
|
|
2880
3527
|
}
|
|
2881
3528
|
progress_func = progress_value.As<Napi::Function>();
|
|
2882
3529
|
}
|
|
2883
3530
|
}
|
|
2884
3531
|
|
|
3532
|
+
// Create promise for async backup operation
|
|
3533
|
+
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
|
|
3534
|
+
|
|
2885
3535
|
// Create and schedule backup job
|
|
2886
|
-
BackupJob *job =
|
|
2887
|
-
|
|
2888
|
-
|
|
3536
|
+
BackupJob *job =
|
|
3537
|
+
new BackupJob(env, this, std::move(*dest_path), std::move(source_db),
|
|
3538
|
+
std::move(target_db), rate, progress_func, deferred);
|
|
2889
3539
|
|
|
2890
3540
|
// Queue the async work - AsyncWorker will delete itself when complete
|
|
2891
3541
|
job->Queue();
|
|
@@ -2990,7 +3640,7 @@ int DatabaseSync::AuthorizerCallback(void *user_data, int action_code,
|
|
|
2990
3640
|
if (int_result != SQLITE_OK && int_result != SQLITE_DENY &&
|
|
2991
3641
|
int_result != SQLITE_IGNORE) {
|
|
2992
3642
|
db->SetDeferredAuthorizerException(
|
|
2993
|
-
"Authorizer callback returned
|
|
3643
|
+
"Authorizer callback returned a invalid authorization code");
|
|
2994
3644
|
db->SetIgnoreNextSQLiteError(true);
|
|
2995
3645
|
return SQLITE_DENY;
|
|
2996
3646
|
}
|