Skip to main content

drizzle_migrations/
traits.rs

1//! Core traits for type-safe migration infrastructure
2//!
3//! This module provides idiomatic Rust traits replacing TypeScript patterns:
4//! - `Version` - Type-safe version markers with const NUMBER
5//! - `Upgradable` - Trait for upgrading snapshots between versions
6//! - `Entity` - Trait for DDL entities with const KIND
7//! - `EntityKind` - Enum replacing string `entity_type` discrimination
8
9use std::fmt;
10use std::hash::Hash;
11use std::marker::PhantomData;
12
13// Import dialect-specific types for associated type definitions
14use crate::postgres::{
15    PostgresDDL, PostgresSnapshot, ddl::PostgresEntity, statements::Generator as PostgresGenerator,
16};
17use crate::sqlite::{
18    SQLiteDDL, SQLiteSnapshot, ddl::SqliteEntity, statements::Generator as SqliteGenerator,
19};
20
21// =============================================================================
22// Version System
23// =============================================================================
24
25/// Type-safe version marker trait.
26///
27/// Each schema version is represented as a zero-sized type implementing this trait.
28/// The version number is available at compile time via the associated constant.
29pub trait Version: Copy + Clone + Default + 'static {
30    /// The version number (5, 6, 7, 8, etc.)
31    const NUMBER: u32;
32}
33
34/// Helper to get version as string at runtime
35#[must_use]
36pub fn version_str<V: Version>() -> String {
37    V::NUMBER.to_string()
38}
39
40/// Version 5 marker
41#[derive(Copy, Clone, Default, Debug)]
42pub struct V5;
43impl Version for V5 {
44    const NUMBER: u32 = 5;
45}
46
47/// Version 6 marker
48#[derive(Copy, Clone, Default, Debug)]
49pub struct V6;
50impl Version for V6 {
51    const NUMBER: u32 = 6;
52}
53
54/// Version 7 marker
55#[derive(Copy, Clone, Default, Debug)]
56pub struct V7;
57impl Version for V7 {
58    const NUMBER: u32 = 7;
59}
60
61/// Version 8 marker
62#[derive(Copy, Clone, Default, Debug)]
63pub struct V8;
64impl Version for V8 {
65    const NUMBER: u32 = 8;
66}
67
68/// Latest version aliases per dialect
69pub type SqliteLatest = V7;
70pub type PostgresLatest = V8;
71pub type MysqlLatest = V5;
72
73// =============================================================================
74// Upgradable Trait
75// =============================================================================
76
77/// Trait for upgrading a snapshot from one version to another.
78///
79/// Implementations are provided for each dialect's version transitions.
80/// The trait is generic over the snapshot type `S`, source version `From`,
81/// and target version `To`.
82///
83/// # Example
84/// ```rust
85/// # let _ = r####"
86/// impl Upgradable<V5, V6> for SqliteSnapshot<V5> {
87///     type Output = SqliteSnapshot<V6>;
88///     type Error = UpgradeError;
89///
90///     fn upgrade(self) -> Result<Self::Output, Self::Error> {
91///         // Transform v5 -> v6
92///     }
93/// }
94/// # "####;
95/// ```
96pub trait Upgradable<From: Version, To: Version> {
97    /// The output snapshot type (same shape, different version)
98    type Output;
99    /// Error type for upgrade failures
100    type Error;
101
102    /// Perform the upgrade transformation
103    ///
104    /// # Errors
105    ///
106    /// Returns the implementation's [`Self::Error`] if the upgrade
107    /// transformation fails (e.g., due to an unsupported input format or a
108    /// corrupted snapshot).
109    fn upgrade(self) -> Result<Self::Output, Self::Error>;
110}
111
112/// Type-safe upgrade function that only compiles for valid upgrade paths.
113///
114/// This function leverages the `CanUpgrade` trait to enforce at compile time
115/// that the specified dialect supports the given version transition.
116///
117/// # Example
118/// ```rust
119/// # let _ = r####"
120/// use drizzle_migrations::{Sqlite, V5, V7, CanUpgrade, Versioned};
121///
122/// // This compiles because Sqlite: CanUpgrade<V5, V7>
123/// fn upgrade_sqlite_snapshot<D>(data: Versioned<MyData, V5>) -> Versioned<MyData, V7>
124/// where
125///     D: CanUpgrade<V5, V7>,
126/// {
127///     // Perform the upgrade
128///     Versioned::new(data.into_inner())
129/// }
130/// # "####;
131/// ```
132#[inline]
133pub const fn assert_can_upgrade<D, From, To>()
134where
135    D: CanUpgrade<From, To>,
136    From: Version,
137    To: Version,
138{
139    // This function exists to provide a clear compile-time error
140    // when an invalid upgrade path is attempted.
141}
142
143// =============================================================================
144// Entity System
145// =============================================================================
146
147/// Entity kind discriminator enum.
148///
149/// Replaces string-based `entity_type` fields with a proper enum.
150/// Uses `#[repr(u8)]` for efficient storage and comparison.
151#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
152#[repr(u8)]
153pub enum EntityKind {
154    // Schema-level entities
155    Schema = 0,
156    Enum = 1,
157    Sequence = 2,
158    Role = 3,
159
160    // Table-level entities
161    Table = 10,
162    Column = 11,
163    Index = 12,
164    ForeignKey = 13,
165    PrimaryKey = 14,
166    UniqueConstraint = 15,
167    CheckConstraint = 16,
168
169    // Other entities
170    Policy = 20,
171    View = 21,
172}
173
174impl EntityKind {
175    /// Get the string representation for JSON serialization compatibility
176    #[must_use]
177    pub const fn as_str(self) -> &'static str {
178        match self {
179            Self::Schema => "schemas",
180            Self::Enum => "enums",
181            Self::Sequence => "sequences",
182            Self::Role => "roles",
183            Self::Table => "tables",
184            Self::Column => "columns",
185            Self::Index => "indexes",
186            Self::ForeignKey => "fks",
187            Self::PrimaryKey => "pks",
188            Self::UniqueConstraint => "uniques",
189            Self::CheckConstraint => "checks",
190            Self::Policy => "policies",
191            Self::View => "views",
192        }
193    }
194
195    /// Parse from string (for deserialization)
196    #[must_use]
197    pub fn parse(s: &str) -> Option<Self> {
198        match s {
199            "schemas" => Some(Self::Schema),
200            "enums" => Some(Self::Enum),
201            "sequences" => Some(Self::Sequence),
202            "roles" => Some(Self::Role),
203            "tables" => Some(Self::Table),
204            "columns" => Some(Self::Column),
205            "indexes" => Some(Self::Index),
206            "fks" => Some(Self::ForeignKey),
207            "pks" => Some(Self::PrimaryKey),
208            "uniques" => Some(Self::UniqueConstraint),
209            "checks" => Some(Self::CheckConstraint),
210            "policies" => Some(Self::Policy),
211            "views" => Some(Self::View),
212            _ => None,
213        }
214    }
215}
216
217impl std::str::FromStr for EntityKind {
218    type Err = ();
219
220    fn from_str(s: &str) -> Result<Self, Self::Err> {
221        Self::parse(s).ok_or(())
222    }
223}
224
225impl fmt::Display for EntityKind {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        write!(f, "{}", self.as_str())
228    }
229}
230
231/// Entity key types for unique identification
232#[derive(Clone, Debug, PartialEq, Eq, Hash)]
233pub enum EntityKey {
234    /// Simple name (e.g., table name, schema name)
235    Simple(String),
236    /// Two-part key (e.g., table.column)
237    Composite2(String, String),
238    /// Three-part key (e.g., schema.table.column for `PostgreSQL`)
239    Composite3(String, String, String),
240}
241
242impl EntityKey {
243    pub fn simple(name: impl Into<String>) -> Self {
244        Self::Simple(name.into())
245    }
246
247    pub fn composite2(a: impl Into<String>, b: impl Into<String>) -> Self {
248        Self::Composite2(a.into(), b.into())
249    }
250
251    pub fn composite3(a: impl Into<String>, b: impl Into<String>, c: impl Into<String>) -> Self {
252        Self::Composite3(a.into(), b.into(), c.into())
253    }
254}
255
256/// Trait for DDL entities.
257///
258/// All DDL entity types (Table, Column, Index, etc.) implement this trait.
259/// The `KIND` constant enables compile-time entity type discrimination.
260pub trait Entity: Clone + PartialEq {
261    /// The entity kind (discriminator)
262    const KIND: EntityKind;
263
264    /// Get the unique key for this entity
265    fn key(&self) -> EntityKey;
266
267    /// Get the parent entity key (if this entity belongs to a parent)
268    fn parent_key(&self) -> Option<EntityKey> {
269        None
270    }
271}
272
273// =============================================================================
274// Versioned Snapshot
275// =============================================================================
276
277/// A snapshot with compile-time version tracking.
278///
279/// Wraps snapshot data with a phantom type parameter for the version.
280/// This enables type-safe upgrade chains and prevents accidental version mixing.
281#[derive(Clone, Debug)]
282pub struct Versioned<Data, V: Version> {
283    /// The actual snapshot data
284    pub data: Data,
285    /// Phantom marker for version
286    _version: PhantomData<V>,
287}
288
289impl<Data, V: Version> Versioned<Data, V> {
290    /// Create a new versioned wrapper
291    pub const fn new(data: Data) -> Self {
292        Self {
293            data,
294            _version: PhantomData,
295        }
296    }
297
298    /// Get the version number
299    #[must_use]
300    pub const fn version() -> u32 {
301        V::NUMBER
302    }
303
304    /// Get the version as a string
305    #[must_use]
306    pub fn version_str() -> String {
307        version_str::<V>()
308    }
309
310    /// Unwrap to get the inner data
311    pub fn into_inner(self) -> Data {
312        self.data
313    }
314}
315
316// =============================================================================
317// Dialect Trait
318// =============================================================================
319
320/// Trait representing a database dialect.
321///
322/// This trait uses associated types to provide compile-time type safety across
323/// dialect-specific operations. Both `MinVersion` and `LatestVersion` are
324/// associated types implementing `Version`, enabling const operations.
325///
326/// # Example
327/// ```rust
328/// # let _ = r####"
329/// fn process<D: Dialect>() {
330///     // All const at compile time
331///     const MIN: u32 = D::MinVersion::NUMBER;
332///     const LATEST: u32 = D::LatestVersion::NUMBER;
333///     println!("Processing {} (v{} to v{})", D::NAME, MIN, LATEST);
334/// }
335/// # "####;
336/// ```
337pub trait Dialect: Sized + 'static {
338    /// Display name of the dialect
339    const NAME: &'static str;
340
341    /// Minimum supported snapshot version (as a type)
342    type MinVersion: Version;
343
344    /// Latest/current snapshot version (as a type)
345    type LatestVersion: Version;
346
347    /// Dialect-specific snapshot type
348    type Snapshot: Clone + Default + std::fmt::Debug;
349
350    /// Dialect-specific DDL collection type
351    type DDL: Clone + Default + std::fmt::Debug;
352
353    /// Dialect-specific entity enum (e.g., `SqliteEntity`, `PostgresEntity`)
354    type Entity: Clone + std::fmt::Debug + PartialEq;
355
356    /// Dialect-specific SQL generator
357    type Generator: Default;
358
359    /// Check if a version number is supported
360    #[inline]
361    #[must_use]
362    fn is_supported_version(version: u32) -> bool {
363        version >= Self::MinVersion::NUMBER && version <= Self::LatestVersion::NUMBER
364    }
365
366    /// Check if a version is the latest
367    #[inline]
368    #[must_use]
369    fn is_latest_version(version: u32) -> bool {
370        version == Self::LatestVersion::NUMBER
371    }
372
373    /// Check if a version needs upgrade
374    #[inline]
375    #[must_use]
376    fn needs_upgrade_from(version: u32) -> bool {
377        version < Self::LatestVersion::NUMBER && version >= Self::MinVersion::NUMBER
378    }
379
380    /// Diff two snapshots and generate SQL migration statements
381    fn diff_and_generate(
382        prev: &Self::Snapshot,
383        cur: &Self::Snapshot,
384        breakpoints: bool,
385    ) -> DiffResult;
386}
387
388/// Marker trait for compile-time upgrade path validation.
389///
390/// Implement this trait to declare that a dialect supports upgrading from
391/// version `From` to version `To`. The compiler will enforce that only
392/// valid upgrade paths are used.
393///
394/// # Example
395/// ```rust
396/// # let _ = r####"
397/// // Declare SQLite can upgrade V5 -> V6
398/// impl CanUpgrade<V5, V6> for Sqlite {}
399/// impl CanUpgrade<V6, V7> for Sqlite {}
400///
401/// // This function only compiles if the upgrade is valid
402/// fn upgrade<D, From, To>(data: Versioned<Data, From>) -> Versioned<Data, To>
403/// where
404///     D: Dialect + CanUpgrade<From, To>,
405///     From: Version,
406///     To: Version,
407/// {
408///     // ...
409/// }
410/// # "####;
411/// ```
412pub trait CanUpgrade<From: Version, To: Version>: Dialect {}
413
414// =============================================================================
415// Dialect Operations Trait
416// =============================================================================
417
418/// Migration result from diffing two snapshots
419#[derive(Debug, Clone)]
420pub struct DiffResult {
421    /// Generated SQL statements
422    pub sql_statements: Vec<String>,
423    /// Whether there are any changes
424    pub has_changes: bool,
425}
426
427impl DiffResult {
428    /// Create an empty result (no changes)
429    #[must_use]
430    pub const fn empty() -> Self {
431        Self {
432            sql_statements: Vec::new(),
433            has_changes: false,
434        }
435    }
436
437    /// Create a result with changes
438    #[must_use]
439    pub const fn with_changes(sql_statements: Vec<String>) -> Self {
440        let has_changes = !sql_statements.is_empty();
441        Self {
442            sql_statements,
443            has_changes,
444        }
445    }
446}
447
448/// `SQLite` dialect marker type
449#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
450pub struct Sqlite;
451
452impl Sqlite {
453    /// Minimum supported snapshot version (inherent alias)
454    pub const MIN_VERSION: u32 = V5::NUMBER;
455    /// Latest snapshot version (inherent alias)
456    pub const LATEST_VERSION: u32 = V7::NUMBER;
457}
458
459impl Dialect for Sqlite {
460    const NAME: &'static str = "sqlite";
461    type MinVersion = V5;
462    type LatestVersion = V7;
463    type Snapshot = SQLiteSnapshot;
464    type DDL = SQLiteDDL;
465    type Entity = SqliteEntity;
466    type Generator = SqliteGenerator;
467
468    fn diff_and_generate(
469        prev: &Self::Snapshot,
470        cur: &Self::Snapshot,
471        breakpoints: bool,
472    ) -> DiffResult {
473        let diff = crate::sqlite::diff_snapshots(prev, cur);
474        if !diff.has_changes() {
475            return DiffResult::empty();
476        }
477        let generator = SqliteGenerator::new().with_breakpoints(breakpoints);
478        let sql = generator.generate_migration(&diff);
479        DiffResult::with_changes(sql)
480    }
481}
482
483// Declare valid SQLite upgrade paths
484impl CanUpgrade<V5, V6> for Sqlite {}
485impl CanUpgrade<V6, V7> for Sqlite {}
486// Transitive: V5 -> V7 requires going through V6
487impl CanUpgrade<V5, V7> for Sqlite {}
488
489/// `PostgreSQL` dialect marker type
490#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
491pub struct Postgres;
492
493impl Postgres {
494    /// Minimum supported snapshot version (inherent alias)
495    pub const MIN_VERSION: u32 = V5::NUMBER;
496    /// Latest snapshot version (inherent alias)
497    pub const LATEST_VERSION: u32 = V8::NUMBER;
498}
499
500impl Dialect for Postgres {
501    const NAME: &'static str = "postgresql";
502    type MinVersion = V5;
503    type LatestVersion = V8;
504    type Snapshot = PostgresSnapshot;
505    type DDL = PostgresDDL;
506    type Entity = PostgresEntity;
507    type Generator = PostgresGenerator;
508
509    fn diff_and_generate(
510        prev: &Self::Snapshot,
511        cur: &Self::Snapshot,
512        breakpoints: bool,
513    ) -> DiffResult {
514        let diff = crate::postgres::diff_snapshots(&prev.ddl, &cur.ddl);
515        if !diff.has_changes() {
516            return DiffResult::empty();
517        }
518        let generator = PostgresGenerator::new().with_breakpoints(breakpoints);
519        let sql = generator.generate(&diff.diffs);
520        DiffResult::with_changes(sql)
521    }
522}
523
524// Declare valid PostgreSQL upgrade paths
525impl CanUpgrade<V5, V6> for Postgres {}
526impl CanUpgrade<V6, V7> for Postgres {}
527impl CanUpgrade<V7, V8> for Postgres {}
528// Transitive paths
529impl CanUpgrade<V5, V7> for Postgres {}
530impl CanUpgrade<V5, V8> for Postgres {}
531impl CanUpgrade<V6, V8> for Postgres {}
532
533/// `MySQL` dialect marker type
534#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
535pub struct Mysql;
536
537impl Mysql {
538    /// Minimum supported snapshot version (inherent alias)
539    pub const MIN_VERSION: u32 = V5::NUMBER;
540    /// Latest snapshot version (inherent alias)
541    pub const LATEST_VERSION: u32 = V5::NUMBER;
542}
543
544impl Dialect for Mysql {
545    const NAME: &'static str = "mysql";
546    type MinVersion = V5;
547    type LatestVersion = V5;
548    // MySQL not yet implemented - use placeholder types
549    type Snapshot = ();
550    type DDL = ();
551    type Entity = ();
552    type Generator = ();
553
554    fn diff_and_generate(
555        _prev: &Self::Snapshot,
556        _cur: &Self::Snapshot,
557        _breakpoints: bool,
558    ) -> DiffResult {
559        unimplemented!("MySQL migrations not yet supported")
560    }
561}
562// No upgrade paths for MySQL - already at latest
563
564// =============================================================================
565// Diff Types
566// =============================================================================
567
568/// Diff operation type
569#[derive(Copy, Clone, Debug, PartialEq, Eq)]
570pub enum DiffType {
571    Create,
572    Drop,
573    Alter,
574}
575
576#[cfg(test)]
577mod tests {
578    use std::str::FromStr;
579
580    use super::*;
581
582    #[test]
583    fn test_version_numbers() {
584        assert_eq!(V5::NUMBER, 5);
585        assert_eq!(V6::NUMBER, 6);
586        assert_eq!(V7::NUMBER, 7);
587        assert_eq!(V8::NUMBER, 8);
588    }
589
590    #[test]
591    fn test_version_str() {
592        assert_eq!(version_str::<V5>(), "5");
593        assert_eq!(version_str::<V7>(), "7");
594    }
595
596    #[test]
597    fn test_entity_kind_str() {
598        assert_eq!(EntityKind::Table.as_str(), "tables");
599        assert_eq!(EntityKind::Column.as_str(), "columns");
600        assert_eq!(EntityKind::ForeignKey.as_str(), "fks");
601    }
602
603    #[test]
604    fn test_entity_kind_parse() {
605        assert_eq!(EntityKind::from_str("tables"), Ok(EntityKind::Table));
606        assert_eq!(EntityKind::from_str("columns"), Ok(EntityKind::Column));
607        assert_eq!(EntityKind::from_str("invalid"), Err(()));
608    }
609
610    #[test]
611    fn test_versioned_snapshot() {
612        #[derive(Clone, Debug)]
613        struct TestData {
614            value: i32,
615        }
616
617        let versioned: Versioned<TestData, V7> = Versioned::new(TestData { value: 42 });
618        assert_eq!(Versioned::<TestData, V7>::version(), 7);
619        assert_eq!(versioned.data.value, 42);
620    }
621
622    #[test]
623    fn test_dialect_version_info() {
624        // SQLite: V5 to V7 - using inherent consts (no trait needed)
625        assert_eq!(Sqlite::MIN_VERSION, 5);
626        assert_eq!(Sqlite::LATEST_VERSION, 7);
627
628        // PostgreSQL: V5 to V8
629        assert_eq!(Postgres::MIN_VERSION, 5);
630        assert_eq!(Postgres::LATEST_VERSION, 8);
631
632        // MySQL: V5 only
633        assert_eq!(Mysql::MIN_VERSION, 5);
634        assert_eq!(Mysql::LATEST_VERSION, 5);
635    }
636
637    #[test]
638    fn test_dialect_version_checks() {
639        // SQLite checks
640        assert!(Sqlite::is_supported_version(5));
641        assert!(Sqlite::is_supported_version(6));
642        assert!(Sqlite::is_supported_version(7));
643        assert!(!Sqlite::is_supported_version(4));
644        assert!(!Sqlite::is_supported_version(8));
645
646        assert!(Sqlite::needs_upgrade_from(5));
647        assert!(Sqlite::needs_upgrade_from(6));
648        assert!(!Sqlite::needs_upgrade_from(7));
649
650        assert!(!Sqlite::is_latest_version(5));
651        assert!(Sqlite::is_latest_version(7));
652    }
653
654    #[test]
655    fn test_can_upgrade_compiles() {
656        // These calls verify that the CanUpgrade impls exist
657        // If they don't, this test won't compile
658        assert_can_upgrade::<Sqlite, V5, V6>();
659        assert_can_upgrade::<Sqlite, V6, V7>();
660        assert_can_upgrade::<Sqlite, V5, V7>(); // Transitive
661
662        assert_can_upgrade::<Postgres, V5, V6>();
663        assert_can_upgrade::<Postgres, V6, V7>();
664        assert_can_upgrade::<Postgres, V7, V8>();
665        assert_can_upgrade::<Postgres, V5, V8>(); // Transitive
666    }
667
668    // This test demonstrates a compile-time error if uncommented:
669    // #[test]
670    // fn test_invalid_upgrade_fails() {
671    //     // This would fail to compile because Sqlite doesn't impl CanUpgrade<V5, V8>
672    //     assert_can_upgrade::<Sqlite, V5, V8>();
673    // }
674}