Merge 'Add opening new connection from a sqlite compatible URI, read-only connections' from Preston Thorpe

@penberg reminded me that this never got integrated :)

Closes #1908
This commit is contained in:
Pekka Enberg
2025-07-02 08:05:36 +03:00
7 changed files with 223 additions and 165 deletions

View File

@@ -6,7 +6,7 @@ use std::{
ffi::{c_char, c_void},
sync::Arc,
};
use turso_core::{Connection, Database, LimboError, IO};
use turso_core::{Connection, LimboError};
/// # Safety
/// Safe to be called from Go with null terminated DSN string.
@@ -20,21 +20,10 @@ pub unsafe extern "C" fn db_open(path: *const c_char) -> *mut c_void {
}
let path = unsafe { std::ffi::CStr::from_ptr(path) };
let path = path.to_str().unwrap();
let io: Arc<dyn IO> = match path {
p if p.contains(":memory:") => Arc::new(turso_core::MemoryIO::new()),
_ => Arc::new(turso_core::PlatformIO::new().expect("Failed to create IO")),
let Ok((io, conn)) = Connection::from_uri(path, false, false) else {
panic!("Failed to open connection with path: {}", path);
};
let db = Database::open_file(io.clone(), path, false, false);
match db {
Ok(db) => {
let conn = db.connect().unwrap();
LimboConn::new(conn, io).to_ptr()
}
Err(e) => {
eprintln!("Error: {}", e);
std::ptr::null_mut()
}
}
LimboConn::new(conn, io).to_ptr()
}
#[allow(dead_code)]

View File

@@ -300,29 +300,13 @@ impl Drop for Connection {
#[allow(clippy::arc_with_non_send_sync)]
#[pyfunction]
pub fn connect(path: &str) -> Result<Connection> {
#[inline(always)]
fn open_or(
io: Arc<dyn turso_core::IO>,
path: &str,
) -> std::result::Result<Arc<turso_core::Database>, PyErr> {
turso_core::Database::open_file(io, path, false, false).map_err(|e| {
PyErr::new::<DatabaseError, _>(format!("Failed to open database: {:?}", e))
})
}
match path {
":memory:" => {
let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::MemoryIO::new());
let db = open_or(io.clone(), path)?;
let conn: Arc<turso_core::Connection> = db.connect().unwrap();
Ok(Connection { conn, io })
}
path => {
let io: Arc<dyn turso_core::IO> = Arc::new(turso_core::PlatformIO::new()?);
let db = open_or(io.clone(), path)?;
let conn: Arc<turso_core::Connection> = db.connect().unwrap();
Ok(Connection { conn, io })
}
match turso_core::Connection::from_uri(path, false, false) {
Ok((io, conn)) => Ok(Connection { conn, io }),
Err(e) => Err(PyErr::new::<ProgrammingError, _>(format!(
"Failed to create connection: {:?}",
e
))
.into()),
}
}

View File

@@ -26,7 +26,7 @@ use std::{
};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use turso_core::{Database, LimboError, Statement, StepResult, Value};
use turso_core::{Connection, Database, LimboError, OpenFlags, Statement, StepResult, Value};
#[derive(Parser)]
#[command(name = "Turso")]
@@ -55,6 +55,8 @@ pub struct Opts {
help = "Select VFS. options are io_uring (if feature enabled), memory, and syscall"
)]
pub vfs: Option<String>,
#[clap(long, help = "Open the database in read-only mode")]
pub readonly: bool,
#[clap(long, help = "Enable experimental MVCC feature")]
pub experimental_mvcc: bool,
#[clap(long, help = "Enable experimental indexing feature")]
@@ -114,32 +116,24 @@ impl Limbo {
.database
.as_ref()
.map_or(":memory:".to_string(), |p| p.to_string_lossy().to_string());
let (io, db) = if let Some(ref vfs) = opts.vfs {
Database::open_new(&db_file, vfs)?
let (io, conn) = if db_file.contains([':', '?', '&', '#']) {
Connection::from_uri(&db_file, opts.experimental_indexes, opts.experimental_mvcc)?
} else {
let io = {
match db_file.as_str() {
":memory:" => get_io(
DbLocation::Memory,
opts.vfs.as_ref().map_or("", |s| s.as_str()),
)?,
_path => get_io(
DbLocation::Path,
opts.vfs.as_ref().map_or("", |s| s.as_str()),
)?,
}
let flags = if opts.readonly {
OpenFlags::ReadOnly
} else {
OpenFlags::default()
};
(
io.clone(),
Database::open_file(
io.clone(),
&db_file,
opts.experimental_mvcc,
opts.experimental_indexes,
)?,
)
let (io, db) = Database::open_new(
&db_file,
opts.vfs.as_ref(),
flags,
opts.experimental_indexes,
opts.experimental_mvcc,
)?;
let conn = db.connect()?;
(io, conn)
};
let conn = db.connect()?;
let mut ext_api = conn.build_turso_ext();
if unsafe { !limbo_completion::register_extension_static(&mut ext_api).is_ok() } {
return Err(anyhow!(

View File

@@ -43,6 +43,7 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use crate::storage::{header_accessor, wal::DummyWAL};
use crate::translate::optimizer::optimize_plan;
use crate::util::{OpenMode, OpenOptions};
use crate::vtab::VirtualTable;
use core::str;
pub use error::LimboError;
@@ -274,6 +275,7 @@ impl Database {
total_changes: Cell::new(0),
_shared_cache: false,
cache_size: Cell::new(default_cache_size),
readonly: Cell::new(false),
});
if let Err(e) = conn.register_builtins() {
return Err(LimboError::ExtensionError(e));
@@ -324,6 +326,7 @@ impl Database {
syms: RefCell::new(SymbolTable::new()),
_shared_cache: false,
cache_size: Cell::new(default_cache_size),
readonly: Cell::new(false),
});
if let Err(e) = conn.register_builtins() {
@@ -332,29 +335,55 @@ impl Database {
Ok(conn)
}
/// Open a new database file with a specified VFS without an existing database
/// Open a new database file with optionally specifying a VFS without an existing database
/// connection and symbol table to register extensions.
#[cfg(feature = "fs")]
#[allow(clippy::arc_with_non_send_sync)]
pub fn open_new(path: &str, vfs: &str) -> Result<(Arc<dyn IO>, Arc<Database>)> {
pub fn open_new<S>(
path: &str,
vfs: Option<S>,
flags: OpenFlags,
indexes: bool,
mvcc: bool,
) -> Result<(Arc<dyn IO>, Arc<Database>)>
where
S: AsRef<str> + std::fmt::Display,
{
use crate::util::MEMORY_PATH;
let vfsmods = ext::add_builtin_vfs_extensions(None)?;
let io: Arc<dyn IO> = match vfsmods.iter().find(|v| v.0 == vfs).map(|v| v.1.clone()) {
Some(vfs) => vfs,
None => match vfs.trim() {
"memory" => Arc::new(MemoryIO::new()),
"syscall" => Arc::new(SyscallIO::new()?),
#[cfg(all(target_os = "linux", feature = "io_uring"))]
"io_uring" => Arc::new(UringIO::new()?),
other => {
return Err(LimboError::InvalidArgument(format!(
"no such VFS: {}",
other
)));
}
},
};
let db = Self::open_file(io.clone(), path, false, false)?;
Ok((io, db))
match vfs {
Some(vfs) => {
let io: Arc<dyn IO> = match vfsmods
.iter()
.find(|v| v.0 == vfs.as_ref())
.map(|v| v.1.clone())
{
Some(vfs) => vfs,
None => match vfs.as_ref() {
"memory" => Arc::new(MemoryIO::new()),
"syscall" => Arc::new(SyscallIO::new()?),
#[cfg(all(target_os = "linux", feature = "io_uring"))]
"io_uring" => Arc::new(UringIO::new()?),
other => {
return Err(LimboError::InvalidArgument(format!(
"no such VFS: {}",
other
)));
}
},
};
let db = Self::open_file_with_flags(io.clone(), path, flags, indexes, mvcc)?;
Ok((io, db))
}
None => {
let io: Arc<dyn IO> = match path.trim() {
MEMORY_PATH => Arc::new(MemoryIO::new()),
_ => Arc::new(PlatformIO::new()?),
};
let db = Self::open_file_with_flags(io.clone(), path, flags, indexes, mvcc)?;
Ok((io, db))
}
}
}
}
@@ -372,6 +401,7 @@ pub struct Connection {
syms: RefCell<SymbolTable>,
_shared_cache: bool,
cache_size: Cell<i32>,
readonly: Cell<bool>,
}
impl Connection {
@@ -559,6 +589,35 @@ impl Connection {
Ok(())
}
#[cfg(feature = "fs")]
pub fn from_uri(
uri: &str,
use_indexes: bool,
mvcc: bool,
) -> Result<(Arc<dyn IO>, Arc<Connection>)> {
use crate::util::MEMORY_PATH;
let opts = OpenOptions::parse(uri)?;
let flags = opts.get_flags()?;
if opts.path == MEMORY_PATH || matches!(opts.mode, OpenMode::Memory) {
let io = Arc::new(MemoryIO::new());
let db = Database::open_file_with_flags(io.clone(), MEMORY_PATH, flags, false, false)?;
let conn = db.connect()?;
return Ok((io, conn));
}
let (io, db) = Database::open_new(&opts.path, opts.vfs.as_ref(), flags, use_indexes, mvcc)?;
if let Some(modeof) = opts.modeof {
let perms = std::fs::metadata(modeof)?;
std::fs::set_permissions(&opts.path, perms.permissions())?;
}
let conn = db.connect()?;
conn.set_readonly(opts.immutable);
Ok((io, conn))
}
pub fn set_readonly(&self, readonly: bool) {
self.readonly.replace(readonly);
}
pub fn wal_frame_count(&self) -> Result<u64> {
self.pager.wal_frame_count()
}

View File

@@ -631,6 +631,8 @@ pub struct OpenOptions<'a> {
pub immutable: bool,
}
pub const MEMORY_PATH: &str = ":memory:";
#[derive(Clone, Default, Debug, Copy, PartialEq)]
pub enum OpenMode {
ReadOnly,
@@ -670,13 +672,6 @@ impl OpenMode {
))),
}
}
#[allow(dead_code)]
pub fn get_flags(&self) -> OpenFlags {
match self {
OpenMode::ReadWriteCreate => OpenFlags::Create,
_ => OpenFlags::None,
}
}
}
fn is_windows_path(path: &str) -> bool {
@@ -705,58 +700,74 @@ fn normalize_windows_path(path: &str) -> String {
normalized
}
/// Parses a SQLite URI, handling Windows and Unix paths separately.
#[allow(dead_code)]
pub fn parse_sqlite_uri(uri: &str) -> Result<OpenOptions> {
if !uri.starts_with("file:") {
return Ok(OpenOptions {
path: uri.to_string(),
..Default::default()
});
impl<'a> OpenOptions<'a> {
/// Parses a SQLite URI, handling Windows and Unix paths separately.
pub fn parse(uri: &'a str) -> Result<OpenOptions<'a>> {
if !uri.starts_with("file:") {
return Ok(OpenOptions {
path: uri.to_string(),
..Default::default()
});
}
let mut opts = OpenOptions::default();
let without_scheme = &uri[5..];
let (without_fragment, _) = without_scheme
.split_once('#')
.unwrap_or((without_scheme, ""));
let (without_query, query) = without_fragment
.split_once('?')
.unwrap_or((without_fragment, ""));
parse_query_params(query, &mut opts)?;
// handle authority + path separately
if let Some(after_slashes) = without_query.strip_prefix("//") {
let (authority, path) = after_slashes.split_once('/').unwrap_or((after_slashes, ""));
// sqlite allows only `localhost` or empty authority.
if !(authority.is_empty() || authority == "localhost") {
return Err(LimboError::InvalidArgument(format!(
"Invalid authority '{}'. Only '' or 'localhost' allowed.",
authority
)));
}
opts.authority = if authority.is_empty() {
None
} else {
Some(authority)
};
if is_windows_path(path) {
opts.path = normalize_windows_path(&decode_percent(path));
} else if !path.is_empty() {
opts.path = format!("/{}", decode_percent(path));
} else {
opts.path = String::new();
}
} else {
// no authority, must be a normal absolute or relative path.
opts.path = decode_percent(without_query);
}
Ok(opts)
}
let mut opts = OpenOptions::default();
let without_scheme = &uri[5..];
let (without_fragment, _) = without_scheme
.split_once('#')
.unwrap_or((without_scheme, ""));
let (without_query, query) = without_fragment
.split_once('?')
.unwrap_or((without_fragment, ""));
parse_query_params(query, &mut opts)?;
// handle authority + path separately
if let Some(after_slashes) = without_query.strip_prefix("//") {
let (authority, path) = after_slashes.split_once('/').unwrap_or((after_slashes, ""));
// sqlite allows only `localhost` or empty authority.
if !(authority.is_empty() || authority == "localhost") {
return Err(LimboError::InvalidArgument(format!(
"Invalid authority '{}'. Only '' or 'localhost' allowed.",
authority
)));
pub fn get_flags(&self) -> Result<OpenFlags> {
// Only use modeof if we're in a mode that can create files
if self.mode != OpenMode::ReadWriteCreate && self.modeof.is_some() {
return Err(LimboError::InvalidArgument(
"modeof is not applicable without mode=rwc".to_string(),
));
}
opts.authority = if authority.is_empty() {
None
} else {
Some(authority)
};
if is_windows_path(path) {
opts.path = normalize_windows_path(&decode_percent(path));
} else if !path.is_empty() {
opts.path = format!("/{}", decode_percent(path));
} else {
opts.path = String::new();
}
} else {
// no authority, must be a normal absolute or relative path.
opts.path = decode_percent(without_query);
// If modeof is not applicable or file doesn't exist, use default flags
Ok(match self.mode {
OpenMode::ReadWriteCreate => OpenFlags::Create,
OpenMode::ReadOnly => OpenFlags::ReadOnly,
_ => OpenFlags::default(),
})
}
Ok(opts)
}
// parses query parameters and updates OpenOptions
@@ -1339,7 +1350,7 @@ pub mod tests {
#[test]
fn test_simple_uri() {
let uri = "file:/home/user/db.sqlite";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite");
assert_eq!(opts.authority, None);
}
@@ -1347,7 +1358,7 @@ pub mod tests {
#[test]
fn test_uri_with_authority() {
let uri = "file://localhost/home/user/db.sqlite";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite");
assert_eq!(opts.authority, Some("localhost"));
}
@@ -1355,14 +1366,14 @@ pub mod tests {
#[test]
fn test_uri_with_invalid_authority() {
let uri = "file://example.com/home/user/db.sqlite";
let result = parse_sqlite_uri(uri);
let result = OpenOptions::parse(uri);
assert!(result.is_err());
}
#[test]
fn test_uri_with_query_params() {
let uri = "file:/home/user/db.sqlite?vfs=unix&mode=ro&immutable=1";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite");
assert_eq!(opts.vfs, Some("unix".to_string()));
assert_eq!(opts.mode, OpenMode::ReadOnly);
@@ -1372,14 +1383,14 @@ pub mod tests {
#[test]
fn test_uri_with_fragment() {
let uri = "file:/home/user/db.sqlite#section1";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite");
}
#[test]
fn test_uri_with_percent_encoding() {
let uri = "file:/home/user/db%20with%20spaces.sqlite?vfs=unix";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db with spaces.sqlite");
assert_eq!(opts.vfs, Some("unix".to_string()));
}
@@ -1387,7 +1398,7 @@ pub mod tests {
#[test]
fn test_uri_without_scheme() {
let uri = "/home/user/db.sqlite";
let result = parse_sqlite_uri(uri);
let result = OpenOptions::parse(uri);
assert!(result.is_ok());
assert_eq!(result.unwrap().path, "/home/user/db.sqlite");
}
@@ -1395,7 +1406,7 @@ pub mod tests {
#[test]
fn test_uri_with_empty_query() {
let uri = "file:/home/user/db.sqlite?";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite");
assert_eq!(opts.vfs, None);
}
@@ -1403,7 +1414,7 @@ pub mod tests {
#[test]
fn test_uri_with_partial_query() {
let uri = "file:/home/user/db.sqlite?mode=rw";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite");
assert_eq!(opts.mode, OpenMode::ReadWrite);
assert_eq!(opts.vfs, None);
@@ -1412,14 +1423,14 @@ pub mod tests {
#[test]
fn test_uri_windows_style_path() {
let uri = "file:///C:/Users/test/db.sqlite";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/C:/Users/test/db.sqlite");
}
#[test]
fn test_uri_with_only_query_params() {
let uri = "file:?mode=memory&cache=shared";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "");
assert_eq!(opts.mode, OpenMode::Memory);
assert_eq!(opts.cache, CacheMode::Shared);
@@ -1428,14 +1439,14 @@ pub mod tests {
#[test]
fn test_uri_with_only_fragment() {
let uri = "file:#fragment";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "");
}
#[test]
fn test_uri_with_invalid_scheme() {
let uri = "http:/home/user/db.sqlite";
let result = parse_sqlite_uri(uri);
let result = OpenOptions::parse(uri);
assert!(result.is_ok());
assert_eq!(result.unwrap().path, "http:/home/user/db.sqlite");
}
@@ -1443,7 +1454,7 @@ pub mod tests {
#[test]
fn test_uri_with_multiple_query_params() {
let uri = "file:/home/user/db.sqlite?vfs=unix&mode=rw&cache=private&immutable=0";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite");
assert_eq!(opts.vfs, Some("unix".to_string()));
assert_eq!(opts.mode, OpenMode::ReadWrite);
@@ -1454,7 +1465,7 @@ pub mod tests {
#[test]
fn test_uri_with_unknown_query_param() {
let uri = "file:/home/user/db.sqlite?unknown=param";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite");
assert_eq!(opts.vfs, None);
}
@@ -1462,7 +1473,7 @@ pub mod tests {
#[test]
fn test_uri_with_multiple_equal_signs() {
let uri = "file:/home/user/db.sqlite?vfs=unix=custom";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite");
assert_eq!(opts.vfs, Some("unix=custom".to_string()));
}
@@ -1470,14 +1481,14 @@ pub mod tests {
#[test]
fn test_uri_with_trailing_slash() {
let uri = "file:/home/user/db.sqlite/";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite/");
}
#[test]
fn test_uri_with_encoded_characters_in_query() {
let uri = "file:/home/user/db.sqlite?vfs=unix%20mode";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/user/db.sqlite");
assert_eq!(opts.vfs, Some("unix mode".to_string()));
}
@@ -1485,21 +1496,21 @@ pub mod tests {
#[test]
fn test_uri_windows_network_path() {
let uri = "file://server/share/db.sqlite";
let result = parse_sqlite_uri(uri);
let result = OpenOptions::parse(uri);
assert!(result.is_err()); // non-localhost authority should fail
}
#[test]
fn test_uri_windows_drive_letter_with_slash() {
let uri = "file:///C:/database.sqlite";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/C:/database.sqlite");
}
#[test]
fn test_localhost_with_double_slash_and_no_path() {
let uri = "file://localhost";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "");
assert_eq!(opts.authority, Some("localhost"));
}
@@ -1507,7 +1518,7 @@ pub mod tests {
#[test]
fn test_uri_windows_drive_letter_without_slash() {
let uri = "file:///C:/database.sqlite";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/C:/database.sqlite");
}
@@ -1516,11 +1527,11 @@ pub mod tests {
// any other mode but ro, rwc, rw, memory should fail per sqlite
let uri = "file:data.db?mode=readonly";
let res = parse_sqlite_uri(uri);
let res = OpenOptions::parse(uri);
assert!(res.is_err());
// including empty
let uri = "file:/home/user/db.sqlite?vfs=&mode=";
let res = parse_sqlite_uri(uri);
let res = OpenOptions::parse(uri);
assert!(res.is_err());
}
@@ -1528,7 +1539,7 @@ pub mod tests {
#[test]
fn test_simple_file_current_dir() {
let uri = "file:data.db";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "data.db");
assert_eq!(opts.authority, None);
assert_eq!(opts.vfs, None);
@@ -1538,7 +1549,7 @@ pub mod tests {
#[test]
fn test_simple_file_three_slash() {
let uri = "file:///home/data/data.db";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/data/data.db");
assert_eq!(opts.authority, None);
assert_eq!(opts.vfs, None);
@@ -1548,7 +1559,7 @@ pub mod tests {
#[test]
fn test_simple_file_two_slash_localhost() {
let uri = "file://localhost/home/fred/data.db";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/home/fred/data.db");
assert_eq!(opts.authority, Some("localhost"));
assert_eq!(opts.vfs, None);
@@ -1557,14 +1568,14 @@ pub mod tests {
#[test]
fn test_windows_double_invalid() {
let uri = "file://C:/home/fred/data.db?mode=ro";
let opts = parse_sqlite_uri(uri);
let opts = OpenOptions::parse(uri);
assert!(opts.is_err());
}
#[test]
fn test_simple_file_two_slash() {
let uri = "file:///C:/Documents%20and%20Settings/fred/Desktop/data.db";
let opts = parse_sqlite_uri(uri).unwrap();
let opts = OpenOptions::parse(uri).unwrap();
assert_eq!(opts.path, "/C:/Documents and Settings/fred/Desktop/data.db");
assert_eq!(opts.vfs, None);
}

View File

@@ -4692,6 +4692,9 @@ pub fn op_open_write(
else {
unreachable!("unexpected Insn {:?}", insn)
};
if program.connection.readonly.get() {
return Err(LimboError::ReadOnly);
}
let root_page = match root_page {
RegisterOrLiteral::Literal(lit) => *lit as u64,
RegisterOrLiteral::Register(reg) => match &state.registers[*reg].get_owned_value() {
@@ -4794,6 +4797,9 @@ pub fn op_create_btree(
let Insn::CreateBtree { db, root, flags } = insn else {
unreachable!("unexpected Insn {:?}", insn)
};
if program.connection.readonly.get() {
return Err(LimboError::ReadOnly);
}
if *db > 0 {
// TODO: implement temp databases
todo!("temp databases not implemented yet");

View File

@@ -274,6 +274,20 @@ def test_insert_default_values():
limbo.quit()
def test_uri_readonly():
turso = TestLimboShell(flags="-q file:testing/testing_small.db?mode=ro", init_commands="")
turso.run_test("read-only-uri-reads-work", "SELECT COUNT(*) FROM demo;", "5")
turso.run_test_fn(
"INSERT INTO demo (id, value) values (6, 'demo');",
lambda res: "read-only" in res,
"read-only-uri-writes-fail",
)
turso.run_test_fn("CREATE TABLE t(a);", lambda res: "read-only" in res, "read-only-uri-cant-create-table")
turso.run_test_fn("DROP TABLE demo;", lambda res: "read-only" in res, "read-only-uri-cant-drop-table")
turso.init_test_db()
turso.quit()
def main():
console.info("Running all Limbo CLI tests...")
test_basic_queries()
@@ -293,6 +307,7 @@ def main():
test_table_patterns()
test_update_with_limit()
test_update_with_limit_and_offset()
test_uri_readonly()
console.info("All tests have passed")