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:
68
src/server/db.rs
Normal file
68
src/server/db.rs
Normal 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(())
|
||||
}
|
||||
@@ -11,5 +11,11 @@ pub mod routes;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod config;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod db;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod state;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod validation;
|
||||
|
||||
42
src/server/state.rs
Normal file
42
src/server/state.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
// server/state.rs — Shared Axum application state
|
||||
//
|
||||
// Axum uses a typed state system: any handler can extract a specific type
|
||||
// from the shared state using the `State<T>` extractor.
|
||||
//
|
||||
// We store two pieces of state:
|
||||
// - `leptos_options` : required by Leptos SSR routes
|
||||
// - `db` : database pool shared across all requests
|
||||
|
||||
use axum::extract::FromRef;
|
||||
use leptos::config::LeptosOptions;
|
||||
use sqlx::AnyPool;
|
||||
|
||||
// ─── AppState ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Shared state available to every Axum handler and Leptos server function.
|
||||
///
|
||||
/// `#[derive(Clone)]` is required by Axum: each request receives its own clone.
|
||||
/// Both fields are cheap to clone — they hold reference-counted pointers
|
||||
/// under the hood, so cloning just increments an atomic counter.
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
/// Leptos configuration (output paths, site address, reload port…).
|
||||
pub leptos_options: LeptosOptions,
|
||||
|
||||
/// Shared connection pool to the database.
|
||||
/// Using a pool (not a single connection) allows concurrent requests
|
||||
/// to run their queries in parallel without contention.
|
||||
pub db: AnyPool,
|
||||
}
|
||||
|
||||
// ─── FromRef implementations ──────────────────────────────────────────────────
|
||||
|
||||
/// Tells Axum how to extract just `LeptosOptions` from the full `AppState`.
|
||||
///
|
||||
/// `.leptos_routes()` in main.rs requires this impl because it stores
|
||||
/// only `LeptosOptions` internally, not the full application state.
|
||||
impl FromRef<AppState> for LeptosOptions {
|
||||
fn from_ref(state: &AppState) -> Self {
|
||||
state.leptos_options.clone()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user