Rustelo/info/database_abstraction.md

345 lines
10 KiB
Markdown
Raw Normal View History

# 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.