use std::{fs::write, thread::sleep, time::Duration};
use ::postgres::{Client, NoTls};
use eyre::eyre;
use native_tls::TlsConnector;
use postgres_native_tls::MakeTlsConnector;
use rusqlite::Connection;
use tracing::{debug, trace};
use crate::{App, error::Result, migration::Migration};
mod postgres;
mod sqlite;
pub use postgres::PostgresAdapter;
pub use sqlite::SqliteAdapter;
pub trait DatabaseAdapter {
fn init_up_sql(&self) -> &'static str;
fn load_migrations(&mut self) -> Result<Vec<Migration>>;
fn run_up_migration(&mut self, migration: &Migration) -> Result<()>;
fn run_down_migration(&mut self, migration: &Migration) -> Result<()>;
fn update_migration_hash(&mut self, name: &str, hash: &str) -> Result<()>;
fn clear_migrations(&mut self) -> Result<()>;
fn record_baseline(&mut self, name: &str, hash: &str) -> Result<()>;
fn dump_schema(&mut self, url: &str) -> Result<Vec<u8>>;
}
pub fn get_db_adapter(opts: &App, wait: bool) -> Result<Box<dyn DatabaseAdapter>> {
let url = &opts.options.url;
if url.starts_with("postgres://") || url.starts_with("postgresql://") {
let mut attempts = 0;
let tls = MakeTlsConnector::new(TlsConnector::builder().build()?);
let client = loop {
let client = if url.contains("sslmode=require") {
Client::connect(url, tls.clone())
} else {
Client::connect(url, NoTls)
};
match client {
Ok(client) => break client,
Err(err) => {
attempts += 1;
if !wait || attempts > 60 {
return Err(err.into());
}
trace!("failed to connect to postgres, retrying...");
sleep(Duration::from_secs(1));
}
}
};
Ok(Box::new(PostgresAdapter::new(client)))
} else if url.starts_with("sqlite://") {
let conn = Connection::open(url)?;
Ok(Box::new(SqliteAdapter::new(conn)))
} else {
Err(eyre!("unsupported database URL: {}", url))
}
}
pub fn maybe_dump_schema(db: &mut Box<dyn DatabaseAdapter>, opts: &App) -> Result<()> {
if let Some(path) = &opts.options.schema {
let schema = db.dump_schema(&opts.options.url)?;
write(path, &schema)?;
debug!("schema dumped to {path}");
}
Ok(())
}