icydb_core/db/index/plan/
mod.rs1mod 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#[derive(Debug)]
18pub struct IndexApplyPlan {
19 pub index: &'static IndexModel,
20 pub store: &'static LocalKey<RefCell<IndexStore>>,
21}
22
23#[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
53pub 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 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}