Repository: nodejs/node
Pull request: sqlite: add readOnly option

Allow opening existing SQLite databases with `SQLITE_OPEN_READONLY` set.

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: doc/api/sqlite.md
patch: @@ -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

filename: src/node_sqlite.cc
patch: @@ -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<Value>& args) {
       open = open_v.As<Boolean>()->Value();
     }
 
+    Local<String> read_only_string =
+        FIXED_ONE_BYTE_STRING(env->isolate(), "readOnly");
+    Local<Value> 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<Boolean>()->Value());
+    }
+
     Local<String> enable_foreign_keys_string =
         FIXED_ONE_BYTE_STRING(env->isolate(), "enableForeignKeyConstraints");
     Local<Value> enable_foreign_keys_v;

filename: src/node_sqlite.h
patch: @@ -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;
 };

filename: test/parallel/test-sqlite-database-sync.js
patch: @@ -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 });