1mod endpoint;
4mod property_defaults;
5mod property_element_types;
6mod record_types;
7
8use std::collections::BTreeSet;
9
10use selene_core::{
11 ByteStringType, CharacterStringType, DbString, DecimalType, LabelSet, PropertyValueType,
12};
13use serde::{Deserialize, Serialize};
14
15pub use endpoint::EdgeEndpointDef;
16pub use property_defaults::{PropertyDefaultRecordField, PropertyDefaultValue};
17pub use property_element_types::PropertyElementType;
18use record_types::validate_record_field_types;
19pub use record_types::{RecordFieldType, RecordFieldTypeDef, RecordFieldTypes};
20
21use crate::error::{GraphError, GraphResult};
22
23pub const MAX_LIST_TYPE_NESTING: u32 = 64;
25
26pub const MAX_RECORD_TYPE_NESTING: u32 = MAX_LIST_TYPE_NESTING;
32
33#[derive(
35 Clone,
36 Debug,
37 Deserialize,
38 PartialEq,
39 rkyv::Archive,
40 rkyv::Deserialize,
41 rkyv::Serialize,
42 Serialize,
43)]
44pub struct GraphTypeDef {
45 pub name: DbString,
47 pub node_types: Vec<NodeTypeDef>,
49 pub edge_types: Vec<EdgeTypeDef>,
51}
52
53impl GraphTypeDef {
54 pub fn validate(self) -> GraphResult<Self> {
62 self.validate_ref()?;
63 Ok(self)
64 }
65
66 #[must_use]
68 pub fn find_node_type(&self, labels: &LabelSet) -> Option<&NodeTypeDef> {
69 self.node_types
70 .iter()
71 .find(|node_type| &node_type.key_labels == labels)
72 }
73
74 #[must_use]
76 pub fn find_node_type_index(&self, labels: &LabelSet) -> Option<u32> {
77 self.node_types
78 .iter()
79 .position(|node_type| &node_type.key_labels == labels)
80 .and_then(|index| u32::try_from(index).ok())
81 }
82
83 #[must_use]
85 pub fn node_type_index_for(&self, name: DbString) -> Option<u32> {
86 self.node_types
87 .iter()
88 .position(|node_type| node_type.name == name)
89 .and_then(|index| u32::try_from(index).ok())
90 }
91
92 #[must_use]
94 pub fn find_edge_type(
95 &self,
96 label: DbString,
97 source_node_type: u32,
98 target_node_type: u32,
99 ) -> Option<&EdgeTypeDef> {
100 self.edge_types.iter().find(|edge_type| {
101 edge_type.label == label
102 && edge_type
103 .source_node_type
104 .matches_node_type(source_node_type)
105 && edge_type
106 .target_node_type
107 .matches_node_type(target_node_type)
108 })
109 }
110
111 #[must_use]
113 pub fn first_edge_type_with_label(&self, label: DbString) -> Option<&EdgeTypeDef> {
114 self.edge_types
115 .iter()
116 .find(|edge_type| edge_type.label == label)
117 }
118
119 #[must_use]
121 pub fn edge_type_index_for(&self, name: DbString) -> Option<u32> {
122 self.edge_types
123 .iter()
124 .position(|edge_type| edge_type.name == name)
125 .and_then(|index| u32::try_from(index).ok())
126 }
127
128 #[must_use]
134 pub fn without_node_type(&self, name: DbString) -> Option<Self> {
135 let index = self
136 .node_types
137 .iter()
138 .position(|node_type| node_type.name == name)?;
139 let mut next = self.clone();
140 next.node_types.remove(index);
141 Some(next)
142 }
143
144 #[must_use]
146 pub fn without_edge_type(&self, name: DbString) -> Option<Self> {
147 let index = self
148 .edge_types
149 .iter()
150 .position(|edge_type| edge_type.name == name)?;
151 let mut next = self.clone();
152 next.edge_types.remove(index);
153 Some(next)
154 }
155
156 pub fn validate_ref(&self) -> GraphResult<()> {
162 ensure_unique_names(
163 "node type",
164 self.node_types
165 .iter()
166 .map(|node_type| node_type.name.clone()),
167 )?;
168 ensure_unique_names(
169 "edge type",
170 self.edge_types
171 .iter()
172 .map(|edge_type| edge_type.name.clone()),
173 )?;
174
175 let mut seen_label_sets = BTreeSet::new();
176 for node_type in &self.node_types {
177 if node_type.key_labels.is_empty() {
178 return Err(GraphError::Inconsistent {
179 reason: format!("node type {} has an empty label set", node_type.name),
180 });
181 }
182 let label_key: Vec<DbString> = node_type.key_labels.iter().cloned().collect();
188 if !seen_label_sets.insert(label_key) {
189 return Err(GraphError::Inconsistent {
190 reason: format!(
191 "node type {} duplicates the key_labels of an earlier node type",
192 node_type.name
193 ),
194 });
195 }
196 ensure_unique_names(
197 "node property",
198 node_type
199 .properties
200 .iter()
201 .map(|property| property.name.clone()),
202 )?;
203 validate_property_element_types(node_type.name.clone(), &node_type.properties)?;
204 }
205
206 let node_type_count = self.node_types.len();
207 for (index, edge_type) in self.edge_types.iter().enumerate() {
208 ensure_endpoint_index(
209 node_type_count,
210 &edge_type.source_node_type,
211 edge_type.name.clone(),
212 )?;
213 ensure_endpoint_index(
214 node_type_count,
215 &edge_type.target_node_type,
216 edge_type.name.clone(),
217 )?;
218 ensure_unique_names(
219 "edge property",
220 edge_type
221 .properties
222 .iter()
223 .map(|property| property.name.clone()),
224 )?;
225 validate_property_element_types(edge_type.name.clone(), &edge_type.properties)?;
226 if self.edge_types[..index].iter().any(|previous| {
227 previous.label == edge_type.label
228 && previous
229 .source_node_type
230 .overlaps(&edge_type.source_node_type)
231 && previous
232 .target_node_type
233 .overlaps(&edge_type.target_node_type)
234 }) {
235 return Err(GraphError::Inconsistent {
236 reason: format!(
237 "ambiguous edge type endpoints ({}, {}, {})",
238 edge_type.label, edge_type.source_node_type, edge_type.target_node_type
239 ),
240 });
241 }
242 }
243 Ok(())
244 }
245}
246
247fn validate_property_element_types(
248 type_name: DbString,
249 properties: &[PropertyTypeDef],
250) -> GraphResult<()> {
251 for property in properties {
252 if property.decimal_type.is_some() && property.value_type != PropertyValueType::Decimal {
253 return Err(GraphError::Inconsistent {
254 reason: format!(
255 "property {} on type {type_name} declares decimal precision for non-DECIMAL value type {}",
256 property.name, property.value_type
257 ),
258 });
259 }
260 if property.character_string_type.is_some()
261 && property.value_type != PropertyValueType::String
262 {
263 return Err(GraphError::Inconsistent {
264 reason: format!(
265 "property {} on type {type_name} declares character-string length for non-STRING value type {}",
266 property.name, property.value_type
267 ),
268 });
269 }
270 if property.byte_string_type.is_some() && property.value_type != PropertyValueType::Bytes {
271 return Err(GraphError::Inconsistent {
272 reason: format!(
273 "property {} on type {type_name} declares byte-string length for non-BYTES value type {}",
274 property.name, property.value_type
275 ),
276 });
277 }
278 if property.value_type == PropertyValueType::List {
279 let Some(element_type) = property.list_element_type.as_ref() else {
280 continue;
285 };
286 validate_property_element_type(
287 type_name.clone(),
288 property.name.clone(),
289 element_type,
290 1,
291 )?;
292 } else if property.value_type == PropertyValueType::RecordTyped {
293 if let Some(fields) = property.record_field_types.as_ref() {
296 validate_record_field_types(type_name.clone(), property.name.clone(), fields, 1)?;
297 }
298 } else if property.list_element_type.is_some() {
299 return Err(GraphError::Inconsistent {
300 reason: format!(
301 "property {} on type {type_name} declares a list element type for non-LIST value type {}",
302 property.name, property.value_type
303 ),
304 });
305 } else if property.record_field_types.is_some() {
306 return Err(GraphError::Inconsistent {
307 reason: format!(
308 "property {} on type {type_name} declares record field types for non-RECORD value type {}",
309 property.name, property.value_type
310 ),
311 });
312 }
313 }
314 Ok(())
315}
316
317fn validate_property_element_type(
318 type_name: DbString,
319 property_name: DbString,
320 element_type: &PropertyElementType,
321 depth: u32,
322) -> GraphResult<()> {
323 if depth > MAX_LIST_TYPE_NESTING {
324 return Err(GraphError::Inconsistent {
325 reason: format!(
326 "property {property_name} on type {type_name} exceeds LIST nesting limit"
327 ),
328 });
329 }
330 match element_type {
331 PropertyElementType::NotNull(inner) => {
332 validate_property_element_type(type_name, property_name, inner, depth)
333 }
334 PropertyElementType::Scalar(
335 PropertyValueType::List | PropertyValueType::Record | PropertyValueType::RecordTyped,
336 ) => Err(GraphError::Inconsistent {
337 reason: format!(
338 "property {property_name} on type {type_name} uses unsupported LIST element type {}",
339 element_type.value_type()
340 ),
341 }),
342 PropertyElementType::Scalar(_)
343 | PropertyElementType::CharacterString(_)
344 | PropertyElementType::Decimal(_)
345 | PropertyElementType::ByteString(_) => Ok(()),
346 PropertyElementType::List(inner) => {
347 validate_property_element_type(type_name, property_name, inner, depth + 1)
348 }
349 }
350}
351
352#[derive(
354 Clone,
355 Debug,
356 Deserialize,
357 PartialEq,
358 rkyv::Archive,
359 rkyv::Deserialize,
360 rkyv::Serialize,
361 Serialize,
362)]
363pub struct NodeTypeDef {
364 pub name: DbString,
366 pub key_labels: LabelSet,
368 pub properties: Vec<PropertyTypeDef>,
370 pub validation_mode: ValidationMode,
372}
373
374#[derive(
376 Clone,
377 Debug,
378 Deserialize,
379 PartialEq,
380 rkyv::Archive,
381 rkyv::Deserialize,
382 rkyv::Serialize,
383 Serialize,
384)]
385pub struct EdgeTypeDef {
386 pub name: DbString,
388 pub label: DbString,
390 pub source_node_type: EdgeEndpointDef,
392 pub target_node_type: EdgeEndpointDef,
394 pub properties: Vec<PropertyTypeDef>,
396 pub validation_mode: ValidationMode,
398}
399
400#[derive(
402 Clone,
403 Debug,
404 Deserialize,
405 PartialEq,
406 rkyv::Archive,
407 rkyv::Deserialize,
408 rkyv::Serialize,
409 Serialize,
410)]
411pub struct PropertyTypeDef {
412 pub name: DbString,
414 pub value_type: PropertyValueType,
416 pub list_element_type: Option<PropertyElementType>,
418 pub required: bool,
420 pub default: Option<PropertyDefaultValue>,
422 pub immutable: bool,
424 pub unique: bool,
426 pub decimal_type: Option<DecimalType>,
429 pub character_string_type: Option<CharacterStringType>,
432 pub byte_string_type: Option<ByteStringType>,
434 pub record_field_types: Option<RecordFieldTypes>,
439}
440
441#[derive(
443 Clone,
444 Copy,
445 Debug,
446 Default,
447 Deserialize,
448 Eq,
449 Hash,
450 PartialEq,
451 rkyv::Archive,
452 rkyv::Deserialize,
453 rkyv::Serialize,
454 Serialize,
455)]
456pub enum ValidationMode {
457 #[default]
459 Strict,
460 Warn,
462}
463
464#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
475pub enum DropBehavior {
476 #[default]
479 Restrict,
480 Cascade,
484}
485
486fn ensure_unique_names(
487 kind: &'static str,
488 names: impl Iterator<Item = DbString>,
489) -> GraphResult<()> {
490 let mut seen = BTreeSet::new();
491 for name in names {
492 if !seen.insert(name.clone()) {
493 return Err(GraphError::Inconsistent {
494 reason: format!("duplicate {kind} name {name}"),
495 });
496 }
497 }
498 Ok(())
499}
500
501fn ensure_node_type_index(count: usize, index: u32, edge_name: DbString) -> GraphResult<()> {
502 if usize::try_from(index).is_ok_and(|index| index < count) {
503 return Ok(());
504 }
505 Err(GraphError::Inconsistent {
506 reason: format!(
507 "edge type {edge_name} references node type index {index}, but only {count} node types exist"
508 ),
509 })
510}
511
512fn ensure_endpoint_index(
513 count: usize,
514 endpoint: &EdgeEndpointDef,
515 edge_name: DbString,
516) -> GraphResult<()> {
517 match endpoint {
518 EdgeEndpointDef::Any => Ok(()),
519 EdgeEndpointDef::NodeType(index) => ensure_node_type_index(count, *index, edge_name),
520 EdgeEndpointDef::OneOf(indices) => {
521 if indices.len() < 2 {
526 return Err(GraphError::Inconsistent {
527 reason: format!(
528 "edge type {edge_name} has a OneOf endpoint with {} indices; OneOf must enumerate at least two distinct node types (singletons must collapse to NodeType)",
529 indices.len()
530 ),
531 });
532 }
533 for window in indices.windows(2) {
534 if window[0] >= window[1] {
535 return Err(GraphError::Inconsistent {
536 reason: format!(
537 "edge type {edge_name} has a OneOf endpoint that is not sorted and deduplicated ({}, {})",
538 window[0], window[1]
539 ),
540 });
541 }
542 }
543 for index in indices {
544 ensure_node_type_index(count, *index, edge_name.clone())?;
545 }
546 Ok(())
547 }
548 }
549}
550
551#[cfg(test)]
552#[path = "graph_types_tests.rs"]
553mod tests;
554
555#[cfg(test)]
556#[path = "graph_types_property_default_tests.rs"]
557mod property_default_tests;