Skip to content

Commit

Permalink
Mysql maria dump (#127)
Browse files Browse the repository at this point in the history
* Removed not needed comments in postgres dump

* Remove mariadbdump in favor of raw queries for mysql

* Replace mariadbdump in favor of raw queries for mariadb

* Bump version to 1.0.9

* Remove Dockerfile.slim as we don't need it anymore as we don't use external binaries for dumping data

* Remove mariadb-client from the docker image

* Remove Docker slim from gh actions

* Ran cargo fix and removed not used struct fiels
  • Loading branch information
emilpriver authored Jun 24, 2024
1 parent 9b949d3 commit 6a55605
Show file tree
Hide file tree
Showing 8 changed files with 471 additions and 117 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ jobs:
- dockerfile: Dockerfile
name:
buildname: Default
- dockerfile: Dockerfile.slim
name: -slim
buildname: Slim
steps:
- uses: actions/checkout@v4
- name: Login to GitHub Container Registry
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "geni"
version = "1.0.8"
version = "1.0.9"
edition = "2021"
resolver = "2"
description = "A standalone database CLI migration tool"
Expand Down
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ RUN cp target/release/geni /usr/src/app/geni
FROM alpine:3.19.1
COPY --from=builder /usr/src/app/geni /usr/src/app/geni

RUN apk add mariadb-client

ENV DATABASE_MIGRATIONS_FOLDER="/migrations"

LABEL org.opencontainers.image.description "Geni: Standalone database migration tool which works for Postgres, MariaDB, MySQL, Sqlite and LibSQL(Turso)."
Expand Down
20 changes: 0 additions & 20 deletions Dockerfile.slim

This file was deleted.

276 changes: 234 additions & 42 deletions src/lib/database_drivers/maria.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
use crate::database_drivers::DatabaseDriver;
use anyhow::{bail, Result};
use log::{error, info};
use regex::Regex;
use sqlx::mysql::MySqlRow;
use sqlx::Executor;
use sqlx::{Connection, MySqlConnection, Row};
use std::future::Future;
use std::pin::Pin;
use tokio::process::Command;

use super::utils;

pub struct MariaDBDriver {
db: MySqlConnection,
url: String,
url_path: url::Url,
db_name: String,
migrations_table: String,
migrations_folder: String,
Expand Down Expand Up @@ -65,7 +62,6 @@ impl<'a> MariaDBDriver {
db: client.unwrap(),
url: db_url.to_string(),
db_name: database_name.to_string(),
url_path,
migrations_folder,
migrations_table,
schema_file,
Expand Down Expand Up @@ -94,10 +90,10 @@ impl DatabaseDriver for MariaDBDriver {
}
}
return Ok(());
} else {
self.db.execute(query).await?;
}

self.db.execute(query).await?;

Ok(())
};

Expand Down Expand Up @@ -195,48 +191,244 @@ impl DatabaseDriver for MariaDBDriver {
&mut self,
) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + '_>> {
let fut = async move {
if let Err(_err) = which::which("mariadb-dump") {
bail!("mysqldump not found in PATH, is i installed?");
};

let host = format!("--host={}", self.url_path.host_str().unwrap());
let username = format!("--user={}", self.url_path.username());
let password = format!("--password={}", self.url_path.password().unwrap());
let port = format!("--port={}", self.url_path.port().unwrap());

let args: Vec<&str> = [
"--opt",
"--skip-dump-date",
"--skip-add-drop-table",
"--no-data",
port.as_str(),
host.as_str(),
username.as_str(),
password.as_str(),
self.db_name.as_str(),
]
.to_vec();

let res = Command::new("mariadb-dump").args(args).output().await?;
if !res.status.success() {
bail!("mysqldump failed: {}", String::from_utf8_lossy(&res.stderr));
let schema = r#"
--
-- SQL Schema dump automatic generated by geni
--
"#;

let mut schema = schema
.lines()
.map(str::trim_start)
.collect::<Vec<&str>>()
.join("\n");

let tables: Vec<String> = sqlx::query(
r#"
SELECT
CONCAT(
'CREATE TABLE ',
TABLE_NAME,
' (\n',
GROUP_CONCAT(
CONCAT(
' ', COLUMN_NAME, ' ', COLUMN_TYPE,
IF(IS_NULLABLE = 'NO', ' NOT NULL', ''),
IF(COLUMN_DEFAULT IS NOT NULL, CONCAT(' DEFAULT ', COLUMN_DEFAULT), '')
) SEPARATOR', \n'
),
'\n);'
) AS create_table_stmt
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_SCHEMA = ? AND TABLE_NAME NOT IN (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_SCHEMA = ?)
GROUP BY
TABLE_NAME
ORDER BY
TABLE_NAME;
"#,
)
.bind(&self.db_name)
.bind(&self.db_name)
.map(|row: MySqlRow| row.get("create_table_stmt"))
.fetch_all(&mut self.db)
.await?;

if tables.len() > 0 {
schema.push_str("-- TABLES \n\n");
for ele in tables.iter() {
schema.push_str(ele.as_str());
schema.push_str("\n\n")
}
}

let schema = String::from_utf8_lossy(&res.stdout);
let views: Vec<String> = sqlx::query(
r#"
SELECT
CONCAT(
'CREATE VIEW ',
TABLE_NAME,
' AS ',
VIEW_DEFINITION,
';'
) AS create_view_stmt
FROM
INFORMATION_SCHEMA.VIEWS
WHERE
TABLE_SCHEMA = ?;
"#,
)
.bind(&self.db_name)
.map(|row: MySqlRow| row.get("create_view_stmt"))
.fetch_all(&mut self.db)
.await?;

if views.len() > 0 {
schema.push_str("-- VIEWS \n\n");
for ele in views.iter() {
schema.push_str(ele.as_str());
schema.push_str("\n\n")
}
}

let re = Regex::new(r"^/\*![0-9]{5}.*\*/").unwrap();
let constraints: Vec<String> = sqlx::query(
r#"
SELECT
CONCAT(
'ALTER TABLE ',
TABLE_NAME,
' ADD CONSTRAINT ',
CASE
WHEN CONSTRAINT_NAME = 'PRIMARY' THEN 'PRIMARY KEY'
WHEN INDEX_NAME != 'PRIMARY' THEN 'UNIQUE'
ELSE 'FOREIGN KEY'
END,
' (',
COLUMN_NAME,
CASE
WHEN REFERENCED_TABLE_NAME IS NOT NULL THEN
CONCAT(') REFERENCES ', REFERENCED_TABLE_NAME, ' (', REFERENCED_COLUMN_NAME, ')')
ELSE ')'
END,
';'
) AS create_constraint_stmt
FROM
(
SELECT
TABLE_NAME,
COLUMN_NAME,
CONSTRAINT_NAME,
NULL AS INDEX_NAME,
NULL AS REFERENCED_TABLE_NAME,
NULL AS REFERENCED_COLUMN_NAME
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
TABLE_SCHEMA = ?
AND CONSTRAINT_NAME = 'PRIMARY'
UNION ALL
SELECT
TABLE_NAME,
COLUMN_NAME,
NULL AS CONSTRAINT_NAME,
INDEX_NAME,
NULL AS REFERENCED_TABLE_NAME,
NULL AS REFERENCED_COLUMN_NAME
FROM
INFORMATION_SCHEMA.STATISTICS
WHERE
TABLE_SCHEMA = ?
AND INDEX_NAME != 'PRIMARY'
UNION ALL
SELECT
TABLE_NAME,
COLUMN_NAME,
CONSTRAINT_NAME,
NULL AS INDEX_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
TABLE_SCHEMA = ?
AND REFERENCED_TABLE_NAME IS NOT NULL
) AS constraints
ORDER BY
TABLE_NAME;
"#,
)
.bind(&self.db_name)
.bind(&self.db_name)
.bind(&self.db_name)
.map(|row: MySqlRow| row.get("create_constraint_stmt"))
.fetch_all(&mut self.db)
.await?;

let final_schema: String = schema.lines().filter(|line| !re.is_match(line)).fold(
String::new(),
|mut acc, line| {
acc.push_str(line);
acc.push('\n');
acc
},
);
if constraints.len() > 0 {
schema.push_str("-- CONSTRAINTS \n\n");
for ele in constraints.iter() {
schema.push_str(ele.as_str());
schema.push_str("\n\n")
}
}

let indexes: Vec<String> = sqlx::query(
r#"
SELECT
CONCAT(
'CREATE INDEX ',
INDEX_NAME,
' ON ',
TABLE_NAME,
' (',
COLUMN_NAME,
');'
) AS create_index_stmt
FROM
INFORMATION_SCHEMA.STATISTICS
WHERE
TABLE_SCHEMA = ?
GROUP BY
TABLE_NAME, INDEX_NAME, COLUMN_NAME
ORDER BY
TABLE_NAME;
"#,
)
.bind(&self.db_name)
.map(|row: MySqlRow| row.get("create_index_stmt"))
.fetch_all(&mut self.db)
.await?;

if indexes.len() > 0 {
schema.push_str("-- INDEXES \n\n");
for ele in indexes.iter() {
schema.push_str(ele.as_str());
schema.push_str("\n\n")
}
}

let comments: Vec<String> = sqlx::query(
r#"
SELECT
CONCAT(
CASE
WHEN TABLE_COMMENT IS NOT NULL THEN
CONCAT('ALTER TABLE ', TABLE_NAME, ' COMMENT = ''', TABLE_COMMENT, ''';')
ELSE
CONCAT('ALTER TABLE ', TABLE_NAME, ' MODIFY COLUMN ', COLUMN_NAME, ' COMMENT ''', COLUMN_COMMENT, ''';')
END
) AS comment_stmt
FROM
(
SELECT TABLE_NAME, TABLE_COMMENT, NULL AS COLUMN_NAME, NULL AS COLUMN_COMMENT
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = ? AND (TABLE_COMMENT IS NOT NULL OR TABLE_COMMENT != '')
UNION ALL
SELECT TABLE_NAME, NULL, COLUMN_NAME, COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = ? AND (COLUMN_COMMENT IS NOT NULL OR COLUMN_COMMENT != '')
) AS comments;
"#,
)
.bind(&self.db_name)
.bind(&self.db_name)
.map(|row: MySqlRow| row.get("comment_stmt"))
.fetch_all(&mut self.db)
.await?;

if comments.len() > 0 {
schema.push_str("-- COMMENTS \n\n");
for ele in comments.iter() {
schema.push_str(ele.as_str());
schema.push_str("\n\n")
}
}

utils::write_to_schema_file(
final_schema,
schema.to_string(),
self.migrations_folder.clone(),
self.schema_file.clone(),
)
Expand Down
Loading

0 comments on commit 6a55605

Please sign in to comment.