use crate::config;
use anyhow::bail;
use serde::{Deserialize, Serialize};
use std::future::Future;
use std::pin::Pin;
pub mod libsql;
pub mod maria;
pub mod mysql;
pub mod postgres;
pub mod sqlite;
pub mod turso;
pub mod utils;
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct SchemaMigration {
pub id: String,
}
pub trait DatabaseDriver {
fn execute<'a>(
&'a mut self,
query: &'a str,
run_in_transaction: bool,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>>;
fn create_database(&mut self) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>>;
fn drop_database(&mut self) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>>;
fn get_or_create_schema_migrations(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, anyhow::Error>> + '_>>;
fn insert_schema_migration<'a>(
&'a mut self,
id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>>;
fn remove_schema_migration<'a>(
&'a mut self,
id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>>;
fn ready(&mut self) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>>;
fn dump_database_schema(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>>;
}
pub async fn new(
db_url: String,
db_token: Option<String>,
migrations_table: String,
migrations_folder: String,
schema_file: String,
wait_timeout: Option<usize>,
with_selected_database: bool,
) -> Result<Box<dyn DatabaseDriver>, anyhow::Error> {
let mut parsed_db_url = url::Url::parse(&db_url)?;
let cloned_db_url = parsed_db_url.clone();
let mut database_name = cloned_db_url.path().trim_start_matches('/');
let driver = config::Database::new(parsed_db_url.scheme())?;
if let (
false,
config::Database::MariaDB | config::Database::Postgres | config::Database::MySQL,
) = (with_selected_database, driver)
{
parsed_db_url.set_path("");
database_name = cloned_db_url.path().trim_start_matches('/');
}
let scheme = parsed_db_url.scheme();
match scheme {
"http" | "https" | "libsql" => {
let driver = libsql::LibSQLDriver::new(
parsed_db_url.as_str(),
db_token,
migrations_table,
migrations_folder,
schema_file,
)
.await?;
Ok(Box::new(driver))
}
"turso" => {
let driver = turso::TursoDriver::new(
parsed_db_url.as_str(),
db_token,
migrations_table,
migrations_folder,
schema_file,
)
.await?;
Ok(Box::new(driver))
}
"postgres" | "psql" | "postgresql" => {
parsed_db_url.set_scheme("postgresql").unwrap();
let driver = postgres::PostgresDriver::new(
parsed_db_url.as_str(),
database_name,
wait_timeout,
migrations_table,
migrations_folder,
schema_file,
)
.await?;
Ok(Box::new(driver))
}
"mysql" => {
let driver = mysql::MySQLDriver::new(
parsed_db_url.as_str(),
database_name,
wait_timeout,
migrations_table,
migrations_folder,
schema_file,
)
.await?;
Ok(Box::new(driver))
}
"sqlite" | "sqlite3" => {
let driver = sqlite::SqliteDriver::new(
&db_url,
migrations_table,
migrations_folder,
schema_file,
)
.await?;
Ok(Box::new(driver))
}
"mariadb" => {
let driver = maria::MariaDBDriver::new(
parsed_db_url.as_str(),
database_name,
wait_timeout,
migrations_table,
migrations_folder,
schema_file,
)
.await?;
Ok(Box::new(driver))
}
_ => bail!("Unsupported database driver: {}", scheme),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::database_test_utils::*;
#[test]
fn test_schema_migration_struct() {
let migration = SchemaMigration {
id: "1234567890".to_string(),
};
assert_eq!(migration.id, "1234567890");
}
#[test]
fn test_extract_database_name_from_url_postgres() {
let url = "postgres://user:pass@localhost:5432/testdb";
let result = extract_database_name_from_url(url).unwrap();
assert_eq!(result, "testdb");
}
#[test]
fn test_extract_database_name_from_url_mysql() {
let url = "mysql://root:password@localhost:3306/myapp";
let result = extract_database_name_from_url(url).unwrap();
assert_eq!(result, "myapp");
}
#[test]
fn test_extract_database_name_from_url_sqlite() {
let url = "sqlite://./test.db";
let result = extract_database_name_from_url(url).unwrap();
assert_eq!(result, "test.db"); }
#[test]
fn test_extract_database_name_from_url_no_db() {
let url = "postgres://localhost:5432/";
let result = extract_database_name_from_url(url).unwrap();
assert_eq!(result, "");
}
#[test]
fn test_extract_database_name_from_url_invalid() {
let url = "not-a-url";
let result = extract_database_name_from_url(url);
assert!(result.is_err());
}
#[test]
fn test_get_database_type_from_url_postgres() {
let url = "postgres://localhost/test";
let result = get_database_type_from_url(url).unwrap();
assert!(matches!(result, crate::config::Database::Postgres));
}
#[test]
fn test_get_database_type_from_url_mysql() {
let url = "mysql://localhost/test";
let result = get_database_type_from_url(url).unwrap();
assert!(matches!(result, crate::config::Database::MySQL));
}
#[test]
fn test_get_database_type_from_url_mariadb() {
let url = "mariadb://localhost/test";
let result = get_database_type_from_url(url).unwrap();
assert!(matches!(result, crate::config::Database::MariaDB));
}
#[test]
fn test_get_database_type_from_url_sqlite() {
let url = "sqlite://test.db";
let result = get_database_type_from_url(url).unwrap();
assert!(matches!(result, crate::config::Database::SQLite));
}
#[test]
fn test_get_database_type_from_url_sqlite3() {
let url = "sqlite3://test.db";
let result = get_database_type_from_url(url).unwrap();
assert!(matches!(result, crate::config::Database::SQLite));
}
#[test]
fn test_get_database_type_from_url_libsql() {
let url = "http://localhost:8080";
let result = get_database_type_from_url(url).unwrap();
assert!(matches!(result, crate::config::Database::LibSQL));
}
#[test]
fn test_get_database_type_from_url_libsql_https() {
let url = "https://my-db.turso.io";
let result = get_database_type_from_url(url).unwrap();
assert!(matches!(result, crate::config::Database::LibSQL));
}
#[test]
fn test_get_database_type_from_url_libsql_scheme() {
let url = "libsql://my-db.turso.io";
let result = get_database_type_from_url(url).unwrap();
assert!(matches!(result, crate::config::Database::LibSQL));
}
#[test]
fn test_get_database_type_from_url_invalid_scheme() {
let url = "redis://localhost:6379";
let result = get_database_type_from_url(url);
assert!(result.is_err());
}
#[test]
fn test_get_database_type_from_url_invalid_url() {
let url = "not-a-valid-url";
let result = get_database_type_from_url(url);
assert!(result.is_err());
}
#[test]
fn test_database_driver_factory_parameters() {
let _params = (
"sqlite://test.db".to_string(), None::<String>, "schema_migrations".to_string(), "./migrations".to_string(), "schema.sql".to_string(), Some(30_usize), true, );
assert!(true);
}
#[test]
fn test_url_parsing_edge_cases() {
let test_cases = vec![
("postgres://user@localhost/db", "postgres"),
("postgresql://user@localhost/db", "postgresql"),
("psql://user@localhost/db", "psql"),
("mysql://root@localhost/app", "mysql"),
("mariadb://user@localhost/test", "mariadb"),
("sqlite://./test.db", "sqlite"),
("sqlite3://./test.db", "sqlite3"),
("http://localhost:8080", "http"),
("https://turso.io", "https"),
("libsql://turso.io", "libsql"),
];
for (url, expected_scheme) in test_cases {
let parsed_url = url::Url::parse(url).unwrap();
assert_eq!(parsed_url.scheme(), expected_scheme);
}
}
#[test]
fn test_postgresql_scheme_normalization() {
let test_urls = vec!["postgres://localhost/test", "psql://localhost/test"];
for url in test_urls {
let mut parsed_url = url::Url::parse(url).unwrap();
parsed_url.set_scheme("postgresql").unwrap();
assert_eq!(parsed_url.scheme(), "postgresql");
}
}
#[test]
fn test_database_path_manipulation() {
let url = "postgres://localhost:5432/testdb";
let mut parsed_url = url::Url::parse(url).unwrap();
let original_path = parsed_url.path();
assert_eq!(original_path, "/testdb");
parsed_url.set_path("");
assert_eq!(parsed_url.path(), "");
}
}