From b14b4a3dd1dac795b4a8816d2ed047f971e797bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Mon, 28 Oct 2024 05:11:27 +0000 Subject: [PATCH] sqlite: add readOnly option Allow opening existing SQLite databases with SQLITE_OPEN_READONLY set. --- doc/api/sqlite.md | 2 ++ src/node_sqlite.cc | 20 ++++++++++++- src/node_sqlite.h | 5 ++++ test/parallel/test-sqlite-database-sync.js | 33 ++++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/doc/api/sqlite.md b/doc/api/sqlite.md index cfb3f48d70a42c..34dfdaf1b11365 100644 --- a/doc/api/sqlite.md +++ b/doc/api/sqlite.md @@ -107,6 +107,8 @@ added: v22.5.0 * `open` {boolean} If `true`, the database is opened by the constructor. When this value is `false`, the database must be opened via the `open()` method. **Default:** `true`. + * `readOnly` {boolean} If `true`, the database is opened in read-only mode. + If the database does not exist, opening it will fail. **Default:** `false`. * `enableForeignKeyConstraints` {boolean} If `true`, foreign key constraints are enabled. This is recommended but can be disabled for compatibility with legacy database schemas. The enforcement of foreign key constraints can be diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index af9e77e1362bc3..742c15233ccd13 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -126,7 +126,9 @@ bool DatabaseSync::Open() { } // TODO(cjihrig): Support additional flags. - int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; + int flags = open_config_.get_read_only() + ? SQLITE_OPEN_READONLY + : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; int r = sqlite3_open_v2( open_config_.location().c_str(), &connection_, flags, nullptr); CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false); @@ -219,6 +221,22 @@ void DatabaseSync::New(const FunctionCallbackInfo& args) { open = open_v.As()->Value(); } + Local read_only_string = + FIXED_ONE_BYTE_STRING(env->isolate(), "readOnly"); + Local read_only_v; + if (!options->Get(env->context(), read_only_string).ToLocal(&read_only_v)) { + return; + } + if (!read_only_v->IsUndefined()) { + if (!read_only_v->IsBoolean()) { + node::THROW_ERR_INVALID_ARG_TYPE( + env->isolate(), + "The \"options.readOnly\" argument must be a boolean."); + return; + } + open_config.set_read_only(read_only_v.As()->Value()); + } + Local enable_foreign_keys_string = FIXED_ONE_BYTE_STRING(env->isolate(), "enableForeignKeyConstraints"); Local enable_foreign_keys_v; diff --git a/src/node_sqlite.h b/src/node_sqlite.h index 933d0b65a3ba08..6d06eb551a469e 100644 --- a/src/node_sqlite.h +++ b/src/node_sqlite.h @@ -21,6 +21,10 @@ class DatabaseOpenConfiguration { inline const std::string& location() const { return location_; } + inline bool get_read_only() const { return read_only_; } + + inline void set_read_only(bool flag) { read_only_ = flag; } + inline bool get_enable_foreign_keys() const { return enable_foreign_keys_; } inline void set_enable_foreign_keys(bool flag) { @@ -33,6 +37,7 @@ class DatabaseOpenConfiguration { private: std::string location_; + bool read_only_ = false; bool enable_foreign_keys_ = true; bool enable_dqs_ = false; }; diff --git a/test/parallel/test-sqlite-database-sync.js b/test/parallel/test-sqlite-database-sync.js index 7979d01b568aa1..0bf5b2c139ca03 100644 --- a/test/parallel/test-sqlite-database-sync.js +++ b/test/parallel/test-sqlite-database-sync.js @@ -51,6 +51,39 @@ suite('DatabaseSync() constructor', () => { }); }); + test('throws if options.readOnly is provided but is not a boolean', (t) => { + t.assert.throws(() => { + new DatabaseSync('foo', { readOnly: 5 }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + message: /The "options\.readOnly" argument must be a boolean/, + }); + }); + + test('is not read-only by default', (t) => { + const dbPath = nextDb(); + const db = new DatabaseSync(dbPath); + db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)'); + }); + + test('is read-only if readOnly is set', (t) => { + const dbPath = nextDb(); + { + const db = new DatabaseSync(dbPath); + db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY)'); + db.close(); + } + { + const db = new DatabaseSync(dbPath, { readOnly: true }); + t.assert.throws(() => { + db.exec('CREATE TABLE bar (id INTEGER PRIMARY KEY)'); + }, { + code: 'ERR_SQLITE_ERROR', + message: /attempt to write a readonly database/, + }); + } + }); + test('throws if options.enableForeignKeyConstraints is provided but is not a boolean', (t) => { t.assert.throws(() => { new DatabaseSync('foo', { enableForeignKeyConstraints: 5 });