@photostructure/sqlite 0.0.1 → 0.2.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/README.md +45 -484
  3. package/SECURITY.md +27 -84
  4. package/binding.gyp +69 -22
  5. package/dist/index.cjs +185 -18
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +552 -100
  8. package/dist/index.d.mts +552 -100
  9. package/dist/index.d.ts +552 -100
  10. package/dist/index.mjs +183 -18
  11. package/dist/index.mjs.map +1 -1
  12. package/package.json +51 -41
  13. package/prebuilds/darwin-arm64/@photostructure+sqlite.glibc.node +0 -0
  14. package/prebuilds/darwin-x64/@photostructure+sqlite.glibc.node +0 -0
  15. package/prebuilds/linux-arm64/@photostructure+sqlite.glibc.node +0 -0
  16. package/prebuilds/linux-arm64/@photostructure+sqlite.musl.node +0 -0
  17. package/prebuilds/linux-x64/@photostructure+sqlite.glibc.node +0 -0
  18. package/prebuilds/linux-x64/@photostructure+sqlite.musl.node +0 -0
  19. package/prebuilds/test_extension.so +0 -0
  20. package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
  21. package/src/aggregate_function.cpp +503 -235
  22. package/src/aggregate_function.h +57 -42
  23. package/src/binding.cpp +117 -14
  24. package/src/dirname.ts +1 -1
  25. package/src/index.ts +122 -332
  26. package/src/lru-cache.ts +84 -0
  27. package/src/shims/env-inl.h +6 -15
  28. package/src/shims/node_errors.h +4 -0
  29. package/src/shims/sqlite_errors.h +162 -0
  30. package/src/shims/util.h +29 -4
  31. package/src/sql-tag-store.ts +140 -0
  32. package/src/sqlite_exception.h +49 -0
  33. package/src/sqlite_impl.cpp +711 -127
  34. package/src/sqlite_impl.h +84 -6
  35. package/src/{stack_path.ts → stack-path.ts} +7 -1
  36. package/src/types/aggregate-options.ts +22 -0
  37. package/src/types/changeset-apply-options.ts +18 -0
  38. package/src/types/database-sync-instance.ts +203 -0
  39. package/src/types/database-sync-options.ts +69 -0
  40. package/src/types/session-options.ts +10 -0
  41. package/src/types/sql-tag-store-instance.ts +51 -0
  42. package/src/types/sqlite-authorization-actions.ts +77 -0
  43. package/src/types/sqlite-authorization-results.ts +15 -0
  44. package/src/types/sqlite-changeset-conflict-types.ts +19 -0
  45. package/src/types/sqlite-changeset-resolution.ts +15 -0
  46. package/src/types/sqlite-open-flags.ts +50 -0
  47. package/src/types/statement-sync-instance.ts +73 -0
  48. package/src/types/user-functions-options.ts +14 -0
  49. package/src/upstream/node_sqlite.cc +960 -259
  50. package/src/upstream/node_sqlite.h +127 -2
  51. package/src/upstream/sqlite.js +1 -14
  52. package/src/upstream/sqlite3.c +4510 -1411
  53. package/src/upstream/sqlite3.h +390 -195
  54. package/src/upstream/sqlite3ext.h +7 -0
  55. package/src/user_function.cpp +88 -36
  56. package/src/user_function.h +2 -1
@@ -4,13 +4,57 @@
4
4
  #include <cctype>
5
5
  #include <climits>
6
6
  #include <cmath>
7
- #include <iostream>
7
+ #include <limits>
8
8
 
9
9
  #include "aggregate_function.h"
10
+ #include "shims/sqlite_errors.h"
11
+
12
+ // Database-aware error handling functions to avoid forward declaration issues
13
+ namespace {
14
+ inline void ThrowErrSqliteErrorWithDb(Napi::Env env,
15
+ photostructure::sqlite::DatabaseSync *db,
16
+ const char *message = nullptr) {
17
+ // Check if we should ignore this SQLite error due to pending JavaScript
18
+ // exception (e.g., from authorizer callback)
19
+ if (db != nullptr && db->ShouldIgnoreSQLiteError()) {
20
+ db->SetIgnoreNextSQLiteError(false);
21
+ // Check for deferred authorizer exception and throw it instead
22
+ if (db->HasDeferredAuthorizerException()) {
23
+ std::string deferred_msg = db->GetDeferredAuthorizerException();
24
+ db->ClearDeferredAuthorizerException();
25
+ Napi::Error::New(env, deferred_msg).ThrowAsJavaScriptException();
26
+ }
27
+ return; // Don't throw SQLite error, JavaScript exception takes precedence
28
+ }
29
+
30
+ const char *msg = (message != nullptr) ? message : "SQLite error";
31
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
32
+ }
33
+
34
+ inline void ThrowEnhancedSqliteErrorWithDB(
35
+ Napi::Env env, photostructure::sqlite::DatabaseSync *db_sync, sqlite3 *db,
36
+ int sqlite_code, const std::string &message) {
37
+ // Check if we should ignore this SQLite error due to pending JavaScript
38
+ // exception (e.g., from authorizer callback)
39
+ if (db_sync != nullptr && db_sync->ShouldIgnoreSQLiteError()) {
40
+ db_sync->SetIgnoreNextSQLiteError(false);
41
+ // Check for deferred authorizer exception and throw it instead
42
+ if (db_sync->HasDeferredAuthorizerException()) {
43
+ std::string deferred_msg = db_sync->GetDeferredAuthorizerException();
44
+ db_sync->ClearDeferredAuthorizerException();
45
+ Napi::Error::New(env, deferred_msg).ThrowAsJavaScriptException();
46
+ }
47
+ return; // Don't throw SQLite error, JavaScript exception takes precedence
48
+ }
49
+
50
+ // Call the original function
51
+ node::ThrowEnhancedSqliteError(env, db, sqlite_code, message);
52
+ }
53
+ } // namespace
54
+ #include "sqlite_exception.h"
10
55
  #include "user_function.h"
11
56
 
12
- namespace photostructure {
13
- namespace sqlite {
57
+ namespace photostructure::sqlite {
14
58
 
15
59
  // JavaScript safe integer limits (2^53 - 1)
16
60
  constexpr int64_t JS_MAX_SAFE_INTEGER = 9007199254740991LL;
@@ -29,8 +73,8 @@ std::optional<std::string> ValidateDatabasePath(Napi::Env env, Napi::Value path,
29
73
  return location;
30
74
  }
31
75
  } else if (path.IsBuffer()) {
32
- Napi::Buffer<uint8_t> buffer = path.As<Napi::Buffer<uint8_t>>();
33
- size_t length = buffer.Length();
76
+ const auto buffer = path.As<Napi::Buffer<uint8_t>>();
77
+ const size_t length = buffer.Length();
34
78
  const uint8_t *data = buffer.Data();
35
79
 
36
80
  // Check for null bytes in buffer
@@ -238,6 +282,7 @@ Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
238
282
  env, "DatabaseSync",
239
283
  {InstanceMethod("open", &DatabaseSync::Open),
240
284
  InstanceMethod("close", &DatabaseSync::Close),
285
+ InstanceMethod("dispose", &DatabaseSync::Dispose),
241
286
  InstanceMethod("prepare", &DatabaseSync::Prepare),
242
287
  InstanceMethod("exec", &DatabaseSync::Exec),
243
288
  InstanceMethod("function", &DatabaseSync::CustomFunction),
@@ -245,9 +290,11 @@ Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
245
290
  InstanceMethod("enableLoadExtension",
246
291
  &DatabaseSync::EnableLoadExtension),
247
292
  InstanceMethod("loadExtension", &DatabaseSync::LoadExtension),
293
+ InstanceMethod("enableDefensive", &DatabaseSync::EnableDefensive),
248
294
  InstanceMethod("createSession", &DatabaseSync::CreateSession),
249
295
  InstanceMethod("applyChangeset", &DatabaseSync::ApplyChangeset),
250
296
  InstanceMethod("backup", &DatabaseSync::Backup),
297
+ InstanceMethod("setAuthorizer", &DatabaseSync::SetAuthorizer),
251
298
  InstanceMethod("location", &DatabaseSync::LocationMethod),
252
299
  InstanceAccessor("isOpen", &DatabaseSync::IsOpenGetter, nullptr),
253
300
  InstanceAccessor("isTransaction", &DatabaseSync::IsTransactionGetter,
@@ -260,13 +307,40 @@ Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
260
307
  Napi::Reference<Napi::Function>::New(func);
261
308
  }
262
309
 
310
+ // Add Symbol.dispose to the prototype (Node.js v25+ compatibility)
311
+ Napi::Value symbolDispose =
312
+ env.Global().Get("Symbol").As<Napi::Object>().Get("dispose");
313
+ if (!symbolDispose.IsUndefined()) {
314
+ func.Get("prototype")
315
+ .As<Napi::Object>()
316
+ .Set(symbolDispose,
317
+ Napi::Function::New(
318
+ env, [](const Napi::CallbackInfo &info) -> Napi::Value {
319
+ DatabaseSync *db =
320
+ DatabaseSync::Unwrap(info.This().As<Napi::Object>());
321
+ return db->Dispose(info);
322
+ }));
323
+ }
324
+
325
+ // Add Symbol.for('sqlite-type') property for type identification
326
+ // See: https://github.com/nodejs/node/pull/59405
327
+ Napi::Object symbolConstructor =
328
+ env.Global().Get("Symbol").As<Napi::Object>();
329
+ Napi::Function symbolFor = symbolConstructor.Get("for").As<Napi::Function>();
330
+ Napi::Value sqliteTypeSymbol = symbolFor.Call(
331
+ symbolConstructor, {Napi::String::New(env, "sqlite-type")});
332
+ func.Get("prototype")
333
+ .As<Napi::Object>()
334
+ .Set(sqliteTypeSymbol, Napi::String::New(env, "node:sqlite"));
335
+
263
336
  exports.Set("DatabaseSync", func);
264
337
  return exports;
265
338
  }
266
339
 
267
340
  DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
268
341
  : Napi::ObjectWrap<DatabaseSync>(info),
269
- creation_thread_(std::this_thread::get_id()), env_(info.Env()) {
342
+ creation_thread_(std::this_thread::get_id()), env_(info.Env()),
343
+ config_("") {
270
344
  // Register this instance for cleanup tracking
271
345
  RegisterDatabaseInstance(info.Env(), this);
272
346
 
@@ -285,6 +359,9 @@ DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
285
359
  try {
286
360
  DatabaseOpenConfiguration config(std::move(location.value()));
287
361
 
362
+ // Track whether to open immediately (default: true)
363
+ bool should_open = true;
364
+
288
365
  // Handle options object if provided as second argument
289
366
  if (info.Length() > 1 && info[1].IsObject()) {
290
367
  Napi::Object options = info[1].As<Napi::Object>();
@@ -324,9 +401,68 @@ DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
324
401
  allow_load_extension_ =
325
402
  options.Get("allowExtension").As<Napi::Boolean>().Value();
326
403
  }
404
+
405
+ if (options.Has("readBigInts") &&
406
+ options.Get("readBigInts").IsBoolean()) {
407
+ config.set_read_big_ints(
408
+ options.Get("readBigInts").As<Napi::Boolean>().Value());
409
+ }
410
+
411
+ if (options.Has("returnArrays") &&
412
+ options.Get("returnArrays").IsBoolean()) {
413
+ config.set_return_arrays(
414
+ options.Get("returnArrays").As<Napi::Boolean>().Value());
415
+ }
416
+
417
+ if (options.Has("allowBareNamedParameters") &&
418
+ options.Get("allowBareNamedParameters").IsBoolean()) {
419
+ config.set_allow_bare_named_params(
420
+ options.Get("allowBareNamedParameters")
421
+ .As<Napi::Boolean>()
422
+ .Value());
423
+ }
424
+
425
+ if (options.Has("allowUnknownNamedParameters") &&
426
+ options.Get("allowUnknownNamedParameters").IsBoolean()) {
427
+ config.set_allow_unknown_named_params(
428
+ options.Get("allowUnknownNamedParameters")
429
+ .As<Napi::Boolean>()
430
+ .Value());
431
+ }
432
+
433
+ if (options.Has("defensive")) {
434
+ Napi::Value defensive_val = options.Get("defensive");
435
+ if (!defensive_val.IsUndefined()) {
436
+ if (!defensive_val.IsBoolean()) {
437
+ node::THROW_ERR_INVALID_ARG_TYPE(
438
+ info.Env(),
439
+ "The \"options.defensive\" argument must be a boolean.");
440
+ return;
441
+ }
442
+ config.set_enable_defensive(
443
+ defensive_val.As<Napi::Boolean>().Value());
444
+ }
445
+ }
446
+
447
+ // Handle the open option
448
+ if (options.Has("open")) {
449
+ Napi::Value open_val = options.Get("open");
450
+ if (open_val.IsBoolean()) {
451
+ should_open = open_val.As<Napi::Boolean>().Value();
452
+ }
453
+ // For non-boolean values, default to true (existing behavior)
454
+ }
327
455
  }
328
456
 
329
- InternalOpen(config);
457
+ // Store configuration for later use
458
+ config_ = std::move(config);
459
+
460
+ // Only open if should_open is true
461
+ if (should_open) {
462
+ InternalOpen(config_);
463
+ }
464
+ } catch (const SqliteException &e) {
465
+ node::ThrowFromSqliteException(info.Env(), e);
330
466
  } catch (const std::exception &e) {
331
467
  node::THROW_ERR_SQLITE_ERROR(info.Env(), e.what());
332
468
  }
@@ -345,65 +481,14 @@ Napi::Value DatabaseSync::Open(const Napi::CallbackInfo &info) {
345
481
  Napi::Env env = info.Env();
346
482
 
347
483
  if (IsOpen()) {
348
- node::THROW_ERR_INVALID_STATE(env, "Database is already open");
349
- return env.Undefined();
350
- }
351
-
352
- if (info.Length() < 1 || !info[0].IsObject()) {
353
- node::THROW_ERR_INVALID_ARG_TYPE(env, "Expected configuration object");
354
- return env.Undefined();
355
- }
356
-
357
- Napi::Object config_obj = info[0].As<Napi::Object>();
358
-
359
- if (!config_obj.Has("location") || !config_obj.Get("location").IsString()) {
360
- node::THROW_ERR_INVALID_ARG_TYPE(env,
361
- "Configuration must have location string");
484
+ node::THROW_ERR_INVALID_STATE(env, "database is already open");
362
485
  return env.Undefined();
363
486
  }
364
487
 
365
- std::string location =
366
- config_obj.Get("location").As<Napi::String>().Utf8Value();
367
- DatabaseOpenConfiguration config(std::move(location));
368
-
369
- // Parse other options
370
- if (config_obj.Has("readOnly") && config_obj.Get("readOnly").IsBoolean()) {
371
- config.set_read_only(
372
- config_obj.Get("readOnly").As<Napi::Boolean>().Value());
373
- }
374
-
375
- // Support both old and new naming for backwards compatibility
376
- if (config_obj.Has("enableForeignKeyConstraints") &&
377
- config_obj.Get("enableForeignKeyConstraints").IsBoolean()) {
378
- config.set_enable_foreign_keys(config_obj.Get("enableForeignKeyConstraints")
379
- .As<Napi::Boolean>()
380
- .Value());
381
- } else if (config_obj.Has("enableForeignKeys") &&
382
- config_obj.Get("enableForeignKeys").IsBoolean()) {
383
- config.set_enable_foreign_keys(
384
- config_obj.Get("enableForeignKeys").As<Napi::Boolean>().Value());
385
- }
386
-
387
- if (config_obj.Has("timeout") && config_obj.Get("timeout").IsNumber()) {
388
- config.set_timeout(
389
- config_obj.Get("timeout").As<Napi::Number>().Int32Value());
390
- }
391
-
392
- if (config_obj.Has("enableDoubleQuotedStringLiterals") &&
393
- config_obj.Get("enableDoubleQuotedStringLiterals").IsBoolean()) {
394
- config.set_enable_dqs(config_obj.Get("enableDoubleQuotedStringLiterals")
395
- .As<Napi::Boolean>()
396
- .Value());
397
- }
398
-
399
- if (config_obj.Has("allowExtension") &&
400
- config_obj.Get("allowExtension").IsBoolean()) {
401
- allow_load_extension_ =
402
- config_obj.Get("allowExtension").As<Napi::Boolean>().Value();
403
- }
404
-
405
488
  try {
406
- InternalOpen(config);
489
+ InternalOpen(config_);
490
+ } catch (const SqliteException &e) {
491
+ node::ThrowFromSqliteException(env, e);
407
492
  } catch (const std::exception &e) {
408
493
  node::THROW_ERR_SQLITE_ERROR(env, e.what());
409
494
  }
@@ -432,6 +517,26 @@ Napi::Value DatabaseSync::Close(const Napi::CallbackInfo &info) {
432
517
  return env.Undefined();
433
518
  }
434
519
 
520
+ Napi::Value DatabaseSync::Dispose(const Napi::CallbackInfo &info) {
521
+ Napi::Env env = info.Env();
522
+
523
+ if (!ValidateThread(env)) {
524
+ return env.Undefined();
525
+ }
526
+
527
+ // Try to close, but ignore errors during disposal (matches Node.js v25
528
+ // behavior)
529
+ try {
530
+ if (IsOpen()) {
531
+ InternalClose();
532
+ }
533
+ } catch (...) {
534
+ // Ignore errors during disposal
535
+ }
536
+
537
+ return env.Undefined();
538
+ }
539
+
435
540
  Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
436
541
  Napi::Env env = info.Env();
437
542
 
@@ -451,6 +556,10 @@ Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
451
556
 
452
557
  std::string sql = info[0].As<Napi::String>().Utf8Value();
453
558
 
559
+ // Clear any stale deferred exception from a previous operation
560
+ ClearDeferredAuthorizerException();
561
+ SetIgnoreNextSQLiteError(false);
562
+
454
563
  try {
455
564
  // Create new StatementSync instance using addon data constructor
456
565
  AddonData *addon_data = GetAddonData(env);
@@ -468,6 +577,25 @@ Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
468
577
 
469
578
  return stmt_obj;
470
579
  } catch (const std::exception &e) {
580
+ // Handle deferred authorizer exceptions:
581
+ //
582
+ // When an authorizer callback throws a JavaScript exception, we use a
583
+ // "marker" exception pattern to safely propagate the error:
584
+ //
585
+ // 1. On Windows (MSVC), std::exception::what() can sometimes return an
586
+ // empty string, causing message loss.
587
+ //
588
+ // 2. By storing the message in the DatabaseSync instance, we can retrieve
589
+ // it here and throw a proper JavaScript exception with the original text.
590
+ //
591
+ // See also: StatementSync::InitStatement for the other half of this pattern.
592
+ if (HasDeferredAuthorizerException()) {
593
+ std::string deferred_msg = GetDeferredAuthorizerException();
594
+ ClearDeferredAuthorizerException();
595
+ SetIgnoreNextSQLiteError(false);
596
+ Napi::Error::New(env, deferred_msg).ThrowAsJavaScriptException();
597
+ return env.Undefined();
598
+ }
471
599
  node::THROW_ERR_SQLITE_ERROR(env, e.what());
472
600
  return env.Undefined();
473
601
  }
@@ -492,15 +620,30 @@ Napi::Value DatabaseSync::Exec(const Napi::CallbackInfo &info) {
492
620
 
493
621
  std::string sql = info[0].As<Napi::String>().Utf8Value();
494
622
 
623
+ // Clear any stale deferred exception from a previous operation
624
+ ClearDeferredAuthorizerException();
625
+ SetIgnoreNextSQLiteError(false);
626
+
495
627
  char *error_msg = nullptr;
496
628
  int result =
497
629
  sqlite3_exec(connection(), sql.c_str(), nullptr, nullptr, &error_msg);
498
630
 
499
631
  if (result != SQLITE_OK) {
632
+ // Check for deferred authorizer exception first
633
+ if (HasDeferredAuthorizerException()) {
634
+ if (error_msg)
635
+ sqlite3_free(error_msg);
636
+ std::string deferred_msg = GetDeferredAuthorizerException();
637
+ ClearDeferredAuthorizerException();
638
+ SetIgnoreNextSQLiteError(false);
639
+ Napi::Error::New(env, deferred_msg).ThrowAsJavaScriptException();
640
+ return env.Undefined();
641
+ }
500
642
  std::string error = error_msg ? error_msg : "Unknown SQLite error";
501
643
  if (error_msg)
502
644
  sqlite3_free(error_msg);
503
- node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
645
+ // Use enhanced error throwing with database handle
646
+ node::ThrowSqliteError(env, connection(), error);
504
647
  }
505
648
 
506
649
  return env.Undefined();
@@ -543,8 +686,10 @@ Napi::Value DatabaseSync::IsTransactionGetter(const Napi::CallbackInfo &info) {
543
686
  }
544
687
 
545
688
  void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
546
- location_ = config.location();
547
- read_only_ = config.get_read_only();
689
+ // Store configuration for later use by statements
690
+ config_ = std::move(config);
691
+ location_ = config_.location();
692
+ read_only_ = config_.get_read_only();
548
693
 
549
694
  int flags = SQLITE_OPEN_CREATE;
550
695
  if (read_only_) {
@@ -557,42 +702,64 @@ void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
557
702
 
558
703
  if (result != SQLITE_OK) {
559
704
  std::string error = sqlite3_errmsg(connection_);
705
+ // Capture error info before closing
706
+ SqliteException ex(connection_, result,
707
+ "Failed to open database: " + error);
560
708
  if (connection_) {
561
709
  sqlite3_close(connection_);
562
710
  connection_ = nullptr;
563
711
  }
564
- throw std::runtime_error("Failed to open database: " + error);
712
+ throw ex;
565
713
  }
566
714
 
567
715
  // Configure database
568
- if (config.get_enable_foreign_keys()) {
716
+ if (config_.get_enable_foreign_keys()) {
569
717
  sqlite3_exec(connection(), "PRAGMA foreign_keys = ON", nullptr, nullptr,
570
718
  nullptr);
571
719
  }
572
720
 
573
- if (config.get_timeout() > 0) {
574
- sqlite3_busy_timeout(connection(), config.get_timeout());
721
+ if (config_.get_timeout() > 0) {
722
+ sqlite3_busy_timeout(connection(), config_.get_timeout());
575
723
  }
576
724
 
577
725
  // Configure double-quoted string literals
578
- if (config.get_enable_dqs()) {
726
+ if (config_.get_enable_dqs()) {
579
727
  int dqs_enable = 1;
580
728
  result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DQS_DML,
581
729
  dqs_enable, nullptr);
582
730
  if (result != SQLITE_OK) {
583
731
  std::string error = sqlite3_errmsg(connection());
732
+ SqliteException ex(connection_, result,
733
+ "Failed to configure DQS_DML: " + error);
584
734
  sqlite3_close(connection_);
585
735
  connection_ = nullptr;
586
- throw std::runtime_error("Failed to configure DQS_DML: " + error);
736
+ throw ex;
587
737
  }
588
738
 
589
739
  result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DQS_DDL,
590
740
  dqs_enable, nullptr);
591
741
  if (result != SQLITE_OK) {
592
742
  std::string error = sqlite3_errmsg(connection());
743
+ SqliteException ex(connection_, result,
744
+ "Failed to configure DQS_DDL: " + error);
745
+ sqlite3_close(connection_);
746
+ connection_ = nullptr;
747
+ throw ex;
748
+ }
749
+ }
750
+
751
+ // Configure defensive mode
752
+ if (config_.get_enable_defensive()) {
753
+ int defensive_enabled;
754
+ result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DEFENSIVE, 1,
755
+ &defensive_enabled);
756
+ if (result != SQLITE_OK) {
757
+ std::string error = sqlite3_errmsg(connection());
758
+ SqliteException ex(connection_, result,
759
+ "Failed to configure DEFENSIVE: " + error);
593
760
  sqlite3_close(connection_);
594
761
  connection_ = nullptr;
595
- throw std::runtime_error("Failed to configure DQS_DDL: " + error);
762
+ throw ex;
596
763
  }
597
764
  }
598
765
  }
@@ -711,7 +878,7 @@ Napi::Value DatabaseSync::CustomFunction(const Napi::CallbackInfo &info) {
711
878
  delete user_data; // Clean up on failure
712
879
  std::string error = "Failed to create function: ";
713
880
  error += sqlite3_errmsg(connection());
714
- node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
881
+ ThrowErrSqliteErrorWithDb(env, this, error.c_str());
715
882
  }
716
883
 
717
884
  return env.Undefined();
@@ -954,6 +1121,36 @@ Napi::Value DatabaseSync::LoadExtension(const Napi::CallbackInfo &info) {
954
1121
  return env.Undefined();
955
1122
  }
956
1123
 
1124
+ Napi::Value DatabaseSync::EnableDefensive(const Napi::CallbackInfo &info) {
1125
+ Napi::Env env = info.Env();
1126
+
1127
+ if (!ValidateThread(env)) {
1128
+ return env.Undefined();
1129
+ }
1130
+
1131
+ if (!IsOpen()) {
1132
+ node::THROW_ERR_INVALID_STATE(env, "database is not open");
1133
+ return env.Undefined();
1134
+ }
1135
+
1136
+ if (info.Length() < 1 || !info[0].IsBoolean()) {
1137
+ node::THROW_ERR_INVALID_ARG_TYPE(
1138
+ env, "The \"active\" argument must be a boolean.");
1139
+ return env.Undefined();
1140
+ }
1141
+
1142
+ int enable = info[0].As<Napi::Boolean>().Value() ? 1 : 0;
1143
+ int defensive_enabled;
1144
+ int result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DEFENSIVE,
1145
+ enable, &defensive_enabled);
1146
+ if (result != SQLITE_OK) {
1147
+ node::ThrowEnhancedSqliteError(env, connection(), result,
1148
+ "Failed to set defensive mode");
1149
+ }
1150
+
1151
+ return env.Undefined();
1152
+ }
1153
+
957
1154
  Napi::Value DatabaseSync::CreateSession(const Napi::CallbackInfo &info) {
958
1155
  Napi::Env env = info.Env();
959
1156
 
@@ -1120,19 +1317,30 @@ Napi::Value DatabaseSync::ApplyChangeset(const Napi::CallbackInfo &info) {
1120
1317
  callbacks.conflictCallback = [env,
1121
1318
  conflictFunc](int conflictType) -> int {
1122
1319
  Napi::HandleScope scope(env);
1123
- Napi::Value result =
1124
- conflictFunc.Call({Napi::Number::New(env, conflictType)});
1320
+ try {
1321
+ Napi::Value result =
1322
+ conflictFunc.Call({Napi::Number::New(env, conflictType)});
1323
+
1324
+ if (env.IsExceptionPending()) {
1325
+ // Clear the exception to prevent propagation
1326
+ env.GetAndClearPendingException();
1327
+ // If callback threw, abort the changeset apply
1328
+ return SQLITE_CHANGESET_ABORT;
1329
+ }
1125
1330
 
1126
- if (env.IsExceptionPending()) {
1127
- // If callback threw, abort the changeset apply
1128
- return SQLITE_CHANGESET_ABORT;
1129
- }
1331
+ if (!result.IsNumber()) {
1332
+ // If the callback returns a non-numeric value, treat it as ABORT
1333
+ return SQLITE_CHANGESET_ABORT;
1334
+ }
1130
1335
 
1131
- if (!result.IsNumber()) {
1132
- return -1; // Invalid value
1336
+ return result.As<Napi::Number>().Int32Value();
1337
+ } catch (...) {
1338
+ // Catch any C++ exceptions
1339
+ if (env.IsExceptionPending()) {
1340
+ env.GetAndClearPendingException();
1341
+ }
1342
+ return SQLITE_CHANGESET_ABORT;
1133
1343
  }
1134
-
1135
- return result.As<Napi::Number>().Int32Value();
1136
1344
  };
1137
1345
  }
1138
1346
  }
@@ -1150,15 +1358,25 @@ Napi::Value DatabaseSync::ApplyChangeset(const Napi::CallbackInfo &info) {
1150
1358
  callbacks.filterCallback = [env,
1151
1359
  filterFunc](std::string tableName) -> bool {
1152
1360
  Napi::HandleScope scope(env);
1153
- Napi::Value result =
1154
- filterFunc.Call({Napi::String::New(env, tableName)});
1361
+ try {
1362
+ Napi::Value result =
1363
+ filterFunc.Call({Napi::String::New(env, tableName)});
1364
+
1365
+ if (env.IsExceptionPending()) {
1366
+ // Clear the exception to prevent propagation
1367
+ env.GetAndClearPendingException();
1368
+ // If callback threw, exclude the table
1369
+ return false;
1370
+ }
1155
1371
 
1156
- if (env.IsExceptionPending()) {
1157
- // If callback threw, exclude the table
1372
+ return result.ToBoolean().Value();
1373
+ } catch (...) {
1374
+ // Catch any C++ exceptions
1375
+ if (env.IsExceptionPending()) {
1376
+ env.GetAndClearPendingException();
1377
+ }
1158
1378
  return false;
1159
1379
  }
1160
-
1161
- return result.ToBoolean().Value();
1162
1380
  };
1163
1381
  }
1164
1382
  }
@@ -1195,13 +1413,18 @@ Napi::Object StatementSync::Init(Napi::Env env, Napi::Object exports) {
1195
1413
  InstanceMethod("all", &StatementSync::All),
1196
1414
  InstanceMethod("iterate", &StatementSync::Iterate),
1197
1415
  InstanceMethod("finalize", &StatementSync::FinalizeStatement),
1416
+ InstanceMethod("dispose", &StatementSync::Dispose),
1198
1417
  InstanceMethod("setReadBigInts", &StatementSync::SetReadBigInts),
1199
1418
  InstanceMethod("setReturnArrays", &StatementSync::SetReturnArrays),
1200
1419
  InstanceMethod("setAllowBareNamedParameters",
1201
1420
  &StatementSync::SetAllowBareNamedParameters),
1421
+ InstanceMethod("setAllowUnknownNamedParameters",
1422
+ &StatementSync::SetAllowUnknownNamedParameters),
1202
1423
  InstanceMethod("columns", &StatementSync::Columns),
1203
1424
  InstanceAccessor("sourceSQL", &StatementSync::SourceSQLGetter, nullptr),
1204
1425
  InstanceAccessor("expandedSQL", &StatementSync::ExpandedSQLGetter,
1426
+ nullptr),
1427
+ InstanceAccessor("finalized", &StatementSync::FinalizedGetter,
1205
1428
  nullptr)});
1206
1429
 
1207
1430
  // Store constructor in per-instance addon data instead of static variable
@@ -1211,6 +1434,21 @@ Napi::Object StatementSync::Init(Napi::Env env, Napi::Object exports) {
1211
1434
  Napi::Reference<Napi::Function>::New(func);
1212
1435
  }
1213
1436
 
1437
+ // Add Symbol.dispose to the prototype (Node.js v25+ compatibility)
1438
+ Napi::Value symbolDispose =
1439
+ env.Global().Get("Symbol").As<Napi::Object>().Get("dispose");
1440
+ if (!symbolDispose.IsUndefined()) {
1441
+ func.Get("prototype")
1442
+ .As<Napi::Object>()
1443
+ .Set(symbolDispose,
1444
+ Napi::Function::New(
1445
+ env, [](const Napi::CallbackInfo &info) -> Napi::Value {
1446
+ StatementSync *stmt =
1447
+ StatementSync::Unwrap(info.This().As<Napi::Object>());
1448
+ return stmt->Dispose(info);
1449
+ }));
1450
+ }
1451
+
1214
1452
  exports.Set("StatementSync", func);
1215
1453
  return exports;
1216
1454
  }
@@ -1228,14 +1466,44 @@ void StatementSync::InitStatement(DatabaseSync *database,
1228
1466
  }
1229
1467
 
1230
1468
  database_ = database;
1469
+ // Create a strong reference to the database object to prevent it from being
1470
+ // garbage collected while this statement exists. This fixes use-after-free
1471
+ // when the database is GC'd before its statements.
1472
+ // See: https://github.com/nodejs/node/pull/56840
1473
+ database_ref_ = Napi::Persistent(database->Value());
1231
1474
  source_sql_ = sql;
1232
1475
 
1476
+ // Apply database-level defaults
1477
+ use_big_ints_ = database->config_.get_read_big_ints();
1478
+ return_arrays_ = database->config_.get_return_arrays();
1479
+ allow_bare_named_params_ = database->config_.get_allow_bare_named_params();
1480
+ allow_unknown_named_params_ =
1481
+ database->config_.get_allow_unknown_named_params();
1482
+
1233
1483
  // Prepare the statement
1234
1484
  const char *tail = nullptr;
1235
1485
  int result = sqlite3_prepare_v2(database->connection(), sql.c_str(), -1,
1236
1486
  &statement_, &tail);
1237
1487
 
1238
1488
  if (result != SQLITE_OK) {
1489
+ // Handle deferred authorizer exceptions:
1490
+ //
1491
+ // When an authorizer callback throws a JavaScript exception, we use a
1492
+ // "marker" exception pattern to safely propagate the error:
1493
+ //
1494
+ // 1. On Windows (MSVC), std::exception::what() can sometimes return an
1495
+ // empty string, causing message loss.
1496
+ //
1497
+ // 2. By storing the message in the DatabaseSync instance, the caller can
1498
+ // retrieve it and throw a proper JavaScript exception with the original text.
1499
+ //
1500
+ // 3. This matches Node.js's behavior where JavaScript exceptions from
1501
+ // authorizer callbacks propagate correctly to the caller.
1502
+ if (database->HasDeferredAuthorizerException()) {
1503
+ // Throw a marker exception - the actual message is stored in the database
1504
+ // object and will be retrieved by the caller.
1505
+ throw std::runtime_error("");
1506
+ }
1239
1507
  std::string error = sqlite3_errmsg(database->connection());
1240
1508
  throw std::runtime_error("Failed to prepare statement: " + error);
1241
1509
  }
@@ -1245,6 +1513,10 @@ StatementSync::~StatementSync() {
1245
1513
  if (statement_ && !finalized_) {
1246
1514
  sqlite3_finalize(statement_);
1247
1515
  }
1516
+ // Release the strong reference to the database object
1517
+ if (!database_ref_.IsEmpty()) {
1518
+ database_ref_.Reset();
1519
+ }
1248
1520
  }
1249
1521
 
1250
1522
  Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
@@ -1273,11 +1545,23 @@ Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
1273
1545
  Reset();
1274
1546
  BindParameters(info);
1275
1547
 
1276
- int result = sqlite3_step(statement_);
1548
+ // Check if BindParameters set a pending exception
1549
+ if (env.IsExceptionPending()) {
1550
+ return env.Undefined();
1551
+ }
1552
+
1553
+ // Execute the statement
1554
+ sqlite3_step(statement_);
1555
+ // Reset immediately after step to ensure sqlite3_changes() returns
1556
+ // correct value. This fixes an issue where RETURNING queries would
1557
+ // report changes: 0 on the first call.
1558
+ // See: https://github.com/nodejs/node/issues/57344
1559
+ int result = sqlite3_reset(statement_);
1277
1560
 
1278
- if (result != SQLITE_DONE && result != SQLITE_ROW) {
1561
+ if (result != SQLITE_OK) {
1279
1562
  std::string error = sqlite3_errmsg(database_->connection());
1280
- node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
1563
+ ThrowEnhancedSqliteErrorWithDB(env, database_, database_->connection(),
1564
+ result, error);
1281
1565
  return env.Undefined();
1282
1566
  }
1283
1567
 
@@ -1300,7 +1584,7 @@ Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
1300
1584
 
1301
1585
  return result_obj;
1302
1586
  } catch (const std::exception &e) {
1303
- node::THROW_ERR_SQLITE_ERROR(env, e.what());
1587
+ ThrowErrSqliteErrorWithDb(env, database_, e.what());
1304
1588
  return env.Undefined();
1305
1589
  }
1306
1590
  }
@@ -1331,6 +1615,11 @@ Napi::Value StatementSync::Get(const Napi::CallbackInfo &info) {
1331
1615
  Reset();
1332
1616
  BindParameters(info);
1333
1617
 
1618
+ // Check if BindParameters set a pending exception
1619
+ if (env.IsExceptionPending()) {
1620
+ return env.Undefined();
1621
+ }
1622
+
1334
1623
  int result = sqlite3_step(statement_);
1335
1624
 
1336
1625
  if (result == SQLITE_ROW) {
@@ -1339,11 +1628,12 @@ Napi::Value StatementSync::Get(const Napi::CallbackInfo &info) {
1339
1628
  return env.Undefined();
1340
1629
  } else {
1341
1630
  std::string error = sqlite3_errmsg(database_->connection());
1342
- node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
1631
+ ThrowEnhancedSqliteErrorWithDB(env, database_, database_->connection(),
1632
+ result, error);
1343
1633
  return env.Undefined();
1344
1634
  }
1345
1635
  } catch (const std::exception &e) {
1346
- node::THROW_ERR_SQLITE_ERROR(env, e.what());
1636
+ ThrowErrSqliteErrorWithDb(env, database_, e.what());
1347
1637
  return env.Undefined();
1348
1638
  }
1349
1639
  }
@@ -1370,6 +1660,11 @@ Napi::Value StatementSync::All(const Napi::CallbackInfo &info) {
1370
1660
  Reset();
1371
1661
  BindParameters(info);
1372
1662
 
1663
+ // Check if BindParameters set a pending exception
1664
+ if (env.IsExceptionPending()) {
1665
+ return env.Undefined();
1666
+ }
1667
+
1373
1668
  Napi::Array results = Napi::Array::New(env);
1374
1669
  uint32_t index = 0;
1375
1670
 
@@ -1422,6 +1717,11 @@ Napi::Value StatementSync::Iterate(const Napi::CallbackInfo &info) {
1422
1717
  // Bind parameters if provided
1423
1718
  BindParameters(info, 0);
1424
1719
 
1720
+ // Check if BindParameters set a pending exception
1721
+ if (info.Env().IsExceptionPending()) {
1722
+ return info.Env().Undefined();
1723
+ }
1724
+
1425
1725
  // Create and return iterator
1426
1726
  return StatementSyncIterator::Create(info.Env(), this);
1427
1727
  }
@@ -1437,6 +1737,21 @@ Napi::Value StatementSync::FinalizeStatement(const Napi::CallbackInfo &info) {
1437
1737
  return info.Env().Undefined();
1438
1738
  }
1439
1739
 
1740
+ Napi::Value StatementSync::Dispose(const Napi::CallbackInfo &info) {
1741
+ // Try to finalize, but ignore errors during disposal (matches Node.js v25
1742
+ // behavior)
1743
+ try {
1744
+ if (statement_ && !finalized_) {
1745
+ sqlite3_finalize(statement_);
1746
+ statement_ = nullptr;
1747
+ finalized_ = true;
1748
+ }
1749
+ } catch (...) {
1750
+ // Ignore errors during disposal
1751
+ }
1752
+ return info.Env().Undefined();
1753
+ }
1754
+
1440
1755
  Napi::Value StatementSync::SourceSQLGetter(const Napi::CallbackInfo &info) {
1441
1756
  return Napi::String::New(info.Env(), source_sql_);
1442
1757
  }
@@ -1463,6 +1778,10 @@ Napi::Value StatementSync::ExpandedSQLGetter(const Napi::CallbackInfo &info) {
1463
1778
  return info.Env().Undefined();
1464
1779
  }
1465
1780
 
1781
+ Napi::Value StatementSync::FinalizedGetter(const Napi::CallbackInfo &info) {
1782
+ return Napi::Boolean::New(info.Env(), finalized_);
1783
+ }
1784
+
1466
1785
  Napi::Value StatementSync::SetReadBigInts(const Napi::CallbackInfo &info) {
1467
1786
  Napi::Env env = info.Env();
1468
1787
 
@@ -1533,6 +1852,30 @@ StatementSync::SetAllowBareNamedParameters(const Napi::CallbackInfo &info) {
1533
1852
  return env.Undefined();
1534
1853
  }
1535
1854
 
1855
+ Napi::Value
1856
+ StatementSync::SetAllowUnknownNamedParameters(const Napi::CallbackInfo &info) {
1857
+ Napi::Env env = info.Env();
1858
+
1859
+ if (finalized_) {
1860
+ node::THROW_ERR_INVALID_STATE(env, "The statement has been finalized");
1861
+ return env.Undefined();
1862
+ }
1863
+
1864
+ if (!database_ || !database_->IsOpen()) {
1865
+ node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
1866
+ return env.Undefined();
1867
+ }
1868
+
1869
+ if (info.Length() < 1 || !info[0].IsBoolean()) {
1870
+ node::THROW_ERR_INVALID_ARG_TYPE(
1871
+ env, "The \"enabled\" argument must be a boolean.");
1872
+ return env.Undefined();
1873
+ }
1874
+
1875
+ allow_unknown_named_params_ = info[0].As<Napi::Boolean>().Value();
1876
+ return env.Undefined();
1877
+ }
1878
+
1536
1879
  Napi::Value StatementSync::Columns(const Napi::CallbackInfo &info) {
1537
1880
  Napi::Env env = info.Env();
1538
1881
 
@@ -1689,6 +2032,17 @@ void StatementSync::BindParameters(const Napi::CallbackInfo &info,
1689
2032
  node::THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str());
1690
2033
  return;
1691
2034
  }
2035
+ } else {
2036
+ // Unknown named parameter
2037
+ if (allow_unknown_named_params_) {
2038
+ // Skip unknown parameters when allowed (matches Node.js v25 behavior)
2039
+ continue;
2040
+ } else {
2041
+ // Throw error when not allowed (default behavior)
2042
+ std::string msg = "Unknown named parameter '" + key_str + "'";
2043
+ node::THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str());
2044
+ return;
2045
+ }
1692
2046
  }
1693
2047
  }
1694
2048
  } else {
@@ -1733,7 +2087,9 @@ void StatementSync::BindSingleParameter(int param_index, Napi::Value param) {
1733
2087
  }
1734
2088
  } else if (param.IsNumber()) {
1735
2089
  double val = param.As<Napi::Number>().DoubleValue();
1736
- if (val == std::floor(val) && val >= INT32_MIN && val <= INT32_MAX) {
2090
+ if (std::abs(val - std::floor(val)) <
2091
+ std::numeric_limits<double>::epsilon() &&
2092
+ val >= INT32_MIN && val <= INT32_MAX) {
1737
2093
  sqlite3_bind_int(statement_, param_index,
1738
2094
  param.As<Napi::Number>().Int32Value());
1739
2095
  } else {
@@ -1747,22 +2103,55 @@ void StatementSync::BindSingleParameter(int param_index, Napi::Value param) {
1747
2103
  } else if (param.IsBoolean()) {
1748
2104
  sqlite3_bind_int(statement_, param_index,
1749
2105
  param.As<Napi::Boolean>().Value() ? 1 : 0);
2106
+ } else if (param.IsDataView()) {
2107
+ // IMPORTANT: Check DataView BEFORE IsBuffer() because N-API's IsBuffer()
2108
+ // returns true for ALL ArrayBufferViews (including DataView), but
2109
+ // Buffer::As() doesn't work correctly for DataView (returns length=0).
2110
+ Napi::DataView dataView = param.As<Napi::DataView>();
2111
+ Napi::ArrayBuffer arrayBuffer = dataView.ArrayBuffer();
2112
+ size_t byteOffset = dataView.ByteOffset();
2113
+ size_t byteLength = dataView.ByteLength();
2114
+
2115
+ if (arrayBuffer.Data() != nullptr && byteLength > 0) {
2116
+ const uint8_t *data =
2117
+ static_cast<const uint8_t *>(arrayBuffer.Data()) + byteOffset;
2118
+ sqlite3_bind_blob(statement_, param_index, data,
2119
+ SafeCastToInt(byteLength), SQLITE_TRANSIENT);
2120
+ } else {
2121
+ sqlite3_bind_null(statement_, param_index);
2122
+ }
1750
2123
  } else if (param.IsBuffer()) {
2124
+ // Handles Buffer and TypedArray (both are ArrayBufferViews that work
2125
+ // correctly with Buffer cast - Buffer::Data() handles byte offsets
2126
+ // internally)
1751
2127
  Napi::Buffer<uint8_t> buffer = param.As<Napi::Buffer<uint8_t>>();
1752
2128
  sqlite3_bind_blob(statement_, param_index, buffer.Data(),
1753
- static_cast<int>(buffer.Length()), SQLITE_TRANSIENT);
2129
+ SafeCastToInt(buffer.Length()), SQLITE_TRANSIENT);
1754
2130
  } else if (param.IsFunction()) {
1755
2131
  // Functions cannot be stored in SQLite - bind as NULL
1756
2132
  sqlite3_bind_null(statement_, param_index);
2133
+ } else if (param.IsArrayBuffer()) {
2134
+ // Handle ArrayBuffer as binary data
2135
+ Napi::ArrayBuffer arrayBuffer = param.As<Napi::ArrayBuffer>();
2136
+ if (!arrayBuffer.IsEmpty() && arrayBuffer.Data() != nullptr) {
2137
+ sqlite3_bind_blob(statement_, param_index, arrayBuffer.Data(),
2138
+ SafeCastToInt(arrayBuffer.ByteLength()),
2139
+ SQLITE_TRANSIENT);
2140
+ } else {
2141
+ sqlite3_bind_null(statement_, param_index);
2142
+ }
1757
2143
  } else if (param.IsObject()) {
1758
- // Try to convert object to string
1759
- Napi::String str_value = param.ToString();
1760
- std::string str = str_value.Utf8Value();
1761
- sqlite3_bind_text(statement_, param_index, str.c_str(), -1,
1762
- SQLITE_TRANSIENT);
2144
+ // Objects and arrays cannot be bound to SQLite parameters (same as
2145
+ // Node.js behavior). Note: DataView, Buffer, TypedArray, and ArrayBuffer
2146
+ // are handled above and don't reach this branch.
2147
+ throw Napi::Error::New(
2148
+ Env(), "Provided value cannot be bound to SQLite parameter " +
2149
+ std::to_string(param_index) + ".");
1763
2150
  } else {
1764
- // For any other type, bind as NULL
1765
- sqlite3_bind_null(statement_, param_index);
2151
+ // For any other type, throw error like Node.js does
2152
+ throw Napi::Error::New(
2153
+ Env(), "Provided value cannot be bound to SQLite parameter " +
2154
+ std::to_string(param_index) + ".");
1766
2155
  }
1767
2156
  } catch (const Napi::Error &e) {
1768
2157
  // Re-throw Napi errors
@@ -1820,14 +2209,25 @@ Napi::Value StatementSync::CreateResult() {
1820
2209
  break;
1821
2210
  case SQLITE_TEXT: {
1822
2211
  const unsigned char *text = sqlite3_column_text(statement_, i);
1823
- value = Napi::String::New(env, reinterpret_cast<const char *>(text));
2212
+ // sqlite3_column_text() can return NULL on OOM or encoding errors
2213
+ if (!text) {
2214
+ value = Napi::String::New(env, "");
2215
+ } else {
2216
+ value = Napi::String::New(env, reinterpret_cast<const char *>(text));
2217
+ }
1824
2218
  break;
1825
2219
  }
1826
2220
  case SQLITE_BLOB: {
1827
2221
  const void *blob_data = sqlite3_column_blob(statement_, i);
1828
2222
  int blob_size = sqlite3_column_bytes(statement_, i);
1829
- value = Napi::Buffer<uint8_t>::Copy(
1830
- env, static_cast<const uint8_t *>(blob_data), blob_size);
2223
+ // sqlite3_column_blob() can return NULL for zero-length BLOBs or on OOM
2224
+ if (!blob_data || blob_size == 0) {
2225
+ // Handle empty/NULL blob - create empty buffer
2226
+ value = Napi::Buffer<uint8_t>::New(env, 0);
2227
+ } else {
2228
+ value = Napi::Buffer<uint8_t>::Copy(
2229
+ env, static_cast<const uint8_t *>(blob_data), blob_size);
2230
+ }
1831
2231
  break;
1832
2232
  }
1833
2233
  default:
@@ -1872,14 +2272,25 @@ Napi::Value StatementSync::CreateResult() {
1872
2272
  break;
1873
2273
  case SQLITE_TEXT: {
1874
2274
  const unsigned char *text = sqlite3_column_text(statement_, i);
1875
- value = Napi::String::New(env, reinterpret_cast<const char *>(text));
2275
+ // sqlite3_column_text() can return NULL on OOM or encoding errors
2276
+ if (!text) {
2277
+ value = Napi::String::New(env, "");
2278
+ } else {
2279
+ value = Napi::String::New(env, reinterpret_cast<const char *>(text));
2280
+ }
1876
2281
  break;
1877
2282
  }
1878
2283
  case SQLITE_BLOB: {
1879
2284
  const void *blob_data = sqlite3_column_blob(statement_, i);
1880
2285
  int blob_size = sqlite3_column_bytes(statement_, i);
1881
- value = Napi::Buffer<uint8_t>::Copy(
1882
- env, static_cast<const uint8_t *>(blob_data), blob_size);
2286
+ // sqlite3_column_blob() can return NULL for zero-length BLOBs or on OOM
2287
+ if (!blob_data || blob_size == 0) {
2288
+ // Handle empty/NULL blob - create empty buffer
2289
+ value = Napi::Buffer<uint8_t>::New(env, 0);
2290
+ } else {
2291
+ value = Napi::Buffer<uint8_t>::Copy(
2292
+ env, static_cast<const uint8_t *>(blob_data), blob_size);
2293
+ }
1883
2294
  break;
1884
2295
  }
1885
2296
  default:
@@ -2037,7 +2448,8 @@ Napi::Object Session::Init(Napi::Env env, Napi::Object exports) {
2037
2448
  DefineClass(env, "Session",
2038
2449
  {InstanceMethod("changeset", &Session::Changeset),
2039
2450
  InstanceMethod("patchset", &Session::Patchset),
2040
- InstanceMethod("close", &Session::Close)});
2451
+ InstanceMethod("close", &Session::Close),
2452
+ InstanceMethod("dispose", &Session::Dispose)});
2041
2453
 
2042
2454
  // Store constructor in per-instance addon data instead of static variable
2043
2455
  AddonData *addon_data = GetAddonData(env);
@@ -2045,6 +2457,21 @@ Napi::Object Session::Init(Napi::Env env, Napi::Object exports) {
2045
2457
  addon_data->sessionConstructor = Napi::Reference<Napi::Function>::New(func);
2046
2458
  }
2047
2459
 
2460
+ // Add Symbol.dispose to the prototype (Node.js v25+ compatibility)
2461
+ Napi::Value symbolDispose =
2462
+ env.Global().Get("Symbol").As<Napi::Object>().Get("dispose");
2463
+ if (!symbolDispose.IsUndefined()) {
2464
+ func.Get("prototype")
2465
+ .As<Napi::Object>()
2466
+ .Set(symbolDispose,
2467
+ Napi::Function::New(
2468
+ env, [](const Napi::CallbackInfo &info) -> Napi::Value {
2469
+ Session *session =
2470
+ Session::Unwrap(info.This().As<Napi::Object>());
2471
+ return session->Dispose(info);
2472
+ }));
2473
+ }
2474
+
2048
2475
  exports.Set("Session", func);
2049
2476
  return exports;
2050
2477
  }
@@ -2154,6 +2581,19 @@ Napi::Value Session::Close(const Napi::CallbackInfo &info) {
2154
2581
  return env.Undefined();
2155
2582
  }
2156
2583
 
2584
+ Napi::Value Session::Dispose(const Napi::CallbackInfo &info) {
2585
+ // Try to close, but ignore errors during disposal (matches Node.js v25
2586
+ // behavior)
2587
+ try {
2588
+ if (session_ != nullptr) {
2589
+ Delete();
2590
+ }
2591
+ } catch (...) {
2592
+ // Ignore errors during disposal
2593
+ }
2594
+ return info.Env().Undefined();
2595
+ }
2596
+
2157
2597
  // Static members for tracking active jobs
2158
2598
  std::atomic<int> BackupJob::active_jobs_(0);
2159
2599
  std::mutex BackupJob::active_jobs_mutex_;
@@ -2161,17 +2601,17 @@ std::set<BackupJob *> BackupJob::active_job_instances_;
2161
2601
 
2162
2602
  // BackupJob Implementation
2163
2603
  BackupJob::BackupJob(Napi::Env env, DatabaseSync *source,
2164
- const std::string &destination_path,
2165
- const std::string &source_db, const std::string &dest_db,
2166
- int pages, Napi::Function progress_func,
2604
+ std::string destination_path, std::string source_db,
2605
+ std::string dest_db, int pages,
2606
+ Napi::Function progress_func,
2167
2607
  Napi::Promise::Deferred deferred)
2168
2608
  : Napi::AsyncProgressWorker<BackupProgress>(
2169
2609
  !progress_func.IsEmpty() && !progress_func.IsUndefined()
2170
2610
  ? progress_func
2171
2611
  : Napi::Function::New(env, [](const Napi::CallbackInfo &) {})),
2172
- source_(source), destination_path_(destination_path),
2173
- source_db_(source_db), dest_db_(dest_db), pages_(pages),
2174
- deferred_(deferred) {
2612
+ source_(source), destination_path_(std::move(destination_path)),
2613
+ source_db_(std::move(source_db)), dest_db_(std::move(dest_db)),
2614
+ pages_(pages), deferred_(deferred) {
2175
2615
  if (!progress_func.IsEmpty() && !progress_func.IsUndefined()) {
2176
2616
  progress_func_ = Napi::Reference<Napi::Function>::New(progress_func);
2177
2617
  }
@@ -2284,15 +2724,27 @@ void BackupJob::OnError(const Napi::Error &error) {
2284
2724
  // This runs on the main thread if Execute encounters an error
2285
2725
  Napi::HandleScope scope(Env());
2286
2726
 
2287
- // Cleanup SQLite resources
2727
+ // Save error info BEFORE cleanup nulls the pointers
2728
+ int saved_status = backup_status_;
2729
+ std::string saved_errmsg;
2730
+ if (dest_) {
2731
+ saved_errmsg = sqlite3_errmsg(dest_);
2732
+ // Capture any final error code from dest
2733
+ int dest_err = sqlite3_errcode(dest_);
2734
+ if (dest_err != SQLITE_OK && saved_status == SQLITE_OK) {
2735
+ saved_status = dest_err;
2736
+ }
2737
+ }
2738
+
2739
+ // Now safe to cleanup
2288
2740
  Cleanup();
2289
2741
 
2290
- // Create a more detailed error if we have SQLite error info
2291
- if (dest_ && backup_status_ != SQLITE_OK) {
2742
+ // Use saved values for error details (matching node:sqlite property names)
2743
+ if (saved_status != SQLITE_OK && saved_status != SQLITE_DONE) {
2292
2744
  Napi::Error detailed_error = Napi::Error::New(Env(), error.Message());
2293
- detailed_error.Set(
2294
- "code", Napi::String::New(Env(), sqlite3_errstr(backup_status_)));
2295
- detailed_error.Set("errno", Napi::Number::New(Env(), backup_status_));
2745
+ detailed_error.Set("errcode", Napi::Number::New(Env(), saved_status));
2746
+ detailed_error.Set("errstr",
2747
+ Napi::String::New(Env(), sqlite3_errstr(saved_status)));
2296
2748
  deferred_.Reject(detailed_error.Value());
2297
2749
  } else {
2298
2750
  deferred_.Reject(error.Value());
@@ -2408,8 +2860,9 @@ Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
2408
2860
  }
2409
2861
 
2410
2862
  // Create and schedule backup job
2411
- BackupJob *job = new BackupJob(env, this, destination_path.value(), source_db,
2412
- target_db, rate, progress_func, deferred);
2863
+ BackupJob *job = new BackupJob(env, this, std::move(destination_path).value(),
2864
+ std::move(source_db), std::move(target_db),
2865
+ rate, progress_func, deferred);
2413
2866
 
2414
2867
  // Queue the async work - AsyncWorker will delete itself when complete
2415
2868
  job->Queue();
@@ -2417,6 +2870,138 @@ Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
2417
2870
  return deferred.Promise();
2418
2871
  }
2419
2872
 
2873
+ // Helper function to convert nullable C string to JavaScript value
2874
+ static Napi::Value NullableSQLiteStringToValue(Napi::Env env, const char *str) {
2875
+ if (str == nullptr) {
2876
+ return env.Null();
2877
+ }
2878
+ return Napi::String::New(env, str);
2879
+ }
2880
+
2881
+ // DatabaseSync::SetAuthorizer implementation
2882
+ Napi::Value DatabaseSync::SetAuthorizer(const Napi::CallbackInfo &info) {
2883
+ Napi::Env env = info.Env();
2884
+
2885
+ if (!IsOpen()) {
2886
+ node::THROW_ERR_INVALID_STATE(env, "database is not open");
2887
+ return env.Undefined();
2888
+ }
2889
+
2890
+ // Handle null to clear the authorizer
2891
+ if (info.Length() > 0 && info[0].IsNull()) {
2892
+ sqlite3_set_authorizer(connection_, nullptr, nullptr);
2893
+ authorizer_callback_.reset();
2894
+ return env.Undefined();
2895
+ }
2896
+
2897
+ // Validate callback argument
2898
+ if (info.Length() < 1 || !info[0].IsFunction()) {
2899
+ node::THROW_ERR_INVALID_ARG_TYPE(
2900
+ env, "The \"callback\" argument must be a function or null.");
2901
+ return env.Undefined();
2902
+ }
2903
+
2904
+ // Store the JavaScript callback
2905
+ Napi::Function fn = info[0].As<Napi::Function>();
2906
+ authorizer_callback_ =
2907
+ std::make_unique<Napi::FunctionReference>(Napi::Persistent(fn));
2908
+
2909
+ // Set the SQLite authorizer with our static callback
2910
+ int r = sqlite3_set_authorizer(connection_, AuthorizerCallback, this);
2911
+
2912
+ if (r != SQLITE_OK) {
2913
+ authorizer_callback_.reset();
2914
+ ThrowEnhancedSqliteErrorWithDB(env, this, connection_, r,
2915
+ "Failed to set authorizer");
2916
+ return env.Undefined();
2917
+ }
2918
+
2919
+ return env.Undefined();
2920
+ }
2921
+
2922
+ // Static callback for SQLite authorization
2923
+ int DatabaseSync::AuthorizerCallback(void *user_data, int action_code,
2924
+ const char *param1, const char *param2,
2925
+ const char *param3, const char *param4) {
2926
+ DatabaseSync *db = static_cast<DatabaseSync *>(user_data);
2927
+
2928
+ // If no callback is set, allow everything
2929
+ if (!db->authorizer_callback_ || db->authorizer_callback_->IsEmpty()) {
2930
+ return SQLITE_OK;
2931
+ }
2932
+
2933
+ Napi::Env env(db->env_);
2934
+ Napi::HandleScope scope(env);
2935
+
2936
+ try {
2937
+ // Convert SQLite authorizer parameters to JavaScript values
2938
+ std::vector<napi_value> args;
2939
+ args.push_back(Napi::Number::New(env, action_code));
2940
+ args.push_back(NullableSQLiteStringToValue(env, param1));
2941
+ args.push_back(NullableSQLiteStringToValue(env, param2));
2942
+ args.push_back(NullableSQLiteStringToValue(env, param3));
2943
+ args.push_back(NullableSQLiteStringToValue(env, param4));
2944
+
2945
+ // Call the JavaScript callback
2946
+ Napi::Value result = db->authorizer_callback_->Call(env.Undefined(), args);
2947
+
2948
+ // Handle JavaScript exceptions - must clear before returning to SQLite
2949
+ if (env.IsExceptionPending()) {
2950
+ Napi::Error error = env.GetAndClearPendingException();
2951
+ db->SetDeferredAuthorizerException(error.Message());
2952
+ db->SetIgnoreNextSQLiteError(true);
2953
+ return SQLITE_DENY;
2954
+ }
2955
+
2956
+ // Check if result is an integer - don't throw in callback context
2957
+ if (!result.IsNumber()) {
2958
+ db->SetDeferredAuthorizerException(
2959
+ "Authorizer callback must return an integer authorization code");
2960
+ db->SetIgnoreNextSQLiteError(true);
2961
+ return SQLITE_DENY;
2962
+ }
2963
+
2964
+ int32_t int_result = result.As<Napi::Number>().Int32Value();
2965
+
2966
+ // Validate the return code - don't throw in callback context
2967
+ if (int_result != SQLITE_OK && int_result != SQLITE_DENY &&
2968
+ int_result != SQLITE_IGNORE) {
2969
+ db->SetDeferredAuthorizerException(
2970
+ "Authorizer callback returned an invalid authorization code");
2971
+ db->SetIgnoreNextSQLiteError(true);
2972
+ return SQLITE_DENY;
2973
+ }
2974
+
2975
+ return int_result;
2976
+ } catch (const Napi::Error &e) {
2977
+ // JavaScript exception occurred - clear any pending exception and store
2978
+ if (env.IsExceptionPending()) {
2979
+ Napi::Error error = env.GetAndClearPendingException();
2980
+ db->SetDeferredAuthorizerException(error.Message());
2981
+ } else {
2982
+ db->SetDeferredAuthorizerException(e.Message());
2983
+ }
2984
+ db->SetIgnoreNextSQLiteError(true);
2985
+ return SQLITE_DENY;
2986
+ } catch (const std::exception &e) {
2987
+ // C++ exception - clear any pending JS exception and store message
2988
+ if (env.IsExceptionPending()) {
2989
+ env.GetAndClearPendingException();
2990
+ }
2991
+ db->SetDeferredAuthorizerException(e.what());
2992
+ db->SetIgnoreNextSQLiteError(true);
2993
+ return SQLITE_DENY;
2994
+ } catch (...) {
2995
+ // Unknown error - clear any pending JS exception and deny
2996
+ if (env.IsExceptionPending()) {
2997
+ env.GetAndClearPendingException();
2998
+ }
2999
+ db->SetDeferredAuthorizerException("Unknown error in authorizer callback");
3000
+ db->SetIgnoreNextSQLiteError(true);
3001
+ return SQLITE_DENY;
3002
+ }
3003
+ }
3004
+
2420
3005
  // Thread validation implementations
2421
3006
  bool DatabaseSync::ValidateThread(Napi::Env env) const {
2422
3007
  if (std::this_thread::get_id() != creation_thread_) {
@@ -2436,5 +3021,4 @@ bool StatementSync::ValidateThread(Napi::Env env) const {
2436
3021
  return true;
2437
3022
  }
2438
3023
 
2439
- } // namespace sqlite
2440
- } // namespace photostructure
3024
+ } // namespace photostructure::sqlite