Skip to main content

vespertide_core/schema/
unique_strategy.rs

1//! Strategy for pre-existing duplicates when adding a UNIQUE constraint to
2//! an existing column.
3//!
4//! `AddConstraint(Unique)` against a populated column would fail at apply
5//! time when duplicate values exist. Vespertide treats this as a fault
6//! the user must resolve explicitly: every revision that adds UNIQUE on
7//! an existing column emits a `DELETE` ahead of the `ADD CONSTRAINT`,
8//! keeping one row per group based on the strategy below.
9//!
10//! There is no "skip cleanup" option — relying on the database to reject
11//! the migration is incompatible with vespertide's safety promise. Users
12//! who *know* their data is clean still get the same SQL: `DELETE` on
13//! a clean table is a no-op.
14//!
15//! `#[non_exhaustive]` so additional strategies (e.g. `KeepWithExpression`
16//! when single-column-PK is insufficient) can be added in a future minor
17//! release without breaking downstream `match`es.
18
19use serde::{Deserialize, Serialize};
20
21/// How `AddConstraint(Unique)` should handle pre-existing duplicate rows.
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
24#[serde(tag = "kind", rename_all = "snake_case")]
25#[non_exhaustive]
26pub enum UniqueConstraintStrategy {
27    /// Emit a `DELETE` statement just before the `ADD CONSTRAINT` so only
28    /// one row per group of duplicates survives. `keep` picks which one.
29    /// This is the only strategy in v0.2 — see module docs.
30    DeleteDuplicates {
31        /// Which row of each duplicate group is preserved. See
32        /// [`KeepPolicy`].
33        keep: KeepPolicy,
34    },
35}
36
37/// Which row of a duplicate group is kept when `DeleteDuplicates` runs.
38///
39/// "First" / "Last" are defined by the table's PRIMARY KEY ordering:
40/// `First = MIN(pk)`, `Last = MAX(pk)`. Requires a single-column PK.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
42#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
43#[serde(rename_all = "snake_case")]
44pub enum KeepPolicy {
45    /// Keep the row with the smallest primary-key value.
46    First,
47    /// Keep the row with the largest primary-key value.
48    Last,
49}
50
51impl Default for UniqueConstraintStrategy {
52    /// Default strategy is `DeleteDuplicates { keep: First }`: keep the
53    /// row with the smallest primary-key value of each duplicate group.
54    /// This is what wire-format-omitted `strategy` (v0.1.x migrations or
55    /// any JSON without the field) deserializes to.
56    ///
57    /// **Wire-format breaking change vs v0.1.x.** v0.1.x emitted no
58    /// pre-cleanup and let the database reject the migration. v0.2 emits
59    /// a `DELETE` automatically. Existing migrations that *already
60    /// applied* under v0.1.x are unaffected (apply happens once); v0.1.x
61    /// migrations *re-run* against a fresh DB will now drop duplicate
62    /// rows instead of failing.
63    fn default() -> Self {
64        Self::DeleteDuplicates {
65            keep: KeepPolicy::First,
66        }
67    }
68}