- 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>
868 lines
24 KiB
Markdown
868 lines
24 KiB
Markdown
# Database Migrations
|
|
|
|
<div align="center">
|
|
<img src="../../../logos/rustelo_dev-logo-h.svg" alt="RUSTELO" width="300" />
|
|
</div>
|
|
|
|
This guide covers database migrations in RUSTELO, including the database-agnostic migration system and how to migrate from legacy PostgreSQL-only code to the new multi-database architecture.
|
|
|
|
## Overview
|
|
|
|
RUSTELO has been upgraded to use a **database-agnostic architecture** that supports both PostgreSQL and SQLite. This guide covers:
|
|
|
|
- Database migration patterns and best practices
|
|
- Migrating from PostgreSQL-only to database-agnostic code
|
|
- Writing database-specific migration files
|
|
- Handling data type differences between databases
|
|
- Testing migration strategies
|
|
|
|
## Migration Architecture
|
|
|
|
### Before (Legacy PostgreSQL-Only)
|
|
|
|
```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<Option<User>> {
|
|
sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
|
|
.fetch_optional(&self.pool)
|
|
.await
|
|
}
|
|
}
|
|
```
|
|
|
|
### After (Database-Agnostic)
|
|
|
|
```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<Option<User>> {
|
|
match self.database.database_type() {
|
|
DatabaseType::PostgreSQL => self.find_user_postgres(id).await,
|
|
DatabaseType::SQLite => self.find_user_sqlite(id).await,
|
|
}
|
|
}
|
|
|
|
async fn find_user_postgres(&self, id: Uuid) -> Result<Option<User>> {
|
|
let row = self.database
|
|
.fetch_optional(
|
|
"SELECT * FROM users WHERE id = $1",
|
|
&[id.into()],
|
|
)
|
|
.await?;
|
|
|
|
if let Some(row) = row {
|
|
Ok(Some(User {
|
|
id: row.get_uuid("id")?,
|
|
email: row.get_string("email")?,
|
|
username: row.get_string("username")?,
|
|
created_at: row.get_datetime("created_at")?,
|
|
}))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
async fn find_user_sqlite(&self, id: Uuid) -> Result<Option<User>> {
|
|
let row = self.database
|
|
.fetch_optional(
|
|
"SELECT * FROM users WHERE id = ?",
|
|
&[id.to_string().into()],
|
|
)
|
|
.await?;
|
|
|
|
if let Some(row) = row {
|
|
Ok(Some(User {
|
|
id: row.get_uuid("id")?,
|
|
email: row.get_string("email")?,
|
|
username: row.get_string("username")?,
|
|
created_at: row.get_datetime("created_at")?,
|
|
}))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 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")?,
|
|
created_at: row.get_datetime("created_at")?,
|
|
};
|
|
}
|
|
```
|
|
|
|
### 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());
|
|
}
|
|
}
|
|
|
|
// JSON handling
|
|
match self.database.database_type() {
|
|
DatabaseType::PostgreSQL => {
|
|
// PostgreSQL supports native JSONB
|
|
params.push(serde_json::to_value(&data)?.into());
|
|
}
|
|
DatabaseType::SQLite => {
|
|
// SQLite stores JSON as text
|
|
params.push(serde_json::to_string(&data)?.into());
|
|
}
|
|
}
|
|
```
|
|
|
|
## Database Migration Files
|
|
|
|
### Migration File Structure
|
|
|
|
```
|
|
migrations/
|
|
├── 001_create_users/
|
|
│ ├── up_postgres.sql
|
|
│ ├── down_postgres.sql
|
|
│ ├── up_sqlite.sql
|
|
│ └── down_sqlite.sql
|
|
├── 002_add_2fa_support/
|
|
│ ├── up_postgres.sql
|
|
│ ├── down_postgres.sql
|
|
│ ├── up_sqlite.sql
|
|
│ └── down_sqlite.sql
|
|
└── 003_create_posts/
|
|
├── up_postgres.sql
|
|
├── down_postgres.sql
|
|
├── up_sqlite.sql
|
|
└── down_sqlite.sql
|
|
```
|
|
|
|
### Example Migration Files
|
|
|
|
**PostgreSQL Migration (001_create_users/up_postgres.sql):**
|
|
```sql
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
username VARCHAR(100) UNIQUE NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
email_verified BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_users_email ON users(email);
|
|
CREATE INDEX idx_users_username ON users(username);
|
|
```
|
|
|
|
**SQLite Migration (001_create_users/up_sqlite.sql):**
|
|
```sql
|
|
CREATE TABLE users (
|
|
id TEXT PRIMARY KEY,
|
|
email TEXT UNIQUE NOT NULL,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
email_verified INTEGER DEFAULT 0,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE INDEX idx_users_email ON users(email);
|
|
CREATE INDEX idx_users_username ON users(username);
|
|
```
|
|
|
|
### Running Migrations
|
|
|
|
```bash
|
|
# Run migrations for PostgreSQL
|
|
cargo run --bin migrate -- --database-url "postgresql://user:pass@localhost/db"
|
|
|
|
# Run migrations for SQLite
|
|
cargo run --bin migrate -- --database-url "sqlite//:database.db"
|
|
|
|
# Roll back migrations
|
|
cargo run --bin migrate -- --database-url "sqlite//:database.db" --rollback
|
|
|
|
# Check migration status
|
|
cargo run --bin migrate -- --database-url "sqlite//:database.db" --status
|
|
```
|
|
|
|
## Migration Patterns
|
|
|
|
### Pattern 1: Simple Repository Migration
|
|
|
|
```rust
|
|
// Old
|
|
pub struct MyRepository {
|
|
pool: PgPool,
|
|
}
|
|
|
|
// New
|
|
pub struct MyRepository {
|
|
database: DatabaseConnection,
|
|
}
|
|
|
|
impl MyRepository {
|
|
pub fn new(database: DatabaseConnection) -> Self {
|
|
Self { database }
|
|
}
|
|
|
|
// 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<Vec<Item>> {
|
|
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, since: DateTime<Utc>) -> Result<Vec<Item>> {
|
|
match self.database.database_type() {
|
|
DatabaseType::PostgreSQL => self.complex_query_postgres(since).await,
|
|
DatabaseType::SQLite => self.complex_query_sqlite(since).await,
|
|
}
|
|
}
|
|
|
|
async fn complex_query_postgres(&self, since: DateTime<Utc>) -> Result<Vec<Item>> {
|
|
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?;
|
|
|
|
self.rows_to_items(rows)
|
|
}
|
|
|
|
async fn complex_query_sqlite(&self, since: DateTime<Utc>) -> Result<Vec<Item>> {
|
|
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 > ?
|
|
ORDER BY i.created_at DESC
|
|
"#,
|
|
&[since.to_rfc3339().into()],
|
|
)
|
|
.await?;
|
|
|
|
self.rows_to_items(rows)
|
|
}
|
|
|
|
fn rows_to_items(&self, rows: Vec<DatabaseRow>) -> Result<Vec<Item>> {
|
|
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)
|
|
}
|
|
```
|
|
|
|
### Pattern 3: Transaction Handling
|
|
|
|
```rust
|
|
pub async fn create_user_with_profile(&self, user: &User, profile: &Profile) -> Result<()> {
|
|
match self.database.database_type() {
|
|
DatabaseType::PostgreSQL => {
|
|
self.create_user_with_profile_postgres(user, profile).await
|
|
}
|
|
DatabaseType::SQLite => {
|
|
self.create_user_with_profile_sqlite(user, profile).await
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn create_user_with_profile_postgres(&self, user: &User, profile: &Profile) -> Result<()> {
|
|
let mut tx = self.database.begin().await?;
|
|
|
|
// Create user
|
|
tx.execute(
|
|
"INSERT INTO users (id, email, username) VALUES ($1, $2, $3)",
|
|
&[user.id.into(), user.email.clone().into(), user.username.clone().into()],
|
|
).await?;
|
|
|
|
// Create profile
|
|
tx.execute(
|
|
"INSERT INTO profiles (user_id, first_name, last_name) VALUES ($1, $2, $3)",
|
|
&[profile.user_id.into(), profile.first_name.clone().into(), profile.last_name.clone().into()],
|
|
).await?;
|
|
|
|
tx.commit().await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn create_user_with_profile_sqlite(&self, user: &User, profile: &Profile) -> Result<()> {
|
|
let mut tx = self.database.begin().await?;
|
|
|
|
// Create user
|
|
tx.execute(
|
|
"INSERT INTO users (id, email, username) VALUES (?, ?, ?)",
|
|
&[user.id.to_string().into(), user.email.clone().into(), user.username.clone().into()],
|
|
).await?;
|
|
|
|
// Create profile
|
|
tx.execute(
|
|
"INSERT INTO profiles (user_id, first_name, last_name) VALUES (?, ?, ?)",
|
|
&[profile.user_id.to_string().into(), profile.first_name.clone().into(), profile.last_name.clone().into()],
|
|
).await?;
|
|
|
|
tx.commit().await?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## 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
|
|
- [ ] Add convenience constructors (`from_pool`)
|
|
- [ ] Update error handling
|
|
|
|
### 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
|
|
- [ ] Add rollback migrations
|
|
- [ ] Document migration dependencies
|
|
|
|
### Configuration
|
|
|
|
- [ ] Update environment variables
|
|
- [ ] Test with different database URLs
|
|
- [ ] Update deployment scripts
|
|
- [ ] Update documentation
|
|
- [ ] Configure connection pooling
|
|
- [ ] Set appropriate timeouts
|
|
|
|
### 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
|
|
- [ ] Test rollback scenarios
|
|
- [ ] Add database-specific test fixtures
|
|
|
|
## 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
|
|
```
|
|
|
|
#### 5. Connection Pool Issues
|
|
**Problem:** Connection pool exhaustion or configuration issues
|
|
**Solution:** Properly configure connection pool settings
|
|
|
|
```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),
|
|
};
|
|
```
|
|
|
|
## 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
|
|
- Test with both databases regularly
|
|
|
|
### 2. Migration Strategy
|
|
- Migrate one module at a time
|
|
- Test thoroughly with both database types
|
|
- Keep the old code until migration is complete
|
|
- Document the migration process
|
|
|
|
### 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
|
|
- Monitor query performance on both databases
|
|
|
|
### 4. Testing Strategy
|
|
- Test all code paths with both databases
|
|
- Use SQLite for fast unit tests
|
|
- Use PostgreSQL for integration tests
|
|
- Test migration rollbacks
|
|
- Use database-specific test fixtures
|
|
|
|
### 5. Error Handling
|
|
- Handle database-specific errors appropriately
|
|
- Use consistent error types across databases
|
|
- Log database operations for debugging
|
|
- Provide meaningful error messages
|
|
|
|
## Example: Complete Repository Migration
|
|
|
|
Here's a complete example of migrating a repository from PostgreSQL-only to database-agnostic:
|
|
|
|
### Before (PostgreSQL-Only)
|
|
```rust
|
|
use sqlx::PgPool;
|
|
use uuid::Uuid;
|
|
use chrono::{DateTime, Utc};
|
|
|
|
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, published) VALUES ($1, $2, $3, $4, $5)",
|
|
post.id,
|
|
post.title,
|
|
post.content,
|
|
post.author_id,
|
|
post.published
|
|
)
|
|
.execute(&self.pool)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn find_post(&self, id: Uuid) -> Result<Option<Post>> {
|
|
let row = sqlx::query_as!(
|
|
Post,
|
|
"SELECT id, title, content, author_id, published, created_at FROM posts WHERE id = $1",
|
|
id
|
|
)
|
|
.fetch_optional(&self.pool)
|
|
.await?;
|
|
Ok(row)
|
|
}
|
|
|
|
pub async fn find_posts_by_author(&self, author_id: Uuid) -> Result<Vec<Post>> {
|
|
let rows = sqlx::query_as!(
|
|
Post,
|
|
"SELECT id, title, content, author_id, published, created_at FROM posts WHERE author_id = $1 ORDER BY created_at DESC",
|
|
author_id
|
|
)
|
|
.fetch_all(&self.pool)
|
|
.await?;
|
|
Ok(rows)
|
|
}
|
|
}
|
|
```
|
|
|
|
### After (Database-Agnostic)
|
|
```rust
|
|
use crate::database::{DatabaseConnection, DatabaseType, DatabaseRow};
|
|
use uuid::Uuid;
|
|
use chrono::{DateTime, Utc};
|
|
|
|
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, published) VALUES ($1, $2, $3, $4, $5)",
|
|
&[
|
|
post.id.into(),
|
|
post.title.clone().into(),
|
|
post.content.clone().into(),
|
|
post.author_id.into(),
|
|
post.published.into(),
|
|
],
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn create_post_sqlite(&self, post: &Post) -> Result<()> {
|
|
self.database
|
|
.execute(
|
|
"INSERT INTO posts (id, title, content, author_id, published) VALUES (?, ?, ?, ?, ?)",
|
|
&[
|
|
post.id.to_string().into(),
|
|
post.title.clone().into(),
|
|
post.content.clone().into(),
|
|
post.author_id.to_string().into(),
|
|
(post.published as i32).into(),
|
|
],
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn find_post(&self, id: Uuid) -> Result<Option<Post>> {
|
|
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<Option<Post>> {
|
|
let row = self
|
|
.database
|
|
.fetch_optional(
|
|
"SELECT id, title, content, author_id, published, created_at FROM posts WHERE id = $1",
|
|
&[id.into()],
|
|
)
|
|
.await?;
|
|
|
|
self.row_to_post(row)
|
|
}
|
|
|
|
async fn find_post_sqlite(&self, id: Uuid) -> Result<Option<Post>> {
|
|
let row = self
|
|
.database
|
|
.fetch_optional(
|
|
"SELECT id, title, content, author_id, published, created_at FROM posts WHERE id = ?",
|
|
&[id.to_string().into()],
|
|
)
|
|
.await?;
|
|
|
|
self.row_to_post(row)
|
|
}
|
|
|
|
pub async fn find_posts_by_author(&self, author_id: Uuid) -> Result<Vec<Post>> {
|
|
match self.database.database_type() {
|
|
DatabaseType::PostgreSQL => self.find_posts_by_author_postgres(author_id).await,
|
|
DatabaseType::SQLite => self.find_posts_by_author_sqlite(author_id).await,
|
|
}
|
|
}
|
|
|
|
async fn find_posts_by_author_postgres(&self, author_id: Uuid) -> Result<Vec<Post>> {
|
|
let rows = self
|
|
.database
|
|
.fetch_all(
|
|
"SELECT id, title, content, author_id, published, created_at FROM posts WHERE author_id = $1 ORDER BY created_at DESC",
|
|
&[author_id.into()],
|
|
)
|
|
.await?;
|
|
|
|
self.rows_to_posts(rows)
|
|
}
|
|
|
|
async fn find_posts_by_author_sqlite(&self, author_id: Uuid) -> Result<Vec<Post>> {
|
|
let rows = self
|
|
.database
|
|
.fetch_all(
|
|
"SELECT id, title, content, author_id, published, created_at FROM posts WHERE author_id = ? ORDER BY created_at DESC",
|
|
&[author_id.to_string().into()],
|
|
)
|
|
.await?;
|
|
|
|
self.rows_to_posts(rows)
|
|
}
|
|
|
|
fn row_to_post(&self, row: Option<DatabaseRow>) -> Result<Option<Post>> {
|
|
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")?,
|
|
published: row.get_bool("published")?,
|
|
created_at: row.get_datetime("created_at")?,
|
|
}))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
fn rows_to_posts(&self, rows: Vec<DatabaseRow>) -> Result<Vec<Post>> {
|
|
let mut posts = Vec::new();
|
|
for row in rows {
|
|
posts.push(Post {
|
|
id: row.get_uuid("id")?,
|
|
title: row.get_string("title")?,
|
|
content: row.get_string("content")?,
|
|
author_id: row.get_uuid("author_id")?,
|
|
published: row.get_bool("published")?,
|
|
created_at: row.get_datetime("created_at")?,
|
|
});
|
|
}
|
|
Ok(posts)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Next Steps
|
|
|
|
- [Database Configuration](../../configuration/database.md)
|
|
- [Performance Optimization](../../performance/database.md)
|
|
- [Testing Strategies](../testing/strategy.md)
|
|
- [Deployment Guide](../../deployment/production.md)
|
|
|
|
This migration guide provides a comprehensive approach to transitioning from PostgreSQL-only to database-agnostic architecture while maintaining functionality and performance across both database systems.
|