use crate::UtilsError;
use scirs2_core::ndarray::{Array1, Array2};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
pub host: String,
pub port: u16,
pub database: String,
pub username: String,
pub password: String,
pub pool_size: usize,
pub connection_timeout: Duration,
pub query_timeout: Duration,
pub ssl_mode: SslMode,
pub additional_params: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SslMode {
Disable,
Prefer,
Require,
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
host: "localhost".to_string(),
port: 5432,
database: "postgres".to_string(),
username: "postgres".to_string(),
password: String::new(),
pool_size: 10,
connection_timeout: Duration::from_secs(30),
query_timeout: Duration::from_secs(60),
ssl_mode: SslMode::Prefer,
additional_params: HashMap::new(),
}
}
}
impl DatabaseConfig {
pub fn new(host: String, database: String, username: String, password: String) -> Self {
Self {
host,
database,
username,
password,
..Default::default()
}
}
pub fn with_port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn with_pool_size(mut self, pool_size: usize) -> Self {
self.pool_size = pool_size;
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.connection_timeout = timeout;
self.query_timeout = timeout;
self
}
pub fn connection_string(&self) -> String {
let ssl_param = match self.ssl_mode {
SslMode::Disable => "sslmode=disable",
SslMode::Prefer => "sslmode=prefer",
SslMode::Require => "sslmode=require",
};
let mut params = vec![
format!("host={}", self.host),
format!("port={}", self.port),
format!("dbname={}", self.database),
format!("user={}", self.username),
ssl_param.to_string(),
];
if !self.password.is_empty() {
params.push(format!("password={}", self.password));
}
for (key, value) in &self.additional_params {
params.push(format!("{key}={value}"));
}
params.join(" ")
}
}
#[derive(thiserror::Error, Debug, Clone)]
pub enum DatabaseError {
#[error("Connection failed: {0}")]
ConnectionFailed(String),
#[error("Query execution failed: {0}")]
QueryFailed(String),
#[error("Transaction failed: {0}")]
TransactionFailed(String),
#[error("Data conversion failed: {0}")]
ConversionFailed(String),
#[error("Connection pool exhausted")]
PoolExhausted,
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
}
impl From<DatabaseError> for UtilsError {
fn from(err: DatabaseError) -> Self {
UtilsError::InvalidParameter(err.to_string())
}
}
#[derive(Debug, Clone)]
pub struct Row {
columns: HashMap<String, Value>,
column_order: Vec<String>,
}
impl Row {
pub fn new() -> Self {
Self {
columns: HashMap::new(),
column_order: Vec::new(),
}
}
pub fn insert<T: Into<Value>>(&mut self, column: String, value: T) {
if !self.columns.contains_key(&column) {
self.column_order.push(column.clone());
}
self.columns.insert(column, value.into());
}
pub fn get(&self, column: &str) -> Option<&Value> {
self.columns.get(column)
}
pub fn get_string(&self, column: &str) -> Option<String> {
self.get(column)?.as_string()
}
pub fn get_f64(&self, column: &str) -> Option<f64> {
self.get(column)?.as_f64()
}
pub fn get_i64(&self, column: &str) -> Option<i64> {
self.get(column)?.as_i64()
}
pub fn columns(&self) -> &[String] {
&self.column_order
}
}
impl Default for Row {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
Float(f64),
String(String),
Bytes(Vec<u8>),
}
impl Value {
pub fn as_string(&self) -> Option<String> {
match self {
Value::String(s) => Some(s.clone()),
Value::Int(i) => Some(i.to_string()),
Value::Float(f) => Some(f.to_string()),
Value::Bool(b) => Some(b.to_string()),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Value::Float(f) => Some(*f),
Value::Int(i) => Some(*i as f64),
Value::String(s) => s.parse().ok(),
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Value::Int(i) => Some(*i),
Value::Float(f) => Some(*f as i64),
Value::String(s) => s.parse().ok(),
_ => None,
}
}
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Value::String(s)
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Value::String(s.to_string())
}
}
impl From<i64> for Value {
fn from(i: i64) -> Self {
Value::Int(i)
}
}
impl From<i32> for Value {
fn from(i: i32) -> Self {
Value::Int(i as i64)
}
}
impl From<f64> for Value {
fn from(f: f64) -> Self {
Value::Float(f)
}
}
impl From<f32> for Value {
fn from(f: f32) -> Self {
Value::Float(f as f64)
}
}
impl From<bool> for Value {
fn from(b: bool) -> Self {
Value::Bool(b)
}
}
pub trait Connection {
fn execute(&self, query: &Query) -> Result<QueryResult, DatabaseError>;
fn query(&self, query: &Query) -> Result<ResultSet, DatabaseError>;
fn begin_transaction(&self) -> Result<Transaction, DatabaseError>;
fn close(&self) -> Result<(), DatabaseError>;
fn is_connected(&self) -> bool;
}
pub struct MockConnection {
connected: bool,
mock_data: HashMap<String, Vec<Row>>,
}
impl MockConnection {
pub fn new() -> Self {
Self {
connected: true,
mock_data: HashMap::new(),
}
}
pub fn add_mock_data(&mut self, table: String, rows: Vec<Row>) {
self.mock_data.insert(table, rows);
}
}
impl Default for MockConnection {
fn default() -> Self {
Self::new()
}
}
impl Connection for MockConnection {
fn execute(&self, _query: &Query) -> Result<QueryResult, DatabaseError> {
if !self.connected {
return Err(DatabaseError::ConnectionFailed("Not connected".to_string()));
}
Ok(QueryResult {
rows_affected: 1,
execution_time: Duration::from_millis(10),
})
}
fn query(&self, _query: &Query) -> Result<ResultSet, DatabaseError> {
if !self.connected {
return Err(DatabaseError::ConnectionFailed("Not connected".to_string()));
}
let mut result = ResultSet::new(vec!["id".to_string(), "value".to_string()]);
result.set_execution_time(Duration::from_millis(5));
Ok(result)
}
fn begin_transaction(&self) -> Result<Transaction, DatabaseError> {
if !self.connected {
return Err(DatabaseError::ConnectionFailed("Not connected".to_string()));
}
Ok(Transaction::new())
}
fn close(&self) -> Result<(), DatabaseError> {
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
}
pub struct DatabasePool {
#[allow(dead_code)]
config: DatabaseConfig,
connections: Arc<Mutex<Vec<Box<dyn Connection + Send + Sync>>>>,
max_size: usize,
}
impl DatabasePool {
pub fn new(config: DatabaseConfig) -> Self {
let max_size = config.pool_size;
Self {
config,
connections: Arc::new(Mutex::new(Vec::new())),
max_size,
}
}
pub fn get_connection(&self) -> Result<Box<dyn Connection + Send + Sync>, DatabaseError> {
Ok(Box::new(MockConnection::new()))
}
pub fn return_connection(&self, _connection: Box<dyn Connection + Send + Sync>) {
}
pub fn size(&self) -> usize {
self.connections
.lock()
.expect("operation should succeed")
.len()
}
pub fn max_size(&self) -> usize {
self.max_size
}
}
pub struct QueryBuilder {
query_type: QueryType,
table: Option<String>,
columns: Vec<String>,
conditions: Vec<String>,
joins: Vec<String>,
order_by: Vec<String>,
group_by: Vec<String>,
having: Vec<String>,
limit: Option<usize>,
offset: Option<usize>,
parameters: Vec<Value>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum QueryType {
Select,
Insert,
Update,
Delete,
}
impl QueryBuilder {
pub fn select() -> Self {
Self {
query_type: QueryType::Select,
table: None,
columns: Vec::new(),
conditions: Vec::new(),
joins: Vec::new(),
order_by: Vec::new(),
group_by: Vec::new(),
having: Vec::new(),
limit: None,
offset: None,
parameters: Vec::new(),
}
}
pub fn from(mut self, table: &str) -> Self {
self.table = Some(table.to_string());
self
}
pub fn columns(mut self, columns: &[&str]) -> Self {
self.columns = columns.iter().map(|s| s.to_string()).collect();
self
}
pub fn where_clause(mut self, condition: &str) -> Self {
self.conditions.push(condition.to_string());
self
}
pub fn join(mut self, join_clause: &str) -> Self {
self.joins.push(join_clause.to_string());
self
}
pub fn order_by(mut self, column: &str, ascending: bool) -> Self {
let direction = if ascending { "ASC" } else { "DESC" };
self.order_by.push(format!("{column} {direction}"));
self
}
pub fn group_by(mut self, columns: &[&str]) -> Self {
self.group_by = columns.iter().map(|s| s.to_string()).collect();
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
pub fn parameter<T: Into<Value>>(mut self, value: T) -> Self {
self.parameters.push(value.into());
self
}
pub fn build(self) -> Query {
let sql = self.build_sql();
Query::new(sql, self.parameters)
}
fn build_sql(&self) -> String {
match self.query_type {
QueryType::Select => self.build_select(),
_ => "".to_string(), }
}
fn build_select(&self) -> String {
let mut query = String::new();
query.push_str("SELECT ");
if self.columns.is_empty() {
query.push('*');
} else {
query.push_str(&self.columns.join(", "));
}
if let Some(table) = &self.table {
query.push_str(&format!(" FROM {table}"));
}
for join in &self.joins {
query.push_str(&format!(" {join}"));
}
if !self.conditions.is_empty() {
query.push_str(&format!(" WHERE {}", self.conditions.join(" AND ")));
}
if !self.group_by.is_empty() {
query.push_str(&format!(" GROUP BY {}", self.group_by.join(", ")));
}
if !self.having.is_empty() {
query.push_str(&format!(" HAVING {}", self.having.join(" AND ")));
}
if !self.order_by.is_empty() {
query.push_str(&format!(" ORDER BY {}", self.order_by.join(", ")));
}
if let Some(limit) = self.limit {
query.push_str(&format!(" LIMIT {limit}"));
}
if let Some(offset) = self.offset {
query.push_str(&format!(" OFFSET {offset}"));
}
query
}
}
#[derive(Debug, Clone)]
pub struct Query {
sql: String,
parameters: Vec<Value>,
}
impl Query {
pub fn new(sql: String, parameters: Vec<Value>) -> Self {
Self { sql, parameters }
}
pub fn sql(&self) -> &str {
&self.sql
}
pub fn parameters(&self) -> &[Value] {
&self.parameters
}
}
#[derive(Debug, Clone)]
pub struct QueryResult {
pub rows_affected: usize,
pub execution_time: Duration,
}
#[derive(Debug, Clone)]
pub struct ResultSet {
rows: Vec<Row>,
columns: Vec<String>,
execution_time: Duration,
#[allow(dead_code)]
rows_affected: Option<usize>,
}
impl ResultSet {
pub fn new(columns: Vec<String>) -> Self {
Self {
rows: Vec::new(),
columns,
execution_time: Duration::from_secs(0),
rows_affected: None,
}
}
pub fn add_row(&mut self, row: Row) {
self.rows.push(row);
}
pub fn rows(&self) -> &[Row] {
&self.rows
}
pub fn columns(&self) -> &[String] {
&self.columns
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn execution_time(&self) -> Duration {
self.execution_time
}
pub fn set_execution_time(&mut self, time: Duration) {
self.execution_time = time;
}
pub fn to_array2(&self) -> Result<Array2<f64>, DatabaseError> {
if self.rows.is_empty() {
return Err(DatabaseError::ConversionFailed(
"Cannot convert empty result set to array".to_string(),
));
}
let n_rows = self.rows.len();
let n_cols = self.columns.len();
let mut data = Array2::zeros((n_rows, n_cols));
for (row_idx, row) in self.rows.iter().enumerate() {
for (col_idx, col_name) in self.columns.iter().enumerate() {
let value = row.get(col_name).ok_or_else(|| {
DatabaseError::ConversionFailed(format!("Column '{col_name}' not found in row"))
})?;
let numeric_value = value.as_f64().ok_or_else(|| {
DatabaseError::ConversionFailed(format!(
"Cannot convert value to f64: {value:?}"
))
})?;
data[[row_idx, col_idx]] = numeric_value;
}
}
Ok(data)
}
pub fn column_to_array1(&self, column: &str) -> Result<Array1<f64>, DatabaseError> {
if !self.columns.contains(&column.to_string()) {
return Err(DatabaseError::ConversionFailed(format!(
"Column '{column}' not found"
)));
}
let mut data = Array1::zeros(self.rows.len());
for (idx, row) in self.rows.iter().enumerate() {
let value = row.get(column).ok_or_else(|| {
DatabaseError::ConversionFailed(format!("Column '{column}' not found in row"))
})?;
let numeric_value = value.as_f64().ok_or_else(|| {
DatabaseError::ConversionFailed(format!("Cannot convert value to f64: {value:?}"))
})?;
data[idx] = numeric_value;
}
Ok(data)
}
pub fn unique_values(&self, column: &str) -> Result<Vec<Value>, DatabaseError> {
if !self.columns.contains(&column.to_string()) {
return Err(DatabaseError::ConversionFailed(format!(
"Column '{column}' not found"
)));
}
let mut unique_values = Vec::new();
for row in &self.rows {
if let Some(value) = row.get(column) {
if !unique_values.contains(value) {
unique_values.push(value.clone());
}
}
}
Ok(unique_values)
}
}
pub struct Transaction {
committed: bool,
rolled_back: bool,
}
impl Transaction {
pub fn new() -> Self {
Self {
committed: false,
rolled_back: false,
}
}
pub fn commit(&mut self) -> Result<(), DatabaseError> {
if self.rolled_back {
return Err(DatabaseError::TransactionFailed(
"Transaction already rolled back".to_string(),
));
}
self.committed = true;
Ok(())
}
pub fn rollback(&mut self) -> Result<(), DatabaseError> {
if self.committed {
return Err(DatabaseError::TransactionFailed(
"Transaction already committed".to_string(),
));
}
self.rolled_back = true;
Ok(())
}
pub fn is_committed(&self) -> bool {
self.committed
}
pub fn is_rolled_back(&self) -> bool {
self.rolled_back
}
}
impl Default for Transaction {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for DatabaseConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}@{}:{}/{}",
self.username, self.host, self.port, self.database
)
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_database_config() {
let config = DatabaseConfig::new(
"localhost".to_string(),
"testdb".to_string(),
"user".to_string(),
"pass".to_string(),
)
.with_port(3306)
.with_pool_size(5);
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 3306);
assert_eq!(config.pool_size, 5);
let conn_str = config.connection_string();
assert!(conn_str.contains("host=localhost"));
assert!(conn_str.contains("port=3306"));
}
#[test]
fn test_value_conversions() {
let int_val = Value::from(42i64);
assert_eq!(int_val.as_i64(), Some(42));
assert_eq!(int_val.as_f64(), Some(42.0));
let float_val = Value::from(std::f64::consts::PI);
assert_eq!(float_val.as_f64(), Some(std::f64::consts::PI));
let string_val = Value::from("hello");
assert_eq!(string_val.as_string(), Some("hello".to_string()));
}
#[test]
fn test_row_operations() {
let mut row = Row::new();
row.insert("id".to_string(), 1i64);
row.insert("name".to_string(), "test");
row.insert("score".to_string(), 95.5f64);
assert_eq!(row.get_i64("id"), Some(1));
assert_eq!(row.get_string("name"), Some("test".to_string()));
assert_eq!(row.get_f64("score"), Some(95.5));
assert_eq!(row.columns().len(), 3);
}
#[test]
fn test_result_set_array_conversion() {
let mut result_set = ResultSet::new(vec!["a".to_string(), "b".to_string()]);
let mut row1 = Row::new();
row1.insert("a".to_string(), 1.0f64);
row1.insert("b".to_string(), 2.0f64);
result_set.add_row(row1);
let mut row2 = Row::new();
row2.insert("a".to_string(), 3.0f64);
row2.insert("b".to_string(), 4.0f64);
result_set.add_row(row2);
let array = result_set.to_array2().expect("operation should succeed");
assert_eq!(array.shape(), &[2, 2]);
assert_eq!(array[[0, 0]], 1.0);
assert_eq!(array[[1, 1]], 4.0);
}
#[test]
fn test_query_builder() {
let query = QueryBuilder::select()
.columns(&["id", "name", "score"])
.from("users")
.where_clause("score > 80")
.order_by("score", false)
.limit(10)
.build();
let sql = query.sql();
assert!(sql.contains("SELECT id, name, score"));
assert!(sql.contains("FROM users"));
assert!(sql.contains("WHERE score > 80"));
assert!(sql.contains("ORDER BY score DESC"));
assert!(sql.contains("LIMIT 10"));
}
#[test]
fn test_mock_connection() {
let connection = MockConnection::new();
assert!(connection.is_connected());
let query = Query::new("SELECT 1".to_string(), vec![]);
let result = connection
.execute(&query)
.expect("operation should succeed");
assert_eq!(result.rows_affected, 1);
let result_set = connection.query(&query).expect("operation should succeed");
assert_eq!(result_set.columns().len(), 2);
}
#[test]
fn test_transaction() {
let mut transaction = Transaction::new();
assert!(!transaction.is_committed());
assert!(!transaction.is_rolled_back());
transaction.commit().expect("operation should succeed");
assert!(transaction.is_committed());
assert!(transaction.rollback().is_err());
}
#[test]
fn test_database_pool() {
let config = DatabaseConfig::default();
let pool = DatabasePool::new(config);
assert_eq!(pool.max_size(), 10);
let connection = pool.get_connection().expect("operation should succeed");
assert!(connection.is_connected());
}
#[test]
fn test_result_set_unique_values() {
let mut result_set = ResultSet::new(vec!["category".to_string()]);
let mut row1 = Row::new();
row1.insert("category".to_string(), "A");
result_set.add_row(row1);
let mut row2 = Row::new();
row2.insert("category".to_string(), "B");
result_set.add_row(row2);
let mut row3 = Row::new();
row3.insert("category".to_string(), "A");
result_set.add_row(row3);
let unique_values = result_set
.unique_values("category")
.expect("operation should succeed");
assert_eq!(unique_values.len(), 2);
assert!(unique_values.contains(&Value::String("A".to_string())));
assert!(unique_values.contains(&Value::String("B".to_string())));
}
}