# Database Migration Guide ## Overview Rustelo has been upgraded to use a **database-agnostic architecture** that supports both PostgreSQL and SQLite. This guide will help you migrate existing code to use the new database abstraction layer. ## What Changed ### Before (Old Architecture) ```rust // Direct PostgreSQL dependency use sqlx::PgPool; // Repository tied to PostgreSQL pub struct AuthRepository { pool: PgPool, } impl AuthRepository { pub fn new(pool: PgPool) -> Self { Self { pool } } pub async fn find_user(&self, id: Uuid) -> Result> { sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id) .fetch_optional(&self.pool) .await } } ``` ### After (New Architecture) ```rust // Database-agnostic abstractions use crate::database::{DatabaseConnection, DatabaseType}; // Repository works with any database pub struct AuthRepository { database: DatabaseConnection, } impl AuthRepository { pub fn new(database: DatabaseConnection) -> Self { Self { database } } pub async fn find_user(&self, id: Uuid) -> Result> { match self.database.database_type() { DatabaseType::PostgreSQL => self.find_user_postgres(id).await, DatabaseType::SQLite => self.find_user_sqlite(id).await, } } } ``` ## Migration Steps ### 1. Update Repository Constructors **Before:** ```rust let auth_repository = Arc::new(AuthRepository::new(pool.clone())); ``` **After:** ```rust // For the new database-agnostic repositories let auth_repository = Arc::new(database::auth::AuthRepository::new(database.clone())); // Or create from pool let auth_repository = Arc::new(database::auth::AuthRepository::from_pool(&database_pool)); ``` ### 2. Replace Direct SQL Queries **Before:** ```rust pub async fn create_user(&self, user: &User) -> Result<()> { sqlx::query!( "INSERT INTO users (id, email, username) VALUES ($1, $2, $3)", user.id, user.email, user.username ) .execute(&self.pool) .await?; Ok(()) } ``` **After:** ```rust pub async fn create_user(&self, user: &User) -> Result<()> { match self.database.database_type() { DatabaseType::PostgreSQL => self.create_user_postgres(user).await, DatabaseType::SQLite => self.create_user_sqlite(user).await, } } async fn create_user_postgres(&self, user: &User) -> Result<()> { self.database .execute( "INSERT INTO users (id, email, username) VALUES ($1, $2, $3)", &[ user.id.into(), user.email.clone().into(), user.username.clone().into(), ], ) .await?; Ok(()) } async fn create_user_sqlite(&self, user: &User) -> Result<()> { self.database .execute( "INSERT INTO users (id, email, username) VALUES (?, ?, ?)", &[ user.id.to_string().into(), user.email.clone().into(), user.username.clone().into(), ], ) .await?; Ok(()) } ``` ### 3. Update Row Processing **Before:** ```rust let row = sqlx::query_as::<_, UserRow>("SELECT * FROM users WHERE id = $1") .bind(id) .fetch_optional(&self.pool) .await?; ``` **After:** ```rust let row = self.database .fetch_optional( "SELECT * FROM users WHERE id = $1", // PostgreSQL &[id.into()], ) .await?; if let Some(row) = row { let user = User { id: row.get_uuid("id")?, email: row.get_string("email")?, username: row.get_string("username")?, // ... other fields }; } ``` ### 4. Database Initialization **Before:** ```rust let pool = PgPool::connect(&database_url).await?; let auth_repository = AuthRepository::new(pool); ``` **After:** ```rust let database_config = DatabaseConfig { url: database_url.to_string(), max_connections: 10, min_connections: 1, connect_timeout: Duration::from_secs(30), idle_timeout: Duration::from_secs(600), max_lifetime: Duration::from_secs(3600), }; let database_pool = DatabasePool::new(&database_config).await?; let database = Database::new(database_pool.clone()); let auth_repository = database::auth::AuthRepository::new(database.create_connection()); ``` ## Database-Specific Considerations ### PostgreSQL vs SQLite Differences | Feature | PostgreSQL | SQLite | |---------|------------|--------| | **Parameter Binding** | `$1, $2, $3` | `?, ?, ?` | | **UUID Storage** | Native `UUID` type | `TEXT` (string) | | **Timestamps** | `TIMESTAMP WITH TIME ZONE` | `TEXT` (ISO 8601) | | **JSON** | `JSONB` | `TEXT` (JSON string) | | **Arrays** | Native arrays | `TEXT` (JSON array) | | **Boolean** | `BOOLEAN` | `INTEGER` (0/1) | ### Handling Data Type Differences ```rust // UUID handling match self.database.database_type() { DatabaseType::PostgreSQL => { // PostgreSQL stores UUIDs natively params.push(user_id.into()); } DatabaseType::SQLite => { // SQLite stores UUIDs as strings params.push(user_id.to_string().into()); } } // Timestamp handling match self.database.database_type() { DatabaseType::PostgreSQL => { // PostgreSQL stores timestamps natively params.push(created_at.into()); } DatabaseType::SQLite => { // SQLite stores timestamps as ISO strings params.push(created_at.to_rfc3339().into()); } } ``` ## Migration Checklist ### ✅ Code Migration - [ ] Replace `PgPool` with `DatabaseConnection` - [ ] Update repository constructors - [ ] Split database operations into database-specific methods - [ ] Handle parameter binding differences (`$1` vs `?`) - [ ] Handle data type differences (UUID, timestamps, etc.) - [ ] Update imports to use new database modules ### ✅ Database Migration Files - [ ] Create separate migration files for PostgreSQL (`*_postgres.sql`) - [ ] Create separate migration files for SQLite (`*_sqlite.sql`) - [ ] Update migration runner configuration - [ ] Test migrations on both database types ### ✅ Configuration - [ ] Update environment variables - [ ] Test with different database URLs - [ ] Update deployment scripts - [ ] Update documentation ### ✅ Testing - [ ] Update unit tests to use new abstractions - [ ] Add integration tests for both database types - [ ] Test migration path from old to new architecture - [ ] Performance testing on both databases ## Common Migration Patterns ### Pattern 1: Simple Repository Migration ```rust // Old pub struct MyRepository { pool: PgPool, } // New pub struct MyRepository { database: DatabaseConnection, } impl MyRepository { // Add convenience constructor pub fn from_pool(pool: &DatabasePool) -> Self { let connection = DatabaseConnection::from_pool(pool); Self::new(connection) } } ``` ### Pattern 2: Complex Query Migration ```rust // Old pub async fn complex_query(&self) -> Result> { sqlx::query_as!( Item, r#" SELECT i.*, u.username FROM items i JOIN users u ON i.user_id = u.id WHERE i.created_at > $1 ORDER BY i.created_at DESC "#, since ) .fetch_all(&self.pool) .await } // New pub async fn complex_query(&self) -> Result> { match self.database.database_type() { DatabaseType::PostgreSQL => self.complex_query_postgres().await, DatabaseType::SQLite => self.complex_query_sqlite().await, } } async fn complex_query_postgres(&self) -> Result> { let rows = self.database .fetch_all( r#" SELECT i.id, i.name, i.user_id, i.created_at, u.username FROM items i JOIN users u ON i.user_id = u.id WHERE i.created_at > $1 ORDER BY i.created_at DESC "#, &[since.into()], ) .await?; // Convert rows to Items let mut items = Vec::new(); for row in rows { items.push(Item { id: row.get_uuid("id")?, name: row.get_string("name")?, user_id: row.get_uuid("user_id")?, username: row.get_string("username")?, created_at: row.get_datetime("created_at")?, }); } Ok(items) } ``` ## Troubleshooting ### Common Issues and Solutions #### 1. UUID Conversion Errors **Problem:** SQLite doesn't support UUID natively **Solution:** Convert UUIDs to strings for SQLite ```rust match self.database.database_type() { DatabaseType::PostgreSQL => params.push(uuid.into()), DatabaseType::SQLite => params.push(uuid.to_string().into()), } ``` #### 2. Parameter Binding Mismatch **Problem:** Using PostgreSQL syntax (`$1`) with SQLite **Solution:** Use database-specific query strings ```rust let query = match self.database.database_type() { DatabaseType::PostgreSQL => "SELECT * FROM users WHERE id = $1", DatabaseType::SQLite => "SELECT * FROM users WHERE id = ?", }; ``` #### 3. Migration File Not Found **Problem:** Missing database-specific migration files **Solution:** Ensure both `*_postgres.sql` and `*_sqlite.sql` files exist #### 4. Type Conversion Errors **Problem:** Data type mismatches between databases **Solution:** Use the `DatabaseRow` trait methods consistently ```rust // Always use the trait methods let user_id = row.get_uuid("user_id")?; // Handles conversion automatically let created_at = row.get_datetime("created_at")?; // Handles conversion automatically ``` ## Best Practices ### 1. Database-Agnostic Design - Write repositories that work with both databases - Use the database abstraction layer consistently - Avoid database-specific features when possible ### 2. Migration Strategy - Migrate one module at a time - Test thoroughly with both database types - Keep the old code until migration is complete ### 3. Performance Considerations - PostgreSQL: Better for complex queries and large datasets - SQLite: Better for simple queries and small datasets - Use appropriate database features for your use case ### 4. Testing Strategy - Test all code paths with both databases - Use SQLite for fast unit tests - Use PostgreSQL for integration tests ## Need Help? If you encounter issues during migration: 1. Check the [example implementations](../server/src/database/) for patterns 2. Review the [test files](../server/tests/) for working examples 3. Consult the [API documentation](../server/src/database/mod.rs) 4. Look at the [migration files](../migrations/) for database schema examples ## Example: Complete Repository Migration Here's a complete example of migrating a repository: ### Before (Old Implementation) ```rust use sqlx::PgPool; use uuid::Uuid; pub struct PostRepository { pool: PgPool, } impl PostRepository { pub fn new(pool: PgPool) -> Self { Self { pool } } pub async fn create_post(&self, post: &Post) -> Result<()> { sqlx::query!( "INSERT INTO posts (id, title, content, author_id) VALUES ($1, $2, $3, $4)", post.id, post.title, post.content, post.author_id ) .execute(&self.pool) .await?; Ok(()) } pub async fn find_post(&self, id: Uuid) -> Result> { let row = sqlx::query_as!( Post, "SELECT id, title, content, author_id, created_at FROM posts WHERE id = $1", id ) .fetch_optional(&self.pool) .await?; Ok(row) } } ``` ### After (New Implementation) ```rust use crate::database::{DatabaseConnection, DatabaseType}; use uuid::Uuid; pub struct PostRepository { database: DatabaseConnection, } impl PostRepository { pub fn new(database: DatabaseConnection) -> Self { Self { database } } pub fn from_pool(pool: &crate::database::DatabasePool) -> Self { let connection = DatabaseConnection::from_pool(pool); Self::new(connection) } pub async fn create_post(&self, post: &Post) -> Result<()> { match self.database.database_type() { DatabaseType::PostgreSQL => self.create_post_postgres(post).await, DatabaseType::SQLite => self.create_post_sqlite(post).await, } } async fn create_post_postgres(&self, post: &Post) -> Result<()> { self.database .execute( "INSERT INTO posts (id, title, content, author_id) VALUES ($1, $2, $3, $4)", &[ post.id.into(), post.title.clone().into(), post.content.clone().into(), post.author_id.into(), ], ) .await?; Ok(()) } async fn create_post_sqlite(&self, post: &Post) -> Result<()> { self.database .execute( "INSERT INTO posts (id, title, content, author_id) VALUES (?, ?, ?, ?)", &[ post.id.to_string().into(), post.title.clone().into(), post.content.clone().into(), post.author_id.to_string().into(), ], ) .await?; Ok(()) } pub async fn find_post(&self, id: Uuid) -> Result> { match self.database.database_type() { DatabaseType::PostgreSQL => self.find_post_postgres(id).await, DatabaseType::SQLite => self.find_post_sqlite(id).await, } } async fn find_post_postgres(&self, id: Uuid) -> Result> { let row = self .database .fetch_optional( "SELECT id, title, content, author_id, created_at FROM posts WHERE id = $1", &[id.into()], ) .await?; if let Some(row) = row { Ok(Some(Post { id: row.get_uuid("id")?, title: row.get_string("title")?, content: row.get_string("content")?, author_id: row.get_uuid("author_id")?, created_at: row.get_datetime("created_at")?, })) } else { Ok(None) } } async fn find_post_sqlite(&self, id: Uuid) -> Result> { let row = self .database .fetch_optional( "SELECT id, title, content, author_id, created_at FROM posts WHERE id = ?", &[id.to_string().into()], ) .await?; if let Some(row) = row { Ok(Some(Post { id: row.get_uuid("id")?, title: row.get_string("title")?, content: row.get_string("content")?, author_id: row.get_uuid("author_id")?, created_at: row.get_datetime("created_at")?, })) } else { Ok(None) } } } ``` This migration guide provides a comprehensive overview of how to transition from the old PostgreSQL-only architecture to the new database-agnostic system.