rustorm_migrate/
history.rs1use chrono::{DateTime, Utc};
2use rustorm_core::error::{OrmError, OrmResult};
3use sha2::{Digest, Sha256};
4use sqlx::PgPool;
5
6pub const MIGRATIONS_TABLE: &str = "_orm_migrations";
7
8#[derive(Debug, Clone, sqlx::FromRow)]
10pub struct MigrationRecord {
11 pub id: i64,
12 pub version: String,
13 pub name: String,
14 pub applied_at: DateTime<Utc>,
15 pub checksum: String,
16 pub duration_ms: i32,
17}
18
19pub async fn ensure_history_table(pool: &PgPool) -> OrmResult<()> {
21 let sql = format!(
22 r#"CREATE TABLE IF NOT EXISTS "{}" (
23 id BIGSERIAL PRIMARY KEY,
24 version VARCHAR(20) NOT NULL UNIQUE,
25 name VARCHAR(255) NOT NULL,
26 applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
27 checksum VARCHAR(64) NOT NULL,
28 duration_ms INTEGER NOT NULL DEFAULT 0,
29 applied_by VARCHAR(100)
30 )"#,
31 MIGRATIONS_TABLE
32 );
33 sqlx::query(&sql)
34 .execute(pool)
35 .await
36 .map_err(OrmError::from_sqlx)?;
37 Ok(())
38}
39
40pub async fn applied_versions(pool: &PgPool) -> OrmResult<Vec<String>> {
42 let sql = format!(
43 "SELECT version FROM \"{}\" ORDER BY version ASC",
44 MIGRATIONS_TABLE
45 );
46 let rows: Vec<(String,)> = sqlx::query_as(&sql)
47 .fetch_all(pool)
48 .await
49 .map_err(OrmError::from_sqlx)?;
50 Ok(rows.into_iter().map(|r| r.0).collect())
51}
52
53pub async fn verify_checksum(pool: &PgPool, version: &str, sql_content: &str) -> OrmResult<()> {
55 let expected = sha256_hex(sql_content);
56 let query = format!(
57 "SELECT checksum FROM \"{}\" WHERE version = $1",
58 MIGRATIONS_TABLE
59 );
60 let row: Option<(String,)> = sqlx::query_as(&query)
61 .bind(version)
62 .fetch_optional(pool)
63 .await
64 .map_err(OrmError::from_sqlx)?;
65
66 if let Some((stored,)) = row {
67 if stored != expected {
68 return Err(OrmError::Migration(format!(
69 "Checksum миграции {} изменился! Файл был изменён после применения.",
70 version
71 )));
72 }
73 }
74 Ok(())
75}
76
77pub async fn record_migration(
79 pool: &PgPool,
80 version: &str,
81 name: &str,
82 sql_content: &str,
83 duration_ms: i32,
84) -> OrmResult<()> {
85 let checksum = sha256_hex(sql_content);
86 let sql = format!(
87 "INSERT INTO \"{}\" (version, name, checksum, duration_ms) VALUES ($1, $2, $3, $4)",
88 MIGRATIONS_TABLE
89 );
90 sqlx::query(&sql)
91 .bind(version)
92 .bind(name)
93 .bind(checksum)
94 .bind(duration_ms)
95 .execute(pool)
96 .await
97 .map_err(OrmError::from_sqlx)?;
98 Ok(())
99}
100
101pub async fn remove_migration_record(pool: &PgPool, version: &str) -> OrmResult<()> {
103 let sql = format!("DELETE FROM \"{}\" WHERE version = $1", MIGRATIONS_TABLE);
104 sqlx::query(&sql)
105 .bind(version)
106 .execute(pool)
107 .await
108 .map_err(OrmError::from_sqlx)?;
109 Ok(())
110}
111
112pub fn sha256_hex(content: &str) -> String {
113 let mut hasher = Sha256::new();
114 hasher.update(content.as_bytes());
115 hex::encode(hasher.finalize())
116}