mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-28 05:24:22 +01:00
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:
@@ -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)]
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
cli/app.rs
42
cli/app.rs
@@ -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!(
|
||||
|
||||
97
core/lib.rs
97
core/lib.rs
@@ -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()
|
||||
}
|
||||
|
||||
179
core/util.rs
179
core/util.rs
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user