vespertide_core/schema/pk_addition_strategy.rs
1//! Strategy for pre-existing duplicates when adding a PRIMARY KEY to
2//! an existing column set.
3//!
4//! `AddConstraint(PrimaryKey)` against a populated table fails at apply
5//! time when the chosen column set already contains duplicate values
6//! (PK ⇒ UNIQUE) or NULLs (PK ⇒ NOT NULL). Vespertide handles the two
7//! failures separately:
8//!
9//! - **Duplicate violation** → this strategy (`DeleteDuplicates`)
10//! emits a `DELETE` ahead of `ADD CONSTRAINT PRIMARY KEY`, keeping
11//! one row per group based on `keep`.
12//! - **NULL violation** → handled by the existing F1 `fill_with`
13//! mechanism (`MigrationAction::AddColumn.fill_with`,
14//! `ModifyColumnDefault.backfill`). The revision CLI prompts the
15//! user for fill values on every nullable PK column.
16//!
17//! This is the F5 mirror of F2 (`UniqueConstraintStrategy`) - same
18//! shape, different constraint family. The variants are kept separate
19//! types to make the constraint family explicit at every call site.
20//!
21//! There is no "skip cleanup" / "fail" option - matches the F2/F3/F4
22//! policy of refusing to let the database reject the migration when
23//! the cleanup is trivially derivable.
24//!
25//! `#[non_exhaustive]` so additional strategies can be added in a
26//! future minor release without breaking downstream `match`es.
27
28use serde::{Deserialize, Serialize};
29
30use crate::schema::unique_strategy::KeepPolicy;
31
32/// How `AddConstraint(PrimaryKey)` should handle pre-existing
33/// duplicate rows in the chosen column set.
34///
35/// `KeepPolicy` is reused from `UniqueConstraintStrategy` (F2) -
36/// "First" / "Last" are defined by the table's surviving primary-key
37/// or row identity ordering. The cleanup SQL is emitted by
38/// `vespertide-query::sql::add_constraint::primary_key`.
39#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41#[serde(tag = "kind", rename_all = "snake_case")]
42#[non_exhaustive]
43pub enum PrimaryKeyAdditionStrategy {
44 /// Emit a `DELETE` statement ahead of `ADD CONSTRAINT PRIMARY KEY`
45 /// so only one row per group of new-PK duplicates survives.
46 DeleteDuplicates {
47 /// Which row of each duplicate group is preserved.
48 keep: KeepPolicy,
49 },
50}
51
52impl Default for PrimaryKeyAdditionStrategy {
53 /// Default strategy is `DeleteDuplicates { keep: First }`. Wire-
54 /// format omitted-field decodes to this value.
55 ///
56 /// **Wire-format breaking change vs v0.1.x.** v0.1.x emitted no
57 /// pre-cleanup and let the database reject the migration. v0.2
58 /// emits a `DELETE` automatically. Already-applied migrations are
59 /// unaffected (apply happens once); v0.1.x migration files re-run
60 /// against a fresh DB now drop duplicate rows instead of failing.
61 fn default() -> Self {
62 Self::DeleteDuplicates {
63 keep: KeepPolicy::First,
64 }
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn default_is_delete_duplicates_first() {
74 assert_eq!(
75 PrimaryKeyAdditionStrategy::default(),
76 PrimaryKeyAdditionStrategy::DeleteDuplicates {
77 keep: KeepPolicy::First
78 }
79 );
80 }
81
82 #[test]
83 fn serde_roundtrip_first() {
84 let s = PrimaryKeyAdditionStrategy::DeleteDuplicates {
85 keep: KeepPolicy::First,
86 };
87 let j = serde_json::to_string(&s).unwrap();
88 assert_eq!(j, r#"{"kind":"delete_duplicates","keep":"first"}"#);
89 let back: PrimaryKeyAdditionStrategy = serde_json::from_str(&j).unwrap();
90 assert_eq!(back, s);
91 }
92
93 #[test]
94 fn serde_roundtrip_last() {
95 let s = PrimaryKeyAdditionStrategy::DeleteDuplicates {
96 keep: KeepPolicy::Last,
97 };
98 let j = serde_json::to_string(&s).unwrap();
99 assert_eq!(j, r#"{"kind":"delete_duplicates","keep":"last"}"#);
100 let back: PrimaryKeyAdditionStrategy = serde_json::from_str(&j).unwrap();
101 assert_eq!(back, s);
102 }
103}