//! Basit şema migration mekanizması (DATA_MODEL.md §3.15). //! //! Her migration bir kez çalışır; `schema_migrations` koleksiyonuna //! `{ _id: sürüm, name, applied_at }` olarak kaydedilir. //! Yeni migration eklemek: `MIGRATIONS` listesine bir sonraki sürüm numarasıyla ekle. use std::{future::Future, pin::Pin}; use mongodb::{ Database, bson::{DateTime, Document, doc}, error::Result, }; use super::col; type MigrationFn = for<'a> fn(&'a Database) -> Pin> + Send + 'a>>; struct Migration { version: i32, name: &'static str, run: MigrationFn, } /// Sıralı migration listesi. Sürümler 1'den başlar ve atlamadan artar. const MIGRATIONS: &[Migration] = &[ Migration { version: 1, name: "initial", run: |db| Box::pin(m001_initial(db)), }, Migration { version: 2, name: "default_assets", run: |db| Box::pin(m002_default_assets(db)), }, ]; /// Uygulanmamış migration'ları sırayla çalıştırır. pub async fn run(db: &Database) -> Result<()> { let coll = db.collection::(col::SCHEMA_MIGRATIONS); for m in MIGRATIONS { if coll.find_one(doc! { "_id": m.version }).await?.is_some() { continue; } tracing::info!("Migration çalışıyor: v{} ({})", m.version, m.name); (m.run)(db).await?; coll.insert_one(doc! { "_id": m.version, "name": m.name, "applied_at": DateTime::now(), }) .await?; } Ok(()) } /// v1: Tüm koleksiyonları açıkça oluşturur. /// (Transaction içinde ilk kez yazılan koleksiyonlar sorun çıkarmasın diye önceden var olsunlar.) async fn m001_initial(db: &Database) -> Result<()> { let existing = db.list_collection_names().await?; let all = col::HOUSEHOLD_SCOPED .iter() .chain(&[col::EXCHANGE_RATES, col::SCHEMA_MIGRATIONS]); for name in all { if !existing.iter().any(|e| e == name) { db.create_collection(*name).await?; } } Ok(()) } /// v2: Faz 8'den önce açılmış hanelere varsayılan varlıklar (TL, altınlar, USD, EUR). /// Varlığı olan haneye dokunmaz → tekrar çalışsa da çift kayıt olmaz. async fn m002_default_assets(db: &Database) -> Result<()> { #[derive(serde::Deserialize)] struct Household { #[serde(rename = "_id")] id: mongodb::bson::Uuid, } let assets = db.collection::(col::ASSETS); let mut cursor = db .collection::(col::HOUSEHOLDS) .find(doc! { "deleted_at": null }) .projection(doc! { "_id": 1 }) .await?; while cursor.advance().await? { let hid = cursor.deserialize_current()?.id; if assets .find_one(doc! { "household_id": hid }) .await? .is_none() { assets .insert_many(crate::services::seed::assets(hid)) .await?; tracing::info!("Varsayılan varlıklar eklendi: hane {hid}"); } } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn versions_start_at_1_and_are_sequential() { for (i, m) in MIGRATIONS.iter().enumerate() { assert_eq!( m.version, i as i32 + 1, "migration sürümleri ardışık olmalı" ); } } }