pub mod banner;
pub mod cli;
pub mod config;
pub mod error;
pub mod migration;
pub mod monitor;
#[cfg(feature = "server")]
pub mod server;
pub mod sql;
pub mod storage;
pub mod watcher;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use crate::config::Config;
use crate::error::Result;
use crate::sql::executor::{ExecuteResult, Executor};
use crate::sql::parser::SqlParser;
use crate::sql::types::Value;
use crate::storage::engine::SharedEngine;
use crate::storage::redb_engine::RedbEngine;
pub struct Database {
engine: SharedEngine,
executor: Executor,
config: Config,
}
impl Database {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let engine = RedbEngine::open(path)?;
let shared = Arc::new(engine);
let executor = Executor::new(shared.clone());
Ok(Self {
engine: shared,
executor,
config: Config::default(),
})
}
pub fn open_with_config<P: AsRef<Path>>(path: P, config: Config) -> Result<Self> {
let engine = RedbEngine::open(path)?;
let shared = Arc::new(engine);
let executor = Executor::new(shared.clone());
Ok(Self {
engine: shared,
executor,
config,
})
}
pub fn execute(&self, sql: &str) -> Result<ExecuteResult> {
let stmt = SqlParser::parse(sql)?;
self.executor.execute(&stmt)
}
pub fn query(&self, sql: &str) -> Result<Vec<HashMap<String, Value>>> {
let result = self.execute(sql)?;
match result {
ExecuteResult::QueryResult { columns, rows, .. } => {
let maps: Vec<HashMap<String, Value>> = rows
.into_iter()
.map(|row| columns.iter().cloned().zip(row.into_iter()).collect())
.collect();
Ok(maps)
}
_ => Err(crate::error::ExecError::NotImplemented(
"query() 只能用于 SELECT 语句,非查询语句请使用 execute()".into()
).into()),
}
}
pub fn executor(&self) -> &Executor {
&self.executor
}
pub fn engine(&self) -> &SharedEngine {
&self.engine
}
pub fn config(&self) -> &Config {
&self.config
}
#[cfg(feature = "server")]
pub async fn serve<A>(self, addr: A) -> Result<()>
where
A: tokio::net::ToSocketAddrs + Send + 'static,
{
self.serve_with_token(addr, None).await
}
#[cfg(feature = "server")]
pub async fn serve_with_token<A>(self, addr: A, api_token: Option<String>) -> Result<()>
where
A: tokio::net::ToSocketAddrs + Send + 'static,
{
use crate::server::build_routes;
use crate::server::error::AppState;
use tracing::info;
let state = AppState::new(".", "data.db", api_token)?;
let app = build_routes(state);
let listener = tokio::net::TcpListener::bind(addr).await?;
info!(
"RustMinidb server starting on {}",
listener.local_addr()?
);
axum::serve(listener, app).await?;
Ok(())
}
}
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_database_open() {
let dir = TempDir::new().unwrap();
let db = Database::open(dir.path().join("test.db")).unwrap();
assert_eq!(db.config().server.port, 8080);
}
#[test]
fn test_database_create_and_insert() {
let dir = TempDir::new().unwrap();
let db = Database::open(dir.path().join("test.db")).unwrap();
db.execute("CREATE TABLE users (id INT PRIMARY KEY, name TEXT, age INT)")
.unwrap();
db.execute("INSERT INTO users VALUES (1, 'Alice', 30)")
.unwrap();
let rows = db.query("SELECT * FROM users").unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0].get("name").unwrap(),
&Value::Text("Alice".into())
);
}
#[test]
fn test_database_full_crud() {
let dir = TempDir::new().unwrap();
let db = Database::open(dir.path().join("test.db")).unwrap();
db.execute("CREATE TABLE items (id INT PRIMARY KEY, label TEXT, price FLOAT)")
.unwrap();
db.execute("INSERT INTO items VALUES (1, 'item1', 10.5)")
.unwrap();
db.execute("INSERT INTO items VALUES (2, 'item2', 20.0)")
.unwrap();
let rows = db.query("SELECT * FROM items").unwrap();
assert_eq!(rows.len(), 2);
db.execute("UPDATE items SET price = 15.0 WHERE id = 1")
.unwrap();
let rows = db.query("SELECT price FROM items WHERE id = 1").unwrap();
assert_eq!(rows[0].get("price").unwrap(), &Value::Float(15.0));
db.execute("DELETE FROM items WHERE id = 2").unwrap();
let rows = db.query("SELECT * FROM items").unwrap();
assert_eq!(rows.len(), 1);
db.execute("DROP TABLE items").unwrap();
assert!(db.execute("SELECT * FROM items").is_err());
}
#[test]
fn test_error_on_missing_table() {
let dir = TempDir::new().unwrap();
let db = Database::open(dir.path().join("test.db")).unwrap();
let result = db.execute("SELECT * FROM nonexistent");
assert!(result.is_err());
}
#[test]
fn test_error_on_duplicate_pk() {
let dir = TempDir::new().unwrap();
let db = Database::open(dir.path().join("test.db")).unwrap();
db.execute("CREATE TABLE t (id INT PRIMARY KEY, val TEXT)")
.unwrap();
db.execute("INSERT INTO t VALUES (1, 'a')").unwrap();
let result = db.execute("INSERT INTO t VALUES (1, 'b')");
assert!(result.is_err());
}
}