1use serde::{Deserialize, Deserializer, Serialize};
20use thiserror::Error;
21
22use crate::dbms::table::{
23 ColumnSnapshot, DataTypeSnapshot, ForeignKeySnapshot, IndexSnapshot, TableSchema,
24};
25use crate::dbms::value::Value;
26use crate::error::DbmsResult;
27
28pub trait Migrate
36where
37 Self: TableSchema,
38{
39 fn default_value(_column: &str) -> Option<Value> {
45 None
46 }
47
48 fn transform_column(_column: &str, _old: Value) -> DbmsResult<Option<Value>> {
56 Ok(None)
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[cfg_attr(feature = "candid", derive(candid::CandidType))]
67pub enum MigrationOp {
68 CreateTable {
70 name: String,
72 schema: crate::dbms::table::TableSchemaSnapshot,
74 },
75 DropTable {
77 name: String,
79 },
80 AddColumn {
86 table: String,
88 column: ColumnSnapshot,
90 },
91 DropColumn {
93 table: String,
95 column: String,
97 },
98 RenameColumn {
100 table: String,
102 old: String,
104 new: String,
106 },
107 AlterColumn {
109 table: String,
111 column: String,
113 changes: ColumnChanges,
115 },
116 WidenColumn {
119 table: String,
121 column: String,
123 old_type: DataTypeSnapshot,
125 new_type: DataTypeSnapshot,
127 },
128 TransformColumn {
131 column: String,
133 table: String,
135 old_type: DataTypeSnapshot,
137 new_type: DataTypeSnapshot,
139 },
140 AddIndex {
142 table: String,
144 index: IndexSnapshot,
146 },
147 DropIndex {
149 table: String,
151 index: IndexSnapshot,
153 },
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
161#[cfg_attr(feature = "candid", derive(candid::CandidType))]
162pub struct ColumnChanges {
163 pub nullable: Option<bool>,
165 pub unique: Option<bool>,
167 pub auto_increment: Option<bool>,
169 pub primary_key: Option<bool>,
171 #[serde(
180 default,
181 skip_serializing_if = "Option::is_none",
182 deserialize_with = "deserialize_foreign_key_change"
183 )]
184 pub foreign_key: Option<Option<ForeignKeySnapshot>>,
185}
186
187fn deserialize_foreign_key_change<'de, D>(
193 deserializer: D,
194) -> Result<Option<Option<ForeignKeySnapshot>>, D::Error>
195where
196 D: Deserializer<'de>,
197{
198 if deserializer.is_human_readable() {
199 Option::<ForeignKeySnapshot>::deserialize(deserializer).map(Some)
200 } else {
201 Option::<Option<ForeignKeySnapshot>>::deserialize(deserializer)
202 }
203}
204
205impl ColumnChanges {
206 pub fn is_empty(&self) -> bool {
208 self.nullable.is_none()
209 && self.unique.is_none()
210 && self.auto_increment.is_none()
211 && self.primary_key.is_none()
212 && self.foreign_key.is_none()
213 }
214}
215
216#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
218#[cfg_attr(feature = "candid", derive(candid::CandidType))]
219pub struct MigrationPolicy {
220 pub allow_destructive: bool,
223}
224
225#[derive(Debug, Error, Clone, PartialEq, Eq, Serialize, Deserialize)]
227#[cfg_attr(feature = "candid", derive(candid::CandidType))]
228pub enum MigrationError {
229 #[error("Schema drift: stored schema differs from compiled schema")]
232 SchemaDrift,
233 #[error(
236 "Incompatible type change for column `{column}` in table `{table}`: {old:?} -> {new:?}"
237 )]
238 IncompatibleType {
239 table: String,
241 column: String,
243 old: DataTypeSnapshot,
245 new: DataTypeSnapshot,
247 },
248 #[error("Missing default for non-nullable new column `{column}` in table `{table}`")]
251 DefaultMissing {
252 table: String,
254 column: String,
256 },
257 #[error("Constraint violation for column `{column}` in table `{table}`: {reason}")]
260 ConstraintViolation {
261 table: String,
263 column: String,
265 reason: String,
267 },
268 #[error("Destructive migration op denied by policy: {op}")]
271 DestructiveOpDenied {
272 op: String,
274 },
275 #[error("Migration transform aborted for column `{column}` in table `{table}`: {reason}")]
277 TransformAborted {
278 table: String,
280 column: String,
282 reason: String,
284 },
285 #[error(
289 "No widening rule for column `{column}` in table `{table}`: {old_type:?} -> {new_type:?}"
290 )]
291 WideningIncompatible {
292 table: String,
294 column: String,
296 old_type: DataTypeSnapshot,
298 new_type: DataTypeSnapshot,
300 },
301 #[error("Migrate::transform_column returned None for column `{column}` in table `{table}`")]
304 TransformReturnedNone {
305 table: String,
307 column: String,
309 },
310 #[error(
313 "Foreign key violation on column `{column}` in table `{table}`: value `{value}` not present in `{target_table}`"
314 )]
315 ForeignKeyViolation {
316 table: String,
318 column: String,
320 target_table: String,
322 value: String,
324 },
325}
326
327#[cfg(test)]
328mod test {
329 use super::*;
330 use crate::dbms::table::{
331 ColumnSnapshot, DataTypeSnapshot, ForeignKeySnapshot, OnDeleteSnapshot,
332 };
333
334 #[test]
335 fn test_should_default_migration_policy_to_non_destructive() {
336 let policy = MigrationPolicy::default();
337 assert!(!policy.allow_destructive);
338 }
339
340 #[test]
341 fn test_should_detect_empty_column_changes() {
342 let changes = ColumnChanges::default();
343 assert!(changes.is_empty());
344
345 let nullable = ColumnChanges {
346 nullable: Some(true),
347 ..Default::default()
348 };
349 assert!(!nullable.is_empty());
350 }
351
352 #[test]
353 fn test_should_display_migration_error() {
354 let err = MigrationError::SchemaDrift;
355 assert_eq!(
356 err.to_string(),
357 "Schema drift: stored schema differs from compiled schema"
358 );
359
360 let err = MigrationError::IncompatibleType {
361 table: "users".into(),
362 column: "id".into(),
363 old: DataTypeSnapshot::Int32,
364 new: DataTypeSnapshot::Text,
365 };
366 assert!(err.to_string().contains("Incompatible type change"));
367
368 let err = MigrationError::DefaultMissing {
369 table: "users".into(),
370 column: "email".into(),
371 };
372 assert!(err.to_string().contains("Missing default"));
373
374 let err = MigrationError::ConstraintViolation {
375 table: "users".into(),
376 column: "email".into(),
377 reason: "duplicate value".into(),
378 };
379 assert!(err.to_string().contains("Constraint violation"));
380
381 let err = MigrationError::DestructiveOpDenied {
382 op: "DropTable".into(),
383 };
384 assert!(err.to_string().contains("Destructive migration op denied"));
385
386 let err = MigrationError::TransformAborted {
387 table: "users".into(),
388 column: "id".into(),
389 reason: "negative ids unsupported".into(),
390 };
391 assert!(err.to_string().contains("Migration transform aborted"));
392 }
393
394 #[test]
395 fn test_should_construct_migration_ops() {
396 let _drop = MigrationOp::DropTable { name: "old".into() };
397 let _add = MigrationOp::AddColumn {
398 table: "users".into(),
399 column: ColumnSnapshot {
400 name: "email".into(),
401 data_type: DataTypeSnapshot::Text,
402 nullable: true,
403 auto_increment: false,
404 unique: false,
405 primary_key: false,
406 foreign_key: None,
407 default: None,
408 },
409 };
410 }
411
412 fn sample_foreign_key() -> ForeignKeySnapshot {
413 ForeignKeySnapshot {
414 table: "users".into(),
415 column: "id".into(),
416 on_delete: OnDeleteSnapshot::Cascade,
417 }
418 }
419
420 fn foreign_key_states() -> [ColumnChanges; 3] {
422 [
423 ColumnChanges {
424 nullable: Some(true),
425 ..Default::default()
426 },
427 ColumnChanges {
428 foreign_key: Some(None),
429 ..Default::default()
430 },
431 ColumnChanges {
432 foreign_key: Some(Some(sample_foreign_key())),
433 ..Default::default()
434 },
435 ]
436 }
437
438 #[test]
439 fn test_should_json_roundtrip_column_changes_foreign_key_states() {
440 for changes in foreign_key_states() {
441 let json = serde_json::to_string(&changes).expect("failed to serialize");
442 let decoded: ColumnChanges =
443 serde_json::from_str(&json).expect("failed to deserialize");
444 assert_eq!(decoded, changes, "json: {json}");
445 assert!(!decoded.is_empty());
446 }
447 }
448
449 #[test]
450 fn test_should_serialize_column_changes_foreign_key_states_distinctly() {
451 let [keep, remove, set] = foreign_key_states()
452 .map(|changes| serde_json::to_value(&changes).expect("failed to serialize"));
453 assert_ne!(keep, remove);
454 assert_ne!(remove, set);
455 assert_ne!(keep, set);
456 assert!(keep.get("foreign_key").is_none());
457 assert_eq!(remove.get("foreign_key"), Some(&serde_json::Value::Null));
458 assert!(set["foreign_key"].is_object());
459 }
460
461 #[test]
462 fn test_should_json_deserialize_column_changes_foreign_key_states() {
463 let keep: ColumnChanges = serde_json::from_str(
464 r#"{"nullable":null,"unique":null,"auto_increment":null,"primary_key":null}"#,
465 )
466 .expect("failed to deserialize keep");
467 assert_eq!(keep.foreign_key, None);
468 assert!(keep.is_empty());
469
470 let remove: ColumnChanges = serde_json::from_str(
471 r#"{"nullable":null,"unique":null,"auto_increment":null,"primary_key":null,"foreign_key":null}"#,
472 )
473 .expect("failed to deserialize remove");
474 assert_eq!(remove.foreign_key, Some(None));
475 assert!(!remove.is_empty());
476
477 let set: ColumnChanges = serde_json::from_str(
478 r#"{"nullable":null,"unique":null,"auto_increment":null,"primary_key":null,"foreign_key":{"table":"users","column":"id","on_delete":"Cascade"}}"#,
479 )
480 .expect("failed to deserialize set");
481 assert_eq!(set.foreign_key, Some(Some(sample_foreign_key())));
482 }
483
484 #[cfg(feature = "candid")]
485 #[test]
486 fn test_should_candid_roundtrip_column_changes_foreign_key_states() {
487 let mut states = foreign_key_states().to_vec();
488 states.push(ColumnChanges::default());
489 for changes in states {
490 let encoded = candid::encode_one(&changes).expect("failed to encode");
491 let decoded: ColumnChanges = candid::decode_one(&encoded).expect("failed to decode");
492 assert_eq!(decoded, changes);
493 }
494 }
495
496 #[cfg(feature = "candid")]
497 #[test]
498 fn test_should_candid_roundtrip_migration_policy() {
499 let policy = MigrationPolicy {
500 allow_destructive: true,
501 };
502 let encoded = candid::encode_one(&policy).expect("failed to encode");
503 let decoded: MigrationPolicy = candid::decode_one(&encoded).expect("failed to decode");
504 assert_eq!(policy, decoded);
505 }
506
507 #[cfg(feature = "candid")]
508 #[test]
509 fn test_should_candid_roundtrip_migration_error() {
510 let err = MigrationError::SchemaDrift;
511 let encoded = candid::encode_one(&err).expect("failed to encode");
512 let decoded: MigrationError = candid::decode_one(&encoded).expect("failed to decode");
513 assert_eq!(err, decoded);
514 }
515
516 #[cfg(feature = "candid")]
517 #[test]
518 fn test_should_candid_roundtrip_new_migration_error_variants() {
519 let cases = vec![
520 MigrationError::DefaultMissing {
521 table: "users".into(),
522 column: "email".into(),
523 },
524 MigrationError::WideningIncompatible {
525 table: "users".into(),
526 column: "id".into(),
527 old_type: DataTypeSnapshot::Int32,
528 new_type: DataTypeSnapshot::Uint8,
529 },
530 MigrationError::TransformReturnedNone {
531 table: "users".into(),
532 column: "id".into(),
533 },
534 MigrationError::ForeignKeyViolation {
535 table: "posts".into(),
536 column: "owner".into(),
537 target_table: "users".into(),
538 value: "Uint32(99)".into(),
539 },
540 ];
541 for err in cases {
542 let encoded = candid::encode_one(&err).expect("failed to encode");
543 let decoded: MigrationError = candid::decode_one(&encoded).expect("failed to decode");
544 assert_eq!(err, decoded);
545 }
546 }
547}