ferro_projection/
entity.rs1use sea_orm::entity::prelude::*;
15use serde_json::Value as JsonValue;
16
17#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
18#[sea_orm(table_name = "projection_snapshots")]
19pub struct Model {
20 #[sea_orm(primary_key, auto_increment = false)]
23 pub projection_name: String,
24
25 #[sea_orm(primary_key, auto_increment = false)]
28 pub key: String,
29
30 pub state: JsonValue,
32
33 pub version: i64,
35
36 pub updated_at: DateTime,
38}
39
40#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
41pub enum Relation {}
42
43impl ActiveModelBehavior for ActiveModel {}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use sea_orm::{ActiveValue, Database, EntityTrait};
49 use sea_orm_migration::MigratorTrait;
50
51 struct TestMigrator;
52
53 #[async_trait::async_trait]
54 impl MigratorTrait for TestMigrator {
55 fn migrations() -> Vec<Box<dyn sea_orm_migration::MigrationTrait>> {
56 vec![Box::new(crate::migration::Migration)]
57 }
58 }
59
60 async fn fresh_db() -> sea_orm::DatabaseConnection {
61 let conn = Database::connect("sqlite::memory:").await.expect("connect");
62 TestMigrator::up(&conn, None).await.expect("migrate");
63 conn
64 }
65
66 #[tokio::test]
67 async fn round_trip_with_composite_pk() {
68 let conn = fresh_db().await;
69
70 let name = "test.projection";
71 let key = "test-key";
72 let state = serde_json::json!({ "count": 7 });
73 let version: i64 = 1;
74 let updated_at = chrono::Utc::now().naive_utc();
75
76 let am = ActiveModel {
77 projection_name: ActiveValue::Set(name.to_string()),
78 key: ActiveValue::Set(key.to_string()),
79 state: ActiveValue::Set(state.clone()),
80 version: ActiveValue::Set(version),
81 updated_at: ActiveValue::Set(updated_at),
82 };
83 Entity::insert(am).exec(&conn).await.expect("insert");
84
85 let fetched = Entity::find_by_id((name.to_string(), key.to_string()))
87 .one(&conn)
88 .await
89 .expect("query")
90 .expect("found");
91
92 assert_eq!(fetched.projection_name, name);
93 assert_eq!(fetched.key, key);
94 assert_eq!(fetched.state, state);
95 assert_eq!(fetched.version, version);
96 assert_eq!(fetched.updated_at, updated_at);
97 }
98
99 #[tokio::test]
100 async fn duplicate_composite_pk_is_constraint_violation() {
101 let conn = fresh_db().await;
102
103 let name = "dup.projection";
104 let key = "dup-key";
105 let state = serde_json::json!({});
106 let now = chrono::Utc::now().naive_utc();
107
108 let am1 = ActiveModel {
109 projection_name: ActiveValue::Set(name.to_string()),
110 key: ActiveValue::Set(key.to_string()),
111 state: ActiveValue::Set(state.clone()),
112 version: ActiveValue::Set(1),
113 updated_at: ActiveValue::Set(now),
114 };
115 Entity::insert(am1).exec(&conn).await.expect("first insert");
116
117 let am2 = ActiveModel {
120 projection_name: ActiveValue::Set(name.to_string()),
121 key: ActiveValue::Set(key.to_string()),
122 state: ActiveValue::Set(state),
123 version: ActiveValue::Set(2),
124 updated_at: ActiveValue::Set(now),
125 };
126 let result = Entity::insert(am2).exec(&conn).await;
127 assert!(
128 result.is_err(),
129 "second insert with same composite PK should fail"
130 );
131 }
132}