Skip to content

Commit aaa74a9

Browse files
committed
feat: add defensive mode support in DatabaseSync with corresponding tests
1 parent be4f59b commit aaa74a9

File tree

4 files changed

+160
-0
lines changed

4 files changed

+160
-0
lines changed

src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ export interface DatabaseSyncOptions {
6060
* @default false
6161
*/
6262
readonly allowUnknownNamedParameters?: boolean;
63+
/**
64+
* If true, enables the defensive flag. When the defensive flag is enabled,
65+
* language features that allow ordinary SQL to deliberately corrupt the
66+
* database file are disabled. The defensive flag can also be set using
67+
* `enableDefensive()`.
68+
* @see https://sqlite.org/c3ref/c_dbconfig_defensive.html
69+
* @default false
70+
*/
71+
readonly defensive?: boolean;
6372
/**
6473
* If true, the database is opened immediately. If false, the database is not opened until the first operation.
6574
* @default true
@@ -305,6 +314,14 @@ export interface DatabaseSyncInstance {
305314
* @param entryPoint Optional entry point function name. If not provided, uses the default entry point.
306315
*/
307316
loadExtension(path: string, entryPoint?: string): void;
317+
/**
318+
* Enables or disables the defensive flag. When the defensive flag is active,
319+
* language features that allow ordinary SQL to deliberately corrupt the
320+
* database file are disabled.
321+
* @param active Whether to enable or disable the defensive flag.
322+
* @see https://sqlite.org/c3ref/c_dbconfig_defensive.html
323+
*/
324+
enableDefensive(active: boolean): void;
308325

309326
/**
310327
* Makes a backup of the database. This method abstracts the sqlite3_backup_init(),

src/sqlite_impl.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ Napi::Object DatabaseSync::Init(Napi::Env env, Napi::Object exports) {
278278
InstanceMethod("enableLoadExtension",
279279
&DatabaseSync::EnableLoadExtension),
280280
InstanceMethod("loadExtension", &DatabaseSync::LoadExtension),
281+
InstanceMethod("enableDefensive", &DatabaseSync::EnableDefensive),
281282
InstanceMethod("createSession", &DatabaseSync::CreateSession),
282283
InstanceMethod("applyChangeset", &DatabaseSync::ApplyChangeset),
283284
InstanceMethod("backup", &DatabaseSync::Backup),
@@ -416,6 +417,20 @@ DatabaseSync::DatabaseSync(const Napi::CallbackInfo &info)
416417
.Value());
417418
}
418419

420+
if (options.Has("defensive")) {
421+
Napi::Value defensive_val = options.Get("defensive");
422+
if (!defensive_val.IsUndefined()) {
423+
if (!defensive_val.IsBoolean()) {
424+
node::THROW_ERR_INVALID_ARG_TYPE(
425+
info.Env(),
426+
"The \"options.defensive\" argument must be a boolean.");
427+
return;
428+
}
429+
config.set_enable_defensive(
430+
defensive_val.As<Napi::Boolean>().Value());
431+
}
432+
}
433+
419434
// Handle the open option
420435
if (options.Has("open")) {
421436
Napi::Value open_val = options.Get("open");
@@ -682,6 +697,21 @@ void DatabaseSync::InternalOpen(DatabaseOpenConfiguration config) {
682697
throw ex;
683698
}
684699
}
700+
701+
// Configure defensive mode
702+
if (config_.get_enable_defensive()) {
703+
int defensive_enabled;
704+
result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DEFENSIVE, 1,
705+
&defensive_enabled);
706+
if (result != SQLITE_OK) {
707+
std::string error = sqlite3_errmsg(connection());
708+
SqliteException ex(connection_, result,
709+
"Failed to configure DEFENSIVE: " + error);
710+
sqlite3_close(connection_);
711+
connection_ = nullptr;
712+
throw ex;
713+
}
714+
}
685715
}
686716

687717
void DatabaseSync::InternalClose() {
@@ -1041,6 +1071,36 @@ Napi::Value DatabaseSync::LoadExtension(const Napi::CallbackInfo &info) {
10411071
return env.Undefined();
10421072
}
10431073

1074+
Napi::Value DatabaseSync::EnableDefensive(const Napi::CallbackInfo &info) {
1075+
Napi::Env env = info.Env();
1076+
1077+
if (!ValidateThread(env)) {
1078+
return env.Undefined();
1079+
}
1080+
1081+
if (!IsOpen()) {
1082+
node::THROW_ERR_INVALID_STATE(env, "database is not open");
1083+
return env.Undefined();
1084+
}
1085+
1086+
if (info.Length() < 1 || !info[0].IsBoolean()) {
1087+
node::THROW_ERR_INVALID_ARG_TYPE(
1088+
env, "The \"active\" argument must be a boolean.");
1089+
return env.Undefined();
1090+
}
1091+
1092+
int enable = info[0].As<Napi::Boolean>().Value() ? 1 : 0;
1093+
int defensive_enabled;
1094+
int result = sqlite3_db_config(connection(), SQLITE_DBCONFIG_DEFENSIVE,
1095+
enable, &defensive_enabled);
1096+
if (result != SQLITE_OK) {
1097+
node::ThrowEnhancedSqliteError(env, connection(), result,
1098+
"Failed to set defensive mode");
1099+
}
1100+
1101+
return env.Undefined();
1102+
}
1103+
10441104
Napi::Value DatabaseSync::CreateSession(const Napi::CallbackInfo &info) {
10451105
Napi::Env env = info.Env();
10461106

src/sqlite_impl.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ class DatabaseOpenConfiguration {
100100
allow_unknown_named_params_ = flag;
101101
}
102102

103+
bool get_enable_defensive() const { return defensive_; }
104+
void set_enable_defensive(bool flag) { defensive_ = flag; }
105+
103106
private:
104107
std::string location_;
105108
bool read_only_ = false;
@@ -110,6 +113,7 @@ class DatabaseOpenConfiguration {
110113
bool return_arrays_ = false;
111114
bool allow_bare_named_params_ = true;
112115
bool allow_unknown_named_params_ = false;
116+
bool defensive_ = false;
113117
};
114118

115119
// Main database class
@@ -148,6 +152,9 @@ class DatabaseSync : public Napi::ObjectWrap<DatabaseSync> {
148152
Napi::Value EnableLoadExtension(const Napi::CallbackInfo &info);
149153
Napi::Value LoadExtension(const Napi::CallbackInfo &info);
150154

155+
// Defensive mode
156+
Napi::Value EnableDefensive(const Napi::CallbackInfo &info);
157+
151158
// Session support
152159
Napi::Value CreateSession(const Napi::CallbackInfo &info);
153160
Napi::Value ApplyChangeset(const Napi::CallbackInfo &info);

test/database.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,3 +724,79 @@ describe("Database Configuration Tests", () => {
724724
db.close();
725725
});
726726
});
727+
728+
describe("Defensive Mode", () => {
729+
/**
730+
* Check if defensive mode is active by testing if journal_mode can be changed.
731+
* When defensive mode is ON, changing journal_mode to OFF is blocked.
732+
* This matches the test approach used in Node.js test-sqlite-config.js
733+
*/
734+
function checkDefensiveMode(db: InstanceType<typeof DatabaseSync>): boolean {
735+
const journalMode = (): string =>
736+
(db.prepare("PRAGMA journal_mode").get() as { journal_mode: string })
737+
.journal_mode;
738+
739+
// In-memory databases start with journal_mode = 'memory'
740+
expect(journalMode()).toBe("memory");
741+
742+
// Try to change journal_mode to OFF
743+
db.exec("PRAGMA journal_mode=OFF");
744+
745+
// If defensive mode is active, journal_mode stays 'memory'
746+
// If defensive mode is off, journal_mode changes to 'off'
747+
return journalMode() === "memory";
748+
}
749+
750+
test("by default, defensive mode is off", () => {
751+
const db = new DatabaseSync(":memory:");
752+
expect(checkDefensiveMode(db)).toBe(false);
753+
db.close();
754+
});
755+
756+
test("when passing { defensive: true } as config, defensive mode is on", () => {
757+
const db = new DatabaseSync(":memory:", {
758+
defensive: true,
759+
});
760+
expect(checkDefensiveMode(db)).toBe(true);
761+
db.close();
762+
});
763+
764+
test("defensive mode on after calling db.enableDefensive(true)", () => {
765+
const db = new DatabaseSync(":memory:");
766+
db.enableDefensive(true);
767+
expect(checkDefensiveMode(db)).toBe(true);
768+
db.close();
769+
});
770+
771+
test("defensive mode should be off after calling db.enableDefensive(false)", () => {
772+
const db = new DatabaseSync(":memory:", {
773+
defensive: true,
774+
});
775+
db.enableDefensive(false);
776+
expect(checkDefensiveMode(db)).toBe(false);
777+
db.close();
778+
});
779+
780+
test("throws if options.defensive is provided but is not a boolean", () => {
781+
expect(() => {
782+
// @ts-expect-error - testing invalid type
783+
new DatabaseSync(":memory:", { defensive: 42 });
784+
}).toThrow(/boolean/);
785+
});
786+
787+
test("enableDefensive throws if argument is not a boolean", () => {
788+
const db = new DatabaseSync(":memory:");
789+
expect(() => {
790+
// @ts-expect-error - testing invalid type
791+
db.enableDefensive("yes");
792+
}).toThrow(/boolean/);
793+
db.close();
794+
});
795+
796+
test("enableDefensive throws if database is not open", () => {
797+
const db = new DatabaseSync(":memory:", { open: false });
798+
expect(() => {
799+
db.enableDefensive(true);
800+
}).toThrow(/not open/);
801+
});
802+
});

0 commit comments

Comments
 (0)