use super::record_form_policy::{validate_record_form_target, RecordForm};
use crate::types::StoragePropertyDef;
use linkrs_core::types::{EdgeStrategy, LabelId, VertexId};
use linkrs_core::Value;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct EdgeRecord {
pub src_vid: VertexId,
pub dst_vid: VertexId,
pub rank: i64,
pub properties: Vec<(Arc<str>, Value)>,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default,
)]
pub enum StorageDirection {
#[default]
Both,
OutOnly,
InOnly,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default,
)]
pub enum EdgeMultiplicity {
One,
#[default]
Many,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default,
)]
pub enum IndexConsistency {
#[default]
BestEffort,
Strong,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EdgeSchema {
pub label_id: LabelId,
pub label_name: String,
pub src_label: LabelId,
pub dst_label: LabelId,
pub properties: Vec<StoragePropertyDef>,
pub oe_strategy: EdgeStrategy,
pub ie_strategy: EdgeStrategy,
pub schema_version: u64,
#[serde(default)]
pub record_form: RecordForm,
}
impl EdgeSchema {
pub fn validate(&self) -> linkrs_core::StorageResult<()> {
if self.oe_strategy == EdgeStrategy::None && self.ie_strategy == EdgeStrategy::None {
return Err(linkrs_core::StorageError::invalid_operation(format!(
"EdgeSchema '{}': at least one of oe_strategy and ie_strategy must be enabled",
self.label_name
)));
}
Ok(())
}
pub fn validate_resolved(&self) -> linkrs_core::StorageResult<()> {
self.validate()?;
validate_record_form_target(
&self.properties,
self.oe_strategy,
self.ie_strategy,
self.record_form,
)
}
pub fn storage_direction(&self) -> StorageDirection {
match (
self.oe_strategy != EdgeStrategy::None,
self.ie_strategy != EdgeStrategy::None,
) {
(true, true) => StorageDirection::Both,
(true, false) => StorageDirection::OutOnly,
(false, true) => StorageDirection::InOnly,
(false, false) => StorageDirection::Both,
}
}
pub fn has_out(&self) -> bool {
self.oe_strategy != EdgeStrategy::None
}
pub fn has_in(&self) -> bool {
self.ie_strategy != EdgeStrategy::None
}
pub fn out_multiplicity(&self) -> EdgeMultiplicity {
if self.oe_strategy == EdgeStrategy::Single {
EdgeMultiplicity::One
} else {
EdgeMultiplicity::Many
}
}
pub fn in_multiplicity(&self) -> EdgeMultiplicity {
if self.ie_strategy == EdgeStrategy::Single {
EdgeMultiplicity::One
} else {
EdgeMultiplicity::Many
}
}
pub fn validate_on_creation(&self) -> linkrs_core::StorageResult<()> {
if self.label_name.is_empty() {
return Err(linkrs_core::StorageError::invalid_operation(
"Edge type name cannot be empty".to_string(),
));
}
Self::validate_identifier_internal(&self.label_name)?;
self.validate()?;
let mut seen_names = std::collections::HashSet::new();
for prop in &self.properties {
if !seen_names.insert(&prop.name) {
return Err(linkrs_core::StorageError::invalid_operation(format!(
"Duplicate property name in edge type '{}': '{}'",
self.label_name, prop.name
)));
}
if prop.name.is_empty() {
return Err(linkrs_core::StorageError::invalid_operation(format!(
"Property name cannot be empty in edge type '{}'",
self.label_name
)));
}
Self::validate_identifier_internal(&prop.name)?;
Self::validate_property_type_internal(&prop.data_type, &prop.name)?;
}
Ok(())
}
fn validate_identifier_internal(name: &str) -> linkrs_core::StorageResult<()> {
let first_char = match name.chars().next() {
Some(c) => c,
None => {
return Err(linkrs_core::StorageError::invalid_operation(
"Identifier cannot be empty".to_string(),
));
}
};
if !first_char.is_ascii_alphabetic() && first_char != '_' {
return Err(linkrs_core::StorageError::invalid_operation(format!(
"Identifier '{}' must start with ASCII letter or underscore, got '{}'",
name, first_char
)));
}
for (i, c) in name.chars().enumerate() {
if !c.is_ascii_alphanumeric() && c != '_' {
return Err(linkrs_core::StorageError::invalid_operation(format!(
"Identifier '{}' contains invalid character '{}' at position {}. \
Only ASCII letters, digits, and underscores are allowed.",
name, c, i
)));
}
}
Ok(())
}
fn validate_property_type_internal(
data_type: &linkrs_core::DataType,
prop_name: &str,
) -> linkrs_core::StorageResult<()> {
use linkrs_core::DataType;
match data_type {
DataType::Empty => Err(linkrs_core::StorageError::invalid_operation(format!(
"Property '{}' cannot have type Empty - properties must have valid types",
prop_name
))),
DataType::Null => Err(linkrs_core::StorageError::invalid_operation(format!(
"Property '{}' cannot have type Null - use nullable=true instead",
prop_name
))),
_ => Ok(()),
}
}
}