1mod property_defaults;
4mod property_element_types;
5mod record_types;
6
7use std::{collections::BTreeSet, fmt};
8
9use selene_core::{
10 ByteStringType, CharacterStringType, DbString, DecimalType, LabelSet, PropertyValueType,
11};
12use serde::{Deserialize, Serialize};
13
14pub use property_defaults::{PropertyDefaultRecordField, PropertyDefaultValue};
15pub use property_element_types::PropertyElementType;
16use record_types::validate_record_field_types;
17pub use record_types::{RecordFieldType, RecordFieldTypeDef, RecordFieldTypes};
18
19use crate::error::{GraphError, GraphResult};
20
21pub const MAX_LIST_TYPE_NESTING: u32 = 64;
23
24pub const MAX_RECORD_TYPE_NESTING: u32 = MAX_LIST_TYPE_NESTING;
30
31#[derive(
33 Clone,
34 Debug,
35 Deserialize,
36 PartialEq,
37 rkyv::Archive,
38 rkyv::Deserialize,
39 rkyv::Serialize,
40 Serialize,
41)]
42pub struct GraphTypeDef {
43 pub name: DbString,
45 pub node_types: Vec<NodeTypeDef>,
47 pub edge_types: Vec<EdgeTypeDef>,
49}
50
51impl GraphTypeDef {
52 pub fn validate(self) -> GraphResult<Self> {
60 self.validate_ref()?;
61 Ok(self)
62 }
63
64 #[must_use]
66 pub fn find_node_type(&self, labels: &LabelSet) -> Option<&NodeTypeDef> {
67 self.node_types
68 .iter()
69 .find(|node_type| &node_type.key_labels == labels)
70 }
71
72 #[must_use]
74 pub fn find_node_type_index(&self, labels: &LabelSet) -> Option<u32> {
75 self.node_types
76 .iter()
77 .position(|node_type| &node_type.key_labels == labels)
78 .and_then(|index| u32::try_from(index).ok())
79 }
80
81 #[must_use]
83 pub fn node_type_index_for(&self, name: DbString) -> Option<u32> {
84 self.node_types
85 .iter()
86 .position(|node_type| node_type.name == name)
87 .and_then(|index| u32::try_from(index).ok())
88 }
89
90 #[must_use]
92 pub fn find_edge_type(
93 &self,
94 label: DbString,
95 source_node_type: u32,
96 target_node_type: u32,
97 ) -> Option<&EdgeTypeDef> {
98 self.edge_types.iter().find(|edge_type| {
99 edge_type.label == label
100 && edge_type
101 .source_node_type
102 .matches_node_type(source_node_type)
103 && edge_type
104 .target_node_type
105 .matches_node_type(target_node_type)
106 })
107 }
108
109 #[must_use]
111 pub fn first_edge_type_with_label(&self, label: DbString) -> Option<&EdgeTypeDef> {
112 self.edge_types
113 .iter()
114 .find(|edge_type| edge_type.label == label)
115 }
116
117 #[must_use]
119 pub fn edge_type_index_for(&self, name: DbString) -> Option<u32> {
120 self.edge_types
121 .iter()
122 .position(|edge_type| edge_type.name == name)
123 .and_then(|index| u32::try_from(index).ok())
124 }
125
126 #[must_use]
132 pub fn without_node_type(&self, name: DbString) -> Option<Self> {
133 let index = self
134 .node_types
135 .iter()
136 .position(|node_type| node_type.name == name)?;
137 let mut next = self.clone();
138 next.node_types.remove(index);
139 Some(next)
140 }
141
142 #[must_use]
144 pub fn without_edge_type(&self, name: DbString) -> Option<Self> {
145 let index = self
146 .edge_types
147 .iter()
148 .position(|edge_type| edge_type.name == name)?;
149 let mut next = self.clone();
150 next.edge_types.remove(index);
151 Some(next)
152 }
153
154 pub fn validate_ref(&self) -> GraphResult<()> {
160 ensure_unique_names(
161 "node type",
162 self.node_types
163 .iter()
164 .map(|node_type| node_type.name.clone()),
165 )?;
166 ensure_unique_names(
167 "edge type",
168 self.edge_types
169 .iter()
170 .map(|edge_type| edge_type.name.clone()),
171 )?;
172
173 let mut seen_label_sets = BTreeSet::new();
174 for node_type in &self.node_types {
175 if node_type.key_labels.is_empty() {
176 return Err(GraphError::Inconsistent {
177 reason: format!("node type {} has an empty label set", node_type.name),
178 });
179 }
180 let label_key: Vec<DbString> = node_type.key_labels.iter().cloned().collect();
186 if !seen_label_sets.insert(label_key) {
187 return Err(GraphError::Inconsistent {
188 reason: format!(
189 "node type {} duplicates the key_labels of an earlier node type",
190 node_type.name
191 ),
192 });
193 }
194 ensure_unique_names(
195 "node property",
196 node_type
197 .properties
198 .iter()
199 .map(|property| property.name.clone()),
200 )?;
201 validate_property_element_types(node_type.name.clone(), &node_type.properties)?;
202 }
203
204 let node_type_count = self.node_types.len();
205 for (index, edge_type) in self.edge_types.iter().enumerate() {
206 ensure_endpoint_index(
207 node_type_count,
208 &edge_type.source_node_type,
209 edge_type.name.clone(),
210 )?;
211 ensure_endpoint_index(
212 node_type_count,
213 &edge_type.target_node_type,
214 edge_type.name.clone(),
215 )?;
216 ensure_unique_names(
217 "edge property",
218 edge_type
219 .properties
220 .iter()
221 .map(|property| property.name.clone()),
222 )?;
223 validate_property_element_types(edge_type.name.clone(), &edge_type.properties)?;
224 if self.edge_types[..index].iter().any(|previous| {
225 previous.label == edge_type.label
226 && previous
227 .source_node_type
228 .overlaps(&edge_type.source_node_type)
229 && previous
230 .target_node_type
231 .overlaps(&edge_type.target_node_type)
232 }) {
233 return Err(GraphError::Inconsistent {
234 reason: format!(
235 "ambiguous edge type endpoints ({}, {}, {})",
236 edge_type.label, edge_type.source_node_type, edge_type.target_node_type
237 ),
238 });
239 }
240 }
241 Ok(())
242 }
243}
244
245fn validate_property_element_types(
246 type_name: DbString,
247 properties: &[PropertyTypeDef],
248) -> GraphResult<()> {
249 for property in properties {
250 if property.decimal_type.is_some() && property.value_type != PropertyValueType::Decimal {
251 return Err(GraphError::Inconsistent {
252 reason: format!(
253 "property {} on type {type_name} declares decimal precision for non-DECIMAL value type {}",
254 property.name, property.value_type
255 ),
256 });
257 }
258 if property.character_string_type.is_some()
259 && property.value_type != PropertyValueType::String
260 {
261 return Err(GraphError::Inconsistent {
262 reason: format!(
263 "property {} on type {type_name} declares character-string length for non-STRING value type {}",
264 property.name, property.value_type
265 ),
266 });
267 }
268 if property.byte_string_type.is_some() && property.value_type != PropertyValueType::Bytes {
269 return Err(GraphError::Inconsistent {
270 reason: format!(
271 "property {} on type {type_name} declares byte-string length for non-BYTES value type {}",
272 property.name, property.value_type
273 ),
274 });
275 }
276 if property.value_type == PropertyValueType::List {
277 let Some(element_type) = property.list_element_type.as_ref() else {
278 continue;
283 };
284 validate_property_element_type(
285 type_name.clone(),
286 property.name.clone(),
287 element_type,
288 1,
289 )?;
290 } else if property.value_type == PropertyValueType::RecordTyped {
291 if let Some(fields) = property.record_field_types.as_ref() {
294 validate_record_field_types(type_name.clone(), property.name.clone(), fields, 1)?;
295 }
296 } else if property.list_element_type.is_some() {
297 return Err(GraphError::Inconsistent {
298 reason: format!(
299 "property {} on type {type_name} declares a list element type for non-LIST value type {}",
300 property.name, property.value_type
301 ),
302 });
303 } else if property.record_field_types.is_some() {
304 return Err(GraphError::Inconsistent {
305 reason: format!(
306 "property {} on type {type_name} declares record field types for non-RECORD value type {}",
307 property.name, property.value_type
308 ),
309 });
310 }
311 }
312 Ok(())
313}
314
315fn validate_property_element_type(
316 type_name: DbString,
317 property_name: DbString,
318 element_type: &PropertyElementType,
319 depth: u32,
320) -> GraphResult<()> {
321 if depth > MAX_LIST_TYPE_NESTING {
322 return Err(GraphError::Inconsistent {
323 reason: format!(
324 "property {property_name} on type {type_name} exceeds LIST nesting limit"
325 ),
326 });
327 }
328 match element_type {
329 PropertyElementType::NotNull(inner) => {
330 validate_property_element_type(type_name, property_name, inner, depth)
331 }
332 PropertyElementType::Scalar(
333 PropertyValueType::List | PropertyValueType::Record | PropertyValueType::RecordTyped,
334 ) => Err(GraphError::Inconsistent {
335 reason: format!(
336 "property {property_name} on type {type_name} uses unsupported LIST element type {}",
337 element_type.value_type()
338 ),
339 }),
340 PropertyElementType::Scalar(_)
341 | PropertyElementType::CharacterString(_)
342 | PropertyElementType::Decimal(_)
343 | PropertyElementType::ByteString(_) => Ok(()),
344 PropertyElementType::List(inner) => {
345 validate_property_element_type(type_name, property_name, inner, depth + 1)
346 }
347 }
348}
349
350#[derive(
352 Clone,
353 Debug,
354 Deserialize,
355 PartialEq,
356 rkyv::Archive,
357 rkyv::Deserialize,
358 rkyv::Serialize,
359 Serialize,
360)]
361pub struct NodeTypeDef {
362 pub name: DbString,
364 pub key_labels: LabelSet,
366 pub properties: Vec<PropertyTypeDef>,
368 pub validation_mode: ValidationMode,
370}
371
372#[derive(
381 Clone,
382 Debug,
383 Deserialize,
384 Eq,
385 Hash,
386 PartialEq,
387 rkyv::Archive,
388 rkyv::Deserialize,
389 rkyv::Serialize,
390 Serialize,
391)]
392pub enum EdgeEndpointDef {
393 Any,
395 NodeType(u32),
397 OneOf(Vec<u32>),
400}
401
402impl EdgeEndpointDef {
403 #[must_use]
415 pub fn one_of(indices: impl IntoIterator<Item = u32>) -> Self {
416 let mut buf: Vec<u32> = indices.into_iter().collect();
417 buf.sort_unstable();
418 buf.dedup();
419 assert!(
420 !buf.is_empty(),
421 "EdgeEndpointDef::one_of called with empty index set"
422 );
423 match buf.len() {
424 1 => Self::NodeType(buf[0]),
425 _ => Self::OneOf(buf),
426 }
427 }
428
429 #[must_use]
431 pub fn matches_node_type(&self, node_type: u32) -> bool {
432 match self {
433 Self::Any => true,
434 Self::NodeType(expected) => *expected == node_type,
435 Self::OneOf(indices) => indices.binary_search(&node_type).is_ok(),
436 }
437 }
438
439 #[must_use]
441 pub fn overlaps(&self, other: &Self) -> bool {
442 match (self, other) {
443 (Self::Any, _) | (_, Self::Any) => true,
444 (Self::NodeType(left), Self::NodeType(right)) => left == right,
445 (Self::NodeType(index), Self::OneOf(indices))
446 | (Self::OneOf(indices), Self::NodeType(index)) => indices.binary_search(index).is_ok(),
447 (Self::OneOf(left), Self::OneOf(right)) => sorted_slices_intersect(left, right),
448 }
449 }
450
451 #[must_use]
458 pub const fn node_type_index(&self) -> Option<u32> {
459 match self {
460 Self::Any | Self::OneOf(_) => None,
461 Self::NodeType(index) => Some(*index),
462 }
463 }
464}
465
466impl fmt::Display for EdgeEndpointDef {
467 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
468 match self {
469 Self::Any => formatter.write_str("Any"),
470 Self::NodeType(index) => write!(formatter, "{index}"),
471 Self::OneOf(indices) => {
472 formatter.write_str("OneOf(")?;
473 for (position, index) in indices.iter().enumerate() {
474 if position > 0 {
475 formatter.write_str(", ")?;
476 }
477 write!(formatter, "{index}")?;
478 }
479 formatter.write_str(")")
480 }
481 }
482 }
483}
484
485fn sorted_slices_intersect(left: &[u32], right: &[u32]) -> bool {
486 let (mut i, mut j) = (0, 0);
487 while i < left.len() && j < right.len() {
488 match left[i].cmp(&right[j]) {
489 std::cmp::Ordering::Less => i += 1,
490 std::cmp::Ordering::Greater => j += 1,
491 std::cmp::Ordering::Equal => return true,
492 }
493 }
494 false
495}
496
497#[derive(
499 Clone,
500 Debug,
501 Deserialize,
502 PartialEq,
503 rkyv::Archive,
504 rkyv::Deserialize,
505 rkyv::Serialize,
506 Serialize,
507)]
508pub struct EdgeTypeDef {
509 pub name: DbString,
511 pub label: DbString,
513 pub source_node_type: EdgeEndpointDef,
515 pub target_node_type: EdgeEndpointDef,
517 pub properties: Vec<PropertyTypeDef>,
519 pub validation_mode: ValidationMode,
521}
522
523#[derive(
525 Clone,
526 Debug,
527 Deserialize,
528 PartialEq,
529 rkyv::Archive,
530 rkyv::Deserialize,
531 rkyv::Serialize,
532 Serialize,
533)]
534pub struct PropertyTypeDef {
535 pub name: DbString,
537 pub value_type: PropertyValueType,
539 pub list_element_type: Option<PropertyElementType>,
541 pub required: bool,
543 pub default: Option<PropertyDefaultValue>,
545 pub immutable: bool,
547 pub unique: bool,
549 pub decimal_type: Option<DecimalType>,
552 pub character_string_type: Option<CharacterStringType>,
555 pub byte_string_type: Option<ByteStringType>,
557 pub record_field_types: Option<RecordFieldTypes>,
562}
563
564#[derive(
566 Clone,
567 Copy,
568 Debug,
569 Default,
570 Deserialize,
571 Eq,
572 Hash,
573 PartialEq,
574 rkyv::Archive,
575 rkyv::Deserialize,
576 rkyv::Serialize,
577 Serialize,
578)]
579pub enum ValidationMode {
580 #[default]
582 Strict,
583 Warn,
585}
586
587#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
598pub enum DropBehavior {
599 #[default]
602 Restrict,
603 Cascade,
607}
608
609fn ensure_unique_names(
610 kind: &'static str,
611 names: impl Iterator<Item = DbString>,
612) -> GraphResult<()> {
613 let mut seen = BTreeSet::new();
614 for name in names {
615 if !seen.insert(name.clone()) {
616 return Err(GraphError::Inconsistent {
617 reason: format!("duplicate {kind} name {name}"),
618 });
619 }
620 }
621 Ok(())
622}
623
624fn ensure_node_type_index(count: usize, index: u32, edge_name: DbString) -> GraphResult<()> {
625 if usize::try_from(index).is_ok_and(|index| index < count) {
626 return Ok(());
627 }
628 Err(GraphError::Inconsistent {
629 reason: format!(
630 "edge type {edge_name} references node type index {index}, but only {count} node types exist"
631 ),
632 })
633}
634
635fn ensure_endpoint_index(
636 count: usize,
637 endpoint: &EdgeEndpointDef,
638 edge_name: DbString,
639) -> GraphResult<()> {
640 match endpoint {
641 EdgeEndpointDef::Any => Ok(()),
642 EdgeEndpointDef::NodeType(index) => ensure_node_type_index(count, *index, edge_name),
643 EdgeEndpointDef::OneOf(indices) => {
644 if indices.len() < 2 {
649 return Err(GraphError::Inconsistent {
650 reason: format!(
651 "edge type {edge_name} has a OneOf endpoint with {} indices; OneOf must enumerate at least two distinct node types (singletons must collapse to NodeType)",
652 indices.len()
653 ),
654 });
655 }
656 for window in indices.windows(2) {
657 if window[0] >= window[1] {
658 return Err(GraphError::Inconsistent {
659 reason: format!(
660 "edge type {edge_name} has a OneOf endpoint that is not sorted and deduplicated ({}, {})",
661 window[0], window[1]
662 ),
663 });
664 }
665 }
666 for index in indices {
667 ensure_node_type_index(count, *index, edge_name.clone())?;
668 }
669 Ok(())
670 }
671 }
672}
673
674#[cfg(test)]
675#[path = "graph_types_tests.rs"]
676mod tests;
677
678#[cfg(test)]
679#[path = "graph_types_property_default_tests.rs"]
680mod property_default_tests;