reddb_server/runtime/
graph_contract.rs1use super::*;
3use reddb_engine::catalog::GraphContract;
4use reddb_engine::transaction::read::{entity_visibility_check, SnapshotContext};
5
6impl RedDBRuntime {
7 pub fn graph_contract(&self, collection: &str) -> RedDBResult<Option<GraphContract>> {
8 self.check_collection_privilege(reddb_rql::ast::PolicyAction::Select, collection)?;
9 let contract = self
10 .db()
11 .collection_contract(collection)
12 .ok_or_else(|| RedDBError::NotFound(format!("collection '{collection}'")))?;
13 Ok(contract.graph_contract)
14 }
15
16 pub(crate) fn has_graph_contract(&self, collection: &str) -> bool {
17 self.db()
18 .collection_contract_arc(collection)
19 .is_some_and(|contract| contract.graph_contract.is_some())
20 }
21
22 pub fn set_graph_contract(
23 &self,
24 collection: &str,
25 graph: Option<GraphContract>,
26 ) -> RedDBResult<()> {
27 self.check_write(super::write_gate::WriteKind::Ddl)?;
28 if let (Some(auth), Some((username, role))) = (
29 self.inner.auth_store.read().clone(),
30 super::execution_context::current_auth_identity(),
31 ) {
32 let tenant = super::execution_context::current_tenant();
33 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
34 self.check_ddl_collection_privilege(
35 &auth,
36 &principal,
37 role,
38 tenant.as_deref(),
39 &username,
40 "alter",
41 collection,
42 )
43 .map_err(RedDBError::Query)?;
44 }
45 let store = self.db().store();
46 let _publication = store.vault_publication_lock.lock();
47 if self.snapshot_manager().oldest_active_xid().is_some() {
48 return Err(RedDBError::InvalidOperation(
49 "finish active transactions before changing a graph contract".into(),
50 ));
51 }
52 let mut contract = self
53 .db()
54 .collection_contract(collection)
55 .ok_or_else(|| RedDBError::NotFound(format!("collection '{collection}'")))?;
56 if contract.declared_model != crate::catalog::CollectionModel::Graph {
57 return Err(RedDBError::InvalidOperation(
58 "graph contracts require a Graph collection".into(),
59 ));
60 }
61 if let Some(graph) = &graph {
62 graph.validate()?;
63 for snapshot in self.vcs_live_graph_validation_contexts(collection)? {
64 self.validate_graph_projection(collection, graph, &snapshot, &[], &[])?;
65 }
66 if store.format_version() < reddb_file::native_store::STORE_VERSION_V14 {
69 let previous = store.format_version();
70 store.set_format_version(reddb_file::native_store::STORE_VERSION_V14);
71 if store.pager().is_some() {
72 if let Err(error) = store.persist() {
73 store.set_format_version(previous);
74 return Err(error.into());
75 }
76 }
77 }
78 }
79 contract.graph_contract = graph;
80 contract.version = contract.version.checked_add(1).ok_or_else(|| {
81 RedDBError::InvalidOperation("collection contract version exhausted".into())
82 })?;
83 contract.updated_at_unix_ms = std::time::SystemTime::now()
84 .duration_since(std::time::UNIX_EPOCH)
85 .unwrap_or_default()
86 .as_millis();
87 self.db()
88 .save_collection_contract(contract)
89 .map_err(|error| RedDBError::Internal(error.to_string()))?;
90 self.invalidate_result_cache();
91 Ok(())
92 }
93
94 pub(crate) fn validate_graph_changes(
95 &self,
96 collection: &str,
97 added: &[&UnifiedEntity],
98 removed: &[EntityId],
99 ) -> RedDBResult<()> {
100 let Some(contract) = self.db().collection_contract_arc(collection) else {
101 return Ok(());
102 };
103 let Some(graph) = &contract.graph_contract else {
104 return Ok(());
105 };
106 let (mut snapshot, _retention) = self.native_read_snapshot()?;
107 snapshot.own_xids = self.own_transaction_xids();
108 if self.current_xid().is_none() {
109 snapshot.snapshot = self.snapshot_manager().fresh_read_snapshot();
110 if let Some(branch) = snapshot.branch.as_mut() {
111 let branch = Arc::make_mut(branch);
112 branch
113 .epochs
114 .insert(branch.writer_epoch, snapshot.snapshot.clone());
115 }
116 }
117 self.validate_graph_projection(collection, graph, &snapshot, added, removed)
118 }
119
120 pub(crate) fn validate_graph_contracts_at_commit(&self) -> RedDBResult<()> {
123 let (mut context, _retention) = self.native_read_snapshot()?;
124 context.snapshot = self.snapshot_manager().fresh_read_snapshot();
125 context.own_xids = self.own_transaction_xids();
126 if let Some(branch) = context.branch.as_mut() {
127 let branch = Arc::make_mut(branch);
128 branch
129 .epochs
130 .insert(branch.writer_epoch, context.snapshot.clone());
131 }
132 for contract in self.db().collection_contracts() {
133 if let Some(graph) = &contract.graph_contract {
134 self.validate_graph_projection(&contract.name, graph, &context, &[], &[])?;
135 }
136 }
137 Ok(())
138 }
139
140 pub(crate) fn validate_graph_entities(
141 &self,
142 collection: &str,
143 graph: &GraphContract,
144 entities: &[&UnifiedEntity],
145 ) -> RedDBResult<()> {
146 let Some(column) = self.tenant_column(collection) else {
147 graph.validate_entities(entities)?;
148 return Ok(());
149 };
150 let mut tenants: HashMap<String, Vec<&UnifiedEntity>> = HashMap::new();
154 for &entity in entities {
155 let properties = match &entity.data {
156 EntityData::Node(node) => &node.properties,
157 EntityData::Edge(edge) => &edge.properties,
158 _ => continue,
159 };
160 let tenant = if let Some((root, tail)) = column.split_once('.') {
161 super::impl_dml_support::dotted_tenant_text(
162 properties.get(root).unwrap_or(&Value::Null),
163 tail,
164 )?
165 } else {
166 properties.get(&column).and_then(|value| match value {
167 Value::Text(value) => Some(value.to_string()),
168 _ => None,
169 })
170 }
171 .ok_or_else(|| {
172 RedDBError::Query("graph contract requires a text tenant discriminator".into())
173 })?;
174 tenants.entry(tenant).or_default().push(entity);
175 }
176 for entities in tenants.values() {
177 graph.validate_entities(entities)?;
178 }
179 Ok(())
180 }
181
182 fn validate_graph_projection(
183 &self,
184 collection: &str,
185 graph: &GraphContract,
186 snapshot: &SnapshotContext,
187 added: &[&UnifiedEntity],
188 removed: &[EntityId],
189 ) -> RedDBResult<()> {
190 let store = self.db().store();
191 let manager = store
192 .get_collection(collection)
193 .ok_or_else(|| RedDBError::NotFound(format!("collection '{collection}'")))?;
194 let (_scan_memory, read_bytes_max) = self.reserve_index_scan(&manager)?;
195 let mut rows = Vec::new();
196 let mut reservations = Vec::new();
197 super::function_budget::try_scan_result(
198 |visit| manager.try_for_each_entity(read_bytes_max, visit),
199 |entity| {
200 if !removed.contains(&entity.id)
201 && entity_visibility_check(snapshot, entity)
202 && matches!(
203 entity.kind,
204 EntityKind::GraphNode(_) | EntityKind::GraphEdge(_)
205 )
206 {
207 let bytes = crate::storage::unified::memory_size::entity_bytes(entity)
208 .saturating_mul(2)
209 .saturating_add(512);
210 reservations.push(
211 self.try_reserve_query_buffer_from_sample(bytes)
212 .ok_or_else(|| {
213 RedDBError::Query(
214 "graph contract validation exceeds the query memory budget"
215 .into(),
216 )
217 })?,
218 );
219 rows.push(entity.clone());
220 }
221 Ok(true)
222 },
223 )?;
224 let entities: Vec<_> = rows.iter().chain(added.iter().copied()).collect();
225 self.validate_graph_entities(collection, graph, &entities)?;
226 Ok(())
227 }
228}