1use mysql_async::Conn;
12use mysql_async::prelude::Queryable;
13use thiserror::Error;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ProtectionModel {
18 TransactionalDmlProtection,
22 NonTransactionalDdlSnapshot,
26 UnprotectedDenied,
29}
30
31impl ProtectionModel {
32 pub fn warnings(&self) -> &'static [&'static str] {
35 match self {
36 ProtectionModel::TransactionalDmlProtection => &[],
37 ProtectionModel::NonTransactionalDdlSnapshot => &[
38 "pre-operation snapshot only",
39 "implicit commit may occur",
40 "snapshot and DDL are not one atomic transaction",
41 "automatic rollback is not guaranteed",
42 ],
43 ProtectionModel::UnprotectedDenied => &["no backup protection can be guaranteed"],
44 }
45 }
46
47 pub fn is_transactional(&self) -> bool {
48 matches!(self, ProtectionModel::TransactionalDmlProtection)
49 }
50}
51
52pub fn protection_model_for(
57 category: crate::policy::model::SqlCategory,
58 ast_type: &str,
59) -> ProtectionModel {
60 use crate::policy::model::SqlCategory;
61 match category {
62 SqlCategory::Ddl => ProtectionModel::NonTransactionalDdlSnapshot,
63 SqlCategory::Write => {
64 if crate::backup::extractor::is_backup_required(ast_type) {
65 ProtectionModel::TransactionalDmlProtection
66 } else {
67 ProtectionModel::TransactionalDmlProtection
70 }
71 }
72 _ => ProtectionModel::TransactionalDmlProtection,
73 }
74}
75
76#[derive(Debug, Clone, Error, PartialEq)]
77pub enum DdlPreflightError {
78 #[error("table {schema}.{table} does not exist: {hint}")]
79 NotFound {
80 schema: String,
81 table: String,
82 hint: &'static str,
83 },
84 #[error("table {schema}.{table} already exists: {hint}")]
85 Conflict {
86 schema: String,
87 table: String,
88 hint: &'static str,
89 },
90 #[error("preflight query failed: {0}")]
91 Query(String),
92}
93
94#[derive(Debug, Clone, PartialEq)]
96pub enum DdlPreflight {
97 Present,
99 MissingNoOp(Vec<(String, String)>),
102 Mixed {
109 existing: Vec<(String, String)>,
110 missing: Vec<(String, String)>,
111 },
112}
113
114pub fn rewrite_drop_subset(object_type: &str, existing: &[(String, String)]) -> Option<String> {
122 let keyword = match object_type {
123 "table" => "DROP TABLE IF EXISTS",
124 "view" => "DROP VIEW IF EXISTS",
125 _ => return None,
126 };
127 if existing.is_empty() {
128 return None;
129 }
130 let targets = existing
131 .iter()
132 .map(|(schema, table)| {
133 format!(
134 "`{}`.`{}`",
135 schema.replace('`', "``"),
136 table.replace('`', "``")
137 )
138 })
139 .collect::<Vec<_>>()
140 .join(", ");
141 Some(format!("{keyword} {targets}"))
142}
143
144pub async fn preflight_ddl(
148 conn: &mut Conn,
149 classified: &crate::policy::classifier::ClassifiedStatement,
150 fallback_db: Option<&str>,
151) -> Result<DdlPreflight, DdlPreflightError> {
152 use crate::policy::model::SqlCategory;
153 if classified.category != SqlCategory::Ddl || classified.mutated_tables.is_empty() {
154 return Ok(DdlPreflight::Present);
155 }
156
157 let resolve_schema = |target: &crate::policy::classifier::TableRef| -> Option<String> {
158 target
159 .database
160 .clone()
161 .or_else(|| fallback_db.map(str::to_string))
162 };
163
164 async fn table_exists(
165 conn: &mut Conn,
166 schema: &str,
167 table: &str,
168 ) -> Result<bool, DdlPreflightError> {
169 let found: Option<i8> = conn
170 .exec_first(
171 "SELECT 1 FROM information_schema.tables
172 WHERE table_schema = ? AND table_name = ?",
173 (schema, table),
174 )
175 .await
176 .map_err(|e| DdlPreflightError::Query(e.to_string()))?;
177 Ok(found.is_some())
178 }
179
180 match classified.ast_type {
181 "create" => Ok(DdlPreflight::Present),
184
185 "rename" => {
190 let pairs: Vec<(
191 &crate::policy::classifier::TableRef,
192 &crate::policy::classifier::TableRef,
193 )> = classified
194 .mutated_tables
195 .chunks(2)
196 .map(|c| (&c[0], &c[1]))
197 .collect();
198 let mut known: Vec<((String, String), bool)> = Vec::new();
199 for (src, dst) in &pairs {
200 let src_schema = resolve_schema(src).ok_or(DdlPreflightError::NotFound {
201 schema: "?".into(),
202 table: src.table.clone(),
203 hint: "no database in scope for the RENAME source",
204 })?;
205 let dst_schema = resolve_schema(dst).ok_or(DdlPreflightError::NotFound {
206 schema: "?".into(),
207 table: dst.table.clone(),
208 hint: "no database in scope for the RENAME destination",
209 })?;
210 let src_id = (src_schema.clone(), src.table.clone());
211 let dst_id = (dst_schema.clone(), dst.table.clone());
212
213 let src_present =
215 if let Some(p) = known.iter().find(|(id, _p)| *id == src_id).map(|(_, p)| *p) {
216 p
217 } else {
218 table_exists(conn, &src_schema, &src.table).await?
219 };
220 if !src_present {
221 return Err(DdlPreflightError::NotFound {
222 schema: src_schema,
223 table: src.table.clone(),
224 hint: "RENAME source does not exist",
225 });
226 }
227
228 let dst_present =
230 if let Some(p) = known.iter().find(|(id, _p)| *id == dst_id).map(|(_, p)| *p) {
231 p
232 } else {
233 table_exists(conn, &dst_schema, &dst.table).await?
234 };
235 if dst_present {
236 return Err(DdlPreflightError::Conflict {
237 schema: dst_schema,
238 table: dst.table.clone(),
239 hint: "RENAME destination already exists",
240 });
241 }
242
243 known.retain(|(id, _)| *id != src_id);
244 known.push((src_id, false));
245 known.push((dst_id, true));
246 }
247 Ok(DdlPreflight::Present)
248 }
249
250 _ => {
257 let mut existing: Vec<(String, String)> = Vec::new();
258 let mut missing: Vec<(String, String)> = Vec::new();
259 for target in &classified.mutated_tables {
260 let schema = resolve_schema(target).ok_or(DdlPreflightError::NotFound {
261 schema: "?".into(),
262 table: target.table.clone(),
263 hint: "no database in scope for the DDL target",
264 })?;
265 if table_exists(conn, &schema, &target.table).await? {
266 existing.push((schema, target.table.clone()));
267 } else {
268 missing.push((schema, target.table.clone()));
269 }
270 }
271 if missing.is_empty() {
272 return Ok(DdlPreflight::Present);
273 }
274 if !classified.if_exists {
275 let (schema, table) = missing[0].clone();
276 return Err(DdlPreflightError::NotFound {
277 schema,
278 table,
279 hint: "DDL target does not exist (no IF EXISTS)",
280 });
281 }
282 if existing.is_empty() {
283 Ok(DdlPreflight::MissingNoOp(missing))
284 } else {
285 Ok(DdlPreflight::Mixed { existing, missing })
286 }
287 }
288 }
289}