Skip to main content

reddb_engine/catalog/
graph.rs

1//! Optional graph admission. Each vertex has exactly one concrete type.
2use 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    /// A RedDB scalar type name (the same names accepted in table contracts).
28    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    /// Validate a projected graph, after applying every change in a statement
96    /// or merge. Additional properties are allowed; declared properties have
97    /// exact native types and required properties cannot be absent or NULL.
98    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}