timezoneDB/src/db/postgres.rs
creations 6bfd298455
# Git Commit Message
refactor: add production features and improve architecture

- Add structured configuration with validation
- Implement Redis connection pooling
- Add database migrations system
- Change API methods: GET /set → POST /set, GET /delete → DELETE /delete
- Add health check endpoint
- Add graceful shutdown and structured logging
- Update frontend for new API methods
- Add open source attribution
2025-06-04 07:56:15 -04:00

122 lines
3.6 KiB
Rust

use crate::config::DatabaseConfig;
use sqlx::{postgres::PgPoolOptions, PgPool};
use std::fs;
use std::path::Path;
use std::time::Duration;
use tracing::{error, info, warn};
pub async fn connect(config: &DatabaseConfig) -> Result<PgPool, sqlx::Error> {
let pool = PgPoolOptions::new()
.max_connections(config.max_connections)
.acquire_timeout(Duration::from_secs(config.connect_timeout_seconds))
.idle_timeout(Some(Duration::from_secs(600)))
.max_lifetime(Some(Duration::from_secs(1800)))
.connect(&config.url)
.await?;
create_migrations_table(&pool).await?;
run_migrations(&pool).await?;
Ok(pool)
}
async fn create_migrations_table(pool: &PgPool) -> Result<(), sqlx::Error> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ DEFAULT NOW()
)
"#,
)
.execute(pool)
.await?;
Ok(())
}
async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::Error> {
let migrations_dir = Path::new("migrations");
if !migrations_dir.exists() {
warn!("Migrations directory not found, skipping migrations");
return Ok(());
}
let mut migration_files = Vec::new();
match fs::read_dir(migrations_dir) {
Ok(entries) => {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("sql") {
if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
migration_files.push(file_name.to_string());
}
}
}
}
}
Err(e) => {
error!("Failed to read migrations directory: {}", e);
return Err(sqlx::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to read migrations directory: {}", e),
)));
}
}
migration_files.sort();
for migration_file in migration_files {
let applied = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = $1)",
)
.bind(&migration_file)
.fetch_one(pool)
.await?;
if applied {
info!("Migration {} already applied, skipping", migration_file);
continue;
}
let migration_path = migrations_dir.join(&migration_file);
let migration_sql = match fs::read_to_string(&migration_path) {
Ok(content) => content,
Err(e) => {
error!("Failed to read migration file {}: {}", migration_file, e);
return Err(sqlx::Error::Io(e));
}
};
info!("Running migration: {}", migration_file);
let mut tx = pool.begin().await?;
let statements: Vec<&str> = migration_sql
.split(';')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
for statement in statements {
if let Err(e) = sqlx::query(statement).execute(&mut *tx).await {
error!("Failed to execute migration {}: {}", migration_file, e);
return Err(e);
}
}
sqlx::query("INSERT INTO schema_migrations (version) VALUES ($1)")
.bind(&migration_file)
.execute(&mut *tx)
.await?;
tx.commit().await?;
info!("Successfully applied migration: {}", migration_file);
}
Ok(())
}