feat(pg-migration): init
This commit is contained in:
2438
package/postgres/pg-migration/Cargo.lock
generated
Normal file
2438
package/postgres/pg-migration/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package/postgres/pg-migration/Cargo.toml
Normal file
25
package/postgres/pg-migration/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "pg-migration"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.39"
|
||||
clap = { version = "4.5.28", features = [ "env" ] }
|
||||
postgres = "0.19.10"
|
||||
rand = "0.9.0"
|
||||
|
||||
[dev-dependencies]
|
||||
testcontainers = { version = "0.23.1", features = [] }
|
||||
|
||||
[lib]
|
||||
name = "pg_migration_lib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "pg-migration"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[test]]
|
||||
name = "test_migrate"
|
||||
path = "test/migrate.rs"
|
||||
15
package/postgres/pg-migration/default.nix
Normal file
15
package/postgres/pg-migration/default.nix
Normal file
@@ -0,0 +1,15 @@
|
||||
{ cargoToml, nativeBuildInputs, pkgs, ... }:
|
||||
let
|
||||
src = ./.;
|
||||
cargo = cargoToml src;
|
||||
in
|
||||
pkgs.rustPlatform.buildRustPackage {
|
||||
pname = cargo.package.name;
|
||||
version = cargo.package.version;
|
||||
|
||||
inherit nativeBuildInputs src;
|
||||
|
||||
cargoLock.lockFile = ./Cargo.lock;
|
||||
|
||||
doCheck = true;
|
||||
}
|
||||
12
package/postgres/pg-migration/src/lib.rs
Normal file
12
package/postgres/pg-migration/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use postgres::Client;
|
||||
|
||||
pub fn init_db(client: &mut Client) {
|
||||
client.batch_execute("
|
||||
CREATE SCHEMA IF NOT EXISTS hectic;
|
||||
CREATE TABLE IF NOT EXISTS hectic.migration (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
").unwrap();
|
||||
}
|
||||
144
package/postgres/pg-migration/src/main.rs
Normal file
144
package/postgres/pg-migration/src/main.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use clap::{Arg, Command, ArgAction};
|
||||
use postgres::{Client, NoTls};
|
||||
use chrono::Utc;
|
||||
use pg_migration_lib::init_db;
|
||||
use std::{fs, path::Path, process::Command as ProcessCommand};
|
||||
use rand::Rng;
|
||||
|
||||
fn main() {
|
||||
check_psql_installed();
|
||||
|
||||
let matches = Command::new("Rust PG Migration Tool")
|
||||
.version("0.1")
|
||||
.arg(
|
||||
Arg::new("db_url")
|
||||
.short('u')
|
||||
.long("db-url")
|
||||
.env("PG_URL")
|
||||
.num_args(1)
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("migration_dir")
|
||||
.short('d')
|
||||
.long("migration-dir")
|
||||
.env("MIGRATION_DIR")
|
||||
.num_args(1)
|
||||
.default_value("migration"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("migrate").arg(
|
||||
Arg::new("force")
|
||||
.short('f')
|
||||
.long("force")
|
||||
.action(ArgAction::SetTrue),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("create").arg(
|
||||
Arg::new("name")
|
||||
.short('n')
|
||||
.long("name")
|
||||
.num_args(1),
|
||||
),
|
||||
)
|
||||
.subcommand(Command::new("fetch"))
|
||||
.get_matches();
|
||||
|
||||
let db_url = matches.get_one::<String>("db_url").unwrap();
|
||||
let migration_dir = matches.get_one::<String>("migration_dir").unwrap();
|
||||
let mut client = Client::connect(db_url, NoTls).expect("DB connection failed");
|
||||
init_db(&mut client);
|
||||
|
||||
match matches.subcommand() {
|
||||
Some(("migrate", sub_m)) => {
|
||||
let force = sub_m.get_flag("force");
|
||||
apply_migrations(&mut client, migration_dir, db_url, force);
|
||||
}
|
||||
Some(("create", sub_m)) => {
|
||||
let name = sub_m
|
||||
.get_one::<String>("name")
|
||||
.cloned()
|
||||
.unwrap_or_else(generate_migration_name);
|
||||
create_migration_file(migration_dir, &name);
|
||||
}
|
||||
Some(("fetch", _)) => {
|
||||
fetch_migrations(&mut client, migration_dir);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_psql_installed() {
|
||||
if ProcessCommand::new("psql")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.is_err()
|
||||
{
|
||||
eprintln!("Error: psql is not installed or not in PATH.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_migrations(client: &mut Client, migration_dir: &str, db_url: &str, _force: bool) {
|
||||
let mut entries: Vec<_> = fs::read_dir(migration_dir)
|
||||
.expect("Reading migration directory failed")
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("sql"))
|
||||
.collect();
|
||||
entries.sort_by_key(|e| e.path());
|
||||
|
||||
// (Migration tree validation omitted)
|
||||
|
||||
for entry in entries {
|
||||
let file_path = entry.path();
|
||||
let file_name = file_path.file_name().unwrap().to_string_lossy();
|
||||
|
||||
if client
|
||||
.query_opt("SELECT 1 FROM hectic.migration WHERE name = $1", &[&file_name])
|
||||
.expect("Query failed")
|
||||
.is_some()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = ProcessCommand::new("psql")
|
||||
.arg("-d")
|
||||
.arg(db_url)
|
||||
.arg("-f")
|
||||
.arg(file_path.to_str().unwrap())
|
||||
.status()
|
||||
.expect("psql execution failed");
|
||||
if !status.success() {
|
||||
eprintln!("Migration failed: {}", file_name);
|
||||
break;
|
||||
}
|
||||
client
|
||||
.execute("INSERT INTO hectic.migration (name) VALUES ($1)", &[&file_name])
|
||||
.expect("Recording migration failed");
|
||||
}
|
||||
}
|
||||
|
||||
fn create_migration_file(migration_dir: &str, name: &str) {
|
||||
fs::create_dir_all(migration_dir).expect("Creating migration directory failed");
|
||||
let timestamp = Utc::now().timestamp();
|
||||
let file_name = format!("{}_{}.sql", timestamp, name);
|
||||
let file_path = Path::new(migration_dir).join(file_name);
|
||||
fs::write(&file_path, "-- Write your migration SQL here\n")
|
||||
.expect("Creating migration file failed");
|
||||
println!("Created migration: {:?}", file_path);
|
||||
}
|
||||
|
||||
fn fetch_migrations(_client: &mut Client, _migration_dir: &str) {
|
||||
// (Fetch implementation omitted)
|
||||
}
|
||||
|
||||
fn generate_migration_name() -> String {
|
||||
let adjectives = ["quick", "lazy", "sleepy", "noisy", "hungry"];
|
||||
let nouns = ["fox", "dog", "cat", "mouse", "bear"];
|
||||
let mut rng = rand::rng();
|
||||
let adj = adjectives[rng.random_range(0..adjectives.len())];
|
||||
let noun = nouns[rng.random_range(0..nouns.len())];
|
||||
format!("{}_{}", adj, noun)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Write your migration SQL here
|
||||
|
||||
CREATE TABLE zalupa ();
|
||||
38
package/postgres/pg-migration/test/migrate.rs
Normal file
38
package/postgres/pg-migration/test/migrate.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use postgres::{Client, NoTls};
|
||||
// use pg_migration_lib::init_db;
|
||||
// use testcontainers::{
|
||||
// core::{IntoContainerPort, WaitFor},
|
||||
// GenericImage, ImageExt,
|
||||
// runners::AsyncRunner
|
||||
// };
|
||||
//
|
||||
// #[test]
|
||||
// async fn test_init_db() {
|
||||
// let container = GenericImage::new("postgres", "latest")
|
||||
// .with_exposed_port(5432.tcp())
|
||||
// .with_wait_for(WaitFor::message_on_stdout("database system is ready"))
|
||||
// .with_env_var("POSTGRES_PASSWORD", "postgres")
|
||||
// .start().await
|
||||
// .expect("Failed to start container");
|
||||
//
|
||||
// let host_port = container.get_host_port(5432).expect("No mapped port");
|
||||
// let db_url = format!(
|
||||
// "postgres://postgres:postgres@127.0.0.1:{}/postgres",
|
||||
// host_port
|
||||
// );
|
||||
//
|
||||
// let mut client = Client::connect(&db_url, NoTls).expect("DB connection failed");
|
||||
// init_db(&mut client);
|
||||
//
|
||||
// let row = client
|
||||
// .query_one(
|
||||
// "SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'hectic'",
|
||||
// &[],
|
||||
// )
|
||||
// .unwrap();
|
||||
// assert_eq!(row.get::<_, &str>(0), "hectic");
|
||||
// }
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user