pub mod entity;
pub use entity::Entity as SeederEntryEntity;
pub use entity::ModelEx as SeederEntryModel;
use sea_orm::{
ActiveValue::Set, ColumnTrait, ConnectionTrait, DatabaseConnection, DatabaseTransaction,
EntityTrait, QueryFilter, TransactionTrait,
};
use std::collections::HashSet;
use std::sync::Arc;
pub use entity::{ActiveModel as SeederEntryActiveModel, ModelEx as SeederEntry};
pub async fn ensure_table(db: &DatabaseConnection) -> anyhow::Result<()> {
db.execute_unprepared(
r#"CREATE TABLE IF NOT EXISTS "__database_seeders" (
"id" BIGSERIAL PRIMARY KEY,
"uid" UUID NOT NULL UNIQUE,
"seederName" VARCHAR(255) NOT NULL UNIQUE,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
)"#,
)
.await?;
Ok(())
}
#[async_trait::async_trait]
pub trait DatabaseSeeder: Send + Sync {
fn name(&self) -> &str {
std::any::type_name::<Self>()
}
fn order(&self) -> i32 {
0
}
async fn seed(&self, txn: Arc<DatabaseTransaction>) -> anyhow::Result<()>;
async fn delete(&self, txn: Arc<DatabaseTransaction>) -> anyhow::Result<()>;
}
pub use DatabaseSeeder as RepositorySeeder;
pub struct EntitySeeder<E>
where
E: EntityTrait,
E::ModelEx: crate::data::BaseEntity + Send + Sync,
{
_marker: std::marker::PhantomData<E>,
}
impl<E> EntitySeeder<E>
where
E: EntityTrait,
E::ModelEx: crate::data::BaseEntity + Send + Sync,
{
pub fn new() -> Self {
Self { _marker: std::marker::PhantomData }
}
}
pub struct DatabaseSeederRunner {
seeders: Vec<Box<dyn DatabaseSeeder>>,
}
impl DatabaseSeederRunner {
pub fn new() -> Self {
Self { seeders: Vec::new() }
}
pub fn add<S: DatabaseSeeder + 'static>(mut self, seeder: S) -> Self {
self.seeders.push(Box::new(seeder));
self
}
pub fn add_boxed(&mut self, seeder: Box<dyn DatabaseSeeder>) {
self.seeders.push(seeder);
}
pub async fn seed(&self, db: &DatabaseConnection) -> anyhow::Result<()> {
if self.seeders.is_empty() {
tracing::info!("No seeders registered; nothing to run");
return Ok(());
}
ensure_table(db).await?;
let mut ordered: Vec<&Box<dyn DatabaseSeeder>> = self.seeders.iter().collect();
ordered.sort_by_key(|s| s.order());
let existing = entity::Entity::load().all(db).await?;
let executed: HashSet<String> = existing.into_iter().map(|e| e.seeder_name).collect();
let txn = Arc::new(db.begin().await?);
let mut in_txn_executed = executed.clone();
for seeder in ordered {
let name = seeder.name().to_string();
if in_txn_executed.contains(&name) {
tracing::info!(seeder = %name, action = "seed", "Skipping already executed seeder");
continue;
}
tracing::info!(seeder = %name, action = "seed", "Running seeder");
seeder.seed(txn.clone()).await.map_err(|e| anyhow::anyhow!("seeder {} failed: {}", name, e))?;
let active = entity::ActiveModel {
seeder_name: Set(name.clone()),
uid: Set(uuid::Uuid::new_v4()),
created_at: Set(chrono::Utc::now().into()),
updated_at: Set(chrono::Utc::now().into()),
..Default::default()
};
entity::Entity::insert(active).exec(txn.as_ref()).await.map_err(|e| anyhow::anyhow!("failed to track seeder {}: {}", name, e))?;
in_txn_executed.insert(name);
}
let owned = Arc::try_unwrap(txn).map_err(|_| anyhow::anyhow!("seeder transaction handle still shared; refusing to commit"))?;
owned.commit().await?;
tracing::info!(action = "seed", seeder_count = in_txn_executed.len(), "Seeder transaction committed");
Ok(())
}
pub async fn delete(&self, db: &DatabaseConnection) -> anyhow::Result<()> {
if self.seeders.is_empty() {
tracing::info!("No seeders registered; nothing to delete");
return Ok(());
}
ensure_table(db).await?;
let mut ordered: Vec<&Box<dyn DatabaseSeeder>> = self.seeders.iter().collect();
ordered.sort_by_key(|s| std::cmp::Reverse(s.order()));
let existing = entity::Entity::load().all(db).await?;
let executed: HashSet<String> = existing.into_iter().map(|e| e.seeder_name).collect();
let txn = Arc::new(db.begin().await?);
let mut in_txn_executed = executed.clone();
for seeder in ordered {
let name = seeder.name().to_string();
if !in_txn_executed.contains(&name) {
tracing::info!(seeder = %name, action = "delete", "Skipping seeder that has not been executed");
continue;
}
tracing::info!(seeder = %name, action = "delete", "Deleting seeded data");
seeder.delete(txn.clone()).await.map_err(|e| anyhow::anyhow!("seeder delete {} failed: {}", name, e))?;
entity::Entity::delete_many()
.filter(entity::Column::SeederName.eq(name.clone()))
.exec(txn.as_ref())
.await?;
in_txn_executed.remove(&name);
}
let owned = Arc::try_unwrap(txn).map_err(|_| anyhow::anyhow!("seeder transaction handle still shared; refusing to commit"))?;
owned.commit().await?;
tracing::info!(action = "delete", "Seeder deletion transaction committed");
Ok(())
}
}
impl Default for DatabaseSeederRunner {
fn default() -> Self { Self::new() }
}
pub type DatabaseSeederHelper = DatabaseSeederRunner;
#[deprecated(note = "Use DatabaseSeederRunner or DatabaseSeederHelper")]
pub type SeederRunner = DatabaseSeederRunner;