use crate::database_drivers::DatabaseDriver;
use anyhow::Result;
use libsql::{params, Builder, Connection};
use std::fs::{self, File};
use std::future::Future;
use std::pin::Pin;
use super::utils;
pub struct SqliteDriver {
db: Connection,
path: String,
migrations_table: String,
migrations_folder: String,
schema_file: String,
}
impl SqliteDriver {
pub async fn new(
db_url: &str,
migrations_table: String,
migrations_folder: String,
schema_file: String,
) -> Result<SqliteDriver> {
let path = if db_url.contains("://") {
std::path::Path::new(db_url.split_once("://").unwrap().1)
} else {
std::path::Path::new(db_url)
};
if File::open(path.to_str().unwrap()).is_err() {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
File::create(path)?;
}
let client = Builder::new_local(path).build().await?.connect()?;
Ok(SqliteDriver {
db: client,
path: path.to_str().unwrap().to_string(),
migrations_table,
migrations_folder,
schema_file,
})
}
}
impl DatabaseDriver for SqliteDriver {
fn execute<'a>(
&'a mut self,
query: &'a str,
run_in_transaction: bool,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>> {
let fut = async move {
if run_in_transaction {
self.db.execute_transactional_batch(query).await?;
} else {
self.db.execute_batch(query).await?;
}
Ok(())
};
Box::pin(fut)
}
fn get_or_create_schema_migrations(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, anyhow::Error>> + '_>> {
let fut = async move {
let table = utils::quote_identifier(&self.migrations_table, "\"");
let query = format!(
"CREATE TABLE IF NOT EXISTS {} (id VARCHAR(255) PRIMARY KEY);",
table
);
self.db.execute(query.as_str(), params![]).await?;
let query = format!("SELECT id FROM {} ORDER BY id DESC;", table);
let mut result = self.db.query(query.as_str(), params![]).await?;
let mut schema_migrations: Vec<String> = vec![];
while let Some(row) = result.next().await.unwrap() {
if let Ok(r) = row.get_str(0) {
schema_migrations.push(r.to_string());
continue;
}
break;
}
Ok(schema_migrations)
};
Box::pin(fut)
}
fn insert_schema_migration<'a>(
&'a mut self,
id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>> {
let fut = async move {
let table = utils::quote_identifier(&self.migrations_table, "\"");
self.db
.execute(
format!("INSERT INTO {} (id) VALUES ('{}');", table, id,).as_str(),
params![],
)
.await?;
Ok(())
};
Box::pin(fut)
}
fn remove_schema_migration<'a>(
&'a mut self,
id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>> {
let fut = async move {
let table = utils::quote_identifier(&self.migrations_table, "\"");
self.db
.execute(
format!("DELETE FROM {} WHERE id = '{}';", table, id).as_str(),
params![],
)
.await?;
Ok(())
};
Box::pin(fut)
}
fn create_database(&mut self) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>> {
let fut = async move { Ok(()) };
Box::pin(fut)
}
fn drop_database(&mut self) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>> {
let fut = async move {
fs::remove_file(&mut self.path)?;
Ok(())
};
Box::pin(fut)
}
fn ready(&mut self) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>> {
let fut = async move { Ok(()) };
Box::pin(fut)
}
fn dump_database_schema(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>> {
let fut = async move {
let schema = r#"
--
-- Sqlite SQL Schema dump automatic generated by geni
--
"#;
let mut schema = schema
.lines()
.map(str::trim_start)
.collect::<Vec<&str>>()
.join("\n");
let mut result = self
.db
.query("SELECT sql FROM sqlite_master", params![])
.await?;
let mut schemas: Vec<String> = vec![];
while let Some(row) = result.next().await.unwrap_or(None) {
if let Ok(r) = row.get_str(0) {
let text = r
.to_string()
.trim_start_matches('"')
.trim_end_matches('"')
.to_string()
.replace("\\n", "\n");
schemas.push(format!("{};", text));
}
}
schema.push_str(schemas.join("\n").as_str());
utils::write_to_schema_file(
schema,
self.migrations_folder.clone(),
self.schema_file.clone(),
)
.await?;
Ok(())
};
Box::pin(fut)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::database_test_utils::*;
use std::path::Path;
#[test]
fn test_parse_sqlite_path_with_scheme() {
let url = "sqlite://test.db";
let result = parse_sqlite_path(url);
assert_eq!(result, "test.db");
}
#[test]
fn test_parse_sqlite_path_with_scheme_complex() {
let url = "sqlite:///absolute/path/to/database.db";
let result = parse_sqlite_path(url);
assert_eq!(result, "/absolute/path/to/database.db");
}
#[test]
fn test_parse_sqlite_path_relative() {
let url = "sqlite://./relative/path.db";
let result = parse_sqlite_path(url);
assert_eq!(result, "./relative/path.db");
}
#[test]
fn test_parse_sqlite_path_without_scheme() {
let url = "test.db";
let result = parse_sqlite_path(url);
assert_eq!(result, "test.db");
}
#[test]
fn test_parse_sqlite_path_without_scheme_complex() {
let url = "/absolute/path/to/database.db";
let result = parse_sqlite_path(url);
assert_eq!(result, "/absolute/path/to/database.db");
}
#[test]
fn test_parse_sqlite_path_memory() {
let url = "sqlite://:memory:";
let result = parse_sqlite_path(url);
assert_eq!(result, ":memory:");
}
#[test]
fn test_sqlite_driver_parameters() {
let _db_url = "sqlite://test.db";
let _migrations_table = "schema_migrations".to_string();
let _migrations_folder = "./migrations".to_string();
let _schema_file = "schema.sql".to_string();
let _test_signature = || async {
let _driver = SqliteDriver::new(
_db_url,
_migrations_table.clone(),
_migrations_folder.clone(),
_schema_file.clone(),
)
.await?;
Ok::<(), anyhow::Error>(())
};
assert!(true);
}
#[test]
fn test_sqlite_driver_struct_fields() {
fn _test_fields() {
let _check_path: fn(&SqliteDriver) -> &String = |driver| &driver.path;
let _check_migrations_table: fn(&SqliteDriver) -> &String =
|driver| &driver.migrations_table;
let _check_migrations_folder: fn(&SqliteDriver) -> &String =
|driver| &driver.migrations_folder;
let _check_schema_file: fn(&SqliteDriver) -> &String = |driver| &driver.schema_file;
}
assert!(true);
}
#[test]
fn test_path_handling_edge_cases() {
let test_cases = vec![
("sqlite://test.db", "test.db"),
("sqlite3://test.db", "test.db"),
("sqlite:///tmp/test.db", "/tmp/test.db"),
("sqlite://./relative.db", "./relative.db"),
("sqlite://:memory:", ":memory:"),
("test.db", "test.db"),
("./test.db", "./test.db"),
("/absolute/test.db", "/absolute/test.db"),
];
for (input, expected) in test_cases {
let result = parse_sqlite_path(input);
assert_eq!(result, expected, "Failed for input: {}", input);
}
}
#[test]
fn test_sqlite_url_variations() {
let urls = vec![
"sqlite://database.db",
"sqlite3://database.db",
"file://database.db",
"database.db",
];
for url in urls {
let path = parse_sqlite_path(url);
assert!(
!path.is_empty(),
"Path should not be empty for URL: {}",
url
);
if url.contains("://") {
assert!(
!path.contains("://"),
"Path should not contain scheme for URL: {}",
url
);
}
}
}
#[test]
fn test_path_validation() {
let test_cases = vec![
("./test.db", false), ("../test.db", false), ("test.db", false), ("/tmp/test.db", true), ("/var/lib/sqlite/test.db", true), ];
for (path_str, should_be_absolute) in test_cases {
let path = Path::new(path_str);
assert_eq!(
path.is_absolute(),
should_be_absolute,
"Path absoluteness mismatch for: {}",
path_str
);
}
}
#[test]
fn test_memory_database_detection() {
let memory_urls = vec!["sqlite://:memory:", ":memory:"];
for url in memory_urls {
let path = parse_sqlite_path(url);
assert_eq!(
path, ":memory:",
"Memory database not detected for: {}",
url
);
}
}
#[test]
fn test_sqlite_quote_identifier_schema_qualified() {
assert_eq!(
utils::quote_identifier("migrations.migrations", "\""),
"\"migrations\".\"migrations\""
);
}
}