feat(db): add SQLx migrations and AppState with connection pool

- Add sqlx 0.8 (AnyPool, runtime-tokio, sqlite, postgres, migrate)
- Create 6 migration files for both SQLite and PostgreSQL backends
- Add server/db.rs: create_pool and run_migrations helpers
- Add server/state.rs: AppState with LeptosOptions + AnyPool
- Run migrations at server startup before accepting requests
- Fix Port model: remove host_id (ports are now a global catalog)
- Add HostPort join struct for the host_ports many-to-many table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 21:46:16 +02:00
parent 18804e740c
commit f13097591c
19 changed files with 1192 additions and 23 deletions

68
src/server/db.rs Normal file
View File

@@ -0,0 +1,68 @@
// server/db.rs — Database connection pool and migrations
//
// This module provides two functions called once at server startup:
// - `create_pool` : opens a connection pool to the database
// - `run_migrations` : applies all pending SQL migrations
//
// `AnyPool` lets the same Rust code target both SQLite (dev) and
// PostgreSQL (production) — only DATABASE_URL changes.
use sqlx::AnyPool;
use thiserror::Error;
use crate::server::config::{AppConfig, DatabaseBackend};
// ─── Errors ───────────────────────────────────────────────────────────────────
#[derive(Debug, Error)]
pub enum DbError {
#[error("Database connection failed: {0}")]
Connection(#[from] sqlx::Error),
#[error("Migration failed: {0}")]
Migration(#[from] sqlx::migrate::MigrateError),
}
// ─── Pool creation ────────────────────────────────────────────────────────────
/// Opens a connection pool to the database specified in `config.database_url`.
///
/// A pool maintains multiple open connections so concurrent requests
/// do not block each other waiting for a single connection.
///
/// `install_default_drivers()` must be called before `AnyPool::connect`
/// to register both the SQLite and PostgreSQL drivers in the `Any` registry.
pub async fn create_pool(config: &AppConfig) -> Result<AnyPool, DbError> {
// Register SQLite and PostgreSQL drivers so `AnyPool` can dispatch
// to the correct one based on the URL scheme (sqlite:// vs postgres://).
sqlx::any::install_default_drivers();
let pool = AnyPool::connect(&config.database_url).await?;
Ok(pool)
}
// ─── Migrations ───────────────────────────────────────────────────────────────
/// Applies all pending migrations from the directory matching the active backend.
///
/// Two separate directories handle SQL syntax differences:
/// - `migrations/sqlite/` : uses `INTEGER PRIMARY KEY AUTOINCREMENT`
/// - `migrations/postgres/` : uses `BIGSERIAL PRIMARY KEY`
///
/// SQLx tracks applied migrations in a `_sqlx_migrations` table, so running
/// this function on an already-migrated database is always safe (idempotent).
///
/// `sqlx::migrate!("path")` is a compile-time macro: it embeds all `.sql`
/// files from the given path directly into the binary. The path is relative
/// to the project root (where Cargo.toml lives).
pub async fn run_migrations(pool: &AnyPool, backend: &DatabaseBackend) -> Result<(), DbError> {
match backend {
DatabaseBackend::Sqlite => {
sqlx::migrate!("migrations/sqlite").run(pool).await?;
}
DatabaseBackend::Postgres => {
sqlx::migrate!("migrations/postgres").run(pool).await?;
}
}
Ok(())
}