Private
Public Access
1
0

feat(M1): Project scaffolding, DB schema, core infrastructure

- Initialize Rust workspace with 7 crates (pm-web, pm-worker, pm-core,
  pm-agent-client, pm-auth, pm-ca, pm-reports)
- React + TypeScript + Vite + MUI frontend scaffold
- Full PostgreSQL schema: all 17 tables with indexes and constraints
- pm-core: config (TOML+env), db (SQLx pool + migrations), error
  (unified AppError + JSON envelope), request_id (ULID middleware),
  logging (tracing JSON/pretty)
- pm-web: Axum skeleton, /status/health endpoint, static file serving
- pm-worker: Tokio skeleton, heartbeat writer, schema version check
- Embedded sqlx migrations with advisory lock (single-writer)
- systemd unit files, setup.sh, build-frontend.sh
- config.example.toml with all configuration keys
- docs/runbooks/restore.md
- cargo check passes with zero warnings

Closes M1.
This commit is contained in:
2026-04-23 15:55:53 +00:00
parent 3eb7fd9f95
commit da5a94d838
50 changed files with 6139 additions and 3 deletions

69
crates/pm-core/src/db.rs Normal file
View File

@ -0,0 +1,69 @@
use sqlx::postgres::{PgPool, PgPoolOptions};
use std::time::Duration;
use crate::config::DatabaseConfig;
/// Initialize and return a PostgreSQL connection pool.
pub async fn init_pool(cfg: &DatabaseConfig) -> Result<PgPool, sqlx::Error> {
let pool = PgPoolOptions::new()
.max_connections(cfg.max_connections)
.min_connections(cfg.min_connections)
.acquire_timeout(Duration::from_secs(cfg.acquire_timeout_secs))
.connect(&cfg.url)
.await?;
tracing::info!(
max_connections = cfg.max_connections,
"PostgreSQL connection pool initialized"
);
Ok(pool)
}
/// Run embedded SQLx migrations.
/// Uses a PostgreSQL advisory lock to ensure only one writer runs migrations.
pub async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::migrate::MigrateError> {
tracing::info!("Acquiring advisory lock for migrations");
// Advisory lock key — consistent hash of the application name
const LOCK_KEY: i64 = 0x7061_7463_686d_6772; // "patchmgr" bytes
// Acquire advisory lock; blocks until granted
sqlx::query("SELECT pg_advisory_lock($1)")
.bind(LOCK_KEY)
.execute(pool)
.await
.map_err(|e| {
tracing::error!(error = %e, "Failed to acquire advisory lock");
e
})
.expect("Advisory lock must be acquired before running migrations");
tracing::info!("Running database migrations");
let result = sqlx::migrate!("../../migrations").run(pool).await;
// Always release the lock
sqlx::query("SELECT pg_advisory_unlock($1)")
.bind(LOCK_KEY)
.execute(pool)
.await
.ok();
match &result {
Ok(_) => tracing::info!("Database migrations completed successfully"),
Err(e) => tracing::error!(error = %e, "Database migrations failed"),
}
result
}
/// Check that the database schema is at the expected version.
/// Used by the worker to wait until migrations have been applied.
pub async fn check_schema_version(pool: &PgPool) -> Result<i64, sqlx::Error> {
let row: (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM _sqlx_migrations WHERE success = true",
)
.fetch_one(pool)
.await?;
Ok(row.0)
}