1use chrono::{DateTime, Utc};
2use sha2::{Digest, Sha256};
3use std::path::Path;
4use tokio::fs;
5
6pub fn generate_migration_version() -> String {
8 let now = Utc::now();
9 now.format("%Y%m%d_%H%M%S").to_string()
10}
11
12pub fn calculate_checksum(content: &str) -> String {
14 let mut hasher = Sha256::new();
15 hasher.update(content.as_bytes());
16 format!("{:x}", hasher.finalize())
17}
18
19pub async fn calculate_file_checksum<P: AsRef<Path>>(file_path: P) -> Result<String, std::io::Error> {
21 let content = fs::read_to_string(file_path).await?;
22 Ok(calculate_checksum(&content))
23}
24
25pub fn extract_description_from_filename(filename: &str) -> String {
27 let stem = filename.trim_end_matches(".cql");
29
30 let parts: Vec<&str> = stem.split('_').collect();
32 if parts.len() >= 3 {
33 parts[2..].join("_").replace('_', " ")
34 } else {
35 stem.to_string()
36 }
37}
38
39pub fn extract_version_from_filename(filename: &str) -> Option<String> {
41 let stem = filename.trim_end_matches(".cql");
43
44 if let Some(version_part) = stem.split('_').take(2).collect::<Vec<_>>().join("_").into() {
46 if version_part.len() >= 9 {
48 let (date_part, seq_part) = version_part.split_at(8);
49 if date_part.chars().all(|c| c.is_ascii_digit()) &&
50 seq_part.starts_with('_') &&
51 seq_part[1..].chars().all(|c| c.is_ascii_digit()) {
52 return Some(stem.to_string());
53 }
54 }
55 }
56
57 None
58}
59
60pub fn format_timestamp(timestamp: DateTime<Utc>) -> String {
62 timestamp.format("%Y-%m-%d %H:%M:%S UTC").to_string()
63}
64
65pub fn is_valid_migration_filename(filename: &str) -> bool {
67 extract_version_from_filename(filename).is_some()
68}
69
70pub fn create_migration_filename(description: &str) -> String {
72 let version = generate_migration_version();
73 let normalized_desc = description
74 .chars()
75 .map(|c| if c.is_alphanumeric() { c } else { '_' })
76 .collect::<String>()
77 .to_lowercase();
78
79 format!("{}_{}.cql", version, normalized_desc)
80}
81
82pub fn parse_migration_content(content: &str) -> Result<(String, Option<String>), String> {
84 let lines: Vec<&str> = content.lines().collect();
85 let mut up_section = Vec::new();
86 let mut down_section = Vec::new();
87 let mut current_section = None;
88
89 for line in lines {
90 let trimmed = line.trim();
91
92 if trimmed.starts_with("-- UP") || trimmed.starts_with("-- +migrate Up") {
93 current_section = Some("UP");
94 continue;
95 } else if trimmed.starts_with("-- DOWN") || trimmed.starts_with("-- +migrate Down") {
96 current_section = Some("DOWN");
97 continue;
98 }
99
100 if current_section.is_none() && (trimmed.is_empty() || trimmed.starts_with("--")) {
102 continue;
103 }
104
105 if current_section.is_none() {
107 current_section = Some("UP");
108 }
109
110 match current_section {
111 Some("UP") => up_section.push(line),
112 Some("DOWN") => down_section.push(line),
113 _ => {}
114 }
115 }
116
117 let up_content = up_section.join("\n").trim().to_string();
118 let down_content = if down_section.is_empty() {
119 None
120 } else {
121 Some(down_section.join("\n").trim().to_string())
122 };
123
124 if up_content.is_empty() {
125 return Err("Migration must contain at least UP section with CQL statements".to_string());
126 }
127
128 Ok((up_content, down_content))
129}
130
131pub fn generate_migration_template(description: &str) -> String {
133 format!(
134 r#"-- Migration: {}
135-- Created at: {}
136
137-- +migrate Up
138-- Add your UP migration statements here
139-- Example:
140-- CREATE TABLE IF NOT EXISTS example_table (
141-- id UUID PRIMARY KEY,
142-- name TEXT,
143-- created_at TIMESTAMP
144-- );
145
146-- +migrate Down
147-- Add your DOWN migration statements here (optional)
148-- Example:
149-- DROP TABLE IF EXISTS example_table;
150"#,
151 description,
152 Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
153 )
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_extract_description_from_filename() {
162 assert_eq!(
163 extract_description_from_filename("20250115_001_add_user_table.cql"),
164 "add user table"
165 );
166 assert_eq!(
167 extract_description_from_filename("20250115_002_create_indexes.cql"),
168 "create indexes"
169 );
170 }
171
172 #[test]
173 fn test_extract_version_from_filename() {
174 assert_eq!(
175 extract_version_from_filename("20250115_001_add_user_table.cql"),
176 Some("20250115_001_add_user_table".to_string())
177 );
178 assert_eq!(
179 extract_version_from_filename("invalid_filename.cql"),
180 None
181 );
182 }
183
184 #[test]
185 fn test_calculate_checksum() {
186 let content = "CREATE TABLE test (id UUID PRIMARY KEY);";
187 let checksum = calculate_checksum(content);
188 assert_eq!(checksum.len(), 64); }
190
191 #[test]
192 fn test_parse_migration_content() {
193 let content = r#"
194-- Migration description
195
196-- +migrate Up
197CREATE TABLE users (id UUID PRIMARY KEY);
198
199-- +migrate Down
200DROP TABLE users;
201"#;
202
203 let (up, down) = parse_migration_content(content).unwrap();
204 assert!(up.contains("CREATE TABLE users"));
205 assert!(down.unwrap().contains("DROP TABLE users"));
206 }
207}