feat(backend): migration mekanizması, indeksler ve ortak model yardımcıları

- db/migrations.rs: schema_migrations ile sürümlü migration (v1: koleksiyonlar)
- db/indexes.rs: DATA_MODEL indeksleri + senkronizasyon indeksi, açılışta otomatik
- models/common.rs: UUID v7, ortak alanlar, hane kapsamlı filtre
- Birim testleri (4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 23:25:55 +03:00
parent 8ec24ecec2
commit 5550c7c43d
13 changed files with 367 additions and 34 deletions
+116
View File
@@ -0,0 +1,116 @@
//! İndeksler (DATA_MODEL.md §4). Açılışta her seferinde çağrılır;
//! MongoDB zaten var olan aynı indeksi yeniden oluşturmaz.
use mongodb::{
Database, IndexModel,
bson::{Document, doc},
error::Result,
options::IndexOptions,
};
use super::col;
pub async fn ensure(db: &Database) -> Result<()> {
for (collection, indexes) in definitions() {
db.collection::<Document>(collection)
.create_indexes(indexes)
.await?;
}
tracing::info!("İndeksler hazır");
Ok(())
}
fn idx(keys: Document) -> IndexModel {
IndexModel::builder().keys(keys).build()
}
fn idx_opts(keys: Document, options: IndexOptions) -> IndexModel {
IndexModel::builder().keys(keys).options(options).build()
}
fn unique() -> IndexOptions {
IndexOptions::builder().unique(true).build()
}
fn definitions() -> Vec<(&'static str, Vec<IndexModel>)> {
let hp = || idx(doc! { "household_id": 1, "period_id": 1 });
let mut defs = vec![
(col::USERS, vec![idx_opts(doc! { "email": 1 }, unique())]),
(col::INVITES, vec![idx_opts(doc! { "code": 1 }, unique())]),
(
col::PERIODS,
vec![
idx(doc! { "household_id": 1, "start_date": 1 }),
// Hanede en fazla bir açık dönem
idx_opts(
doc! { "household_id": 1 },
IndexOptions::builder()
.name("one_open_period_per_household".to_string())
.unique(true)
.partial_filter_expression(doc! { "status": "open" })
.build(),
),
],
),
(
col::CATEGORIES,
vec![idx(doc! { "household_id": 1, "sort_order": 1 })],
),
(
col::PAYMENT_METHODS,
vec![idx(doc! { "household_id": 1, "sort_order": 1 })],
),
(col::PLAN_ITEMS, vec![hp()]),
(col::BUDGETS, vec![hp()]),
(col::NOTES, vec![hp()]),
(
col::ENTRIES,
vec![
idx(doc! { "household_id": 1, "period_id": 1, "date": 1 }),
idx(doc! { "household_id": 1, "date": 1 }),
],
),
(
col::CARRYOVERS,
vec![idx(doc! { "household_id": 1, "to_period_id": 1 })],
),
(
col::ASSET_TRANSACTIONS,
vec![idx(doc! { "household_id": 1, "asset_id": 1, "date": 1 })],
),
(
col::EXCHANGE_RATES,
vec![idx(doc! { "code": 1, "household_id": 1, "rate_date": -1 })],
),
];
// Senkronizasyon (Faz 11): "şu tarihten sonra değişenler" sorgusu.
for &name in col::HOUSEHOLD_SCOPED {
let sync = idx(doc! { "household_id": 1, "updated_at": 1 });
match defs.iter_mut().find(|(c, _)| *c == name) {
Some((_, list)) => list.push(sync),
None => defs.push((name, vec![sync])),
}
}
defs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_household_collection_has_sync_index() {
let defs = definitions();
for &name in col::HOUSEHOLD_SCOPED {
let (_, list) = defs.iter().find(|(c, _)| *c == name).unwrap();
assert!(
list.iter()
.any(|i| i.keys == doc! { "household_id": 1, "updated_at": 1 }),
"{name} koleksiyonunda senkronizasyon indeksi yok"
);
}
}
}