1use std::fmt;
4
5use selene_core::{
6 Change, DbString, EdgeId, LabelSet, NodeId, PropertyMap, PropertyValueType, Value,
7 byte_string_fits_type, character_string_fits_type, decimal_fits_type,
8};
9
10use crate::graph::SeleneGraph;
11use crate::graph_types::{EdgeEndpointDef, GraphTypeDef, PropertyTypeDef, ValidationMode};
12
13mod unique;
14
15#[cfg(test)]
16pub(crate) use unique::unique_property_check_required;
17pub(crate) use unique::{validate_unique_property_changes, validate_unique_property_state};
18
19#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
21pub enum EntityId {
22 Node(NodeId),
24 Edge(EdgeId),
26}
27
28impl fmt::Display for EntityId {
29 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::Node(id) => write!(formatter, "node {id}"),
32 Self::Edge(id) => write!(formatter, "edge {id}"),
33 }
34 }
35}
36
37#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error, miette::Diagnostic)]
39#[non_exhaustive]
40pub enum TypeViolation {
41 #[error("node {id} has labels {labels:?}, which do not match any node type")]
43 #[diagnostic(code(SLENE_G_030))]
44 UnknownNodeLabel {
45 id: NodeId,
47 labels: LabelSet,
49 },
50
51 #[error("edge {id} has label {label}, which does not match any edge type")]
53 #[diagnostic(code(SLENE_G_031))]
54 UnknownEdgeLabel {
55 id: EdgeId,
57 label: DbString,
59 },
60
61 #[error(
63 "edge {id} label {label} expected endpoint types ({expected_source_type}, {expected_target_type}) but observed ({observed_source_type}, {observed_target_type})"
64 )]
65 #[diagnostic(code(SLENE_G_032))]
66 EdgeEndpointTypeMismatch {
67 id: EdgeId,
69 label: DbString,
71 expected_source_type: EdgeEndpointDef,
73 observed_source_type: u32,
75 expected_target_type: EdgeEndpointDef,
77 observed_target_type: u32,
79 },
80
81 #[error("{entity_id} is missing required property {property} declared in {declared_in}")]
83 #[diagnostic(code(SLENE_G_033))]
84 MissingRequiredProperty {
85 entity_id: EntityId,
87 property: DbString,
89 declared_in: DbString,
91 },
92
93 #[error("{entity_id} property {property} expected {expected} but observed {observed}")]
95 #[diagnostic(code(SLENE_G_034))]
96 PropertyTypeMismatch {
97 entity_id: EntityId,
99 property: DbString,
101 expected: PropertyValueType,
103 observed: &'static str,
105 },
106
107 #[error("{entity_id} property {property} uses a Value::Extended payload")]
109 #[diagnostic(code(SLENE_G_035))]
110 ExtensionValueRejected {
111 entity_id: EntityId,
113 property: DbString,
115 },
116
117 #[error("{entity_id} property {property} is not declared by the matched type")]
119 #[diagnostic(code(SLENE_G_036))]
120 UndeclaredProperty {
121 entity_id: EntityId,
123 property: DbString,
125 },
126
127 #[error("{entity_id} property {property} declared in {declared_in} is immutable")]
129 #[diagnostic(code(SLENE_G_037))]
130 ImmutablePropertyUpdate {
131 entity_id: EntityId,
133 property: DbString,
135 declared_in: DbString,
137 },
138
139 #[error(
141 "{entity_id} property {property} declared in {declared_in} duplicates {conflicting_entity_id}"
142 )]
143 #[diagnostic(code(SLENE_G_038))]
144 UniquePropertyDuplicate {
145 entity_id: EntityId,
147 conflicting_entity_id: EntityId,
149 property: DbString,
151 declared_in: DbString,
153 },
154}
155
156#[derive(Clone, Debug, Eq, PartialEq)]
158pub struct TypeWarning {
159 pub violation: TypeViolation,
161}
162
163pub fn validate_change(
169 change: &Change,
170 graph: &SeleneGraph,
171 type_def: &GraphTypeDef,
172) -> Result<Vec<TypeWarning>, TypeViolation> {
173 match change {
174 Change::NodeCreated { id, .. } => {
175 if !graph.is_node_alive(*id) {
180 return Ok(Vec::new());
181 }
182 validate_node_state(*id, graph, type_def).map(|(_, warnings)| warnings)
183 }
184 Change::NodeUpdated {
185 id,
186 labels_diff,
187 properties_diff,
188 } => {
189 if !graph.is_node_alive(*id) {
190 return Ok(Vec::new());
191 }
192 let (node_type_index, mut warnings) = validate_node_state(*id, graph, type_def)?;
193 let node_type = &type_def.node_types[node_type_index as usize];
194 reject_immutable_property_update(
195 EntityId::Node(*id),
196 node_type.name.clone(),
197 &node_type.properties,
198 properties_diff,
199 )?;
200 if !labels_diff.is_empty() {
201 warnings.extend(revalidate_incident_edges(*id, graph, type_def)?);
206 }
207 Ok(warnings)
208 }
209 Change::EdgeCreated { id, .. } => {
210 if !graph.is_edge_alive(*id) {
211 return Ok(Vec::new());
212 }
213 validate_edge_state(*id, graph, type_def).map(|(_, warnings)| warnings)
214 }
215 Change::EdgeUpdated {
216 id,
217 properties_diff,
218 } => {
219 if !graph.is_edge_alive(*id) {
220 return Ok(Vec::new());
221 }
222 let (edge_type, warnings) = validate_edge_state(*id, graph, type_def)?;
223 reject_immutable_property_update(
224 EntityId::Edge(*id),
225 edge_type.name.clone(),
226 &edge_type.properties,
227 properties_diff,
228 )?;
229 Ok(warnings)
230 }
231 Change::NodePropertyRemoved { id, property } => {
232 if !graph.is_node_alive(*id) {
233 return Ok(Vec::new());
234 }
235 let (node_type_index, warnings) = validate_node_state(*id, graph, type_def)?;
236 let node_type = &type_def.node_types[node_type_index as usize];
237 reject_if_immutable(
238 EntityId::Node(*id),
239 node_type.name.clone(),
240 &node_type.properties,
241 property.clone(),
242 )?;
243 Ok(warnings)
244 }
245 Change::EdgePropertyRemoved { id, property } => {
246 if !graph.is_edge_alive(*id) {
247 return Ok(Vec::new());
248 }
249 let (edge_type, warnings) = validate_edge_state(*id, graph, type_def)?;
250 reject_if_immutable(
251 EntityId::Edge(*id),
252 edge_type.name.clone(),
253 &edge_type.properties,
254 property.clone(),
255 )?;
256 Ok(warnings)
257 }
258 Change::NodeLabelRemoved { id, .. } => {
259 if !graph.is_node_alive(*id) {
260 return Ok(Vec::new());
261 }
262 let (_, mut warnings) = validate_node_state(*id, graph, type_def)?;
263 warnings.extend(revalidate_incident_edges(*id, graph, type_def)?);
264 Ok(warnings)
265 }
266 Change::NodeDeleted { .. }
275 | Change::EdgeDeleted { .. }
276 | Change::NodesOfTypeTruncated { .. }
277 | Change::EdgesOfTypeTruncated { .. }
278 | Change::GraphReset { .. }
279 | Change::SchemaChanged { .. } => Ok(Vec::new()),
280 }
281}
282
283fn revalidate_incident_edges(
284 node: NodeId,
285 graph: &SeleneGraph,
286 type_def: &GraphTypeDef,
287) -> Result<Vec<TypeWarning>, TypeViolation> {
288 let mut warnings = Vec::new();
289 if let Some(entry) = graph.outgoing_edges(node) {
290 for edge in entry.iter() {
291 if graph.is_edge_alive(edge.edge_id) {
292 warnings.extend(validate_edge_state(edge.edge_id, graph, type_def)?.1);
293 }
294 }
295 }
296 if let Some(entry) = graph.incoming_edges(node) {
297 for edge in entry.iter() {
298 if graph.is_edge_alive(edge.edge_id) {
299 warnings.extend(validate_edge_state(edge.edge_id, graph, type_def)?.1);
300 }
301 }
302 }
303 Ok(warnings)
304}
305
306pub fn validate_entity_state(
308 graph: &SeleneGraph,
309 type_def: &GraphTypeDef,
310) -> Result<Vec<TypeWarning>, TypeViolation> {
311 let mut warnings = Vec::new();
312 for row in graph.node_store.alive.iter() {
313 let id = graph
314 .node_id_for_row(crate::store::RowIndex::new(row))
315 .expect("alive node row has a mapped external id (BRIEF-Item-4a)");
316 warnings.extend(validate_node_state(id, graph, type_def)?.1);
317 }
318 for row in graph.edge_store.alive.iter() {
319 let id = graph
320 .edge_id_for_row(crate::store::RowIndex::new(row))
321 .expect("alive edge row has a mapped external id (BRIEF-Item-4a)");
322 warnings.extend(validate_edge_state(id, graph, type_def)?.1);
323 }
324 validate_unique_property_state(graph, type_def)?;
325 Ok(warnings)
326}
327
328fn validate_node_state(
329 id: NodeId,
330 graph: &SeleneGraph,
331 type_def: &GraphTypeDef,
332) -> Result<(u32, Vec<TypeWarning>), TypeViolation> {
333 let empty_labels = LabelSet::new();
338 let labels = graph.node_labels(id).unwrap_or(&empty_labels);
339 let node_type_index =
340 type_def
341 .find_node_type_index(labels)
342 .ok_or_else(|| TypeViolation::UnknownNodeLabel {
343 id,
344 labels: labels.clone(),
345 })?;
346 let node_type = &type_def.node_types[node_type_index as usize];
347 let empty_props = PropertyMap::new();
348 let properties = graph.node_properties(id).unwrap_or(&empty_props);
349 let warnings = validate_properties(
350 EntityId::Node(id),
351 node_type.name.clone(),
352 node_type.validation_mode,
353 &node_type.properties,
354 properties,
355 )?;
356 Ok((node_type_index, warnings))
357}
358
359fn validate_edge_state<'a>(
360 id: EdgeId,
361 graph: &SeleneGraph,
362 type_def: &'a GraphTypeDef,
363) -> Result<(&'a crate::graph_types::EdgeTypeDef, Vec<TypeWarning>), TypeViolation> {
364 let label = graph
365 .edge_label(id)
366 .cloned()
367 .ok_or(TypeViolation::UnknownEdgeLabel {
368 id,
369 label: selene_core::db_string("__selene_missing_edge_label")
370 .expect("static label admits"),
371 })?;
372 let (source, target) =
373 graph
374 .edge_endpoints(id)
375 .ok_or_else(|| TypeViolation::UnknownEdgeLabel {
376 id,
377 label: label.clone(),
378 })?;
379 let (source_type, mut warnings) = validate_node_state(source, graph, type_def)?;
380 let (target_type, target_warnings) = validate_node_state(target, graph, type_def)?;
381 warnings.extend(target_warnings);
382
383 let Some(edge_type) = type_def.find_edge_type(label.clone(), source_type, target_type) else {
384 let Some(expected) = type_def.first_edge_type_with_label(label.clone()) else {
385 return Err(TypeViolation::UnknownEdgeLabel { id, label });
386 };
387 return Err(TypeViolation::EdgeEndpointTypeMismatch {
388 id,
389 label,
390 expected_source_type: expected.source_node_type.clone(),
391 observed_source_type: source_type,
392 expected_target_type: expected.target_node_type.clone(),
393 observed_target_type: target_type,
394 });
395 };
396 let empty_props = PropertyMap::new();
397 let properties = graph.edge_properties(id).unwrap_or(&empty_props);
398 warnings.extend(validate_properties(
399 EntityId::Edge(id),
400 edge_type.name.clone(),
401 edge_type.validation_mode,
402 &edge_type.properties,
403 properties,
404 )?);
405 Ok((edge_type, warnings))
406}
407
408fn reject_immutable_property_update(
409 entity_id: EntityId,
410 declared_in: DbString,
411 declarations: &[PropertyTypeDef],
412 diff: &selene_core::PropertyDiff,
413) -> Result<(), TypeViolation> {
414 for (key, _) in &diff.set {
415 reject_if_immutable(entity_id, declared_in.clone(), declarations, key.clone())?;
416 }
417 for key in &diff.removed {
418 reject_if_immutable(entity_id, declared_in.clone(), declarations, key.clone())?;
419 }
420 Ok(())
421}
422
423fn reject_if_immutable(
424 entity_id: EntityId,
425 declared_in: DbString,
426 declarations: &[PropertyTypeDef],
427 property: DbString,
428) -> Result<(), TypeViolation> {
429 if declarations
430 .iter()
431 .any(|declaration| declaration.name == property && declaration.immutable)
432 {
433 return Err(TypeViolation::ImmutablePropertyUpdate {
434 entity_id,
435 property,
436 declared_in,
437 });
438 }
439 Ok(())
440}
441
442fn validate_properties(
443 entity_id: EntityId,
444 declared_in: DbString,
445 validation_mode: ValidationMode,
446 declarations: &[PropertyTypeDef],
447 properties: &PropertyMap,
448) -> Result<Vec<TypeWarning>, TypeViolation> {
449 let mut warnings = Vec::new();
450 for (key, value) in properties.iter() {
451 let Some(declaration) = declarations.iter().find(|decl| decl.name == *key) else {
452 let violation = TypeViolation::UndeclaredProperty {
453 entity_id,
454 property: key.clone(),
455 };
456 if validation_mode == ValidationMode::Warn {
457 warnings.push(TypeWarning { violation });
458 continue;
459 }
460 return Err(violation);
461 };
462 if matches!(value, Value::Extended { .. }) {
463 return Err(TypeViolation::ExtensionValueRejected {
464 entity_id,
465 property: key.clone(),
466 });
467 }
468 if matches!(value, Value::Null) {
469 if declaration.required {
470 return Err(TypeViolation::MissingRequiredProperty {
471 entity_id,
472 property: key.clone(),
473 declared_in: declared_in.clone(),
474 });
475 }
476 continue;
477 }
478 if !property_value_matches(declaration, value) {
479 return Err(TypeViolation::PropertyTypeMismatch {
480 entity_id,
481 property: key.clone(),
482 expected: declaration.value_type,
483 observed: PropertyValueType::observed_name(value),
484 });
485 }
486 }
487
488 for declaration in declarations.iter().filter(|decl| decl.required) {
489 if properties
490 .get(&declaration.name)
491 .is_none_or(|value| matches!(value, Value::Null))
492 {
493 return Err(TypeViolation::MissingRequiredProperty {
494 entity_id,
495 property: declaration.name.clone(),
496 declared_in: declared_in.clone(),
497 });
498 }
499 }
500 Ok(warnings)
501}
502
503fn property_value_matches(declaration: &PropertyTypeDef, value: &Value) -> bool {
504 match declaration.value_type {
505 PropertyValueType::List => {
506 let Some(element_type) = declaration.list_element_type.as_ref() else {
507 return matches!(value, Value::List(_));
508 };
509 match value {
510 Value::List(values) => values.iter().all(|value| element_type.matches(value)),
511 _ => false,
512 }
513 }
514 PropertyValueType::Record | PropertyValueType::RecordTyped => {
524 if !matches!(value, Value::Record(_) | Value::RecordTyped(_)) {
525 return false;
526 }
527 match declaration.record_field_types.as_ref() {
528 Some(fields) => fields.matches(value),
529 None => true,
530 }
531 }
532 PropertyValueType::Decimal => match declaration.decimal_type {
533 Some(decimal_type) => {
534 matches!(value, Value::Decimal(value) if decimal_fits_type(*value, decimal_type))
535 }
536 None => declaration.value_type.matches(value),
537 },
538 PropertyValueType::String => match declaration.character_string_type {
539 Some(character_string_type) => {
540 matches!(value, Value::String(value) if character_string_fits_type(value, character_string_type))
541 }
542 None => declaration.value_type.matches(value),
543 },
544 PropertyValueType::Bytes => match declaration.byte_string_type {
545 Some(byte_string_type) => {
546 matches!(value, Value::Bytes(value) if byte_string_fits_type(value, byte_string_type))
547 }
548 None => declaration.value_type.matches(value),
549 },
550 _ => declaration.value_type.matches(value),
551 }
552}
553
554#[cfg(test)]
555#[path = "type_validator_tests.rs"]
556mod tests;