1use crate::DbkitError;
2use crate::base_handler::{BaseHandler, FetchMode, WriteOp};
3use crate::config::Backend;
4use sqlx::{AnyPool, Row};
5use std::collections::hash_map::DefaultHasher;
6use std::hash::{Hash, Hasher};
7use tracing::{error, info};
8
9pub struct InitializationHandler {
20 handler: BaseHandler,
21 backend: Backend,
22}
23
24impl InitializationHandler {
25 pub fn new(pool: AnyPool, backend: Backend) -> Self {
30 Self {
31 handler: BaseHandler::new(pool),
32 backend,
33 }
34 }
35
36 fn placeholder(&self, n: usize) -> String {
38 match self.backend {
39 Backend::Postgres => format!("${n}"),
40 Backend::MySql | Backend::Sqlite => "?".to_string(),
41 }
42 }
43
44 fn tracking_table_ddl(&self) -> &'static str {
49 match self.backend {
50 Backend::Postgres => {
51 "CREATE TABLE IF NOT EXISTS _dbkit_migrations (
52 name TEXT PRIMARY KEY,
53 hash TEXT NOT NULL,
54 applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
55 )"
56 }
57 Backend::MySql => {
58 "CREATE TABLE IF NOT EXISTS _dbkit_migrations (
60 name VARCHAR(255) PRIMARY KEY,
61 hash TEXT NOT NULL,
62 applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
63 )"
64 }
65 Backend::Sqlite => {
66 "CREATE TABLE IF NOT EXISTS _dbkit_migrations (
67 name TEXT PRIMARY KEY,
68 hash TEXT NOT NULL,
69 applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
70 )"
71 }
72 }
73 }
74
75 fn applied_at_as_text(&self) -> &'static str {
78 match self.backend {
79 Backend::MySql => "CAST(applied_at AS CHAR)",
81 Backend::Postgres | Backend::Sqlite => "CAST(applied_at AS TEXT)",
82 }
83 }
84
85 async fn ensure_tracking_table(&self) -> Result<(), DbkitError> {
87 self.handler
88 .execute_write(WriteOp::BatchDDL {
89 queries: &[self.tracking_table_ddl()],
90 })
91 .await?;
92 Ok(())
93 }
94
95 fn hash_sql(sql: &str) -> String {
104 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
105 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
106 let mut h = FNV_OFFSET;
107 for b in sql.as_bytes() {
108 h ^= u64::from(*b);
109 h = h.wrapping_mul(FNV_PRIME);
110 }
111 format!("{h:016x}")
112 }
113
114 fn legacy_hash_sql(sql: &str) -> String {
118 let mut hasher = DefaultHasher::new();
119 sql.hash(&mut hasher);
120 format!("{:016x}", hasher.finish())
121 }
122
123 pub async fn run_named_migration(&self, name: &str, sql: &str) -> Result<(), DbkitError> {
128 self.ensure_tracking_table().await?;
129
130 let hash = Self::hash_sql(sql);
131
132 let select = format!(
134 "SELECT hash FROM _dbkit_migrations WHERE name = {}",
135 self.placeholder(1)
136 );
137 let result = self
138 .handler
139 .execute_write(WriteOp::Single {
140 query: select.as_str(),
141 params: vec![name.into()],
142 mode: FetchMode::Optional,
143 })
144 .await?;
145
146 if let Some(row) = result.optional()? {
147 let existing_hash: String = row.get(0);
148 if existing_hash == hash {
149 info!("migration '{}' already applied, skipping", name);
150 return Ok(());
151 }
152 if existing_hash == Self::legacy_hash_sql(sql) {
156 info!(
157 "migration '{}' already applied (pre-0.5 hash), upgrading stored hash",
158 name
159 );
160 let update = format!(
161 "UPDATE _dbkit_migrations SET hash = {} WHERE name = {}",
162 self.placeholder(1),
163 self.placeholder(2)
164 );
165 self.handler
166 .execute_write(WriteOp::Single {
167 query: update.as_str(),
168 params: vec![hash.into(), name.into()],
169 mode: FetchMode::None,
170 })
171 .await?;
172 return Ok(());
173 }
174 return Err(DbkitError::Migration(format!(
175 "migration '{}' was already applied but content has changed (hash {} → {})",
176 name, existing_hash, hash
177 )));
178 }
179
180 info!("applying migration '{}'...", name);
182 let queries: Vec<String> = sql
183 .split(';')
184 .map(|s| s.trim().to_string())
185 .filter(|s| !s.is_empty())
186 .collect();
187
188 let query_refs: Vec<&str> = queries.iter().map(|s| s.as_str()).collect();
189
190 match self
191 .handler
192 .execute_write(WriteOp::BatchDDL {
193 queries: &query_refs,
194 })
195 .await
196 {
197 Ok(_) => {
198 info!(
199 "migration '{}': {} DDL statements executed",
200 name,
201 query_refs.len()
202 );
203 }
204 Err(e) => {
205 error!("migration '{}' failed: {:?}", name, e);
206 return Err(DbkitError::Migration(e.to_string()));
207 }
208 }
209
210 let insert = format!(
212 "INSERT INTO _dbkit_migrations (name, hash) VALUES ({}, {})",
213 self.placeholder(1),
214 self.placeholder(2)
215 );
216 self.handler
217 .execute_write(WriteOp::Single {
218 query: insert.as_str(),
219 params: vec![name.into(), hash.clone().into()],
220 mode: FetchMode::None,
221 })
222 .await?;
223
224 info!("migration '{}' recorded", name);
225 Ok(())
226 }
227
228 pub async fn run_migrations(&self, sql: &str) -> Result<(), DbkitError> {
239 info!("running database migrations...");
240
241 let queries: Vec<String> = sql
242 .split(';')
243 .map(|s| s.trim().to_string())
244 .filter(|s| !s.is_empty())
245 .collect();
246
247 let query_refs: Vec<&str> = queries.iter().map(|s| s.as_str()).collect();
248
249 match self
250 .handler
251 .execute_write(WriteOp::BatchDDL {
252 queries: &query_refs,
253 })
254 .await
255 {
256 Ok(_) => {
257 info!("{} DDL statements executed", query_refs.len());
258 }
259 Err(e) => {
260 error!("migration failed: {:?}", e);
261 return Err(DbkitError::Migration(e.to_string()));
262 }
263 }
264
265 Ok(())
266 }
267
268 pub async fn applied_migrations(&self) -> Result<Vec<(String, String, String)>, DbkitError> {
270 self.ensure_tracking_table().await?;
271
272 let select = format!(
273 "SELECT name, hash, {} FROM _dbkit_migrations ORDER BY applied_at, name",
274 self.applied_at_as_text()
275 );
276 let result = self
277 .handler
278 .execute_write(WriteOp::Single {
279 query: select.as_str(),
280 params: vec![],
281 mode: FetchMode::All,
282 })
283 .await?;
284
285 let rows = result.all()?;
286 Ok(rows
287 .iter()
288 .map(|row| {
289 let name: String = row.get(0);
290 let hash: String = row.get(1);
291 let applied_at: String = row.get(2);
292 (name, hash, applied_at)
293 })
294 .collect())
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
305 fn hash_sql_is_stable_fnv1a() {
306 assert_eq!(InitializationHandler::hash_sql(""), "cbf29ce484222325");
307 assert_eq!(InitializationHandler::hash_sql("a"), "af63dc4c8601ec8c");
308 assert_eq!(InitializationHandler::hash_sql("foobar"), "85944171f73967e8");
309 }
310}