- Add client/networks.rs: Leptos page with ServerAction + Resource pattern * ActionForm for CIDR creation (auto-clears after submit) * delete button dispatches DeleteNetwork action per row * Resource re-fetches after each create/delete via action.version() * Suspense shows "Loading…" while Resource is pending - Register /networks route in app.rs with temporary nav bar - Fix db.rs: create_pool now creates the SQLite file if missing (AnyPool has no create_if_missing option unlike SqlitePool) - Remove redundant directory creation from main.rs (handled in db.rs) - Fix ActionForm import: use leptos::form::ActionForm (not leptos_router) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
4.6 KiB
Rust
111 lines
4.6 KiB
Rust
// 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),
|
|
|
|
#[error("Record not found: {0}")]
|
|
NotFound(String),
|
|
}
|
|
|
|
// ─── 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.
|
|
///
|
|
/// For SQLite, this function also creates the database file and its parent
|
|
/// directory if they do not exist yet. The `AnyPool` driver cannot create
|
|
/// a new SQLite file by itself — unlike the `SqlitePool` which has an
|
|
/// explicit `create_if_missing` option.
|
|
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();
|
|
|
|
// SQLite-specific setup: ensure the file exists before connecting.
|
|
//
|
|
// `AnyPool` does not expose `create_if_missing` like `SqlitePool` does,
|
|
// so we must touch the file ourselves.
|
|
//
|
|
// The URL is parsed the same way SQLx does it internally:
|
|
// sqlite://data/ipam.db → strip sqlite:// → path = data/ipam.db
|
|
if let DatabaseBackend::Sqlite = &config.backend {
|
|
// Strip both possible prefixes (SQLx accepts both forms)
|
|
let path_str = config
|
|
.database_url
|
|
.trim_start_matches("sqlite://")
|
|
.trim_start_matches("sqlite:");
|
|
|
|
// Skip special filenames (in-memory, shared cache)
|
|
if path_str != ":memory:" && !path_str.is_empty() {
|
|
let path = std::path::Path::new(path_str);
|
|
|
|
// Create the parent directory if it does not exist
|
|
if let Some(parent) = path.parent() {
|
|
if !parent.as_os_str().is_empty() {
|
|
std::fs::create_dir_all(parent)
|
|
.map_err(|e| sqlx::Error::Io(e))?;
|
|
}
|
|
}
|
|
|
|
// Create an empty file so SQLite can open it
|
|
if !path.exists() {
|
|
std::fs::File::create(path)
|
|
.map_err(|e| sqlx::Error::Io(e))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
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(())
|
|
}
|