Skip to main content

icydb_core/db/index/plan/
mod.rs

1mod commit_ops;
2mod load;
3mod unique;
4
5use crate::{
6    db::{CommitIndexOp, index::IndexStore},
7    error::{ErrorClass, ErrorOrigin, InternalError},
8    model::index::IndexModel,
9    traits::{EntityKind, EntityValue},
10};
11use std::{cell::RefCell, thread::LocalKey};
12
13///
14/// IndexApplyPlan
15///
16
17#[derive(Debug)]
18pub struct IndexApplyPlan {
19    pub index: &'static IndexModel,
20    pub store: &'static LocalKey<RefCell<IndexStore>>,
21}
22
23///
24/// IndexMutationPlan
25///
26
27#[derive(Debug)]
28pub struct IndexMutationPlan {
29    pub apply: Vec<IndexApplyPlan>,
30    pub commit_ops: Vec<CommitIndexOp>,
31}
32
33pub(super) fn corruption_error(origin: ErrorOrigin, message: impl Into<String>) -> InternalError {
34    let message = message.into();
35    InternalError::new(
36        ErrorClass::Corruption,
37        origin,
38        format!("corruption detected ({origin}): {message}"),
39    )
40}
41
42pub(super) fn index_violation_error(path: &str, index_fields: &[&str]) -> InternalError {
43    InternalError::new(
44        ErrorClass::Conflict,
45        ErrorOrigin::Index,
46        format!(
47            "index constraint violation: {path} ({})",
48            index_fields.join(", ")
49        ),
50    )
51}
52
53/// Plan all index mutations for a single entity transition.
54///
55/// This function:
56/// - Loads existing index entries
57/// - Validates unique constraints
58/// - Computes the exact index writes/deletes required
59///
60/// All fallible work happens here. The returned plan is safe to apply
61/// infallibly after a commit marker is written.
62pub fn plan_index_mutation_for_entity<E: EntityKind + EntityValue>(
63    db: &crate::db::Db<E::Canister>,
64    old: Option<&E>,
65    new: Option<&E>,
66) -> Result<IndexMutationPlan, InternalError> {
67    let old_entity_key = old.map(|entity| entity.id().key());
68    let new_entity_key = new.map(|entity| entity.id().key());
69
70    let mut apply = Vec::with_capacity(E::INDEXES.len());
71    let mut commit_ops = Vec::new();
72
73    for index in E::INDEXES {
74        let store = db
75            .with_store_registry(|registry| registry.try_get_store(index.store))?
76            .index_store();
77
78        let old_key = match old {
79            Some(entity) => crate::db::index::IndexKey::new(entity, index)?,
80            None => None,
81        };
82        let new_key = match new {
83            Some(entity) => crate::db::index::IndexKey::new(entity, index)?,
84            None => None,
85        };
86
87        let old_entry = load::load_existing_entry(store, index, old)?;
88
89        // Prevalidate membership so commit-phase mutations cannot surface corruption.
90        if let Some(old_key) = &old_key {
91            let Some(old_entity_key) = old_entity_key else {
92                return Err(InternalError::new(
93                    ErrorClass::Internal,
94                    ErrorOrigin::Index,
95                    "missing old entity key for index removal".to_string(),
96                ));
97            };
98
99            let entry = old_entry.as_ref().ok_or_else(|| {
100                corruption_error(
101                    ErrorOrigin::Index,
102                    format!(
103                        "index corrupted: {} ({}) -> {}",
104                        E::PATH,
105                        index.fields.join(", "),
106                        crate::db::index::IndexEntryCorruption::missing_key(
107                            old_key.to_raw(),
108                            old_entity_key,
109                        )
110                    ),
111                )
112            })?;
113
114            if index.unique && entry.len() > 1 {
115                return Err(corruption_error(
116                    ErrorOrigin::Index,
117                    format!(
118                        "index corrupted: {} ({}) -> {}",
119                        E::PATH,
120                        index.fields.join(", "),
121                        crate::db::index::IndexEntryCorruption::NonUniqueEntry {
122                            keys: entry.len(),
123                        }
124                    ),
125                ));
126            }
127
128            if !entry.contains(old_entity_key) {
129                return Err(corruption_error(
130                    ErrorOrigin::Index,
131                    format!(
132                        "index corrupted: {} ({}) -> {}",
133                        E::PATH,
134                        index.fields.join(", "),
135                        crate::db::index::IndexEntryCorruption::missing_key(
136                            old_key.to_raw(),
137                            old_entity_key,
138                        )
139                    ),
140                ));
141            }
142        }
143
144        let new_entry = if old_key == new_key {
145            old_entry.clone()
146        } else {
147            load::load_existing_entry(store, index, new)?
148        };
149
150        unique::validate_unique_constraint::<E>(
151            db,
152            index,
153            new_entry.as_ref(),
154            new_entity_key.as_ref(),
155            new,
156        )?;
157
158        commit_ops::build_commit_ops_for_index::<E>(
159            &mut commit_ops,
160            index,
161            old_key,
162            new_key,
163            old_entry,
164            new_entry,
165            old_entity_key,
166            new_entity_key,
167        )?;
168
169        apply.push(IndexApplyPlan { index, store });
170    }
171
172    Ok(IndexMutationPlan { apply, commit_ops })
173}