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