Skip to main content

ferro_projection/
migration.rs

1//! SeaORM migration for the `projection_snapshots` table (D-23, D-24).
2//!
3//! Schema columns (D-24):
4//! - `projection_name VARCHAR NOT NULL` — `P::NAME` (D-06)
5//! - `key VARCHAR NOT NULL` — `ProjectionKey::as_str()` (D-11)
6//! - `state JSON NOT NULL` — serialized `P::State` (D-26)
7//! - `version BIGINT NOT NULL` — monotonic counter (D-25)
8//! - `updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP` (D-27)
9//!
10//! Primary key is the composite `(projection_name, key)` (D-24) — every
11//! lookup path hits it directly; no secondary indexes in v0.
12
13use sea_orm_migration::prelude::*;
14
15pub struct Migration;
16
17impl sea_orm_migration::MigrationName for Migration {
18    fn name(&self) -> &str {
19        "m20260514_000001_create_projection_snapshots_table"
20    }
21}
22
23#[async_trait::async_trait]
24impl MigrationTrait for Migration {
25    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
26        manager
27            .create_table(
28                Table::create()
29                    .table(ProjectionSnapshots::Table)
30                    .if_not_exists()
31                    // projection_name VARCHAR NOT NULL — part of composite PK (D-24)
32                    .col(
33                        ColumnDef::new(ProjectionSnapshots::ProjectionName)
34                            .string()
35                            .not_null(),
36                    )
37                    // key VARCHAR NOT NULL — part of composite PK (D-24)
38                    .col(ColumnDef::new(ProjectionSnapshots::Key).string().not_null())
39                    // state JSON NOT NULL (D-26)
40                    .col(ColumnDef::new(ProjectionSnapshots::State).json().not_null())
41                    // version BIGINT NOT NULL (D-25)
42                    .col(
43                        ColumnDef::new(ProjectionSnapshots::Version)
44                            .big_integer()
45                            .not_null(),
46                    )
47                    // updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP (D-27)
48                    .col(
49                        ColumnDef::new(ProjectionSnapshots::UpdatedAt)
50                            .timestamp()
51                            .not_null()
52                            .default(Expr::current_timestamp()),
53                    )
54                    // Composite primary key (D-24) — only lookup path
55                    .primary_key(
56                        Index::create()
57                            .col(ProjectionSnapshots::ProjectionName)
58                            .col(ProjectionSnapshots::Key),
59                    )
60                    .to_owned(),
61            )
62            .await
63    }
64
65    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
66        manager
67            .drop_table(Table::drop().table(ProjectionSnapshots::Table).to_owned())
68            .await
69    }
70}
71
72#[derive(DeriveIden)]
73enum ProjectionSnapshots {
74    Table,
75    ProjectionName,
76    Key,
77    State,
78    Version,
79    UpdatedAt,
80}
81
82#[cfg(test)]
83mod tests {
84    use sea_orm::{ConnectionTrait, Database, Statement};
85    use sea_orm_migration::MigratorTrait;
86
87    struct TestMigrator;
88
89    #[async_trait::async_trait]
90    impl MigratorTrait for TestMigrator {
91        fn migrations() -> Vec<Box<dyn sea_orm_migration::MigrationTrait>> {
92            vec![Box::new(super::Migration)]
93        }
94    }
95
96    #[tokio::test]
97    async fn migration_creates_projection_snapshots_table() {
98        let conn = Database::connect("sqlite::memory:").await.expect("connect");
99        TestMigrator::up(&conn, None).await.expect("migrate up");
100
101        let row = conn
102            .query_one(Statement::from_string(
103                sea_orm::DatabaseBackend::Sqlite,
104                "SELECT name FROM sqlite_master WHERE type='table' AND name='projection_snapshots'"
105                    .to_string(),
106            ))
107            .await
108            .expect("query sqlite_master");
109        assert!(row.is_some(), "projection_snapshots table not created");
110    }
111}