geni 1.3.2

A standalone database CLI migration tool
Documentation
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)
    }

    // SQlite don't have a HTTP connection so we don't need to check if it's ready
    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() {
        // Test that SqliteDriver::new has the expected parameter types
        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();

        // Test that parameters are in the expected order (compile-time check)
        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() {
        // Test that SqliteDriver has expected fields (compile-time validation)
        // This ensures the struct maintains its expected interface

        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() {
        // Test various SQLite URL formats
        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() {
        // Test that various SQLite URL schemes are parsed correctly
        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() {
        // Test that paths are correctly identified as relative or absolute
        let test_cases = vec![
            ("./test.db", false),              // relative
            ("../test.db", false),             // relative
            ("test.db", false),                // relative
            ("/tmp/test.db", true),            // absolute
            ("/var/lib/sqlite/test.db", true), // absolute
        ];

        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\""
        );
    }
}