Skip to main content

velesdb_core/collection/graph/
schema.rs

1//! Graph schema definitions for heterogeneous knowledge graphs.
2//!
3//! This module provides schema definitions for graph collections,
4//! supporting both strict schemas (with predefined node/edge types)
5//! and schemaless mode (accepting arbitrary types).
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10use crate::error::{Error, Result};
11
12/// Value types supported for node and edge properties.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum ValueType {
16    /// String value.
17    String,
18    /// Integer value (i64).
19    Integer,
20    /// Floating-point value (f64).
21    Float,
22    /// Boolean value.
23    Boolean,
24    /// Vector embedding (for hybrid graph+vector queries).
25    Vector,
26}
27
28/// Definition of a node type in the graph schema.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct NodeType {
31    name: String,
32    properties: HashMap<String, ValueType>,
33}
34
35impl NodeType {
36    /// Creates a new node type with the given name.
37    #[must_use]
38    pub fn new(name: &str) -> Self {
39        Self {
40            name: name.to_string(),
41            properties: HashMap::new(),
42        }
43    }
44
45    /// Adds properties to this node type (builder pattern).
46    #[must_use]
47    pub fn with_properties(mut self, properties: HashMap<String, ValueType>) -> Self {
48        self.properties = properties;
49        self
50    }
51
52    /// Returns the name of this node type.
53    #[must_use]
54    pub fn name(&self) -> &str {
55        &self.name
56    }
57
58    /// Returns all properties of this node type.
59    #[must_use]
60    pub fn properties(&self) -> &HashMap<String, ValueType> {
61        &self.properties
62    }
63
64    /// Returns the type of a specific property, if it exists.
65    #[must_use]
66    pub fn property_type(&self, name: &str) -> Option<&ValueType> {
67        self.properties.get(name)
68    }
69}
70
71/// Definition of an edge type in the graph schema.
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct EdgeType {
74    name: String,
75    from_type: String,
76    to_type: String,
77    properties: HashMap<String, ValueType>,
78}
79
80impl EdgeType {
81    /// Creates a new edge type with the given name and endpoint types.
82    #[must_use]
83    pub fn new(name: &str, from_type: &str, to_type: &str) -> Self {
84        Self {
85            name: name.to_string(),
86            from_type: from_type.to_string(),
87            to_type: to_type.to_string(),
88            properties: HashMap::new(),
89        }
90    }
91
92    /// Adds properties to this edge type (builder pattern).
93    #[must_use]
94    pub fn with_properties(mut self, properties: HashMap<String, ValueType>) -> Self {
95        self.properties = properties;
96        self
97    }
98
99    /// Returns the name of this edge type.
100    #[must_use]
101    pub fn name(&self) -> &str {
102        &self.name
103    }
104
105    /// Returns the source node type.
106    #[must_use]
107    pub fn from_type(&self) -> &str {
108        &self.from_type
109    }
110
111    /// Returns the target node type.
112    #[must_use]
113    pub fn to_type(&self) -> &str {
114        &self.to_type
115    }
116
117    /// Returns all properties of this edge type.
118    #[must_use]
119    pub fn properties(&self) -> &HashMap<String, ValueType> {
120        &self.properties
121    }
122}
123
124/// Schema for a graph collection.
125///
126/// Supports two modes:
127/// - **Strict mode**: Only predefined node/edge types are allowed.
128/// - **Schemaless mode**: Any node/edge type is accepted.
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub struct GraphSchema {
131    schemaless: bool,
132    node_types: Vec<NodeType>,
133    edge_types: Vec<EdgeType>,
134}
135
136impl Default for GraphSchema {
137    fn default() -> Self {
138        Self::schemaless()
139    }
140}
141
142impl GraphSchema {
143    /// Creates a new strict (non-schemaless) graph schema.
144    ///
145    /// Use `with_node_type` and `with_edge_type` to add allowed types.
146    #[must_use]
147    pub fn new() -> Self {
148        Self {
149            schemaless: false,
150            node_types: Vec::new(),
151            edge_types: Vec::new(),
152        }
153    }
154
155    /// Creates a schemaless graph schema that accepts any types.
156    #[must_use]
157    pub fn schemaless() -> Self {
158        Self {
159            schemaless: true,
160            node_types: Vec::new(),
161            edge_types: Vec::new(),
162        }
163    }
164
165    /// Adds a node type to the schema (builder pattern).
166    #[must_use]
167    pub fn with_node_type(mut self, node_type: NodeType) -> Self {
168        self.node_types.push(node_type);
169        self
170    }
171
172    /// Adds an edge type to the schema (builder pattern).
173    #[must_use]
174    pub fn with_edge_type(mut self, edge_type: EdgeType) -> Self {
175        self.edge_types.push(edge_type);
176        self
177    }
178
179    /// Returns whether this schema is schemaless.
180    #[must_use]
181    pub fn is_schemaless(&self) -> bool {
182        self.schemaless
183    }
184
185    /// Returns all node types in this schema.
186    #[must_use]
187    pub fn node_types(&self) -> &[NodeType] {
188        &self.node_types
189    }
190
191    /// Returns all edge types in this schema.
192    #[must_use]
193    pub fn edge_types(&self) -> &[EdgeType] {
194        &self.edge_types
195    }
196
197    /// Checks if a node type exists in this schema.
198    #[must_use]
199    pub fn has_node_type(&self, name: &str) -> bool {
200        self.node_types.iter().any(|nt| nt.name == name)
201    }
202
203    /// Checks if an edge type exists in this schema.
204    #[must_use]
205    pub fn has_edge_type(&self, name: &str) -> bool {
206        self.edge_types.iter().any(|et| et.name == name)
207    }
208
209    /// Validates a node type against this schema.
210    ///
211    /// Returns `Ok(())` if the type is valid, or an error with details.
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if schema is strict and `type_name` is not declared.
216    pub fn validate_node_type(&self, type_name: &str) -> Result<()> {
217        if self.schemaless {
218            return Ok(());
219        }
220
221        if self.has_node_type(type_name) {
222            return Ok(());
223        }
224
225        let allowed: Vec<&str> = self.node_types.iter().map(|nt| nt.name.as_str()).collect();
226        Err(Error::SchemaValidation(format!(
227            "Node type '{type_name}' not allowed. Valid types: {allowed:?}",
228        )))
229    }
230
231    /// Validates an edge type against this schema.
232    ///
233    /// Checks the edge type name and that source/target node types are valid.
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if schema is strict and edge type or endpoint node types
238    /// violate schema constraints.
239    pub fn validate_edge_type(
240        &self,
241        edge_type: &str,
242        from_type: &str,
243        to_type: &str,
244    ) -> Result<()> {
245        if self.schemaless {
246            return Ok(());
247        }
248
249        // Find the edge type definition
250        let edge_def = self.edge_types.iter().find(|et| et.name == edge_type);
251
252        if let Some(def) = edge_def {
253            // Validate source node type matches edge definition
254            if def.from_type != from_type {
255                return Err(Error::SchemaValidation(format!(
256                    "Edge '{edge_type}' expects source type '{}', got '{from_type}'",
257                    def.from_type
258                )));
259            }
260            // Validate target node type matches edge definition
261            if def.to_type != to_type {
262                return Err(Error::SchemaValidation(format!(
263                    "Edge '{edge_type}' expects target type '{}', got '{to_type}'",
264                    def.to_type
265                )));
266            }
267            // Validate that endpoint node types are declared in schema
268            if !self.has_node_type(from_type) {
269                return Err(Error::SchemaValidation(format!(
270                    "Edge '{edge_type}' references undeclared source node type '{from_type}'",
271                )));
272            }
273            if !self.has_node_type(to_type) {
274                return Err(Error::SchemaValidation(format!(
275                    "Edge '{edge_type}' references undeclared target node type '{to_type}'",
276                )));
277            }
278            Ok(())
279        } else {
280            let allowed: Vec<&str> = self.edge_types.iter().map(|et| et.name.as_str()).collect();
281            Err(Error::SchemaValidation(format!(
282                "Edge type '{edge_type}' not allowed. Valid types: {allowed:?}",
283            )))
284        }
285    }
286
287    /// Gets a node type by name.
288    #[must_use]
289    pub fn get_node_type(&self, name: &str) -> Option<&NodeType> {
290        self.node_types.iter().find(|nt| nt.name == name)
291    }
292
293    /// Gets an edge type by name.
294    #[must_use]
295    pub fn get_edge_type(&self, name: &str) -> Option<&EdgeType> {
296        self.edge_types.iter().find(|et| et.name == name)
297    }
298}