Skip to content

Commit

Permalink
Add more subcommands and format code
Browse files Browse the repository at this point in the history
Signed-off-by: Gabriel de Maeztu <[email protected]>
  • Loading branch information
merqurio committed Aug 24, 2024
1 parent b060afa commit 545c16c
Show file tree
Hide file tree
Showing 11 changed files with 2,238 additions and 250 deletions.
1,483 changes: 1,378 additions & 105 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ env_logger = "0.8.4"
indexmap = "1.9.3"
log = "0.4"
sqlparser = { version = "0.50.0", features = ["visitor"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "mysql", "sqlite", "tls-rustls-ring"] }
tokio = { version = "1.0", features = ["full"] }
walkdir = "2.3"

Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Oxigration: SQL Migration Manager
# Oxigration: DBMS Schema Migration Manager

Oxigration is a SQL migration manager designed to implement database schemas based on individual files defining SQL objects organized in directories. This tool simplifies the process of managing and applying database schema changes across different environments.
Oxigration is a tool for managing database schema migrations, where a DBMS (Database Management System) organizes and maintains the structure of data, and a schema defines the blueprint of how data is stored and accessed. This tool simplifies the process of managing and applying database schema changes across different environments.

**Features**

- Takes DBMS objects represented in files and migrates the DBMS
- Schemas are represented as directories
- Each object type is stored in a directory within the schema directory with its object types
- Tracks schema version and applied migrations
- Supports multiple database environments
- Migrate the DBMS by applying SQL changes accurately
- Takes DBMS objects represented in files and migrates the DBMS
- Tracks schema version and applied migrations
- Generate DBMS objects source code from the DBMS
- Supports multiple database environments and conditional logic
- Schemas are directories, with each DBMS object type in its own subdirectory

**Example Directory Layout**

Expand Down
6 changes: 6 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# NOTE: this does NOT indicate a Minimum Supported Rust Version (MSRV) of SQLx.
# We reserve the right to increase this version at any time without considering it to be a breaking change.
# See the answer in FAQ.md for details.
[toolchain]
channel = "1.80"
profile = "minimal"
206 changes: 206 additions & 0 deletions src/deploy_log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
use indexmap::IndexMap;
use sqlx::{query, query_scalar, AnyPool, Executor, Row};
use std::error::Error;
use std::sync::atomic::Ordering;

use crate::relational_object::DatabaseObject;
use crate::utils::{format_query_with_schema, SCHEMA_SUPPORT};

/// This function initializes the deploy log and the configuration settings in the database.
/// It performs the following steps:
///
/// 1. Creates a deploy schema if it does not already exist.
/// 2. Creates a `deploy_log` table to keep track of all the changes that have been applied to the database.
/// 3. Creates a `deploy_log_config` table to store configuration settings related to the deployment process.
/// 4. Inserts the initial configuration settings into the `deploy_log_config` table.
///
/// The `deploy_log` table is crucial for tracking which changes have been applied to the database, ensuring that
/// changes are not reapplied, and enabling rollback functionality. The `deploy_log_config` table stores settings
/// that can influence the deployment process, such as environment-specific configurations.
///
/// # Arguments
///
/// * `connection_string` - A string slice that holds the connection string to the target database.
///
/// # Returns
///
/// This function returns a `Result`:
/// * `Ok(true)` if the deploy log is successfully initialized.
/// * `Err(Box<dyn Error>)` if there is an error during the initialization process.
///
/// # Errors
///
/// This function will return an error if:
/// * There is an issue connecting to the database.
/// * There is an error executing the SQL statements to create the schema, tables, or insert the configuration settings.
pub async fn init_deploy_log(connection_string: &str) -> Result<bool, Box<dyn Error>> {
let pool = AnyPool::connect(connection_string).await?;

// Check if the database is SQLite
let is_sqlite = connection_string.starts_with("sqlite");

if !is_sqlite {
// Check if the database supports schemas
let supports_schemas: bool = query_scalar(
"SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'information_schema');"
)
.fetch_one(&pool)
.await?;

SCHEMA_SUPPORT.store(supports_schemas, Ordering::Relaxed);

if supports_schemas {
// Create oxigration schema if it does not exist
pool.execute("CREATE SCHEMA IF NOT EXISTS oxigration;")
.await?;
}
}

// Create deploy_log table if it does not exist
pool.execute(
&*format_query_with_schema(
"CREATE TABLE IF NOT EXISTS {schema_prefix}deploy_log (
id INTEGER PRIMARY KEY,
change_name TEXT NOT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);",
)
.to_string(),
)
.await?;

// Create deploy_log_config table if it does not exist
let query = format_query_with_schema(
"CREATE TABLE IF NOT EXISTS {schema_prefix}deploy_log_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);",
)
.to_string();
pool.execute(&*query).await?;

Ok(true)
}

/// The function checks if the deploy log exists in the database by checking the deploy_log table and that there is content in the deploy_log table.
/// This is crucial for determining if a rollback is possible. The function follows these steps:
///
/// 1. It verifies if the `deploy_log` table exists in the database.
/// 2. It checks if there are any entries in the `deploy_log` table.
/// 3. If the `deploy_log` table exists and has entries, it indicates that rollback is possible.
/// 4. If the `deploy_log` table does not exist or has no entries, rollback is not possible.
///
/// The function returns a boolean indicating if the deploy log exists and has content.
///
/// # Arguments
///
/// * `connection_string` - A string slice that holds the connection string to the target database.
///
/// # Returns
///
/// This function returns a `Result`:
/// * `Ok(true)` if the deploy log exists and has content.
/// * `Ok(false)` if the deploy log does not exist or has no content.
/// * `Err(Box<dyn Error>)` if there is an error during the check.
///
/// # Errors
///
/// This function will return an error if:
/// * There is an issue connecting to the database.
/// * There is an error executing the query to check the `deploy_log` table.
pub async fn check_deploy_log_in_db(connection_string: &str) -> Result<bool, Box<dyn Error>> {
let pool = AnyPool::connect(connection_string).await?;

// Check if the deploy_log table exists
let table_exists: bool = query_scalar(
"SELECT EXISTS (SELECT table_name FROM information_schema.tables WHERE table_schema = 'oxigration' AND table_name = 'deploy_log');"
)
.fetch_one(&pool)
.await?;

if !table_exists {
return Ok(false);
}

// Check if the deploy_log table has entries
let log_has_entries: bool =
query_scalar("SELECT EXISTS (SELECT 1 FROM oxigration.deploy_log LIMIT 1);")
.fetch_one(&pool)
.await?;

Ok(log_has_entries)
}

/// The function reads the deploy log from the database
/// Returns an indexmap of DatabaseObject
pub async fn read_deploy_log(
connection_string: &str,
) -> Result<IndexMap<String, DatabaseObject>, Box<dyn Error>> {
if !check_deploy_log_in_db(connection_string).await? {
return Err("Deploy log does not exist in the database".into());
}

let pool = AnyPool::connect(connection_string).await?;
let mut deploy_log = IndexMap::new();

let rows = query("SELECT change_name FROM oxigration.deploy_log;")
.fetch_all(&pool)
.await?;

for _ in rows {
// let change_name: String = row.try_get("change_name")?;
// Assuming DatabaseObject can be created from change_name
// let db_object = DatabaseObject::new(change_name.clone(), /* other required args */);
// deploy_log.insert(change_name, db_object);
}

Ok(deploy_log)
}

#[cfg(test)]
mod tests {
use super::*;
use sqlx::AnyPool;

#[tokio::test]
async fn test_init_deploy_log() -> Result<(), Box<dyn Error>> {
// Install the default drivers
sqlx::any::install_default_drivers();

// Use an in-memory SQLite database for testing
let connection_string = "postgresql://[email protected]/postgres";
let pool = AnyPool::connect(connection_string).await?;

// Initialize the deploy log
let result = init_deploy_log(connection_string).await?;
assert!(result, "Initialization should return true");

// Verify the oxigration schema exists (only if not SQLite)
if !connection_string.starts_with("sqlite") {
let schema_exists: bool = query_scalar(
"SELECT EXISTS (SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'oxigration');"
)
.fetch_one(&pool)
.await?;
assert!(schema_exists, "oxigration schema should exist");
}

// Verify the deploy_log table exists
let table_exists: bool = query_scalar(
"SELECT EXISTS (SELECT table_name FROM information_schema.tables WHERE table_name = 'deploy_log');"
)
.fetch_one(&pool)
.await?;
assert!(table_exists, "deploy_log table should exist");

// Verify the deploy_log_config table exists
let config_table_exists: bool = query_scalar(
"SELECT EXISTS (SELECT table_name FROM information_schema.tables WHERE table_name = 'deploy_log_config');"
)
.fetch_one(&pool)
.await?;
assert!(config_table_exists, "deploy_log_config table should exist");

Ok(())
}
}
Loading

0 comments on commit 545c16c

Please sign in to comment.