Skip to main content

rustorm_migrate/
history.rs

1use 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/// Запись о применённой миграции.
9#[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
19/// Создаёт таблицу истории если не существует.
20pub 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
40/// Возвращает список уже применённых версий.
41pub 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
53/// Проверяет checksum применённой миграции.
54pub 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
77/// Записывает применённую миграцию в историю.
78pub 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
101/// Удаляет запись о миграции (при rollback).
102pub 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}