oxide_sql_core/migrations/
state.rs1use std::collections::HashSet;
7
8pub const MIGRATIONS_TABLE_SQL: &str = r"
10CREATE TABLE IF NOT EXISTS _oxide_migrations (
11 id VARCHAR(255) PRIMARY KEY,
12 applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
13)
14";
15
16pub const INSERT_MIGRATION_SQL: &str =
18 "INSERT INTO _oxide_migrations (id, applied_at) VALUES (?, CURRENT_TIMESTAMP)";
19
20pub const DELETE_MIGRATION_SQL: &str = "DELETE FROM _oxide_migrations WHERE id = ?";
22
23pub const CHECK_MIGRATION_SQL: &str = "SELECT 1 FROM _oxide_migrations WHERE id = ?";
25
26pub const LIST_MIGRATIONS_SQL: &str =
28 "SELECT id, applied_at FROM _oxide_migrations ORDER BY applied_at";
29
30#[derive(Debug, Clone, Default)]
55pub struct MigrationState {
56 applied: HashSet<String>,
58}
59
60impl MigrationState {
61 #[must_use]
63 pub fn new() -> Self {
64 Self::default()
65 }
66
67 #[must_use]
71 pub fn from_applied(applied: impl IntoIterator<Item = String>) -> Self {
72 Self {
73 applied: applied.into_iter().collect(),
74 }
75 }
76
77 #[must_use]
79 pub fn is_applied(&self, id: &str) -> bool {
80 self.applied.contains(id)
81 }
82
83 pub fn mark_applied(&mut self, id: impl Into<String>) {
85 self.applied.insert(id.into());
86 }
87
88 pub fn mark_unapplied(&mut self, id: &str) {
90 self.applied.remove(id);
91 }
92
93 pub fn applied_migrations(&self) -> impl Iterator<Item = &str> {
95 self.applied.iter().map(String::as_str)
96 }
97
98 #[must_use]
100 pub fn applied_count(&self) -> usize {
101 self.applied.len()
102 }
103
104 #[must_use]
106 pub const fn create_table_sql() -> &'static str {
107 MIGRATIONS_TABLE_SQL
108 }
109
110 #[must_use]
112 pub const fn insert_sql() -> &'static str {
113 INSERT_MIGRATION_SQL
114 }
115
116 #[must_use]
118 pub const fn delete_sql() -> &'static str {
119 DELETE_MIGRATION_SQL
120 }
121
122 #[must_use]
124 pub const fn check_sql() -> &'static str {
125 CHECK_MIGRATION_SQL
126 }
127
128 #[must_use]
130 pub const fn list_sql() -> &'static str {
131 LIST_MIGRATIONS_SQL
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137#[allow(dead_code)]
138pub struct AppliedMigration {
139 pub id: String,
141 pub applied_at: String,
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_new_state_is_empty() {
151 let state = MigrationState::new();
152 assert_eq!(state.applied_count(), 0);
153 assert!(!state.is_applied("anything"));
154 }
155
156 #[test]
157 fn test_mark_applied() {
158 let mut state = MigrationState::new();
159 state.mark_applied("0001_initial");
160
161 assert!(state.is_applied("0001_initial"));
162 assert!(!state.is_applied("0002_add_users"));
163 assert_eq!(state.applied_count(), 1);
164 }
165
166 #[test]
167 fn test_mark_unapplied() {
168 let mut state = MigrationState::new();
169 state.mark_applied("0001_initial");
170 state.mark_applied("0002_add_users");
171
172 assert_eq!(state.applied_count(), 2);
173
174 state.mark_unapplied("0002_add_users");
175 assert!(!state.is_applied("0002_add_users"));
176 assert!(state.is_applied("0001_initial"));
177 assert_eq!(state.applied_count(), 1);
178 }
179
180 #[test]
181 fn test_from_applied() {
182 let state = MigrationState::from_applied(vec![
183 "0001_initial".to_string(),
184 "0002_add_users".to_string(),
185 ]);
186
187 assert!(state.is_applied("0001_initial"));
188 assert!(state.is_applied("0002_add_users"));
189 assert!(!state.is_applied("0003_something"));
190 assert_eq!(state.applied_count(), 2);
191 }
192
193 #[test]
194 fn test_applied_migrations_iterator() {
195 let mut state = MigrationState::new();
196 state.mark_applied("0001_initial");
197 state.mark_applied("0002_add_users");
198
199 let applied: HashSet<&str> = state.applied_migrations().collect();
200 assert!(applied.contains("0001_initial"));
201 assert!(applied.contains("0002_add_users"));
202 }
203
204 #[test]
205 fn test_sql_constants() {
206 assert!(MigrationState::create_table_sql().contains("CREATE TABLE"));
207 assert!(MigrationState::insert_sql().contains("INSERT"));
208 assert!(MigrationState::delete_sql().contains("DELETE"));
209 assert!(MigrationState::check_sql().contains("SELECT"));
210 assert!(MigrationState::list_sql().contains("SELECT"));
211 }
212}