- Add complete dark mode system with theme context and toggle - Implement dark mode toggle component in navigation menu - Add client-side routing with SSR-safe signal handling - Fix language selector styling for better dark mode compatibility - Add documentation system with mdBook integration - Improve navigation menu with proper external/internal link handling - Add comprehensive project documentation and configuration - Enhance theme system with localStorage persistence - Fix arena panic issues during server-side rendering - Add proper TypeScript configuration and build optimizations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
10 KiB
Database Abstraction Layer
Why Database Abstraction is the Better Solution
You were absolutely right to question why we don't use a database abstraction and database-agnostic auth service instead of forcing users to choose between PostgreSQL or disabling features. Here's why a database abstraction layer is the superior architectural approach:
Current Problems
1. Tight Coupling
- Auth services are hardcoded to
PgPool - Can't easily switch between databases
- Forces architectural decisions on users
2. Limited Flexibility
- SQLite users must disable auth features
- PostgreSQL requirement creates setup barriers
- No support for other databases (MySQL, etc.)
3. Development Friction
- New developers need PostgreSQL setup
- Docker dependency for simple development
- Complex local environment requirements
4. Testing Complexity
- Hard to test with different databases
- No in-memory testing options
- Database-specific test setups
Database Abstraction Benefits
1. Loose Coupling
// Instead of this (tight coupling):
pub struct AuthRepository {
pool: PgPool, // ❌ Hardcoded to PostgreSQL
}
// We use this (loose coupling):
pub struct AuthRepository {
database: Arc<dyn DatabaseConnection>, // ✅ Database agnostic
}
2. Database Flexibility
// Works with any database:
let database = match db_url {
url if url.starts_with("sqlite:") => SQLiteDatabase::new(url).await?,
url if url.starts_with("postgres://") => PostgreSQLDatabase::new(url).await?,
url if url.starts_with("mysql://") => MySQLDatabase::new(url).await?,
_ => return Err("Unsupported database"),
};
let auth_repo = AuthRepository::new(database);
3. Easy Development Setup
// Development: Just works with SQLite
let config = DatabaseConfig {
url: "sqlite:data/development.db".to_string(),
// ... other settings
};
// Production: Use PostgreSQL for performance
let config = DatabaseConfig {
url: "postgresql://user:pass@host/db".to_string(),
// ... other settings
};
4. Better Testing
// Unit tests with in-memory database
#[tokio::test]
async fn test_user_creation() {
let db = InMemoryDatabase::new().await;
let auth_repo = AuthRepository::new(db);
let user = auth_repo.create_user(&user_data).await?;
assert_eq!(user.email, "test@example.com");
}
Implementation Architecture
Core Traits
#[async_trait]
pub trait DatabaseConnection: Send + Sync + Clone + 'static {
type Row: DatabaseRow;
async fn execute(&self, query: &str, params: &[&dyn DatabaseParam]) -> Result<u64>;
async fn fetch_one(&self, query: &str, params: &[&dyn DatabaseParam]) -> Result<Self::Row>;
async fn fetch_optional(&self, query: &str, params: &[&dyn DatabaseParam]) -> Result<Option<Self::Row>>;
async fn fetch_all(&self, query: &str, params: &[&dyn DatabaseParam]) -> Result<Vec<Self::Row>>;
async fn begin_transaction(&self) -> Result<Box<dyn DatabaseTransaction<Row = Self::Row>>>;
}
pub trait DatabaseRow: Debug + Send + Sync {
fn get_string(&self, column: &str) -> Result<String>;
fn get_i32(&self, column: &str) -> Result<i32>;
fn get_uuid(&self, column: &str) -> Result<Uuid>;
// ... other type getters
}
Database-Agnostic Repository
#[async_trait]
pub trait AuthRepositoryTrait: Send + Sync + Clone + 'static {
async fn create_user(&self, user: &CreateUserRequest) -> Result<User>;
async fn find_user_by_email(&self, email: &str) -> Result<Option<User>>;
async fn update_user(&self, user: &User) -> Result<()>;
// ... other auth operations
}
pub struct AuthRepository {
database: Arc<dyn DatabaseConnection>,
}
impl AuthRepository {
pub fn new(database: Arc<dyn DatabaseConnection>) -> Self {
Self { database }
}
}
#[async_trait]
impl AuthRepositoryTrait for AuthRepository {
async fn create_user(&self, user: &CreateUserRequest) -> Result<User> {
// Database-agnostic implementation
let query = match self.database.database_type() {
DatabaseType::PostgreSQL => "INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING *",
DatabaseType::SQLite => "INSERT INTO users (email, password_hash) VALUES (?1, ?2) RETURNING *",
};
let row = self.database.fetch_one(query, &[&user.email, &user.password_hash]).await?;
Ok(User {
id: row.get_uuid("id")?,
email: row.get_string("email")?,
// ... map other fields
})
}
}
Migration System
pub struct Migration {
pub version: i64,
pub name: String,
pub postgres_sql: String,
pub sqlite_sql: String,
pub mysql_sql: String,
}
#[async_trait]
pub trait MigrationRunner: Send + Sync {
async fn run_migrations(&self) -> Result<Vec<MigrationStatus>>;
async fn rollback_to(&self, version: i64) -> Result<()>;
async fn get_status(&self) -> Result<Vec<MigrationStatus>>;
}
Real-World Usage Examples
1. Development Setup (SQLite)
# config.dev.toml
[database]
url = "sqlite:data/development.db"
max_connections = 1
# No external dependencies needed!
cargo run --bin server
2. Production Setup (PostgreSQL)
# config.prod.toml
[database]
url = "postgresql://user:pass@prod-db:5432/myapp"
max_connections = 20
3. Testing Setup (In-Memory)
#[tokio::test]
async fn integration_test() {
let db = InMemoryDatabase::new().await;
let app = create_app_with_database(db).await;
// Test full application with real database operations
let response = app.post("/auth/register")
.json(&user_data)
.send()
.await?;
assert_eq!(response.status(), 201);
}
4. Multi-Database Support
// Same codebase works with different databases
match config.database.provider {
"sqlite" => SQLiteDatabase::new(&config.database.url).await?,
"postgresql" => PostgreSQLDatabase::new(&config.database.url).await?,
"mysql" => MySQLDatabase::new(&config.database.url).await?,
_ => return Err("Unsupported database"),
}
Performance Considerations
Connection Pooling
pub struct DatabasePool {
inner: Arc<dyn DatabaseConnection>,
max_connections: u32,
current_connections: AtomicU32,
}
impl DatabasePool {
pub async fn get_connection(&self) -> Result<DatabaseConnection> {
// Intelligent connection management
// Works with any database backend
}
}
Query Optimization
impl AuthRepository {
async fn find_user_optimized(&self, email: &str) -> Result<Option<User>> {
let query = match self.database.database_type() {
DatabaseType::PostgreSQL => {
// Use PostgreSQL-specific optimizations
"SELECT * FROM users WHERE email = $1 LIMIT 1"
},
DatabaseType::SQLite => {
// Use SQLite-specific optimizations
"SELECT * FROM users WHERE email = ?1 LIMIT 1"
},
};
self.database.fetch_optional(query, &[&email]).await
}
}
Migration Strategy
Gradual Migration Path
-
Phase 1: Create Abstraction Layer
- Define traits and interfaces
- Implement PostgreSQL backend
- Keep existing code working
-
Phase 2: Add SQLite Support
- Implement SQLite backend
- Add database-agnostic migrations
- Test with both databases
-
Phase 3: Migrate Services
- Update AuthRepository to use traits
- Update other repositories gradually
- Maintain backward compatibility
-
Phase 4: Cleanup
- Remove direct database dependencies
- Optimize for new architecture
- Add additional database backends
Backward Compatibility
impl AuthRepository {
// Legacy method for existing code
pub fn from_pg_pool(pool: PgPool) -> Self {
let database = PostgreSQLDatabase::from_pool(pool);
Self::new(Arc::new(database))
}
// New method for database-agnostic code
pub fn new(database: Arc<dyn DatabaseConnection>) -> Self {
Self { database }
}
}
Benefits Summary
For Developers
- ✅ Easy Setup: SQLite for local development
- ✅ No Dependencies: No PostgreSQL installation required
- ✅ Fast Testing: In-memory databases for unit tests
- ✅ Flexible Deployment: Choose the right database for the job
For Applications
- ✅ Database Freedom: Not locked into PostgreSQL
- ✅ Better Testing: Database-specific test strategies
- ✅ Performance Tuning: Database-specific optimizations
- ✅ Easier Scaling: Migrate databases as needs change
For Architecture
- ✅ Loose Coupling: Services don't depend on specific databases
- ✅ Single Responsibility: Database logic separated from business logic
- ✅ Testability: Easy to mock and test database interactions
- ✅ Maintainability: Database changes don't affect business logic
Conclusion
The database abstraction approach provides:
- Better Developer Experience: No forced PostgreSQL setup
- Architectural Flexibility: Choose the right database for each environment
- Future-Proofing: Easy to add new database backends
- Testing Excellence: Multiple testing strategies available
- Production Ready: Can use PostgreSQL in production while developing with SQLite
This is a much more robust, flexible, and developer-friendly approach than forcing database choices or disabling features based on database selection.
Current Status
The basic abstraction layer has been implemented in:
server/src/database/mod.rs- Core traits and typesserver/src/database/auth.rs- Database-agnostic auth repositoryserver/src/database/migrations.rs- Database-agnostic migration system
To complete the implementation, we need to:
- Fix compilation issues with SQLX query macros
- Align User struct between shared and database layers
- Complete the trait implementations
- Add comprehensive tests
- Update main.rs to use the new abstraction
This represents a significant architectural improvement that makes the application much more flexible and developer-friendly.