use std::sync::Arc;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use vibesql_ast::Statement;
use vibesql_storage::Database;
use crate::{
errors::ExecutorError,
select::{SelectExecutor, SelectResult},
};
#[derive(Debug)]
pub enum ReadOnlyError {
NotReadOnly { statement_type: String },
ParseError(String),
ExecutionError(ExecutorError),
}
impl std::fmt::Display for ReadOnlyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReadOnlyError::NotReadOnly { statement_type } => {
write!(
f,
"{} is not allowed in read-only mode. Only SELECT queries are permitted.",
statement_type
)
}
ReadOnlyError::ParseError(msg) => write!(f, "SQL parse error: {}", msg),
ReadOnlyError::ExecutionError(e) => write!(f, "Execution error: {:?}", e),
}
}
}
impl std::error::Error for ReadOnlyError {}
impl From<ExecutorError> for ReadOnlyError {
fn from(e: ExecutorError) -> Self {
ReadOnlyError::ExecutionError(e)
}
}
pub trait ReadOnlyQuery {
fn query(&self, sql: &str) -> Result<SelectResult, ReadOnlyError>;
}
impl ReadOnlyQuery for Database {
fn query(&self, sql: &str) -> Result<SelectResult, ReadOnlyError> {
let statement = vibesql_parser::Parser::parse_sql(sql)
.map_err(|e| ReadOnlyError::ParseError(format!("{:?}", e)))?;
match &statement {
Statement::Select(select_stmt) => {
let executor = SelectExecutor::new(self);
executor.execute_with_columns(select_stmt.as_ref()).map_err(ReadOnlyError::from)
}
Statement::Insert(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "INSERT".to_string() })
}
Statement::Update(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "UPDATE".to_string() })
}
Statement::Delete(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "DELETE".to_string() })
}
Statement::CreateTable(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "CREATE TABLE".to_string() })
}
Statement::DropTable(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "DROP TABLE".to_string() })
}
Statement::CreateIndex(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "CREATE INDEX".to_string() })
}
Statement::DropIndex(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "DROP INDEX".to_string() })
}
Statement::CreateView(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "CREATE VIEW".to_string() })
}
Statement::DropView(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "DROP VIEW".to_string() })
}
Statement::AlterTable(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "ALTER TABLE".to_string() })
}
Statement::TruncateTable(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "TRUNCATE".to_string() })
}
Statement::BeginTransaction(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "BEGIN TRANSACTION".to_string() })
}
Statement::Commit(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "COMMIT".to_string() })
}
Statement::Rollback(_) => {
Err(ReadOnlyError::NotReadOnly { statement_type: "ROLLBACK".to_string() })
}
_ => {
Err(ReadOnlyError::NotReadOnly {
statement_type: format!("{:?}", std::mem::discriminant(&statement)),
})
}
}
}
}
#[cfg(test)]
mod tests {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_storage::Row;
use vibesql_types::{DataType, SqlValue};
use super::*;
fn create_test_db() -> Database {
let mut db = Database::new();
db.catalog.set_case_sensitive_identifiers(false);
let columns = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(100) },
true,
),
];
let schema =
TableSchema::with_primary_key("users".to_string(), columns, vec!["id".to_string()]);
db.create_table(schema).unwrap();
let row1 =
Row::new(vec![SqlValue::Integer(1), SqlValue::Varchar(arcstr::ArcStr::from("Alice"))]);
let row2 =
Row::new(vec![SqlValue::Integer(2), SqlValue::Varchar(arcstr::ArcStr::from("Bob"))]);
let row3 = Row::new(vec![
SqlValue::Integer(3),
SqlValue::Varchar(arcstr::ArcStr::from("Charlie")),
]);
db.insert_row("users", row1).unwrap();
db.insert_row("users", row2).unwrap();
db.insert_row("users", row3).unwrap();
db
}
#[test]
fn test_query_select_all() {
let db = create_test_db();
let result = db.query("SELECT * FROM users").unwrap();
assert_eq!(result.rows.len(), 3);
assert_eq!(result.columns.len(), 2);
}
#[test]
fn test_query_select_with_where() {
let db = create_test_db();
let result = db.query("SELECT * FROM users WHERE id = 1").unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0].values[0], SqlValue::Integer(1));
assert_eq!(result.rows[0].values[1], SqlValue::Varchar(arcstr::ArcStr::from("Alice")));
}
#[test]
fn test_query_select_specific_columns() {
let db = create_test_db();
let result = db.query("SELECT name FROM users WHERE id = 2").unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.columns.len(), 1);
assert_eq!(result.columns[0].to_lowercase(), "name");
assert_eq!(result.rows[0].values[0], SqlValue::Varchar(arcstr::ArcStr::from("Bob")));
}
#[test]
fn test_query_select_count() {
let db = create_test_db();
let result = db.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0].values[0], SqlValue::Integer(3));
}
#[test]
fn test_query_rejects_insert() {
let db = create_test_db();
let result = db.query("INSERT INTO users (id, name) VALUES (4, 'David')");
assert!(matches!(
result,
Err(ReadOnlyError::NotReadOnly { statement_type }) if statement_type == "INSERT"
));
}
#[test]
fn test_query_rejects_update() {
let db = create_test_db();
let result = db.query("UPDATE users SET name = 'Alicia' WHERE id = 1");
assert!(matches!(
result,
Err(ReadOnlyError::NotReadOnly { statement_type }) if statement_type == "UPDATE"
));
}
#[test]
fn test_query_rejects_delete() {
let db = create_test_db();
let result = db.query("DELETE FROM users WHERE id = 1");
assert!(matches!(
result,
Err(ReadOnlyError::NotReadOnly { statement_type }) if statement_type == "DELETE"
));
}
#[test]
fn test_query_rejects_create_table() {
let db = create_test_db();
let result = db.query("CREATE TABLE test (id INT)");
assert!(matches!(
result,
Err(ReadOnlyError::NotReadOnly { statement_type }) if statement_type == "CREATE TABLE"
));
}
#[test]
fn test_query_rejects_drop_table() {
let db = create_test_db();
let result = db.query("DROP TABLE users");
assert!(matches!(
result,
Err(ReadOnlyError::NotReadOnly { statement_type }) if statement_type == "DROP TABLE"
));
}
#[test]
fn test_query_rejects_truncate() {
let db = create_test_db();
let result = db.query("TRUNCATE TABLE users");
assert!(matches!(
result,
Err(ReadOnlyError::NotReadOnly { statement_type }) if statement_type == "TRUNCATE"
));
}
#[test]
fn test_query_parse_error() {
let db = create_test_db();
let result = db.query("SELEKT * FROM users");
assert!(matches!(result, Err(ReadOnlyError::ParseError(_))));
}
#[test]
fn test_query_execution_error_table_not_found() {
let db = create_test_db();
let result = db.query("SELECT * FROM nonexistent");
assert!(matches!(result, Err(ReadOnlyError::ExecutionError(_))));
}
#[test]
fn test_query_with_order_by() {
let db = create_test_db();
let result = db.query("SELECT * FROM users ORDER BY id DESC").unwrap();
assert_eq!(result.rows.len(), 3);
assert_eq!(result.rows[0].values[0], SqlValue::Integer(3));
assert_eq!(result.rows[2].values[0], SqlValue::Integer(1));
}
#[test]
fn test_query_with_limit() {
let db = create_test_db();
let result = db.query("SELECT * FROM users LIMIT 2").unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_query_with_aggregation() {
let db = create_test_db();
let result = db.query("SELECT COUNT(*), MAX(id), MIN(id) FROM users").unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0].values[0], SqlValue::Integer(3)); assert_eq!(result.rows[0].values[1], SqlValue::Integer(3)); assert_eq!(result.rows[0].values[2], SqlValue::Integer(1)); }
#[test]
fn test_query_immutability() {
let db = create_test_db();
let result1 = db.query("SELECT COUNT(*) FROM users").unwrap();
let result2 = db.query("SELECT * FROM users WHERE id = 1").unwrap();
let result3 = db.query("SELECT name FROM users").unwrap();
assert_eq!(result1.rows[0].values[0], SqlValue::Integer(3));
assert_eq!(result2.rows.len(), 1);
assert_eq!(result3.rows.len(), 3);
}
}
#[derive(Clone)]
pub struct SharedDatabase {
inner: Arc<RwLock<Database>>,
}
impl SharedDatabase {
pub fn new(db: Database) -> Self {
Self { inner: Arc::new(RwLock::new(db)) }
}
pub fn from_arc(inner: Arc<RwLock<Database>>) -> Self {
Self { inner }
}
pub fn into_inner(self) -> Arc<RwLock<Database>> {
self.inner
}
pub fn as_arc(&self) -> &Arc<RwLock<Database>> {
&self.inner
}
pub async fn read(&self) -> RwLockReadGuard<'_, Database> {
self.inner.read().await
}
pub async fn write(&self) -> RwLockWriteGuard<'_, Database> {
self.inner.write().await
}
pub async fn query(&self, sql: &str) -> Result<SelectResult, ReadOnlyError> {
let guard = self.read().await;
guard.query(sql)
}
}
impl Default for SharedDatabase {
fn default() -> Self {
Self::new(Database::new())
}
}
impl std::fmt::Debug for SharedDatabase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedDatabase").field("inner", &"Arc<RwLock<Database>>").finish()
}
}
#[cfg(test)]
mod shared_database_tests {
use super::*;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_storage::Row;
use vibesql_types::{DataType, SqlValue};
async fn create_shared_test_db() -> SharedDatabase {
let mut db = Database::new();
db.catalog.set_case_sensitive_identifiers(false);
let columns = vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(100) },
true,
),
];
let schema =
TableSchema::with_primary_key("users".to_string(), columns, vec!["id".to_string()]);
db.create_table(schema).unwrap();
let row1 =
Row::new(vec![SqlValue::Integer(1), SqlValue::Varchar(arcstr::ArcStr::from("Alice"))]);
let row2 =
Row::new(vec![SqlValue::Integer(2), SqlValue::Varchar(arcstr::ArcStr::from("Bob"))]);
db.insert_row("users", row1).unwrap();
db.insert_row("users", row2).unwrap();
SharedDatabase::new(db)
}
#[tokio::test]
async fn test_shared_query() {
let db = create_shared_test_db().await;
let result = db.query("SELECT * FROM users").await.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[tokio::test]
async fn test_shared_query_with_filter() {
let db = create_shared_test_db().await;
let result = db.query("SELECT * FROM users WHERE id = 1").await.unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0].values[0], SqlValue::Integer(1));
}
#[tokio::test]
async fn test_shared_query_rejects_mutations() {
let db = create_shared_test_db().await;
let result = db.query("INSERT INTO users VALUES (3, 'Charlie')").await;
assert!(matches!(result, Err(ReadOnlyError::NotReadOnly { .. })));
}
#[tokio::test]
async fn test_concurrent_reads() {
let db = create_shared_test_db().await;
let mut handles = Vec::new();
for i in 0..10 {
let db_clone = db.clone();
handles.push(tokio::spawn(async move {
let result = db_clone.query("SELECT COUNT(*) FROM users").await.unwrap();
(i, result.rows[0].values[0].clone())
}));
}
for handle in handles {
let (_, count) = handle.await.unwrap();
assert_eq!(count, SqlValue::Integer(2));
}
}
#[tokio::test]
async fn test_read_write_isolation() {
let db = create_shared_test_db().await;
let result_before = db.query("SELECT COUNT(*) FROM users").await.unwrap();
assert_eq!(result_before.rows[0].values[0], SqlValue::Integer(2));
{
let mut guard = db.write().await;
let row = Row::new(vec![
SqlValue::Integer(3),
SqlValue::Varchar(arcstr::ArcStr::from("Charlie")),
]);
guard.insert_row("users", row).unwrap();
}
let result_after = db.query("SELECT COUNT(*) FROM users").await.unwrap();
assert_eq!(result_after.rows[0].values[0], SqlValue::Integer(3));
}
#[tokio::test]
async fn test_from_arc() {
let inner = Arc::new(RwLock::new(Database::new()));
let db = SharedDatabase::from_arc(inner.clone());
assert!(Arc::ptr_eq(db.as_arc(), &inner));
}
}