rust_ef/migration/
history.rs1use super::dialect::MigrationDialect;
4use super::snapshot_io::{migration_io_err, snapshot_to_json};
5use super::types::{Migration, ModelSnapshot};
6use crate::error::EFResult;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
14pub struct MigrationHistoryEntry {
15 pub migration_id: String,
16 pub product_version: String,
17}
18
19pub const MIGRATION_HISTORY_TABLE: &str = "__ef_migrations_history";
21
22pub const PRODUCT_VERSION: &str = env!("CARGO_PKG_VERSION");
24
25pub(super) fn seed_insert_sql(
26 dialect: MigrationDialect,
27 table: &str,
28 columns: &[&str],
29 gen: &dyn crate::provider::ISqlGenerator,
30) -> String {
31 let quoted_table = dialect.quote(table);
32 let quoted_cols: Vec<String> = columns.iter().map(|c| dialect.quote(c)).collect();
33 let placeholders: Vec<String> = (0..columns.len())
34 .map(|i| gen.parameter_placeholder(i + 1))
35 .collect();
36 let cols = quoted_cols.join(", ");
37 let vals = placeholders.join(", ");
38 match dialect {
39 MigrationDialect::Sqlite => {
40 format!("INSERT OR IGNORE INTO {quoted_table} ({cols}) VALUES ({vals})")
41 }
42 MigrationDialect::Postgres => {
43 format!("INSERT INTO {quoted_table} ({cols}) VALUES ({vals}) ON CONFLICT DO NOTHING")
44 }
45 MigrationDialect::MySql => {
46 format!("INSERT IGNORE INTO {quoted_table} ({cols}) VALUES ({vals})")
47 }
48 }
49}
50
51pub(super) fn split_sql_statements(sql: &str) -> Vec<String> {
53 sql.lines()
54 .filter(|l| {
55 let trimmed = l.trim();
56 !trimmed.is_empty() && !trimmed.starts_with("--")
57 })
58 .collect::<Vec<_>>()
59 .join("\n")
60 .split(';')
61 .map(|s| s.trim().to_string())
62 .filter(|s| !s.is_empty())
63 .collect()
64}
65
66pub fn create_migration_history_table_sql(dialect: MigrationDialect) -> String {
68 match dialect {
69 MigrationDialect::Postgres => format!(
70 r#"CREATE TABLE IF NOT EXISTS "{table}" (
71 "migration_id" VARCHAR(150) NOT NULL PRIMARY KEY,
72 "product_version" VARCHAR(32) NOT NULL,
73 "applied_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
74);"#,
75 table = MIGRATION_HISTORY_TABLE
76 ),
77 MigrationDialect::MySql => format!(
78 r#"CREATE TABLE IF NOT EXISTS `{table}` (
79 `migration_id` VARCHAR(150) NOT NULL PRIMARY KEY,
80 `product_version` VARCHAR(32) NOT NULL,
81 `applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
82) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"#,
83 table = MIGRATION_HISTORY_TABLE
84 ),
85 MigrationDialect::Sqlite => format!(
86 r#"CREATE TABLE IF NOT EXISTS "{table}" (
87 "migration_id" TEXT NOT NULL PRIMARY KEY,
88 "product_version" TEXT NOT NULL
89);"#,
90 table = MIGRATION_HISTORY_TABLE
91 ),
92 }
93}
94
95#[derive(Debug, Clone)]
109pub struct MigrationStore {
110 root: PathBuf,
111}
112
113impl MigrationStore {
114 pub fn new(root: impl Into<PathBuf>) -> Self {
115 Self { root: root.into() }
116 }
117
118 pub fn root(&self) -> &Path {
119 &self.root
120 }
121
122 pub fn save(&self, migration: &Migration) -> EFResult<()> {
124 fs::create_dir_all(&self.root).map_err(migration_io_err)?;
125 let dir = self.root.join(&migration.id);
126 fs::create_dir_all(&dir).map_err(migration_io_err)?;
127 fs::write(dir.join("up.sql"), &migration.up_sql).map_err(migration_io_err)?;
128 fs::write(dir.join("down.sql"), &migration.down_sql).map_err(migration_io_err)?;
129 Ok(())
130 }
131
132 pub fn load_all(&self) -> EFResult<Vec<Migration>> {
134 if !self.root.exists() {
135 return Ok(Vec::new());
136 }
137 let mut ids: Vec<String> = fs::read_dir(&self.root)
138 .map_err(migration_io_err)?
139 .filter_map(|e| e.ok())
140 .filter(|e| e.path().is_dir())
141 .filter_map(|e| e.file_name().into_string().ok())
142 .collect();
143 ids.sort();
144 ids.into_iter().map(|id| self.load(&id)).collect()
145 }
146
147 pub fn load(&self, id: &str) -> EFResult<Migration> {
148 let dir = self.root.join(id);
149 let up_sql = fs::read_to_string(dir.join("up.sql")).map_err(migration_io_err)?;
150 let down_sql = fs::read_to_string(dir.join("down.sql")).map_err(migration_io_err)?;
151 Ok(Migration {
152 id: id.to_string(),
153 description: id.to_string(),
154 up_sql,
155 down_sql,
156 })
157 }
158
159 pub fn save_snapshot(&self, snapshot: &ModelSnapshot) -> EFResult<()> {
161 fs::create_dir_all(&self.root).map_err(migration_io_err)?;
162 let json = snapshot_to_json(snapshot);
163 fs::write(self.root.join("model_snapshot.json"), json).map_err(migration_io_err)
164 }
165
166 pub fn load_snapshot(&self) -> EFResult<Option<ModelSnapshot>> {
167 let path = self.root.join("model_snapshot.json");
168 if !path.exists() {
169 return Ok(None);
170 }
171 let text = fs::read_to_string(path).map_err(migration_io_err)?;
172 super::snapshot_io::parse_model_snapshot_json(&text)
173 }
174}