1use sea_orm_migration::prelude::*;
17
18#[derive(DeriveMigrationName)]
19pub struct Migration;
20
21#[async_trait::async_trait]
22impl MigrationTrait for Migration {
23 async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
24 manager
25 .create_table(
26 Table::create()
27 .table(AuditLog::Table)
28 .if_not_exists()
29 .col(ColumnDef::new(AuditLog::Id).uuid().not_null().primary_key())
30 .col(ColumnDef::new(AuditLog::TenantId).string().null())
31 .col(ColumnDef::new(AuditLog::ActorKind).string().not_null())
32 .col(ColumnDef::new(AuditLog::ActorId).string().null())
33 .col(ColumnDef::new(AuditLog::Action).string().not_null())
34 .col(ColumnDef::new(AuditLog::TargetKind).string().null())
35 .col(ColumnDef::new(AuditLog::TargetId).string().null())
36 .col(ColumnDef::new(AuditLog::Before).json().null())
37 .col(ColumnDef::new(AuditLog::After).json().null())
38 .col(ColumnDef::new(AuditLog::Reason).string().null())
39 .col(ColumnDef::new(AuditLog::CorrelationId).uuid().null())
40 .col(
41 ColumnDef::new(AuditLog::CreatedAt)
42 .timestamp()
43 .not_null()
44 .default(Expr::current_timestamp()),
45 )
46 .to_owned(),
47 )
48 .await?;
49
50 manager
52 .create_index(
53 Index::create()
54 .name("idx_audit_target")
55 .table(AuditLog::Table)
56 .col(AuditLog::TenantId)
57 .col(AuditLog::TargetKind)
58 .col(AuditLog::TargetId)
59 .col(AuditLog::CreatedAt)
60 .to_owned(),
61 )
62 .await?;
63
64 manager
66 .create_index(
67 Index::create()
68 .name("idx_audit_actor")
69 .table(AuditLog::Table)
70 .col(AuditLog::TenantId)
71 .col(AuditLog::ActorKind)
72 .col(AuditLog::ActorId)
73 .col(AuditLog::CreatedAt)
74 .to_owned(),
75 )
76 .await
77 }
78
79 async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
80 manager
81 .drop_table(Table::drop().table(AuditLog::Table).to_owned())
82 .await
83 }
84}
85
86#[derive(DeriveIden)]
87enum AuditLog {
88 Table,
89 Id,
90 TenantId,
91 ActorKind,
92 ActorId,
93 Action,
94 TargetKind,
95 TargetId,
96 Before,
97 After,
98 Reason,
99 CorrelationId,
100 CreatedAt,
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use sea_orm::{ConnectionTrait, Database, DatabaseBackend, Statement};
107 use sea_orm_migration::MigratorTrait;
108
109 struct TestMigrator;
110
111 #[async_trait::async_trait]
112 impl MigratorTrait for TestMigrator {
113 fn migrations() -> Vec<Box<dyn MigrationTrait>> {
114 vec![Box::new(super::Migration)]
115 }
116 }
117
118 #[tokio::test]
119 async fn migration_creates_table_and_indexes() {
120 let conn = Database::connect("sqlite::memory:")
121 .await
122 .expect("connect to in-memory sqlite");
123
124 TestMigrator::up(&conn, None)
125 .await
126 .expect("run migration up");
127
128 let table_row = conn
130 .query_one(Statement::from_string(
131 DatabaseBackend::Sqlite,
132 "SELECT name FROM sqlite_master WHERE type='table' AND name='audit_log'"
133 .to_string(),
134 ))
135 .await
136 .expect("query sqlite_master for table");
137 assert!(
138 table_row.is_some(),
139 "audit_log table not created by migration"
140 );
141
142 for idx_name in &["idx_audit_target", "idx_audit_actor"] {
144 let idx_row = conn
145 .query_one(Statement::from_string(
146 DatabaseBackend::Sqlite,
147 format!(
148 "SELECT name FROM sqlite_master WHERE type='index' AND name='{idx_name}'"
149 ),
150 ))
151 .await
152 .expect("query sqlite_master for index");
153 assert!(
154 idx_row.is_some(),
155 "index {idx_name} not created by migration"
156 );
157 }
158
159 TestMigrator::down(&conn, None)
161 .await
162 .expect("run migration down");
163 let table_after_down = conn
164 .query_one(Statement::from_string(
165 DatabaseBackend::Sqlite,
166 "SELECT name FROM sqlite_master WHERE type='table' AND name='audit_log'"
167 .to_string(),
168 ))
169 .await
170 .expect("query sqlite_master after down");
171 assert!(
172 table_after_down.is_none(),
173 "audit_log table should be dropped by down()"
174 );
175 }
176}