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 {
97 let mut hasher = DefaultHasher::new();
98 sql.hash(&mut hasher);
99 format!("{:016x}", hasher.finish())
100 }
101
102 pub async fn run_named_migration(&self, name: &str, sql: &str) -> Result<(), DbkitError> {
107 self.ensure_tracking_table().await?;
108
109 let hash = Self::hash_sql(sql);
110
111 let select = format!(
113 "SELECT hash FROM _dbkit_migrations WHERE name = {}",
114 self.placeholder(1)
115 );
116 let result = self
117 .handler
118 .execute_write(WriteOp::Single {
119 query: select.as_str(),
120 params: vec![name.into()],
121 mode: FetchMode::Optional,
122 })
123 .await?;
124
125 if let Some(row) = result.optional()? {
126 let existing_hash: String = row.get(0);
127 if existing_hash == hash {
128 info!("migration '{}' already applied, skipping", name);
129 return Ok(());
130 } else {
131 return Err(DbkitError::Migration(format!(
132 "migration '{}' was already applied but content has changed (hash {} → {})",
133 name, existing_hash, hash
134 )));
135 }
136 }
137
138 info!("applying migration '{}'...", name);
140 let queries: Vec<String> = sql
141 .split(';')
142 .map(|s| s.trim().to_string())
143 .filter(|s| !s.is_empty())
144 .collect();
145
146 let query_refs: Vec<&str> = queries.iter().map(|s| s.as_str()).collect();
147
148 match self
149 .handler
150 .execute_write(WriteOp::BatchDDL {
151 queries: &query_refs,
152 })
153 .await
154 {
155 Ok(_) => {
156 info!(
157 "migration '{}': {} DDL statements executed",
158 name,
159 query_refs.len()
160 );
161 }
162 Err(e) => {
163 error!("migration '{}' failed: {:?}", name, e);
164 return Err(DbkitError::Migration(e.to_string()));
165 }
166 }
167
168 let insert = format!(
170 "INSERT INTO _dbkit_migrations (name, hash) VALUES ({}, {})",
171 self.placeholder(1),
172 self.placeholder(2)
173 );
174 self.handler
175 .execute_write(WriteOp::Single {
176 query: insert.as_str(),
177 params: vec![name.into(), hash.clone().into()],
178 mode: FetchMode::None,
179 })
180 .await?;
181
182 info!("migration '{}' recorded", name);
183 Ok(())
184 }
185
186 pub async fn run_migrations(&self, sql: &str) -> Result<(), DbkitError> {
192 info!("running database migrations...");
193
194 let queries: Vec<String> = sql
195 .split(';')
196 .map(|s| s.trim().to_string())
197 .filter(|s| !s.is_empty())
198 .collect();
199
200 let query_refs: Vec<&str> = queries.iter().map(|s| s.as_str()).collect();
201
202 match self
203 .handler
204 .execute_write(WriteOp::BatchDDL {
205 queries: &query_refs,
206 })
207 .await
208 {
209 Ok(_) => {
210 info!("{} DDL statements executed", query_refs.len());
211 }
212 Err(e) => {
213 error!("migration failed: {:?}", e);
214 return Err(DbkitError::Migration(e.to_string()));
215 }
216 }
217
218 Ok(())
219 }
220
221 pub async fn applied_migrations(&self) -> Result<Vec<(String, String, String)>, DbkitError> {
223 self.ensure_tracking_table().await?;
224
225 let select = format!(
226 "SELECT name, hash, {} FROM _dbkit_migrations ORDER BY applied_at, name",
227 self.applied_at_as_text()
228 );
229 let result = self
230 .handler
231 .execute_write(WriteOp::Single {
232 query: select.as_str(),
233 params: vec![],
234 mode: FetchMode::All,
235 })
236 .await?;
237
238 let rows = result.all()?;
239 Ok(rows
240 .iter()
241 .map(|row| {
242 let name: String = row.get(0);
243 let hash: String = row.get(1);
244 let applied_at: String = row.get(2);
245 (name, hash, applied_at)
246 })
247 .collect())
248 }
249}