From b5c3f4f782d3cdc1a7c51b03f597799e1c9c773b Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Mon, 3 Feb 2025 16:41:45 +0530 Subject: [PATCH] fix(ext/node): support read-only database in `node:sqlite` (#27930) Implements the `readOnly` option for `DatabaseSync`. Permissions: `--allow-read=test.db --allow-write=test.db` => all works `--allow-read=test.db` => only `readOnly` dbs work, cannot create new db --- ext/node/ops/sqlite/database.rs | 40 ++++++++++++++++++++++----------- tests/unit_node/sqlite_test.ts | 29 +++++++++++++++++++++++- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/ext/node/ops/sqlite/database.rs b/ext/node/ops/sqlite/database.rs index 5f2808a918d06e..03ccc7fb53b5fe 100644 --- a/ext/node/ops/sqlite/database.rs +++ b/ext/node/ops/sqlite/database.rs @@ -20,6 +20,7 @@ struct DatabaseSyncOptions { open: bool, #[serde(default = "true_fn")] enable_foreign_key_constraints: bool, + read_only: bool, } fn true_fn() -> bool { @@ -31,6 +32,7 @@ impl Default for DatabaseSyncOptions { DatabaseSyncOptions { open: true, enable_foreign_key_constraints: true, + read_only: false, } } } @@ -43,17 +45,31 @@ pub struct DatabaseSync { impl GarbageCollected for DatabaseSync {} -fn check_perms(state: &mut OpState, location: &str) -> Result<(), SqliteError> { - if location != ":memory:" { - state - .borrow::() - .check_read_with_api_name(location, Some("node:sqlite"))?; - state - .borrow::() - .check_write_with_api_name(location, Some("node:sqlite"))?; +fn open_db( + state: &mut OpState, + readonly: bool, + location: &str, +) -> Result { + if location == ":memory:" { + return Ok(rusqlite::Connection::open_in_memory()?); } - Ok(()) + state + .borrow::() + .check_read_with_api_name(location, Some("node:sqlite"))?; + + if readonly { + return Ok(rusqlite::Connection::open_with_flags( + location, + rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, + )?); + } + + state + .borrow::() + .check_write_with_api_name(location, Some("node:sqlite"))?; + + Ok(rusqlite::Connection::open(location)?) } // Represents a single connection to a SQLite database. @@ -75,9 +91,8 @@ impl DatabaseSync { let options = options.unwrap_or_default(); let db = if options.open { - check_perms(state, &location)?; + let db = open_db(state, options.read_only, &location)?; - let db = rusqlite::Connection::open(&location)?; if options.enable_foreign_key_constraints { db.execute("PRAGMA foreign_keys = ON", [])?; } @@ -104,8 +119,7 @@ impl DatabaseSync { return Err(SqliteError::AlreadyOpen); } - check_perms(state, &self.location)?; - let db = rusqlite::Connection::open(&self.location)?; + let db = open_db(state, self.options.read_only, &self.location)?; if self.options.enable_foreign_key_constraints { db.execute("PRAGMA foreign_keys = ON", [])?; } diff --git a/tests/unit_node/sqlite_test.ts b/tests/unit_node/sqlite_test.ts index 207efca8cd8d16..ca485099430b3a 100644 --- a/tests/unit_node/sqlite_test.ts +++ b/tests/unit_node/sqlite_test.ts @@ -2,6 +2,8 @@ import { DatabaseSync } from "node:sqlite"; import { assert, assertEquals, assertThrows } from "@std/assert"; +const tempDir = Deno.makeTempDirSync(); + Deno.test("[node/sqlite] in-memory databases", () => { const db1 = new DatabaseSync(":memory:"); const db2 = new DatabaseSync(":memory:"); @@ -40,7 +42,6 @@ Deno.test( name: "[node/sqlite] PRAGMAs are supported", }, () => { - const tempDir = Deno.makeTempDirSync(); const db = new DatabaseSync(`${tempDir}/test.db`); assertEquals(db.prepare("PRAGMA journal_mode = WAL").get(), { @@ -99,5 +100,31 @@ Deno.test({ assertThrows(() => { new DatabaseSync("test.db"); }, Deno.errors.NotCapable); + assertThrows(() => { + new DatabaseSync("test.db", { readOnly: true }); + }, Deno.errors.NotCapable); + }, +}); + +Deno.test({ + name: "[node/sqlite] readOnly database", + permissions: { read: true, write: true }, + fn() { + { + const db = new DatabaseSync(`${tempDir}/test3.db`); + db.exec("CREATE TABLE foo (id INTEGER PRIMARY KEY)"); + db.close(); + } + { + const db = new DatabaseSync(`${tempDir}/test3.db`, { readOnly: true }); + assertThrows( + () => { + db.exec("CREATE TABLE test(key INTEGER PRIMARY KEY)"); + }, + Error, + "attempt to write a readonly database", + ); + db.close(); + } }, });