use crate::db::config::{DatabaseConfig, DatabaseType};
use sqlx::{Pool, Postgres, Row, Sqlite};
use std::str::FromStr;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct QueryBuilder {
db_type: DatabaseType,
param_count: usize,
}
impl QueryBuilder {
pub fn new(db_type: DatabaseType) -> Self {
Self {
db_type,
param_count: 0,
}
}
pub fn placeholder(&mut self) -> String {
self.param_count += 1;
match self.db_type {
DatabaseType::Postgres => format!("${}", self.param_count),
DatabaseType::Sqlite => "?".to_string(),
}
}
pub fn placeholders(&mut self, count: usize) -> String {
(0..count)
.map(|_| self.placeholder())
.collect::<Vec<_>>()
.join(", ")
}
pub fn reset(&mut self) {
self.param_count = 0;
}
pub fn insert_query(&mut self, table: &str, columns: &[&str]) -> String {
self.reset();
let cols = columns.join(", ");
let placeholders = self.placeholders(columns.len());
format!("INSERT INTO {} ({}) VALUES ({})", table, cols, placeholders)
}
pub fn insert_returning_query(
&mut self,
table: &str,
columns: &[&str],
returning_col: &str,
) -> String {
self.reset();
let cols = columns.join(", ");
let placeholders = self.placeholders(columns.len());
match self.db_type {
DatabaseType::Postgres => format!(
"INSERT INTO {} ({}) VALUES ({}) RETURNING {}",
table, cols, placeholders, returning_col
),
DatabaseType::Sqlite => {
format!("INSERT INTO {} ({}) VALUES ({})", table, cols, placeholders)
}
}
}
pub fn select_where_query(&mut self, table: &str, columns: &str, where_col: &str) -> String {
self.reset();
let placeholder = self.placeholder();
format!(
"SELECT {} FROM {} WHERE {} = {}",
columns, table, where_col, placeholder
)
}
pub fn batch_insert_query(
&mut self,
table: &str,
columns: &[&str],
row_count: usize,
) -> String {
self.reset();
let cols = columns.join(", ");
let value_sets: Vec<String> = (0..row_count)
.map(|_| {
let placeholders = self.placeholders(columns.len());
format!("({})", placeholders)
})
.collect();
format!(
"INSERT INTO {} ({}) VALUES {}",
table,
cols,
value_sets.join(", ")
)
}
}
#[derive(Clone)]
pub enum DatabasePool {
Postgres(Pool<Postgres>),
Sqlite(Pool<Sqlite>),
}
impl DatabasePool {
pub async fn new(config: &DatabaseConfig) -> crate::Result<Self> {
let pool = match config.db_type {
DatabaseType::Postgres => {
let connection_string = config.connection_string()?;
let max_connections = config.max_connections.unwrap_or(10);
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(max_connections)
.acquire_timeout(Duration::from_secs(30))
.connect(&connection_string)
.await
.map_err(|e| {
crate::TlsError::DatabaseError(format!(
"PostgreSQL connection failed: {}",
e
))
})?;
DatabasePool::Postgres(pool)
}
DatabaseType::Sqlite => {
let connection_string = config.connection_string()?;
let connect_options =
sqlx::sqlite::SqliteConnectOptions::from_str(&connection_string)
.map_err(|e| {
crate::TlsError::DatabaseError(format!(
"Failed to parse SQLite connection string: {}",
e
))
})?
.create_if_missing(true);
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1) .acquire_timeout(Duration::from_secs(30))
.connect_with(connect_options)
.await
.map_err(|e| {
crate::TlsError::DatabaseError(format!("SQLite connection failed: {}", e))
})?;
DatabasePool::Sqlite(pool)
}
};
Ok(pool)
}
pub fn db_type(&self) -> DatabaseType {
match self {
DatabasePool::Postgres(_) => DatabaseType::Postgres,
DatabasePool::Sqlite(_) => DatabaseType::Sqlite,
}
}
pub async fn close(&self) {
match self {
DatabasePool::Postgres(pool) => pool.close().await,
DatabasePool::Sqlite(pool) => pool.close().await,
}
}
pub fn as_postgres(&self) -> &Pool<Postgres> {
match self {
DatabasePool::Postgres(pool) => pool,
DatabasePool::Sqlite(_) => panic!("Expected PostgreSQL pool, got SQLite"),
}
}
pub fn as_sqlite(&self) -> &Pool<Sqlite> {
match self {
DatabasePool::Sqlite(pool) => pool,
DatabasePool::Postgres(_) => panic!("Expected SQLite pool, got PostgreSQL"),
}
}
pub fn try_as_postgres(&self) -> Option<&Pool<Postgres>> {
match self {
DatabasePool::Postgres(pool) => Some(pool),
DatabasePool::Sqlite(_) => None,
}
}
pub fn try_as_sqlite(&self) -> Option<&Pool<Sqlite>> {
match self {
DatabasePool::Sqlite(pool) => Some(pool),
DatabasePool::Postgres(_) => None,
}
}
pub fn query_builder(&self) -> QueryBuilder {
QueryBuilder::new(self.db_type())
}
pub async fn execute_insert_returning(
&self,
query: &str,
bindings: Vec<BindValue>,
) -> crate::Result<i64> {
match self {
DatabasePool::Postgres(pool) => {
let mut q = sqlx::query(query);
for binding in bindings {
q = binding.bind_postgres(q);
}
let row = q.fetch_one(pool).await.map_err(|e| {
crate::TlsError::DatabaseError(format!("Insert query failed: {}", e))
})?;
Ok(row.get::<i64, _>(0))
}
DatabasePool::Sqlite(pool) => {
let mut q = sqlx::query(query);
for binding in bindings {
q = binding.bind_sqlite(q);
}
let result = q.execute(pool).await.map_err(|e| {
crate::TlsError::DatabaseError(format!("Insert query failed: {}", e))
})?;
Ok(result.last_insert_rowid())
}
}
}
pub async fn execute(&self, query: &str, bindings: Vec<BindValue>) -> crate::Result<()> {
match self {
DatabasePool::Postgres(pool) => {
let mut q = sqlx::query(query);
for binding in bindings {
q = binding.bind_postgres(q);
}
q.execute(pool)
.await
.map_err(|e| crate::TlsError::DatabaseError(format!("Query failed: {}", e)))?;
}
DatabasePool::Sqlite(pool) => {
let mut q = sqlx::query(query);
for binding in bindings {
q = binding.bind_sqlite(q);
}
q.execute(pool)
.await
.map_err(|e| crate::TlsError::DatabaseError(format!("Query failed: {}", e)))?;
}
}
Ok(())
}
pub async fn fetch_optional_id(
&self,
query: &str,
bindings: Vec<BindValue>,
) -> crate::Result<Option<i64>> {
match self {
DatabasePool::Postgres(pool) => {
let mut q = sqlx::query(query);
for binding in bindings {
q = binding.bind_postgres(q);
}
let row = q
.fetch_optional(pool)
.await
.map_err(|e| crate::TlsError::DatabaseError(format!("Query failed: {}", e)))?;
Ok(row.map(|r| r.get::<i64, _>(0)))
}
DatabasePool::Sqlite(pool) => {
let mut q = sqlx::query(query);
for binding in bindings {
q = binding.bind_sqlite(q);
}
let row = q
.fetch_optional(pool)
.await
.map_err(|e| crate::TlsError::DatabaseError(format!("Query failed: {}", e)))?;
Ok(row.map(|r| r.get::<i64, _>(0)))
}
}
}
}
#[derive(Debug, Clone)]
pub enum BindValue {
Int64(i64),
Int32(i32),
Int16(i16),
String(String),
Bool(bool),
OptInt32(Option<i32>),
OptString(Option<String>),
OptBytes(Option<Vec<u8>>),
DateTime(chrono::DateTime<chrono::Utc>),
}
impl BindValue {
fn bind_postgres<'q>(
self,
query: sqlx::query::Query<'q, Postgres, sqlx::postgres::PgArguments>,
) -> sqlx::query::Query<'q, Postgres, sqlx::postgres::PgArguments> {
match self {
BindValue::Int64(v) => query.bind(v),
BindValue::Int32(v) => query.bind(v),
BindValue::Int16(v) => query.bind(v),
BindValue::String(v) => query.bind(v),
BindValue::Bool(v) => query.bind(v),
BindValue::OptInt32(v) => query.bind(v),
BindValue::OptString(v) => query.bind(v),
BindValue::OptBytes(v) => query.bind(v),
BindValue::DateTime(v) => query.bind(v),
}
}
fn bind_sqlite<'q>(
self,
query: sqlx::query::Query<'q, Sqlite, sqlx::sqlite::SqliteArguments<'q>>,
) -> sqlx::query::Query<'q, Sqlite, sqlx::sqlite::SqliteArguments<'q>> {
match self {
BindValue::Int64(v) => query.bind(v),
BindValue::Int32(v) => query.bind(v),
BindValue::Int16(v) => query.bind(v as i32), BindValue::String(v) => query.bind(v),
BindValue::Bool(v) => query.bind(v),
BindValue::OptInt32(v) => query.bind(v),
BindValue::OptString(v) => query.bind(v),
BindValue::OptBytes(v) => query.bind(v),
BindValue::DateTime(v) => query.bind(v),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[tokio::test]
async fn test_sqlite_pool_creation() {
let config = DatabaseConfig::sqlite(PathBuf::from(":memory:"));
let pool = DatabasePool::new(&config)
.await
.expect("test assertion should succeed");
assert_eq!(pool.db_type(), DatabaseType::Sqlite);
pool.close().await;
}
}