@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.
- package/CHANGELOG.md +36 -2
- package/README.md +45 -484
- package/SECURITY.md +27 -84
- package/binding.gyp +69 -22
- package/dist/index.cjs +185 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +552 -100
- package/dist/index.d.mts +552 -100
- package/dist/index.d.ts +552 -100
- package/dist/index.mjs +183 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +51 -41
- package/prebuilds/darwin-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/darwin-x64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-arm64/@photostructure+sqlite.musl.node +0 -0
- package/prebuilds/linux-x64/@photostructure+sqlite.glibc.node +0 -0
- package/prebuilds/linux-x64/@photostructure+sqlite.musl.node +0 -0
- package/prebuilds/test_extension.so +0 -0
- package/prebuilds/win32-x64/@photostructure+sqlite.glibc.node +0 -0
- package/src/aggregate_function.cpp +503 -235
- package/src/aggregate_function.h +57 -42
- package/src/binding.cpp +117 -14
- package/src/dirname.ts +1 -1
- package/src/index.ts +122 -332
- package/src/lru-cache.ts +84 -0
- package/src/shims/env-inl.h +6 -15
- package/src/shims/node_errors.h +4 -0
- package/src/shims/sqlite_errors.h +162 -0
- package/src/shims/util.h +29 -4
- package/src/sql-tag-store.ts +140 -0
- package/src/sqlite_exception.h +49 -0
- package/src/sqlite_impl.cpp +711 -127
- package/src/sqlite_impl.h +84 -6
- package/src/{stack_path.ts → stack-path.ts} +7 -1
- package/src/types/aggregate-options.ts +22 -0
- package/src/types/changeset-apply-options.ts +18 -0
- package/src/types/database-sync-instance.ts +203 -0
- package/src/types/database-sync-options.ts +69 -0
- package/src/types/session-options.ts +10 -0
- package/src/types/sql-tag-store-instance.ts +51 -0
- package/src/types/sqlite-authorization-actions.ts +77 -0
- package/src/types/sqlite-authorization-results.ts +15 -0
- package/src/types/sqlite-changeset-conflict-types.ts +19 -0
- package/src/types/sqlite-changeset-resolution.ts +15 -0
- package/src/types/sqlite-open-flags.ts +50 -0
- package/src/types/statement-sync-instance.ts +73 -0
- package/src/types/user-functions-options.ts +14 -0
- package/src/upstream/node_sqlite.cc +960 -259
- package/src/upstream/node_sqlite.h +127 -2
- package/src/upstream/sqlite.js +1 -14
- package/src/upstream/sqlite3.c +4510 -1411
- package/src/upstream/sqlite3.h +390 -195
- package/src/upstream/sqlite3ext.h +7 -0
- package/src/user_function.cpp +88 -36
- package/src/user_function.h +2 -1
package/src/sqlite_impl.cpp
CHANGED
|
@@ -4,13 +4,57 @@
|
|
|
4
4
|
#include <cctype>
|
|
5
5
|
#include <climits>
|
|
6
6
|
#include <cmath>
|
|
7
|
-
#include <
|
|
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
|
-
|
|
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
|
-
|
|
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, "
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
547
|
-
|
|
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
|
|
712
|
+
throw ex;
|
|
565
713
|
}
|
|
566
714
|
|
|
567
715
|
// Configure database
|
|
568
|
-
if (
|
|
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 (
|
|
574
|
-
sqlite3_busy_timeout(connection(),
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
1132
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
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 !=
|
|
1561
|
+
if (result != SQLITE_OK) {
|
|
1279
1562
|
std::string error = sqlite3_errmsg(database_->connection());
|
|
1280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1631
|
+
ThrowEnhancedSqliteErrorWithDB(env, database_, database_->connection(),
|
|
1632
|
+
result, error);
|
|
1343
1633
|
return env.Undefined();
|
|
1344
1634
|
}
|
|
1345
1635
|
} catch (const std::exception &e) {
|
|
1346
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
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,
|
|
1765
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1830
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1882
|
-
|
|
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
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2291
|
-
if (
|
|
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
|
-
|
|
2295
|
-
|
|
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(),
|
|
2412
|
-
|
|
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
|