feat: Implement conditional compilation for SQLite and Postgres database backends using Cargo features.

This commit is contained in:
2025-12-25 22:53:51 +01:00
parent b53dbf2ea8
commit 19e16a09f8
6 changed files with 114 additions and 19 deletions

View File

@@ -3,14 +3,19 @@ name = "notes-infra"
version = "0.1.0"
edition = "2024"
[features]
default = ["sqlite"]
sqlite = ["sqlx/sqlite", "tower-sessions-sqlx-store/sqlite"]
postgres = ["sqlx/postgres", "tower-sessions-sqlx-store/postgres"]
[dependencies]
notes-domain = { path = "../notes-domain" }
async-trait = "0.1.89"
chrono = { version = "0.4.42", features = ["serde"] }
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "chrono", "migrate", "postgres"] }
sqlx = { version = "0.8.6", features = ["runtime-tokio", "chrono", "migrate"] }
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
tracing = "0.1"
uuid = { version = "1.19.0", features = ["v4", "serde"] }
tower-sessions = "0.14.0"
tower-sessions-sqlx-store = { version = "0.15.0", features = ["sqlite", "postgres"] }
tower-sessions-sqlx-store = { version = "0.15.0", default-features = false }

View File

@@ -1,7 +1,13 @@
//! Database connection pool management
use sqlx::Pool;
#[cfg(feature = "postgres")]
use sqlx::Postgres;
#[cfg(feature = "sqlite")]
use sqlx::Sqlite;
#[cfg(feature = "sqlite")]
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
use sqlx::{Pool, Postgres, Sqlite};
#[cfg(feature = "sqlite")]
use std::str::FromStr;
use std::time::Duration;
@@ -45,11 +51,14 @@ impl DatabaseConfig {
#[derive(Clone, Debug)]
pub enum DatabasePool {
#[cfg(feature = "sqlite")]
Sqlite(Pool<Sqlite>),
#[cfg(feature = "postgres")]
Postgres(Pool<Postgres>),
}
/// Create a database connection pool
#[cfg(feature = "sqlite")]
pub async fn create_pool(config: &DatabaseConfig) -> Result<SqlitePool, sqlx::Error> {
let options = SqliteConnectOptions::from_str(&config.url)?
.create_if_missing(true)
@@ -70,9 +79,11 @@ pub async fn create_pool(config: &DatabaseConfig) -> Result<SqlitePool, sqlx::Er
/// Run database migrations
pub async fn run_migrations(pool: &DatabasePool) -> Result<(), sqlx::Error> {
match pool {
#[cfg(feature = "sqlite")]
DatabasePool::Sqlite(pool) => {
sqlx::migrate!("../migrations").run(pool).await?;
}
#[cfg(feature = "postgres")]
DatabasePool::Postgres(_pool) => {
// Placeholder for Postgres migrations
// sqlx::migrate!("../migrations/postgres").run(_pool).await?;
@@ -81,6 +92,12 @@ pub async fn run_migrations(pool: &DatabasePool) -> Result<(), sqlx::Error> {
"Postgres migrations not yet implemented".into(),
));
}
#[allow(unreachable_patterns)]
_ => {
return Err(sqlx::Error::Configuration(
"No database feature enabled".into(),
));
}
}
tracing::info!("Database migrations completed successfully");

View File

@@ -1,10 +1,10 @@
use std::sync::Arc;
use crate::{DatabaseConfig, db::DatabasePool};
#[cfg(feature = "sqlite")]
use crate::{SqliteNoteRepository, SqliteTagRepository, SqliteUserRepository};
use notes_domain::{NoteRepository, TagRepository, UserRepository};
pub use crate::db::DatabasePool;
use crate::{DatabaseConfig, SqliteNoteRepository, SqliteTagRepository, SqliteUserRepository};
#[derive(Debug, thiserror::Error)]
pub enum FactoryError {
#[error("Database error: {0}")]
@@ -17,17 +17,31 @@ pub type FactoryResult<T> = Result<T, FactoryError>;
pub async fn build_database_pool(db_config: &DatabaseConfig) -> FactoryResult<DatabasePool> {
if db_config.url.starts_with("sqlite:") {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(5)
.connect(&db_config.url)
.await?;
Ok(DatabasePool::Sqlite(pool))
#[cfg(feature = "sqlite")]
{
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(5)
.connect(&db_config.url)
.await?;
Ok(DatabasePool::Sqlite(pool))
}
#[cfg(not(feature = "sqlite"))]
Err(FactoryError::NotImplemented(
"SQLite feature not enabled".to_string(),
))
} else if db_config.url.starts_with("postgres:") {
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(5)
.connect(&db_config.url)
.await?;
Ok(DatabasePool::Postgres(pool))
#[cfg(feature = "postgres")]
{
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(5)
.connect(&db_config.url)
.await?;
Ok(DatabasePool::Postgres(pool))
}
#[cfg(not(feature = "postgres"))]
Err(FactoryError::NotImplemented(
"Postgres feature not enabled".to_string(),
))
} else {
Err(FactoryError::NotImplemented(format!(
"Unsupported database URL scheme in: {}",
@@ -38,28 +52,46 @@ pub async fn build_database_pool(db_config: &DatabaseConfig) -> FactoryResult<Da
pub async fn build_note_repository(pool: &DatabasePool) -> FactoryResult<Arc<dyn NoteRepository>> {
match pool {
#[cfg(feature = "sqlite")]
DatabasePool::Sqlite(pool) => Ok(Arc::new(SqliteNoteRepository::new(pool.clone()))),
#[cfg(feature = "postgres")]
DatabasePool::Postgres(_) => Err(FactoryError::NotImplemented(
"Postgres NoteRepository".to_string(),
)),
#[allow(unreachable_patterns)]
_ => Err(FactoryError::NotImplemented(
"No database feature enabled".to_string(),
)),
}
}
pub async fn build_tag_repository(pool: &DatabasePool) -> FactoryResult<Arc<dyn TagRepository>> {
match pool {
#[cfg(feature = "sqlite")]
DatabasePool::Sqlite(pool) => Ok(Arc::new(SqliteTagRepository::new(pool.clone()))),
#[cfg(feature = "postgres")]
DatabasePool::Postgres(_) => Err(FactoryError::NotImplemented(
"Postgres TagRepository".to_string(),
)),
#[allow(unreachable_patterns)]
_ => Err(FactoryError::NotImplemented(
"No database feature enabled".to_string(),
)),
}
}
pub async fn build_user_repository(pool: &DatabasePool) -> FactoryResult<Arc<dyn UserRepository>> {
match pool {
#[cfg(feature = "sqlite")]
DatabasePool::Sqlite(pool) => Ok(Arc::new(SqliteUserRepository::new(pool.clone()))),
#[cfg(feature = "postgres")]
DatabasePool::Postgres(_) => Err(FactoryError::NotImplemented(
"Postgres UserRepository".to_string(),
)),
#[allow(unreachable_patterns)]
_ => Err(FactoryError::NotImplemented(
"No database feature enabled".to_string(),
)),
}
}
@@ -67,13 +99,19 @@ pub async fn build_session_store(
pool: &DatabasePool,
) -> FactoryResult<crate::session_store::InfraSessionStore> {
match pool {
#[cfg(feature = "sqlite")]
DatabasePool::Sqlite(pool) => {
let store = tower_sessions_sqlx_store::SqliteStore::new(pool.clone());
Ok(crate::session_store::InfraSessionStore::Sqlite(store))
}
#[cfg(feature = "postgres")]
DatabasePool::Postgres(pool) => {
let store = tower_sessions_sqlx_store::PostgresStore::new(pool.clone());
Ok(crate::session_store::InfraSessionStore::Postgres(store))
}
#[allow(unreachable_patterns)]
_ => Err(FactoryError::NotImplemented(
"No database feature enabled".to_string(),
)),
}
}

View File

@@ -16,13 +16,21 @@
pub mod db;
pub mod factory;
#[cfg(feature = "sqlite")]
pub mod note_repository;
pub mod session_store;
#[cfg(feature = "sqlite")]
pub mod tag_repository;
#[cfg(feature = "sqlite")]
pub mod user_repository;
// Re-export for convenience
pub use db::{DatabaseConfig, create_pool, run_migrations};
#[cfg(feature = "sqlite")]
pub use db::create_pool;
pub use db::{DatabaseConfig, run_migrations};
#[cfg(feature = "sqlite")]
pub use note_repository::SqliteNoteRepository;
#[cfg(feature = "sqlite")]
pub use tag_repository::SqliteTagRepository;
#[cfg(feature = "sqlite")]
pub use user_repository::SqliteUserRepository;

View File

@@ -4,11 +4,16 @@ use tower_sessions::{
SessionStore,
session::{Id, Record},
};
use tower_sessions_sqlx_store::{PostgresStore, SqliteStore};
#[cfg(feature = "postgres")]
use tower_sessions_sqlx_store::PostgresStore;
#[cfg(feature = "sqlite")]
use tower_sessions_sqlx_store::SqliteStore;
#[derive(Clone, Debug)]
pub enum InfraSessionStore {
#[cfg(feature = "sqlite")]
Sqlite(SqliteStore),
#[cfg(feature = "postgres")]
Postgres(PostgresStore),
}
@@ -16,22 +21,40 @@ pub enum InfraSessionStore {
impl SessionStore for InfraSessionStore {
async fn save(&self, session_record: &Record) -> tower_sessions::session_store::Result<()> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.save(session_record).await,
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.save(session_record).await,
#[allow(unreachable_patterns)]
_ => Err(tower_sessions::session_store::Error::Backend(
"No backend enabled".to_string(),
)),
}
}
async fn load(&self, session_id: &Id) -> tower_sessions::session_store::Result<Option<Record>> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.load(session_id).await,
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.load(session_id).await,
#[allow(unreachable_patterns)]
_ => Err(tower_sessions::session_store::Error::Backend(
"No backend enabled".to_string(),
)),
}
}
async fn delete(&self, session_id: &Id) -> tower_sessions::session_store::Result<()> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.delete(session_id).await,
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.delete(session_id).await,
#[allow(unreachable_patterns)]
_ => Err(tower_sessions::session_store::Error::Backend(
"No backend enabled".to_string(),
)),
}
}
}
@@ -39,8 +62,12 @@ impl SessionStore for InfraSessionStore {
impl InfraSessionStore {
pub async fn migrate(&self) -> Result<(), sqlx::Error> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(store) => store.migrate().await,
#[cfg(feature = "postgres")]
Self::Postgres(store) => store.migrate().await,
#[allow(unreachable_patterns)]
_ => Err(sqlx::Error::Configuration("No backend enabled".into())),
}
}
}