1use super::enforcement::{value_matches_declared_type, ContractError};
3use crate::storage::entity::{EntityData, EntityKind, UnifiedEntity};
4use reddb_types::Value;
5use std::collections::{BTreeMap, HashMap};
6
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub struct GraphContract {
9 pub vertex_types: BTreeMap<String, GraphVertexContract>,
10 pub edge_types: BTreeMap<String, GraphEdgeContract>,
11}
12
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct GraphVertexContract {
15 pub properties: BTreeMap<String, GraphPropertyContract>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct GraphEdgeContract {
20 pub from_type: String,
21 pub to_type: String,
22 pub properties: BTreeMap<String, GraphPropertyContract>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct GraphPropertyContract {
27 pub data_type: String,
29 pub required: bool,
30}
31
32fn invalid(message: impl Into<String>) -> ContractError {
33 ContractError::InvalidOperation(format!("graph contract: {}", message.into()))
34}
35
36impl GraphContract {
37 pub fn from_json(json: &str) -> Result<Self, ContractError> {
38 let persisted = reddb_file::decode_physical_graph_contract_json(json)
39 .map_err(|error| invalid(error.to_string()))?;
40 from_persisted(persisted)
41 }
42
43 pub fn to_json(&self) -> Result<String, ContractError> {
44 reddb_file::encode_physical_graph_contract_json(&to_persisted(self))
45 .map_err(|error| invalid(error.to_string()))
46 }
47
48 pub fn validate(&self) -> Result<(), ContractError> {
49 if self.vertex_types.is_empty() || self.vertex_types.len() + self.edge_types.len() > 1024 {
50 return Err(invalid(
51 "declare at least one vertex type and at most 1024 total types",
52 ));
53 }
54 let mut property_count = 0usize;
55 for (name, properties) in self
56 .vertex_types
57 .iter()
58 .map(|(name, vertex)| (name, &vertex.properties))
59 .chain(
60 self.edge_types
61 .iter()
62 .map(|(name, edge)| (name, &edge.properties)),
63 )
64 {
65 if name.trim().is_empty() || name.len() > 256 {
66 return Err(invalid("type names must contain 1 to 256 bytes"));
67 }
68 property_count += properties.len();
69 if property_count > 16384 {
70 return Err(invalid("at most 16384 properties may be declared"));
71 }
72 for (property, rule) in properties {
73 if property.trim().is_empty() || property.len() > 256 || rule.data_type.len() > 256
74 {
75 return Err(invalid(
76 "property names and type declarations must fit in 256 bytes",
77 ));
78 }
79 reddb_rql::analyzer::resolve_declared_data_type(&rule.data_type)
80 .map_err(|error| invalid(format!("{name}.{property}: {error}")))?;
81 }
82 }
83 for (name, edge) in &self.edge_types {
84 if !self.vertex_types.contains_key(&edge.from_type)
85 || !self.vertex_types.contains_key(&edge.to_type)
86 {
87 return Err(invalid(format!(
88 "edge '{name}' references an undeclared vertex type"
89 )));
90 }
91 }
92 Ok(())
93 }
94
95 pub fn validate_entities(&self, entities: &[&UnifiedEntity]) -> Result<(), ContractError> {
99 self.validate()?;
100 let mut vertices: HashMap<String, Vec<&str>> = HashMap::new();
101 for entity in entities {
102 if let EntityKind::GraphNode(node) = &entity.kind {
103 let rule = self.vertex_types.get(&node.node_type).ok_or_else(|| {
104 invalid(format!("undeclared vertex type '{}'", node.node_type))
105 })?;
106 let EntityData::Node(data) = &entity.data else {
107 return Err(invalid("vertex requires node data"));
108 };
109 validate_properties(&node.node_type, &rule.properties, &data.properties)?;
110 for alias in [
111 node.label.clone(),
112 entity.id.raw().to_string(),
113 entity.logical_id().raw().to_string(),
114 ] {
115 vertices.entry(alias).or_default().push(&node.node_type);
116 }
117 }
118 }
119 for entity in entities {
120 if let EntityKind::GraphEdge(edge) = &entity.kind {
121 let rule = self
122 .edge_types
123 .get(&edge.label)
124 .ok_or_else(|| invalid(format!("undeclared edge type '{}'", edge.label)))?;
125 let EntityData::Edge(data) = &entity.data else {
126 return Err(invalid("edge requires edge data"));
127 };
128 validate_properties(&edge.label, &rule.properties, &data.properties)?;
129 for (endpoint, expected) in [
130 (&edge.from_node, &rule.from_type),
131 (&edge.to_node, &rule.to_type),
132 ] {
133 let types = vertices.get(endpoint).ok_or_else(|| {
134 invalid(format!(
135 "edge '{}' has missing endpoint '{endpoint}'",
136 edge.label
137 ))
138 })?;
139 if types.iter().any(|actual| *actual != expected) {
140 return Err(invalid(format!(
141 "edge '{}' requires endpoint '{endpoint}' to have type '{expected}'",
142 edge.label
143 )));
144 }
145 }
146 }
147 }
148 Ok(())
149 }
150}
151
152fn validate_properties(
153 owner: &str,
154 rules: &BTreeMap<String, GraphPropertyContract>,
155 properties: &HashMap<String, Value>,
156) -> Result<(), ContractError> {
157 for (name, rule) in rules {
158 let value = properties.get(name);
159 if rule.required && value.is_none_or(|value| matches!(value, Value::Null)) {
160 return Err(invalid(format!("{owner}.{name} is required")));
161 }
162 if let Some(value) = value {
163 let data_type = reddb_rql::analyzer::resolve_declared_data_type(&rule.data_type)
164 .map_err(|error| invalid(error.to_string()))?;
165 if !value_matches_declared_type(value, data_type) {
166 return Err(invalid(format!(
167 "{owner}.{name} requires {}",
168 rule.data_type
169 )));
170 }
171 }
172 }
173 Ok(())
174}
175
176pub fn to_persisted(contract: &GraphContract) -> reddb_file::PhysicalGraphContract {
177 let properties = |properties: &BTreeMap<String, GraphPropertyContract>| {
178 properties
179 .iter()
180 .map(|(name, property)| {
181 (
182 name.clone(),
183 reddb_file::PhysicalGraphPropertyContract {
184 data_type: property.data_type.clone(),
185 required: property.required,
186 },
187 )
188 })
189 .collect()
190 };
191 reddb_file::PhysicalGraphContract {
192 vertex_types: contract
193 .vertex_types
194 .iter()
195 .map(|(name, vertex)| {
196 (
197 name.clone(),
198 reddb_file::PhysicalGraphVertexContract {
199 properties: properties(&vertex.properties),
200 },
201 )
202 })
203 .collect(),
204 edge_types: contract
205 .edge_types
206 .iter()
207 .map(|(name, edge)| {
208 (
209 name.clone(),
210 reddb_file::PhysicalGraphEdgeContract {
211 from_type: edge.from_type.clone(),
212 to_type: edge.to_type.clone(),
213 properties: properties(&edge.properties),
214 },
215 )
216 })
217 .collect(),
218 }
219}
220
221pub fn from_persisted(
222 contract: reddb_file::PhysicalGraphContract,
223) -> Result<GraphContract, ContractError> {
224 let properties = |properties: BTreeMap<String, reddb_file::PhysicalGraphPropertyContract>| {
225 properties
226 .into_iter()
227 .map(|(name, property)| {
228 (
229 name,
230 GraphPropertyContract {
231 data_type: property.data_type,
232 required: property.required,
233 },
234 )
235 })
236 .collect()
237 };
238 let contract = GraphContract {
239 vertex_types: contract
240 .vertex_types
241 .into_iter()
242 .map(|(name, vertex)| {
243 (
244 name,
245 GraphVertexContract {
246 properties: properties(vertex.properties),
247 },
248 )
249 })
250 .collect(),
251 edge_types: contract
252 .edge_types
253 .into_iter()
254 .map(|(name, edge)| {
255 (
256 name,
257 GraphEdgeContract {
258 from_type: edge.from_type,
259 to_type: edge.to_type,
260 properties: properties(edge.properties),
261 },
262 )
263 })
264 .collect(),
265 };
266 contract.validate()?;
267 Ok(contract)
268}