@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.
- package/CHANGELOG.md +38 -2
- package/README.md +47 -483
- package/SECURITY.md +27 -83
- 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-arm64/@photostructure+sqlite.glibc.node +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 +7 -1
- package/src/shims/sqlite_errors.h +168 -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 +736 -129
- 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,59 @@
|
|
|
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
|
+
// 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
|
-
|
|
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
|
-
|
|
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, "
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
547
|
-
|
|
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
|
|
731
|
+
throw ex;
|
|
565
732
|
}
|
|
566
733
|
|
|
567
734
|
// Configure database
|
|
568
|
-
if (
|
|
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 (
|
|
574
|
-
sqlite3_busy_timeout(connection(),
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
1132
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
1571
|
+
// Check if BindParameters set a pending exception
|
|
1572
|
+
if (env.IsExceptionPending()) {
|
|
1573
|
+
return env.Undefined();
|
|
1574
|
+
}
|
|
1277
1575
|
|
|
1278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1654
|
+
ThrowEnhancedSqliteErrorWithDB(env, database_, database_->connection(),
|
|
1655
|
+
result, error);
|
|
1343
1656
|
return env.Undefined();
|
|
1344
1657
|
}
|
|
1345
1658
|
} catch (const std::exception &e) {
|
|
1346
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
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,
|
|
1765
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1830
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1882
|
-
|
|
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
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
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)
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2291
|
-
if (
|
|
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
|
-
|
|
2295
|
-
|
|
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(),
|
|
2412
|
-
|
|
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
|