use crate::project::Project;
use crate::{migrations::Migrations, writer::Writer};
use eyre::Context;
use sqlx::{PgPool, Pool, Postgres};
use std::path::PathBuf;
const DOCKER_COMPOSE_SNIPPET: &str = r#"
local-postgres:
image: "postgres:16.10"
shm_size: 128mb
ports:
- "5432:5432"
volumes:
- "{{DB_VOLUME_PATH}}:/var/lib/postgresql/data"
environment:
POSTGRES_PASSWORD: localdbpassword
"#;
pub struct LocalSqlDB<'a> {
project: Project,
with_migrations: bool,
migrations_path: PathBuf,
writer: &'a Writer,
}
impl<'a> LocalSqlDB<'a> {
pub fn new(project: &Project, writer: &'a Writer) -> Self {
Self {
migrations_path: project.path.join("migrations"),
with_migrations: false,
project: project.clone(),
writer,
}
}
pub fn docker_compose_snippet(&self) -> String {
DOCKER_COMPOSE_SNIPPET
.replace(
"{{DB_VOLUME_PATH}}",
format!("/tmp/kinetics_db_{}", self.project.name).as_str(),
)
.to_string()
}
pub fn connection_string(&self) -> String {
"postgres://postgres:localdbpassword@localhost:5432/postgres?sslmode=disable".to_string()
}
pub fn with_migrations(&mut self, migrations_path: Option<&str>) -> &mut Self {
self.with_migrations = true;
if let Some(migrations_path) = migrations_path {
self.migrations_path = self.project.path.join(migrations_path);
}
self
}
pub async fn provision(&self) -> eyre::Result<()> {
let max_retries = 10;
let retry_delay_ms = 1000;
for attempt in 1..=max_retries {
let result = PgPool::connect(&self.connection_string()).await;
match result {
Ok(connection) => {
self.cleanup(&connection).await?;
self.create_migrations_table(&connection).await?;
break; }
Err(_) if attempt < max_retries => {
tokio::time::sleep(tokio::time::Duration::from_millis(retry_delay_ms)).await;
}
Err(e) => return Err(e.into()),
}
}
if self.with_migrations {
Migrations::new(self.migrations_path.as_path(), self.writer)?
.apply(self.connection_string())
.await?;
}
Ok(())
}
async fn cleanup(&self, connection: &Pool<Postgres>) -> eyre::Result<()> {
sqlx::raw_sql(
r#"
DROP SCHEMA public CASCADE;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO postgres;
GRANT ALL ON SCHEMA public TO public;
"#,
)
.execute(connection)
.await
.wrap_err("Failed to clean database")?;
Ok(())
}
async fn create_migrations_table(&self, connection: &Pool<Postgres>) -> eyre::Result<()> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS schema_migrations (
id VARCHAR(255) PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"#,
)
.execute(connection)
.await
.wrap_err("Failed to create migrations table")?;
Ok(())
}
}