@photostructure/sqlite 0.0.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +38 -2
  2. package/README.md +47 -483
  3. package/SECURITY.md +27 -83
  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-arm64/@photostructure+sqlite.glibc.node +0 -0
  21. package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
  22. package/src/aggregate_function.cpp +503 -235
  23. package/src/aggregate_function.h +57 -42
  24. package/src/binding.cpp +117 -14
  25. package/src/dirname.ts +1 -1
  26. package/src/index.ts +122 -332
  27. package/src/lru-cache.ts +84 -0
  28. package/src/shims/env-inl.h +6 -15
  29. package/src/shims/node_errors.h +7 -1
  30. package/src/shims/sqlite_errors.h +168 -0
  31. package/src/shims/util.h +29 -4
  32. package/src/sql-tag-store.ts +140 -0
  33. package/src/sqlite_exception.h +49 -0
  34. package/src/sqlite_impl.cpp +736 -129
  35. package/src/sqlite_impl.h +84 -6
  36. package/src/{stack_path.ts → stack-path.ts} +7 -1
  37. package/src/types/aggregate-options.ts +22 -0
  38. package/src/types/changeset-apply-options.ts +18 -0
  39. package/src/types/database-sync-instance.ts +203 -0
  40. package/src/types/database-sync-options.ts +69 -0
  41. package/src/types/session-options.ts +10 -0
  42. package/src/types/sql-tag-store-instance.ts +51 -0
  43. package/src/types/sqlite-authorization-actions.ts +77 -0
  44. package/src/types/sqlite-authorization-results.ts +15 -0
  45. package/src/types/sqlite-changeset-conflict-types.ts +19 -0
  46. package/src/types/sqlite-changeset-resolution.ts +15 -0
  47. package/src/types/sqlite-open-flags.ts +50 -0
  48. package/src/types/statement-sync-instance.ts +73 -0
  49. package/src/types/user-functions-options.ts +14 -0
  50. package/src/upstream/node_sqlite.cc +960 -259
  51. package/src/upstream/node_sqlite.h +127 -2
  52. package/src/upstream/sqlite.js +1 -14
  53. package/src/upstream/sqlite3.c +4510 -1411
  54. package/src/upstream/sqlite3.h +390 -195
  55. package/src/upstream/sqlite3ext.h +7 -0
  56. package/src/user_function.cpp +88 -36
  57. package/src/user_function.h +2 -1
@@ -4,13 +4,59 @@
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
+ // Use c_str() explicitly to avoid potential ABI issues on Windows ARM
26
+ Napi::Error::New(env, deferred_msg.c_str()).ThrowAsJavaScriptException();
27
+ }
28
+ return; // Don't throw SQLite error, JavaScript exception takes precedence
29
+ }
30
+
31
+ const char *msg = (message != nullptr) ? message : "SQLite error";
32
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
33
+ }
34
+
35
+ inline void ThrowEnhancedSqliteErrorWithDB(
36
+ Napi::Env env, photostructure::sqlite::DatabaseSync *db_sync, sqlite3 *db,
37
+ int sqlite_code, const std::string &message) {
38
+ // Check if we should ignore this SQLite error due to pending JavaScript
39
+ // exception (e.g., from authorizer callback)
40
+ if (db_sync != nullptr && db_sync->ShouldIgnoreSQLiteError()) {
41
+ db_sync->SetIgnoreNextSQLiteError(false);
42
+ // Check for deferred authorizer exception and throw it instead
43
+ if (db_sync->HasDeferredAuthorizerException()) {
44
+ std::string deferred_msg = db_sync->GetDeferredAuthorizerException();
45
+ db_sync->ClearDeferredAuthorizerException();
46
+ // Use c_str() explicitly to avoid potential ABI issues on Windows ARM
47
+ Napi::Error::New(env, deferred_msg.c_str()).ThrowAsJavaScriptException();
48
+ }
49
+ return; // Don't throw SQLite error, JavaScript exception takes precedence
50
+ }
51
+
52
+ // Call the original function
53
+ node::ThrowEnhancedSqliteError(env, db, sqlite_code, message);
54
+ }
55
+ } // namespace
56
+ #include "sqlite_exception.h"
10
57
  #include "user_function.h"
11
58
 
12
- namespace photostructure {
13
- namespace sqlite {
59
+ namespace photostructure::sqlite {
14
60
 
15
61
  // JavaScript safe integer limits (2^53 - 1)
16
62
  constexpr int64_t JS_MAX_SAFE_INTEGER = 9007199254740991LL;
@@ -29,8 +75,8 @@ std::optional<std::string> ValidateDatabasePath(Napi::Env env, Napi::Value path,
29
75
  return location;
30
76
  }
31
77
  } else if (path.IsBuffer()) {
32
- Napi::Buffer<uint8_t> buffer = path.As<Napi::Buffer<uint8_t>>();
33
- size_t length = buffer.Length();
78
+ const auto buffer = path.As<Napi::Buffer<uint8_t>>();
79
+ const size_t length = buffer.Length();
34
80
  const uint8_t *data = buffer.Data();
35
81
 
36
82
  // Check for null bytes in buffer
@@ -238,6 +284,7 @@ Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
238
284
  env, "DatabaseSync",
239
285
  {InstanceMethod("open", &DatabaseSync::Open),
240
286
  InstanceMethod("close", &DatabaseSync::Close),
287
+ InstanceMethod("dispose", &DatabaseSync::Dispose),
241
288
  InstanceMethod("prepare", &DatabaseSync::Prepare),
242
289
  InstanceMethod("exec", &DatabaseSync::Exec),
243
290
  InstanceMethod("function", &DatabaseSync::CustomFunction),
@@ -245,9 +292,11 @@ Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
245
292
  InstanceMethod("enableLoadExtension",
246
293
  &DatabaseSync::EnableLoadExtension),
247
294
  InstanceMethod("loadExtension", &DatabaseSync::LoadExtension),
295
+ InstanceMethod("enableDefensive", &DatabaseSync::EnableDefensive),
248
296
  InstanceMethod("createSession", &DatabaseSync::CreateSession),
249
297
  InstanceMethod("applyChangeset", &DatabaseSync::ApplyChangeset),
250
298
  InstanceMethod("backup", &DatabaseSync::Backup),
299
+ InstanceMethod("setAuthorizer", &DatabaseSync::SetAuthorizer),
251
300
  InstanceMethod("location", &DatabaseSync::LocationMethod),
252
301
  InstanceAccessor("isOpen", &DatabaseSync::IsOpenGetter, nullptr),
253
302
  InstanceAccessor("isTransaction", &DatabaseSync::IsTransactionGetter,
@@ -260,13 +309,40 @@ Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
260
309
  Napi::Reference<Napi::Function>::New(func);
261
310
  }
262
311
 
312
+ // Add Symbol.dispose to the prototype (Node.js v25+ compatibility)
313
+ Napi::Value symbolDispose =
314
+ env.Global().Get("Symbol").As<Napi::Object>().Get("dispose");
315
+ if (!symbolDispose.IsUndefined()) {
316
+ func.Get("prototype")
317
+ .As<Napi::Object>()
318
+ .Set(symbolDispose,
319
+ Napi::Function::New(
320
+ env, [](const Napi::CallbackInfo &info) -> Napi::Value {
321
+ DatabaseSync *db =
322
+ DatabaseSync::Unwrap(info.This().As<Napi::Object>());
323
+ return db->Dispose(info);
324
+ }));
325
+ }
326
+
327
+ // Add Symbol.for('sqlite-type') property for type identification
328
+ // See: https://github.com/nodejs/node/pull/59405
329
+ Napi::Object symbolConstructor =
330
+ env.Global().Get("Symbol").As<Napi::Object>();
331
+ Napi::Function symbolFor = symbolConstructor.Get("for").As<Napi::Function>();
332
+ Napi::Value sqliteTypeSymbol = symbolFor.Call(
333
+ symbolConstructor, {Napi::String::New(env, "sqlite-type")});
334
+ func.Get("prototype")
335
+ .As<Napi::Object>()
336
+ .Set(sqliteTypeSymbol, Napi::String::New(env, "node:sqlite"));
337
+
263
338
  exports.Set("DatabaseSync", func);
264
339
  return exports;
265
340
  }
266
341
 
267
342
  DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
268
343
  : Napi::ObjectWrap<DatabaseSync>(info),
269
- creation_thread_(std::this_thread::get_id()), env_(info.Env()) {
344
+ creation_thread_(std::this_thread::get_id()), env_(info.Env()),
345
+ config_("") {
270
346
  // Register this instance for cleanup tracking
271
347
  RegisterDatabaseInstance(info.Env(), this);
272
348
 
@@ -285,6 +361,9 @@ DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
285
361
  try {
286
362
  DatabaseOpenConfiguration config(std::move(location.value()));
287
363
 
364
+ // Track whether to open immediately (default: true)
365
+ bool should_open = true;
366
+
288
367
  // Handle options object if provided as second argument
289
368
  if (info.Length() > 1 && info[1].IsObject()) {
290
369
  Napi::Object options = info[1].As<Napi::Object>();
@@ -324,9 +403,68 @@ DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
324
403
  allow_load_extension_ =
325
404
  options.Get("allowExtension").As<Napi::Boolean>().Value();
326
405
  }
406
+
407
+ if (options.Has("readBigInts") &&
408
+ options.Get("readBigInts").IsBoolean()) {
409
+ config.set_read_big_ints(
410
+ options.Get("readBigInts").As<Napi::Boolean>().Value());
411
+ }
412
+
413
+ if (options.Has("returnArrays") &&
414
+ options.Get("returnArrays").IsBoolean()) {
415
+ config.set_return_arrays(
416
+ options.Get("returnArrays").As<Napi::Boolean>().Value());
417
+ }
418
+
419
+ if (options.Has("allowBareNamedParameters") &&
420
+ options.Get("allowBareNamedParameters").IsBoolean()) {
421
+ config.set_allow_bare_named_params(
422
+ options.Get("allowBareNamedParameters")
423
+ .As<Napi::Boolean>()
424
+ .Value());
425
+ }
426
+
427
+ if (options.Has("allowUnknownNamedParameters") &&
428
+ options.Get("allowUnknownNamedParameters").IsBoolean()) {
429
+ config.set_allow_unknown_named_params(
430
+ options.Get("allowUnknownNamedParameters")
431
+ .As<Napi::Boolean>()
432
+ .Value());
433
+ }
434
+
435
+ if (options.Has("defensive")) {
436
+ Napi::Value defensive_val = options.Get("defensive");
437
+ if (!defensive_val.IsUndefined()) {
438
+ if (!defensive_val.IsBoolean()) {
439
+ node::THROW_ERR_INVALID_ARG_TYPE(
440
+ info.Env(),
441
+ "The \"options.defensive\" argument must be a boolean.");
442
+ return;
443
+ }
444
+ config.set_enable_defensive(
445
+ defensive_val.As<Napi::Boolean>().Value());
446
+ }
447
+ }
448
+
449
+ // Handle the open option
450
+ if (options.Has("open")) {
451
+ Napi::Value open_val = options.Get("open");
452
+ if (open_val.IsBoolean()) {
453
+ should_open = open_val.As<Napi::Boolean>().Value();
454
+ }
455
+ // For non-boolean values, default to true (existing behavior)
456
+ }
327
457
  }
328
458
 
329
- InternalOpen(config);
459
+ // Store configuration for later use
460
+ config_ = std::move(config);
461
+
462
+ // Only open if should_open is true
463
+ if (should_open) {
464
+ InternalOpen(config_);
465
+ }
466
+ } catch (const SqliteException &e) {
467
+ node::ThrowFromSqliteException(info.Env(), e);
330
468
  } catch (const std::exception &e) {
331
469
  node::THROW_ERR_SQLITE_ERROR(info.Env(), e.what());
332
470
  }
@@ -345,65 +483,14 @@ Napi::Value DatabaseSync::Open(const Napi::CallbackInfo &info) {
345
483
  Napi::Env env = info.Env();
346
484
 
347
485
  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");
486
+ node::THROW_ERR_INVALID_STATE(env, "database is already open");
354
487
  return env.Undefined();
355
488
  }
356
489
 
357
- Napi::Object config_obj = info[0].As<Napi::Object>();
358
-
359
- if (!config_obj.Has("location") || !config_obj.Get("location").IsString()) {
360
- node::THROW_ERR_INVALID_ARG_TYPE(env,
361
- "Configuration must have location string");
362
- return env.Undefined();
363
- }
364
-
365
- std::string location =
366
- config_obj.Get("location").As<Napi::String>().Utf8Value();
367
- DatabaseOpenConfiguration config(std::move(location));
368
-
369
- // Parse other options
370
- if (config_obj.Has("readOnly") && config_obj.Get("readOnly").IsBoolean()) {
371
- config.set_read_only(
372
- config_obj.Get("readOnly").As<Napi::Boolean>().Value());
373
- }
374
-
375
- // Support both old and new naming for backwards compatibility
376
- if (config_obj.Has("enableForeignKeyConstraints") &&
377
- config_obj.Get("enableForeignKeyConstraints").IsBoolean()) {
378
- config.set_enable_foreign_keys(config_obj.Get("enableForeignKeyConstraints")
379
- .As<Napi::Boolean>()
380
- .Value());
381
- } else if (config_obj.Has("enableForeignKeys") &&
382
- config_obj.Get("enableForeignKeys").IsBoolean()) {
383
- config.set_enable_foreign_keys(
384
- config_obj.Get("enableForeignKeys").As<Napi::Boolean>().Value());
385
- }
386
-
387
- if (config_obj.Has("timeout") && config_obj.Get("timeout").IsNumber()) {
388
- config.set_timeout(
389
- config_obj.Get("timeout").As<Napi::Number>().Int32Value());
390
- }
391
-
392
- if (config_obj.Has("enableDoubleQuotedStringLiterals") &&
393
- config_obj.Get("enableDoubleQuotedStringLiterals").IsBoolean()) {
394
- config.set_enable_dqs(config_obj.Get("enableDoubleQuotedStringLiterals")
395
- .As<Napi::Boolean>()
396
- .Value());
397
- }
398
-
399
- if (config_obj.Has("allowExtension") &&
400
- config_obj.Get("allowExtension").IsBoolean()) {
401
- allow_load_extension_ =
402
- config_obj.Get("allowExtension").As<Napi::Boolean>().Value();
403
- }
404
-
405
490
  try {
406
- InternalOpen(config);
491
+ InternalOpen(config_);
492
+ } catch (const SqliteException &e) {
493
+ node::ThrowFromSqliteException(env, e);
407
494
  } catch (const std::exception &e) {
408
495
  node::THROW_ERR_SQLITE_ERROR(env, e.what());
409
496
  }
@@ -432,6 +519,26 @@ Napi::Value DatabaseSync::Close(const Napi::CallbackInfo &info) {
432
519
  return env.Undefined();
433
520
  }
434
521
 
522
+ Napi::Value DatabaseSync::Dispose(const Napi::CallbackInfo &info) {
523
+ Napi::Env env = info.Env();
524
+
525
+ if (!ValidateThread(env)) {
526
+ return env.Undefined();
527
+ }
528
+
529
+ // Try to close, but ignore errors during disposal (matches Node.js v25
530
+ // behavior)
531
+ try {
532
+ if (IsOpen()) {
533
+ InternalClose();
534
+ }
535
+ } catch (...) {
536
+ // Ignore errors during disposal
537
+ }
538
+
539
+ return env.Undefined();
540
+ }
541
+
435
542
  Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
436
543
  Napi::Env env = info.Env();
437
544
 
@@ -451,6 +558,10 @@ Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
451
558
 
452
559
  std::string sql = info[0].As<Napi::String>().Utf8Value();
453
560
 
561
+ // Clear any stale deferred exception from a previous operation
562
+ ClearDeferredAuthorizerException();
563
+ SetIgnoreNextSQLiteError(false);
564
+
454
565
  try {
455
566
  // Create new StatementSync instance using addon data constructor
456
567
  AddonData *addon_data = GetAddonData(env);
@@ -467,7 +578,42 @@ Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
467
578
  stmt->InitStatement(this, sql);
468
579
 
469
580
  return stmt_obj;
581
+ } catch (const SqliteException &e) {
582
+ // SqliteException stores message in std::string, avoiding Windows ARM ABI
583
+ // issues where std::exception::what() can return corrupted strings
584
+ if (HasDeferredAuthorizerException()) {
585
+ std::string deferred_msg = GetDeferredAuthorizerException();
586
+ ClearDeferredAuthorizerException();
587
+ SetIgnoreNextSQLiteError(false);
588
+ // Use c_str() explicitly to avoid potential ABI issues on Windows ARM
589
+ Napi::Error::New(env, deferred_msg.c_str()).ThrowAsJavaScriptException();
590
+ return env.Undefined();
591
+ }
592
+ node::ThrowFromSqliteException(env, e);
593
+ return env.Undefined();
470
594
  } catch (const std::exception &e) {
595
+ // Handle deferred authorizer exceptions:
596
+ //
597
+ // When an authorizer callback throws a JavaScript exception, we use a
598
+ // "marker" exception pattern to safely propagate the error:
599
+ //
600
+ // 1. On Windows (MSVC), std::exception::what() can sometimes return an
601
+ // empty string, causing message loss.
602
+ //
603
+ // 2. By storing the message in the DatabaseSync instance, we can retrieve
604
+ // it here and throw a proper JavaScript exception with the original
605
+ // text.
606
+ //
607
+ // See also: StatementSync::InitStatement for the other half of this
608
+ // pattern.
609
+ if (HasDeferredAuthorizerException()) {
610
+ std::string deferred_msg = GetDeferredAuthorizerException();
611
+ ClearDeferredAuthorizerException();
612
+ SetIgnoreNextSQLiteError(false);
613
+ // Use c_str() explicitly to avoid potential ABI issues on Windows ARM
614
+ Napi::Error::New(env, deferred_msg.c_str()).ThrowAsJavaScriptException();
615
+ return env.Undefined();
616
+ }
471
617
  node::THROW_ERR_SQLITE_ERROR(env, e.what());
472
618
  return env.Undefined();
473
619
  }
@@ -492,15 +638,31 @@ Napi::Value DatabaseSync::Exec(const Napi::CallbackInfo &info) {
492
638
 
493
639
  std::string sql = info[0].As<Napi::String>().Utf8Value();
494
640
 
641
+ // Clear any stale deferred exception from a previous operation
642
+ ClearDeferredAuthorizerException();
643
+ SetIgnoreNextSQLiteError(false);
644
+
495
645
  char *error_msg = nullptr;
496
646
  int result =
497
647
  sqlite3_exec(connection(), sql.c_str(), nullptr, nullptr, &error_msg);
498
648
 
499
649
  if (result != SQLITE_OK) {
650
+ // Check for deferred authorizer exception first
651
+ if (HasDeferredAuthorizerException()) {
652
+ if (error_msg)
653
+ sqlite3_free(error_msg);
654
+ std::string deferred_msg = GetDeferredAuthorizerException();
655
+ ClearDeferredAuthorizerException();
656
+ SetIgnoreNextSQLiteError(false);
657
+ // Use c_str() explicitly to avoid potential ABI issues on Windows ARM
658
+ Napi::Error::New(env, deferred_msg.c_str()).ThrowAsJavaScriptException();
659
+ return env.Undefined();
660
+ }
500
661
  std::string error = error_msg ? error_msg : "Unknown SQLite error";
501
662
  if (error_msg)
502
663
  sqlite3_free(error_msg);
503
- node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
664
+ // Use enhanced error throwing with database handle
665
+ node::ThrowSqliteError(env, connection(), error);
504
666
  }
505
667
 
506
668
  return env.Undefined();
@@ -543,8 +705,10 @@ Napi::Value DatabaseSync::IsTransactionGetter(const Napi::CallbackInfo &info) {
543
705
  }
544
706
 
545
707
  void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
546
- location_ = config.location();
547
- read_only_ = config.get_read_only();
708
+ // Store configuration for later use by statements
709
+ config_ = std::move(config);
710
+ location_ = config_.location();
711
+ read_only_ = config_.get_read_only();
548
712
 
549
713
  int flags = SQLITE_OPEN_CREATE;
550
714
  if (read_only_) {
@@ -557,42 +721,64 @@ void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
557
721
 
558
722
  if (result != SQLITE_OK) {
559
723
  std::string error = sqlite3_errmsg(connection_);
724
+ // Capture error info before closing
725
+ SqliteException ex(connection_, result,
726
+ "Failed to open database: " + error);
560
727
  if (connection_) {
561
728
  sqlite3_close(connection_);
562
729
  connection_ = nullptr;
563
730
  }
564
- throw std::runtime_error("Failed to open database: " + error);
731
+ throw ex;
565
732
  }
566
733
 
567
734
  // Configure database
568
- if (config.get_enable_foreign_keys()) {
735
+ if (config_.get_enable_foreign_keys()) {
569
736
  sqlite3_exec(connection(), "PRAGMA foreign_keys = ON", nullptr, nullptr,
570
737
  nullptr);
571
738
  }
572
739
 
573
- if (config.get_timeout() > 0) {
574
- sqlite3_busy_timeout(connection(), config.get_timeout());
740
+ if (config_.get_timeout() > 0) {
741
+ sqlite3_busy_timeout(connection(), config_.get_timeout());
575
742
  }
576
743
 
577
744
  // Configure double-quoted string literals
578
- if (config.get_enable_dqs()) {
745
+ if (config_.get_enable_dqs()) {
579
746
  int dqs_enable = 1;
580
747
  result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DQS_DML,
581
748
  dqs_enable, nullptr);
582
749
  if (result != SQLITE_OK) {
583
750
  std::string error = sqlite3_errmsg(connection());
751
+ SqliteException ex(connection_, result,
752
+ "Failed to configure DQS_DML: " + error);
584
753
  sqlite3_close(connection_);
585
754
  connection_ = nullptr;
586
- throw std::runtime_error("Failed to configure DQS_DML: " + error);
755
+ throw ex;
587
756
  }
588
757
 
589
758
  result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DQS_DDL,
590
759
  dqs_enable, nullptr);
591
760
  if (result != SQLITE_OK) {
592
761
  std::string error = sqlite3_errmsg(connection());
762
+ SqliteException ex(connection_, result,
763
+ "Failed to configure DQS_DDL: " + error);
593
764
  sqlite3_close(connection_);
594
765
  connection_ = nullptr;
595
- throw std::runtime_error("Failed to configure DQS_DDL: " + error);
766
+ throw ex;
767
+ }
768
+ }
769
+
770
+ // Configure defensive mode
771
+ if (config_.get_enable_defensive()) {
772
+ int defensive_enabled;
773
+ result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DEFENSIVE, 1,
774
+ &defensive_enabled);
775
+ if (result != SQLITE_OK) {
776
+ std::string error = sqlite3_errmsg(connection());
777
+ SqliteException ex(connection_, result,
778
+ "Failed to configure DEFENSIVE: " + error);
779
+ sqlite3_close(connection_);
780
+ connection_ = nullptr;
781
+ throw ex;
596
782
  }
597
783
  }
598
784
  }
@@ -711,7 +897,7 @@ Napi::Value DatabaseSync::CustomFunction(const Napi::CallbackInfo &info) {
711
897
  delete user_data; // Clean up on failure
712
898
  std::string error = "Failed to create function: ";
713
899
  error += sqlite3_errmsg(connection());
714
- node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
900
+ ThrowErrSqliteErrorWithDb(env, this, error.c_str());
715
901
  }
716
902
 
717
903
  return env.Undefined();
@@ -954,6 +1140,36 @@ Napi::Value DatabaseSync::LoadExtension(const Napi::CallbackInfo &info) {
954
1140
  return env.Undefined();
955
1141
  }
956
1142
 
1143
+ Napi::Value DatabaseSync::EnableDefensive(const Napi::CallbackInfo &info) {
1144
+ Napi::Env env = info.Env();
1145
+
1146
+ if (!ValidateThread(env)) {
1147
+ return env.Undefined();
1148
+ }
1149
+
1150
+ if (!IsOpen()) {
1151
+ node::THROW_ERR_INVALID_STATE(env, "database is not open");
1152
+ return env.Undefined();
1153
+ }
1154
+
1155
+ if (info.Length() < 1 || !info[0].IsBoolean()) {
1156
+ node::THROW_ERR_INVALID_ARG_TYPE(
1157
+ env, "The \"active\" argument must be a boolean.");
1158
+ return env.Undefined();
1159
+ }
1160
+
1161
+ int enable = info[0].As<Napi::Boolean>().Value() ? 1 : 0;
1162
+ int defensive_enabled;
1163
+ int result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DEFENSIVE,
1164
+ enable, &defensive_enabled);
1165
+ if (result != SQLITE_OK) {
1166
+ node::ThrowEnhancedSqliteError(env, connection(), result,
1167
+ "Failed to set defensive mode");
1168
+ }
1169
+
1170
+ return env.Undefined();
1171
+ }
1172
+
957
1173
  Napi::Value DatabaseSync::CreateSession(const Napi::CallbackInfo &info) {
958
1174
  Napi::Env env = info.Env();
959
1175
 
@@ -1120,19 +1336,30 @@ Napi::Value DatabaseSync::ApplyChangeset(const Napi::CallbackInfo &info) {
1120
1336
  callbacks.conflictCallback = [env,
1121
1337
  conflictFunc](int conflictType) -> int {
1122
1338
  Napi::HandleScope scope(env);
1123
- Napi::Value result =
1124
- conflictFunc.Call({Napi::Number::New(env, conflictType)});
1339
+ try {
1340
+ Napi::Value result =
1341
+ conflictFunc.Call({Napi::Number::New(env, conflictType)});
1342
+
1343
+ if (env.IsExceptionPending()) {
1344
+ // Clear the exception to prevent propagation
1345
+ env.GetAndClearPendingException();
1346
+ // If callback threw, abort the changeset apply
1347
+ return SQLITE_CHANGESET_ABORT;
1348
+ }
1125
1349
 
1126
- if (env.IsExceptionPending()) {
1127
- // If callback threw, abort the changeset apply
1128
- return SQLITE_CHANGESET_ABORT;
1129
- }
1350
+ if (!result.IsNumber()) {
1351
+ // If the callback returns a non-numeric value, treat it as ABORT
1352
+ return SQLITE_CHANGESET_ABORT;
1353
+ }
1130
1354
 
1131
- if (!result.IsNumber()) {
1132
- return -1; // Invalid value
1355
+ return result.As<Napi::Number>().Int32Value();
1356
+ } catch (...) {
1357
+ // Catch any C++ exceptions
1358
+ if (env.IsExceptionPending()) {
1359
+ env.GetAndClearPendingException();
1360
+ }
1361
+ return SQLITE_CHANGESET_ABORT;
1133
1362
  }
1134
-
1135
- return result.As<Napi::Number>().Int32Value();
1136
1363
  };
1137
1364
  }
1138
1365
  }
@@ -1150,15 +1377,25 @@ Napi::Value DatabaseSync::ApplyChangeset(const Napi::CallbackInfo &info) {
1150
1377
  callbacks.filterCallback = [env,
1151
1378
  filterFunc](std::string tableName) -> bool {
1152
1379
  Napi::HandleScope scope(env);
1153
- Napi::Value result =
1154
- filterFunc.Call({Napi::String::New(env, tableName)});
1380
+ try {
1381
+ Napi::Value result =
1382
+ filterFunc.Call({Napi::String::New(env, tableName)});
1383
+
1384
+ if (env.IsExceptionPending()) {
1385
+ // Clear the exception to prevent propagation
1386
+ env.GetAndClearPendingException();
1387
+ // If callback threw, exclude the table
1388
+ return false;
1389
+ }
1155
1390
 
1156
- if (env.IsExceptionPending()) {
1157
- // If callback threw, exclude the table
1391
+ return result.ToBoolean().Value();
1392
+ } catch (...) {
1393
+ // Catch any C++ exceptions
1394
+ if (env.IsExceptionPending()) {
1395
+ env.GetAndClearPendingException();
1396
+ }
1158
1397
  return false;
1159
1398
  }
1160
-
1161
- return result.ToBoolean().Value();
1162
1399
  };
1163
1400
  }
1164
1401
  }
@@ -1195,13 +1432,18 @@ Napi::Object StatementSync::Init(Napi::Env env, Napi::Object exports) {
1195
1432
  InstanceMethod("all", &StatementSync::All),
1196
1433
  InstanceMethod("iterate", &StatementSync::Iterate),
1197
1434
  InstanceMethod("finalize", &StatementSync::FinalizeStatement),
1435
+ InstanceMethod("dispose", &StatementSync::Dispose),
1198
1436
  InstanceMethod("setReadBigInts", &StatementSync::SetReadBigInts),
1199
1437
  InstanceMethod("setReturnArrays", &StatementSync::SetReturnArrays),
1200
1438
  InstanceMethod("setAllowBareNamedParameters",
1201
1439
  &StatementSync::SetAllowBareNamedParameters),
1440
+ InstanceMethod("setAllowUnknownNamedParameters",
1441
+ &StatementSync::SetAllowUnknownNamedParameters),
1202
1442
  InstanceMethod("columns", &StatementSync::Columns),
1203
1443
  InstanceAccessor("sourceSQL", &StatementSync::SourceSQLGetter, nullptr),
1204
1444
  InstanceAccessor("expandedSQL", &StatementSync::ExpandedSQLGetter,
1445
+ nullptr),
1446
+ InstanceAccessor("finalized", &StatementSync::FinalizedGetter,
1205
1447
  nullptr)});
1206
1448
 
1207
1449
  // Store constructor in per-instance addon data instead of static variable
@@ -1211,6 +1453,21 @@ Napi::Object StatementSync::Init(Napi::Env env, Napi::Object exports) {
1211
1453
  Napi::Reference<Napi::Function>::New(func);
1212
1454
  }
1213
1455
 
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
+
1214
1471
  exports.Set("StatementSync", func);
1215
1472
  return exports;
1216
1473
  }
@@ -1228,16 +1485,50 @@ void StatementSync::InitStatement(DatabaseSync *database,
1228
1485
  }
1229
1486
 
1230
1487
  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());
1231
1493
  source_sql_ = sql;
1232
1494
 
1495
+ // Apply database-level defaults
1496
+ use_big_ints_ = database->config_.get_read_big_ints();
1497
+ return_arrays_ = database->config_.get_return_arrays();
1498
+ allow_bare_named_params_ = database->config_.get_allow_bare_named_params();
1499
+ allow_unknown_named_params_ =
1500
+ database->config_.get_allow_unknown_named_params();
1501
+
1233
1502
  // Prepare the statement
1234
1503
  const char *tail = nullptr;
1235
1504
  int result = sqlite3_prepare_v2(database->connection(), sql.c_str(), -1,
1236
1505
  &statement_, &tail);
1237
1506
 
1238
1507
  if (result != SQLITE_OK) {
1239
- std::string error = sqlite3_errmsg(database->connection());
1240
- throw std::runtime_error("Failed to prepare statement: " + error);
1508
+ // Handle deferred authorizer exceptions:
1509
+ //
1510
+ // When an authorizer callback throws a JavaScript exception, we use a
1511
+ // "marker" exception pattern to safely propagate the error:
1512
+ //
1513
+ // 1. On Windows (MSVC), std::exception::what() can sometimes return an
1514
+ // empty string, causing message loss.
1515
+ //
1516
+ // 2. By storing the message in the DatabaseSync instance, the caller can
1517
+ // retrieve it and throw a proper JavaScript exception with the original
1518
+ // text.
1519
+ //
1520
+ // 3. This matches Node.js's behavior where JavaScript exceptions from
1521
+ // authorizer callbacks propagate correctly to the caller.
1522
+ if (database->HasDeferredAuthorizerException()) {
1523
+ // Throw a marker exception - the actual message is stored in the database
1524
+ // object and will be retrieved by the caller.
1525
+ throw std::runtime_error("");
1526
+ }
1527
+ std::string error = "Failed to prepare statement: ";
1528
+ error += sqlite3_errmsg(database->connection());
1529
+ // Use SqliteException to capture error info - avoids Windows ARM ABI issues
1530
+ // with std::runtime_error::what() returning corrupted strings
1531
+ throw SqliteException(database->connection(), result, error);
1241
1532
  }
1242
1533
  }
1243
1534
 
@@ -1245,6 +1536,10 @@ StatementSync::~StatementSync() {
1245
1536
  if (statement_ && !finalized_) {
1246
1537
  sqlite3_finalize(statement_);
1247
1538
  }
1539
+ // Release the strong reference to the database object
1540
+ if (!database_ref_.IsEmpty()) {
1541
+ database_ref_.Reset();
1542
+ }
1248
1543
  }
1249
1544
 
1250
1545
  Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
@@ -1273,11 +1568,23 @@ Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
1273
1568
  Reset();
1274
1569
  BindParameters(info);
1275
1570
 
1276
- int result = sqlite3_step(statement_);
1571
+ // Check if BindParameters set a pending exception
1572
+ if (env.IsExceptionPending()) {
1573
+ return env.Undefined();
1574
+ }
1277
1575
 
1278
- if (result != SQLITE_DONE && result != SQLITE_ROW) {
1576
+ // Execute the statement
1577
+ sqlite3_step(statement_);
1578
+ // Reset immediately after step to ensure sqlite3_changes() returns
1579
+ // correct value. This fixes an issue where RETURNING queries would
1580
+ // report changes: 0 on the first call.
1581
+ // See: https://github.com/nodejs/node/issues/57344
1582
+ int result = sqlite3_reset(statement_);
1583
+
1584
+ if (result != SQLITE_OK) {
1279
1585
  std::string error = sqlite3_errmsg(database_->connection());
1280
- node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
1586
+ ThrowEnhancedSqliteErrorWithDB(env, database_, database_->connection(),
1587
+ result, error);
1281
1588
  return env.Undefined();
1282
1589
  }
1283
1590
 
@@ -1300,7 +1607,7 @@ Napi::Value StatementSync::Run(const Napi::CallbackInfo &info) {
1300
1607
 
1301
1608
  return result_obj;
1302
1609
  } catch (const std::exception &e) {
1303
- node::THROW_ERR_SQLITE_ERROR(env, e.what());
1610
+ ThrowErrSqliteErrorWithDb(env, database_, e.what());
1304
1611
  return env.Undefined();
1305
1612
  }
1306
1613
  }
@@ -1331,6 +1638,11 @@ Napi::Value StatementSync::Get(const Napi::CallbackInfo &info) {
1331
1638
  Reset();
1332
1639
  BindParameters(info);
1333
1640
 
1641
+ // Check if BindParameters set a pending exception
1642
+ if (env.IsExceptionPending()) {
1643
+ return env.Undefined();
1644
+ }
1645
+
1334
1646
  int result = sqlite3_step(statement_);
1335
1647
 
1336
1648
  if (result == SQLITE_ROW) {
@@ -1339,11 +1651,12 @@ Napi::Value StatementSync::Get(const Napi::CallbackInfo &info) {
1339
1651
  return env.Undefined();
1340
1652
  } else {
1341
1653
  std::string error = sqlite3_errmsg(database_->connection());
1342
- node::THROW_ERR_SQLITE_ERROR(env, error.c_str());
1654
+ ThrowEnhancedSqliteErrorWithDB(env, database_, database_->connection(),
1655
+ result, error);
1343
1656
  return env.Undefined();
1344
1657
  }
1345
1658
  } catch (const std::exception &e) {
1346
- node::THROW_ERR_SQLITE_ERROR(env, e.what());
1659
+ ThrowErrSqliteErrorWithDb(env, database_, e.what());
1347
1660
  return env.Undefined();
1348
1661
  }
1349
1662
  }
@@ -1370,6 +1683,11 @@ Napi::Value StatementSync::All(const Napi::CallbackInfo &info) {
1370
1683
  Reset();
1371
1684
  BindParameters(info);
1372
1685
 
1686
+ // Check if BindParameters set a pending exception
1687
+ if (env.IsExceptionPending()) {
1688
+ return env.Undefined();
1689
+ }
1690
+
1373
1691
  Napi::Array results = Napi::Array::New(env);
1374
1692
  uint32_t index = 0;
1375
1693
 
@@ -1422,6 +1740,11 @@ Napi::Value StatementSync::Iterate(const Napi::CallbackInfo &info) {
1422
1740
  // Bind parameters if provided
1423
1741
  BindParameters(info, 0);
1424
1742
 
1743
+ // Check if BindParameters set a pending exception
1744
+ if (info.Env().IsExceptionPending()) {
1745
+ return info.Env().Undefined();
1746
+ }
1747
+
1425
1748
  // Create and return iterator
1426
1749
  return StatementSyncIterator::Create(info.Env(), this);
1427
1750
  }
@@ -1437,6 +1760,21 @@ Napi::Value StatementSync::FinalizeStatement(const Napi::CallbackInfo &info) {
1437
1760
  return info.Env().Undefined();
1438
1761
  }
1439
1762
 
1763
+ Napi::Value StatementSync::Dispose(const Napi::CallbackInfo &info) {
1764
+ // Try to finalize, but ignore errors during disposal (matches Node.js v25
1765
+ // behavior)
1766
+ try {
1767
+ if (statement_ && !finalized_) {
1768
+ sqlite3_finalize(statement_);
1769
+ statement_ = nullptr;
1770
+ finalized_ = true;
1771
+ }
1772
+ } catch (...) {
1773
+ // Ignore errors during disposal
1774
+ }
1775
+ return info.Env().Undefined();
1776
+ }
1777
+
1440
1778
  Napi::Value StatementSync::SourceSQLGetter(const Napi::CallbackInfo &info) {
1441
1779
  return Napi::String::New(info.Env(), source_sql_);
1442
1780
  }
@@ -1463,6 +1801,10 @@ Napi::Value StatementSync::ExpandedSQLGetter(const Napi::CallbackInfo &info) {
1463
1801
  return info.Env().Undefined();
1464
1802
  }
1465
1803
 
1804
+ Napi::Value StatementSync::FinalizedGetter(const Napi::CallbackInfo &info) {
1805
+ return Napi::Boolean::New(info.Env(), finalized_);
1806
+ }
1807
+
1466
1808
  Napi::Value StatementSync::SetReadBigInts(const Napi::CallbackInfo &info) {
1467
1809
  Napi::Env env = info.Env();
1468
1810
 
@@ -1533,6 +1875,30 @@ StatementSync::SetAllowBareNamedParameters(const Napi::CallbackInfo &info) {
1533
1875
  return env.Undefined();
1534
1876
  }
1535
1877
 
1878
+ Napi::Value
1879
+ StatementSync::SetAllowUnknownNamedParameters(const Napi::CallbackInfo &info) {
1880
+ Napi::Env env = info.Env();
1881
+
1882
+ if (finalized_) {
1883
+ node::THROW_ERR_INVALID_STATE(env, "The statement has been finalized");
1884
+ return env.Undefined();
1885
+ }
1886
+
1887
+ if (!database_ || !database_->IsOpen()) {
1888
+ node::THROW_ERR_INVALID_STATE(env, "Database connection is closed");
1889
+ return env.Undefined();
1890
+ }
1891
+
1892
+ if (info.Length() < 1 || !info[0].IsBoolean()) {
1893
+ node::THROW_ERR_INVALID_ARG_TYPE(
1894
+ env, "The \"enabled\" argument must be a boolean.");
1895
+ return env.Undefined();
1896
+ }
1897
+
1898
+ allow_unknown_named_params_ = info[0].As<Napi::Boolean>().Value();
1899
+ return env.Undefined();
1900
+ }
1901
+
1536
1902
  Napi::Value StatementSync::Columns(const Napi::CallbackInfo &info) {
1537
1903
  Napi::Env env = info.Env();
1538
1904
 
@@ -1689,6 +2055,17 @@ void StatementSync::BindParameters(const Napi::CallbackInfo &info,
1689
2055
  node::THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str());
1690
2056
  return;
1691
2057
  }
2058
+ } else {
2059
+ // Unknown named parameter
2060
+ if (allow_unknown_named_params_) {
2061
+ // Skip unknown parameters when allowed (matches Node.js v25 behavior)
2062
+ continue;
2063
+ } else {
2064
+ // Throw error when not allowed (default behavior)
2065
+ std::string msg = "Unknown named parameter '" + key_str + "'";
2066
+ node::THROW_ERR_INVALID_ARG_VALUE(env, msg.c_str());
2067
+ return;
2068
+ }
1692
2069
  }
1693
2070
  }
1694
2071
  } else {
@@ -1733,7 +2110,9 @@ void StatementSync::BindSingleParameter(int param_index, Napi::Value param) {
1733
2110
  }
1734
2111
  } else if (param.IsNumber()) {
1735
2112
  double val = param.As<Napi::Number>().DoubleValue();
1736
- if (val == std::floor(val) && val >= INT32_MIN && val <= INT32_MAX) {
2113
+ if (std::abs(val - std::floor(val)) <
2114
+ std::numeric_limits<double>::epsilon() &&
2115
+ val >= INT32_MIN && val <= INT32_MAX) {
1737
2116
  sqlite3_bind_int(statement_, param_index,
1738
2117
  param.As<Napi::Number>().Int32Value());
1739
2118
  } else {
@@ -1747,22 +2126,55 @@ void StatementSync::BindSingleParameter(int param_index, Napi::Value param) {
1747
2126
  } else if (param.IsBoolean()) {
1748
2127
  sqlite3_bind_int(statement_, param_index,
1749
2128
  param.As<Napi::Boolean>().Value() ? 1 : 0);
2129
+ } else if (param.IsDataView()) {
2130
+ // IMPORTANT: Check DataView BEFORE IsBuffer() because N-API's IsBuffer()
2131
+ // returns true for ALL ArrayBufferViews (including DataView), but
2132
+ // Buffer::As() doesn't work correctly for DataView (returns length=0).
2133
+ Napi::DataView dataView = param.As<Napi::DataView>();
2134
+ Napi::ArrayBuffer arrayBuffer = dataView.ArrayBuffer();
2135
+ size_t byteOffset = dataView.ByteOffset();
2136
+ size_t byteLength = dataView.ByteLength();
2137
+
2138
+ if (arrayBuffer.Data() != nullptr && byteLength > 0) {
2139
+ const uint8_t *data =
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);
2145
+ }
1750
2146
  } else if (param.IsBuffer()) {
2147
+ // Handles Buffer and TypedArray (both are ArrayBufferViews that work
2148
+ // correctly with Buffer cast - Buffer::Data() handles byte offsets
2149
+ // internally)
1751
2150
  Napi::Buffer<uint8_t> buffer = param.As<Napi::Buffer<uint8_t>>();
1752
2151
  sqlite3_bind_blob(statement_, param_index, buffer.Data(),
1753
- static_cast<int>(buffer.Length()), SQLITE_TRANSIENT);
2152
+ SafeCastToInt(buffer.Length()), SQLITE_TRANSIENT);
1754
2153
  } else if (param.IsFunction()) {
1755
2154
  // Functions cannot be stored in SQLite - bind as NULL
1756
2155
  sqlite3_bind_null(statement_, param_index);
2156
+ } else if (param.IsArrayBuffer()) {
2157
+ // Handle ArrayBuffer as binary data
2158
+ Napi::ArrayBuffer arrayBuffer = param.As<Napi::ArrayBuffer>();
2159
+ if (!arrayBuffer.IsEmpty() && arrayBuffer.Data() != nullptr) {
2160
+ sqlite3_bind_blob(statement_, param_index, arrayBuffer.Data(),
2161
+ SafeCastToInt(arrayBuffer.ByteLength()),
2162
+ SQLITE_TRANSIENT);
2163
+ } else {
2164
+ sqlite3_bind_null(statement_, param_index);
2165
+ }
1757
2166
  } 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);
2167
+ // Objects and arrays cannot be bound to SQLite parameters (same as
2168
+ // Node.js behavior). Note: DataView, Buffer, TypedArray, and ArrayBuffer
2169
+ // are handled above and don't reach this branch.
2170
+ throw Napi::Error::New(
2171
+ Env(), "Provided value cannot be bound to SQLite parameter " +
2172
+ std::to_string(param_index) + ".");
1763
2173
  } else {
1764
- // For any other type, bind as NULL
1765
- sqlite3_bind_null(statement_, param_index);
2174
+ // For any other type, throw error like Node.js does
2175
+ throw Napi::Error::New(
2176
+ Env(), "Provided value cannot be bound to SQLite parameter " +
2177
+ std::to_string(param_index) + ".");
1766
2178
  }
1767
2179
  } catch (const Napi::Error &e) {
1768
2180
  // Re-throw Napi errors
@@ -1820,14 +2232,25 @@ Napi::Value StatementSync::CreateResult() {
1820
2232
  break;
1821
2233
  case SQLITE_TEXT: {
1822
2234
  const unsigned char *text = sqlite3_column_text(statement_, i);
1823
- value = Napi::String::New(env, reinterpret_cast<const char *>(text));
2235
+ // sqlite3_column_text() can return NULL on OOM or encoding errors
2236
+ if (!text) {
2237
+ value = Napi::String::New(env, "");
2238
+ } else {
2239
+ value = Napi::String::New(env, reinterpret_cast<const char *>(text));
2240
+ }
1824
2241
  break;
1825
2242
  }
1826
2243
  case SQLITE_BLOB: {
1827
2244
  const void *blob_data = sqlite3_column_blob(statement_, i);
1828
2245
  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);
2246
+ // sqlite3_column_blob() can return NULL for zero-length BLOBs or on OOM
2247
+ if (!blob_data || blob_size == 0) {
2248
+ // Handle empty/NULL blob - create empty buffer
2249
+ value = Napi::Buffer<uint8_t>::New(env, 0);
2250
+ } else {
2251
+ value = Napi::Buffer<uint8_t>::Copy(
2252
+ env, static_cast<const uint8_t *>(blob_data), blob_size);
2253
+ }
1831
2254
  break;
1832
2255
  }
1833
2256
  default:
@@ -1872,14 +2295,25 @@ Napi::Value StatementSync::CreateResult() {
1872
2295
  break;
1873
2296
  case SQLITE_TEXT: {
1874
2297
  const unsigned char *text = sqlite3_column_text(statement_, i);
1875
- value = Napi::String::New(env, reinterpret_cast<const char *>(text));
2298
+ // sqlite3_column_text() can return NULL on OOM or encoding errors
2299
+ if (!text) {
2300
+ value = Napi::String::New(env, "");
2301
+ } else {
2302
+ value = Napi::String::New(env, reinterpret_cast<const char *>(text));
2303
+ }
1876
2304
  break;
1877
2305
  }
1878
2306
  case SQLITE_BLOB: {
1879
2307
  const void *blob_data = sqlite3_column_blob(statement_, i);
1880
2308
  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);
2309
+ // sqlite3_column_blob() can return NULL for zero-length BLOBs or on OOM
2310
+ if (!blob_data || blob_size == 0) {
2311
+ // Handle empty/NULL blob - create empty buffer
2312
+ value = Napi::Buffer<uint8_t>::New(env, 0);
2313
+ } else {
2314
+ value = Napi::Buffer<uint8_t>::Copy(
2315
+ env, static_cast<const uint8_t *>(blob_data), blob_size);
2316
+ }
1883
2317
  break;
1884
2318
  }
1885
2319
  default:
@@ -2037,7 +2471,8 @@ Napi::Object Session::Init(Napi::Env env, Napi::Object exports) {
2037
2471
  DefineClass(env, "Session",
2038
2472
  {InstanceMethod("changeset", &Session::Changeset),
2039
2473
  InstanceMethod("patchset", &Session::Patchset),
2040
- InstanceMethod("close", &Session::Close)});
2474
+ InstanceMethod("close", &Session::Close),
2475
+ InstanceMethod("dispose", &Session::Dispose)});
2041
2476
 
2042
2477
  // Store constructor in per-instance addon data instead of static variable
2043
2478
  AddonData *addon_data = GetAddonData(env);
@@ -2045,6 +2480,21 @@ Napi::Object Session::Init(Napi::Env env, Napi::Object exports) {
2045
2480
  addon_data->sessionConstructor = Napi::Reference<Napi::Function>::New(func);
2046
2481
  }
2047
2482
 
2483
+ // Add Symbol.dispose to the prototype (Node.js v25+ compatibility)
2484
+ Napi::Value symbolDispose =
2485
+ env.Global().Get("Symbol").As<Napi::Object>().Get("dispose");
2486
+ if (!symbolDispose.IsUndefined()) {
2487
+ func.Get("prototype")
2488
+ .As<Napi::Object>()
2489
+ .Set(symbolDispose,
2490
+ Napi::Function::New(
2491
+ env, [](const Napi::CallbackInfo &info) -> Napi::Value {
2492
+ Session *session =
2493
+ Session::Unwrap(info.This().As<Napi::Object>());
2494
+ return session->Dispose(info);
2495
+ }));
2496
+ }
2497
+
2048
2498
  exports.Set("Session", func);
2049
2499
  return exports;
2050
2500
  }
@@ -2154,6 +2604,19 @@ Napi::Value Session::Close(const Napi::CallbackInfo &info) {
2154
2604
  return env.Undefined();
2155
2605
  }
2156
2606
 
2607
+ Napi::Value Session::Dispose(const Napi::CallbackInfo &info) {
2608
+ // Try to close, but ignore errors during disposal (matches Node.js v25
2609
+ // behavior)
2610
+ try {
2611
+ if (session_ != nullptr) {
2612
+ Delete();
2613
+ }
2614
+ } catch (...) {
2615
+ // Ignore errors during disposal
2616
+ }
2617
+ return info.Env().Undefined();
2618
+ }
2619
+
2157
2620
  // Static members for tracking active jobs
2158
2621
  std::atomic<int> BackupJob::active_jobs_(0);
2159
2622
  std::mutex BackupJob::active_jobs_mutex_;
@@ -2161,17 +2624,17 @@ std::set<BackupJob *> BackupJob::active_job_instances_;
2161
2624
 
2162
2625
  // BackupJob Implementation
2163
2626
  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,
2627
+ std::string destination_path, std::string source_db,
2628
+ std::string dest_db, int pages,
2629
+ Napi::Function progress_func,
2167
2630
  Napi::Promise::Deferred deferred)
2168
2631
  : Napi::AsyncProgressWorker<BackupProgress>(
2169
2632
  !progress_func.IsEmpty() && !progress_func.IsUndefined()
2170
2633
  ? progress_func
2171
2634
  : 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) {
2635
+ source_(source), destination_path_(std::move(destination_path)),
2636
+ source_db_(std::move(source_db)), dest_db_(std::move(dest_db)),
2637
+ pages_(pages), deferred_(deferred) {
2175
2638
  if (!progress_func.IsEmpty() && !progress_func.IsUndefined()) {
2176
2639
  progress_func_ = Napi::Reference<Napi::Function>::New(progress_func);
2177
2640
  }
@@ -2284,15 +2747,27 @@ void BackupJob::OnError(const Napi::Error &error) {
2284
2747
  // This runs on the main thread if Execute encounters an error
2285
2748
  Napi::HandleScope scope(Env());
2286
2749
 
2287
- // Cleanup SQLite resources
2750
+ // Save error info BEFORE cleanup nulls the pointers
2751
+ int saved_status = backup_status_;
2752
+ std::string saved_errmsg;
2753
+ if (dest_) {
2754
+ saved_errmsg = sqlite3_errmsg(dest_);
2755
+ // Capture any final error code from dest
2756
+ int dest_err = sqlite3_errcode(dest_);
2757
+ if (dest_err != SQLITE_OK && saved_status == SQLITE_OK) {
2758
+ saved_status = dest_err;
2759
+ }
2760
+ }
2761
+
2762
+ // Now safe to cleanup
2288
2763
  Cleanup();
2289
2764
 
2290
- // Create a more detailed error if we have SQLite error info
2291
- if (dest_ && backup_status_ != SQLITE_OK) {
2765
+ // Use saved values for error details (matching node:sqlite property names)
2766
+ if (saved_status != SQLITE_OK && saved_status != SQLITE_DONE) {
2292
2767
  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_));
2768
+ detailed_error.Set("errcode", Napi::Number::New(Env(), saved_status));
2769
+ detailed_error.Set("errstr",
2770
+ Napi::String::New(Env(), sqlite3_errstr(saved_status)));
2296
2771
  deferred_.Reject(detailed_error.Value());
2297
2772
  } else {
2298
2773
  deferred_.Reject(error.Value());
@@ -2408,8 +2883,9 @@ Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
2408
2883
  }
2409
2884
 
2410
2885
  // Create and schedule backup job
2411
- BackupJob *job = new BackupJob(env, this, destination_path.value(), source_db,
2412
- target_db, rate, progress_func, deferred);
2886
+ BackupJob *job = new BackupJob(env, this, std::move(destination_path).value(),
2887
+ std::move(source_db), std::move(target_db),
2888
+ rate, progress_func, deferred);
2413
2889
 
2414
2890
  // Queue the async work - AsyncWorker will delete itself when complete
2415
2891
  job->Queue();
@@ -2417,6 +2893,138 @@ Napi::Value DatabaseSync::Backup(const Napi::CallbackInfo &info) {
2417
2893
  return deferred.Promise();
2418
2894
  }
2419
2895
 
2896
+ // Helper function to convert nullable C string to JavaScript value
2897
+ static Napi::Value NullableSQLiteStringToValue(Napi::Env env, const char *str) {
2898
+ if (str == nullptr) {
2899
+ return env.Null();
2900
+ }
2901
+ return Napi::String::New(env, str);
2902
+ }
2903
+
2904
+ // DatabaseSync::SetAuthorizer implementation
2905
+ Napi::Value DatabaseSync::SetAuthorizer(const Napi::CallbackInfo &info) {
2906
+ Napi::Env env = info.Env();
2907
+
2908
+ if (!IsOpen()) {
2909
+ node::THROW_ERR_INVALID_STATE(env, "database is not open");
2910
+ return env.Undefined();
2911
+ }
2912
+
2913
+ // Handle null to clear the authorizer
2914
+ if (info.Length() > 0 && info[0].IsNull()) {
2915
+ sqlite3_set_authorizer(connection_, nullptr, nullptr);
2916
+ authorizer_callback_.reset();
2917
+ return env.Undefined();
2918
+ }
2919
+
2920
+ // Validate callback argument
2921
+ if (info.Length() < 1 || !info[0].IsFunction()) {
2922
+ node::THROW_ERR_INVALID_ARG_TYPE(
2923
+ env, "The \"callback\" argument must be a function or null.");
2924
+ return env.Undefined();
2925
+ }
2926
+
2927
+ // Store the JavaScript callback
2928
+ Napi::Function fn = info[0].As<Napi::Function>();
2929
+ authorizer_callback_ =
2930
+ std::make_unique<Napi::FunctionReference>(Napi::Persistent(fn));
2931
+
2932
+ // Set the SQLite authorizer with our static callback
2933
+ int r = sqlite3_set_authorizer(connection_, AuthorizerCallback, this);
2934
+
2935
+ if (r != SQLITE_OK) {
2936
+ authorizer_callback_.reset();
2937
+ ThrowEnhancedSqliteErrorWithDB(env, this, connection_, r,
2938
+ "Failed to set authorizer");
2939
+ return env.Undefined();
2940
+ }
2941
+
2942
+ return env.Undefined();
2943
+ }
2944
+
2945
+ // Static callback for SQLite authorization
2946
+ int DatabaseSync::AuthorizerCallback(void *user_data, int action_code,
2947
+ const char *param1, const char *param2,
2948
+ const char *param3, const char *param4) {
2949
+ DatabaseSync *db = static_cast<DatabaseSync *>(user_data);
2950
+
2951
+ // If no callback is set, allow everything
2952
+ if (!db->authorizer_callback_ || db->authorizer_callback_->IsEmpty()) {
2953
+ return SQLITE_OK;
2954
+ }
2955
+
2956
+ Napi::Env env(db->env_);
2957
+ Napi::HandleScope scope(env);
2958
+
2959
+ try {
2960
+ // Convert SQLite authorizer parameters to JavaScript values
2961
+ std::vector<napi_value> args;
2962
+ args.push_back(Napi::Number::New(env, action_code));
2963
+ args.push_back(NullableSQLiteStringToValue(env, param1));
2964
+ args.push_back(NullableSQLiteStringToValue(env, param2));
2965
+ args.push_back(NullableSQLiteStringToValue(env, param3));
2966
+ args.push_back(NullableSQLiteStringToValue(env, param4));
2967
+
2968
+ // Call the JavaScript callback
2969
+ Napi::Value result = db->authorizer_callback_->Call(env.Undefined(), args);
2970
+
2971
+ // Handle JavaScript exceptions - must clear before returning to SQLite
2972
+ if (env.IsExceptionPending()) {
2973
+ Napi::Error error = env.GetAndClearPendingException();
2974
+ db->SetDeferredAuthorizerException(error.Message());
2975
+ db->SetIgnoreNextSQLiteError(true);
2976
+ return SQLITE_DENY;
2977
+ }
2978
+
2979
+ // Check if result is an integer - don't throw in callback context
2980
+ if (!result.IsNumber()) {
2981
+ db->SetDeferredAuthorizerException(
2982
+ "Authorizer callback must return an integer authorization code");
2983
+ db->SetIgnoreNextSQLiteError(true);
2984
+ return SQLITE_DENY;
2985
+ }
2986
+
2987
+ int32_t int_result = result.As<Napi::Number>().Int32Value();
2988
+
2989
+ // Validate the return code - don't throw in callback context
2990
+ if (int_result != SQLITE_OK && int_result != SQLITE_DENY &&
2991
+ int_result != SQLITE_IGNORE) {
2992
+ db->SetDeferredAuthorizerException(
2993
+ "Authorizer callback returned an invalid authorization code");
2994
+ db->SetIgnoreNextSQLiteError(true);
2995
+ return SQLITE_DENY;
2996
+ }
2997
+
2998
+ return int_result;
2999
+ } catch (const Napi::Error &e) {
3000
+ // JavaScript exception occurred - clear any pending exception and store
3001
+ if (env.IsExceptionPending()) {
3002
+ Napi::Error error = env.GetAndClearPendingException();
3003
+ db->SetDeferredAuthorizerException(error.Message());
3004
+ } else {
3005
+ db->SetDeferredAuthorizerException(e.Message());
3006
+ }
3007
+ db->SetIgnoreNextSQLiteError(true);
3008
+ return SQLITE_DENY;
3009
+ } catch (const std::exception &e) {
3010
+ // C++ exception - clear any pending JS exception and store message
3011
+ if (env.IsExceptionPending()) {
3012
+ env.GetAndClearPendingException();
3013
+ }
3014
+ db->SetDeferredAuthorizerException(e.what());
3015
+ db->SetIgnoreNextSQLiteError(true);
3016
+ return SQLITE_DENY;
3017
+ } catch (...) {
3018
+ // Unknown error - clear any pending JS exception and deny
3019
+ if (env.IsExceptionPending()) {
3020
+ env.GetAndClearPendingException();
3021
+ }
3022
+ db->SetDeferredAuthorizerException("Unknown error in authorizer callback");
3023
+ db->SetIgnoreNextSQLiteError(true);
3024
+ return SQLITE_DENY;
3025
+ }
3026
+ }
3027
+
2420
3028
  // Thread validation implementations
2421
3029
  bool DatabaseSync::ValidateThread(Napi::Env env) const {
2422
3030
  if (std::this_thread::get_id() != creation_thread_) {
@@ -2436,5 +3044,4 @@ bool StatementSync::ValidateThread(Napi::Env env) const {
2436
3044
  return true;
2437
3045
  }
2438
3046
 
2439
- } // namespace sqlite
2440
- } // namespace photostructure
3047
+ } // namespace photostructure::sqlite