Skip to main content

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}