- 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>
345 lines
10 KiB
Markdown
345 lines
10 KiB
Markdown
# 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**
|
|
```rust
|
|
// 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**
|
|
```rust
|
|
// 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**
|
|
```rust
|
|
// 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**
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
#[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
|
|
|
|
```rust
|
|
#[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
|
|
|
|
```rust
|
|
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)
|
|
```toml
|
|
# config.dev.toml
|
|
[database]
|
|
url = "sqlite:data/development.db"
|
|
max_connections = 1
|
|
```
|
|
|
|
```bash
|
|
# No external dependencies needed!
|
|
cargo run --bin server
|
|
```
|
|
|
|
### 2. **Production Setup** (PostgreSQL)
|
|
```toml
|
|
# config.prod.toml
|
|
[database]
|
|
url = "postgresql://user:pass@prod-db:5432/myapp"
|
|
max_connections = 20
|
|
```
|
|
|
|
### 3. **Testing Setup** (In-Memory)
|
|
```rust
|
|
#[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**
|
|
```rust
|
|
// 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
|
|
```rust
|
|
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
|
|
```rust
|
|
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
|
|
|
|
1. **Phase 1: Create Abstraction Layer**
|
|
- Define traits and interfaces
|
|
- Implement PostgreSQL backend
|
|
- Keep existing code working
|
|
|
|
2. **Phase 2: Add SQLite Support**
|
|
- Implement SQLite backend
|
|
- Add database-agnostic migrations
|
|
- Test with both databases
|
|
|
|
3. **Phase 3: Migrate Services**
|
|
- Update AuthRepository to use traits
|
|
- Update other repositories gradually
|
|
- Maintain backward compatibility
|
|
|
|
4. **Phase 4: Cleanup**
|
|
- Remove direct database dependencies
|
|
- Optimize for new architecture
|
|
- Add additional database backends
|
|
|
|
### Backward Compatibility
|
|
|
|
```rust
|
|
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:
|
|
|
|
1. **Better Developer Experience**: No forced PostgreSQL setup
|
|
2. **Architectural Flexibility**: Choose the right database for each environment
|
|
3. **Future-Proofing**: Easy to add new database backends
|
|
4. **Testing Excellence**: Multiple testing strategies available
|
|
5. **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 types
|
|
- `server/src/database/auth.rs` - Database-agnostic auth repository
|
|
- `server/src/database/migrations.rs` - Database-agnostic migration system
|
|
|
|
To complete the implementation, we need to:
|
|
1. Fix compilation issues with SQLX query macros
|
|
2. Align User struct between shared and database layers
|
|
3. Complete the trait implementations
|
|
4. Add comprehensive tests
|
|
5. Update main.rs to use the new abstraction
|
|
|
|
This represents a significant architectural improvement that makes the application much more flexible and developer-friendly. |