use crate::database_drivers::DatabaseDriver;
use anyhow::{bail, Result};
use libsql::{params, Builder, Connection};
use log::info;
use std::future::Future;
use std::pin::Pin;
use super::utils;
pub struct LibSQLDriver {
db: Connection,
migrations_table: String,
migrations_folder: String,
schema_file: String,
}
impl LibSQLDriver {
pub async fn new(
db_url: &str,
token: Option<String>,
migrations_table: String,
migrations_folder: String,
schema_file: String,
) -> Result<LibSQLDriver> {
let db = if !db_url.starts_with("libsql://./") {
let auth_token = if let Some(t) = token {
t
} else {
info!("Token is not set, using empty string");
"".to_string()
};
Builder::new_remote(db_url.to_owned(), auth_token)
.build()
.await
.unwrap()
} else {
bail!("libsql:// should only be used with remote database. Use sqlite:// protocol when running local sqlite files")
};
let client = db.connect()?;
Ok(LibSQLDriver {
db: client,
migrations_folder,
migrations_table,
schema_file,
})
}
}
impl DatabaseDriver for LibSQLDriver {
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, "\"");
self.db
.execute(
format!(
"CREATE TABLE IF NOT EXISTS {} (id VARCHAR(255) NOT NULL PRIMARY KEY);",
table
)
.as_str(),
params![],
)
.await?;
let mut result = self
.db
.query(
format!(" SELECT id FROM {} ORDER BY id DESC;", table).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 = std::prelude::v1::Result<(), anyhow::Error>> + '_>> {
let fut = async move {
bail!("Geni does not support creating a database, it should be done via the respective interface")
};
Box::pin(fut)
}
fn drop_database(
&mut self,
) -> Pin<Box<dyn Future<Output = std::prelude::v1::Result<(), anyhow::Error>> + '_>> {
let fut = async move {
bail!("Geni does not support dropping a database, it should be done via the respective interface")
};
Box::pin(fut)
}
fn ready(&mut self) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>> {
let fut = async move {
self.db.execute("SELECT 1", params![]).await?;
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#"
--
-- LibSQL 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::*;
#[test]
fn test_validate_libsql_url_valid_http() {
let url = "http://localhost:8080";
let result = validate_libsql_url(url);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_validate_libsql_url_valid_https() {
let url = "https://my-db.turso.io";
let result = validate_libsql_url(url);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_validate_libsql_url_valid_libsql_remote() {
let url = "libsql://my-db.turso.io";
let result = validate_libsql_url(url);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_validate_libsql_url_invalid_local_libsql() {
let url = "libsql://./local.db";
let result = validate_libsql_url(url);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("libsql:// should only be used with remote database"));
}
#[test]
fn test_validate_libsql_url_invalid_scheme() {
let invalid_urls = vec![
"sqlite://test.db",
"postgres://localhost/test",
"mysql://localhost/test",
"file://test.db",
"ftp://example.com",
];
for url in invalid_urls {
let result = validate_libsql_url(url);
assert!(result.is_err(), "URL should be invalid: {}", url);
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid LibSQL URL scheme"));
}
}
#[test]
fn test_get_auth_token_or_default_with_token() {
let token = Some("auth_token_123".to_string());
let result = get_auth_token_or_default(token);
assert_eq!(result, "auth_token_123");
}
#[test]
fn test_get_auth_token_or_default_no_token() {
let token = None;
let result = get_auth_token_or_default(token);
assert_eq!(result, "");
}
#[test]
fn test_get_auth_token_or_default_empty_token() {
let token = Some("".to_string());
let result = get_auth_token_or_default(token);
assert_eq!(result, "");
}
#[test]
fn test_libsql_driver_parameters() {
let _db_url = "https://my-db.turso.io".to_string();
let _token: Option<String> = Some("token".to_string());
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 _future = LibSQLDriver::new(
&_db_url,
_token.clone(),
_migrations_table.clone(),
_migrations_folder.clone(),
_schema_file.clone(),
);
Ok::<(), anyhow::Error>(())
};
assert!(true);
}
#[test]
fn test_libsql_driver_struct_fields() {
fn _test_fields() {
let _check_migrations_table: fn(&LibSQLDriver) -> &String =
|driver| &driver.migrations_table;
let _check_migrations_folder: fn(&LibSQLDriver) -> &String =
|driver| &driver.migrations_folder;
let _check_schema_file: fn(&LibSQLDriver) -> &String = |driver| &driver.schema_file;
}
assert!(true);
}
#[test]
fn test_url_scheme_detection() {
let test_cases = vec![
("http://localhost:8080", true),
("https://my-db.turso.io", true),
("libsql://remote-db.example.com", true),
("libsql://./local.db", false), ("postgres://localhost/test", false),
("sqlite://test.db", false),
("invalid-url", false),
];
for (url, should_be_valid) in test_cases {
let result = validate_libsql_url(url);
if should_be_valid {
assert!(result.is_ok(), "URL should be valid: {}", url);
} else {
assert!(result.is_err(), "URL should be invalid: {}", url);
}
}
}
#[test]
fn test_local_path_rejection() {
let local_paths = vec![
"libsql://./test.db",
"libsql://./nested/path/test.db",
"libsql://./relative.sqlite",
];
for path in local_paths {
let result = validate_libsql_url(path);
assert!(result.is_err(), "Local path should be rejected: {}", path);
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("should only be used with remote database"));
}
}
#[test]
fn test_remote_url_acceptance() {
let remote_urls = vec![
"http://localhost:8080",
"https://my-database.turso.io",
"libsql://prod-db.example.com",
"https://staging-db.company.com:8080",
"http://192.168.1.100:3000",
];
for url in remote_urls {
let result = validate_libsql_url(url);
assert!(result.is_ok(), "Remote URL should be accepted: {}", url);
}
}
#[test]
fn test_edge_case_urls() {
let edge_cases = vec![
("http://", false), ("https://", false), ("libsql://", false), ("http://localhost", true), ("https://example.com", true), ];
for (url, should_be_valid) in edge_cases {
let result = validate_libsql_url(url);
if should_be_valid {
assert!(result.is_ok(), "Edge case URL should be valid: {}", url);
} else {
assert!(
result.is_ok(),
"Edge case URL with valid scheme should pass validation: {}",
url
);
}
}
}
#[test]
fn test_libsql_quote_identifier_schema_qualified() {
assert_eq!(
utils::quote_identifier("migrations.migrations", "\""),
"\"migrations\".\"migrations\""
);
}
}