1use super::record_form_policy::{validate_record_form_target, RecordForm};
5use crate::types::StoragePropertyDef;
6use linkrs_core::types::{EdgeStrategy, LabelId, VertexId};
7use linkrs_core::Value;
8use std::sync::Arc;
9
10#[derive(Debug, Clone)]
11pub struct EdgeRecord {
12 pub src_vid: VertexId,
13 pub dst_vid: VertexId,
14 pub rank: i64,
15 pub properties: Vec<(Arc<str>, Value)>,
16}
17
18#[derive(
24 Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default,
25)]
26pub enum StorageDirection {
27 #[default]
29 Both,
30 OutOnly,
32 InOnly,
34}
35
36#[derive(
42 Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default,
43)]
44pub enum EdgeMultiplicity {
45 One,
47 #[default]
49 Many,
50}
51
52#[derive(
58 Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default,
59)]
60pub enum IndexConsistency {
61 #[default]
63 BestEffort,
64 Strong,
66}
67
68#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
69pub struct EdgeSchema {
70 pub label_id: LabelId,
71 pub label_name: String,
72 pub src_label: LabelId,
73 pub dst_label: LabelId,
74 pub properties: Vec<StoragePropertyDef>,
75 pub oe_strategy: EdgeStrategy,
76 pub ie_strategy: EdgeStrategy,
77 pub schema_version: u64,
78 #[serde(default)]
82 pub record_form: RecordForm,
83}
84
85impl EdgeSchema {
86 pub fn validate(&self) -> linkrs_core::StorageResult<()> {
95 if self.oe_strategy == EdgeStrategy::None && self.ie_strategy == EdgeStrategy::None {
96 return Err(linkrs_core::StorageError::invalid_operation(format!(
97 "EdgeSchema '{}': at least one of oe_strategy and ie_strategy must be enabled",
98 self.label_name
99 )));
100 }
101 Ok(())
102 }
103
104 pub fn validate_resolved(&self) -> linkrs_core::StorageResult<()> {
111 self.validate()?;
112 validate_record_form_target(
113 &self.properties,
114 self.oe_strategy,
115 self.ie_strategy,
116 self.record_form,
117 )
118 }
119
120 pub fn storage_direction(&self) -> StorageDirection {
122 match (
123 self.oe_strategy != EdgeStrategy::None,
124 self.ie_strategy != EdgeStrategy::None,
125 ) {
126 (true, true) => StorageDirection::Both,
127 (true, false) => StorageDirection::OutOnly,
128 (false, true) => StorageDirection::InOnly,
129 (false, false) => StorageDirection::Both,
130 }
131 }
132
133 pub fn has_out(&self) -> bool {
135 self.oe_strategy != EdgeStrategy::None
136 }
137
138 pub fn has_in(&self) -> bool {
140 self.ie_strategy != EdgeStrategy::None
141 }
142
143 pub fn out_multiplicity(&self) -> EdgeMultiplicity {
145 if self.oe_strategy == EdgeStrategy::Single {
146 EdgeMultiplicity::One
147 } else {
148 EdgeMultiplicity::Many
149 }
150 }
151
152 pub fn in_multiplicity(&self) -> EdgeMultiplicity {
154 if self.ie_strategy == EdgeStrategy::Single {
155 EdgeMultiplicity::One
156 } else {
157 EdgeMultiplicity::Many
158 }
159 }
160
161 pub fn validate_on_creation(&self) -> linkrs_core::StorageResult<()> {
164 if self.label_name.is_empty() {
166 return Err(linkrs_core::StorageError::invalid_operation(
167 "Edge type name cannot be empty".to_string(),
168 ));
169 }
170
171 Self::validate_identifier_internal(&self.label_name)?;
172
173 self.validate()?;
175
176 let mut seen_names = std::collections::HashSet::new();
178 for prop in &self.properties {
179 if !seen_names.insert(&prop.name) {
180 return Err(linkrs_core::StorageError::invalid_operation(format!(
181 "Duplicate property name in edge type '{}': '{}'",
182 self.label_name, prop.name
183 )));
184 }
185
186 if prop.name.is_empty() {
188 return Err(linkrs_core::StorageError::invalid_operation(format!(
189 "Property name cannot be empty in edge type '{}'",
190 self.label_name
191 )));
192 }
193
194 Self::validate_identifier_internal(&prop.name)?;
195
196 Self::validate_property_type_internal(&prop.data_type, &prop.name)?;
198 }
199
200 Ok(())
201 }
202
203 fn validate_identifier_internal(name: &str) -> linkrs_core::StorageResult<()> {
205 let first_char = match name.chars().next() {
206 Some(c) => c,
207 None => {
208 return Err(linkrs_core::StorageError::invalid_operation(
209 "Identifier cannot be empty".to_string(),
210 ));
211 }
212 };
213
214 if !first_char.is_ascii_alphabetic() && first_char != '_' {
215 return Err(linkrs_core::StorageError::invalid_operation(format!(
216 "Identifier '{}' must start with ASCII letter or underscore, got '{}'",
217 name, first_char
218 )));
219 }
220
221 for (i, c) in name.chars().enumerate() {
222 if !c.is_ascii_alphanumeric() && c != '_' {
223 return Err(linkrs_core::StorageError::invalid_operation(format!(
224 "Identifier '{}' contains invalid character '{}' at position {}. \
225 Only ASCII letters, digits, and underscores are allowed.",
226 name, c, i
227 )));
228 }
229 }
230
231 Ok(())
232 }
233
234 fn validate_property_type_internal(
236 data_type: &linkrs_core::DataType,
237 prop_name: &str,
238 ) -> linkrs_core::StorageResult<()> {
239 use linkrs_core::DataType;
240
241 match data_type {
242 DataType::Empty => Err(linkrs_core::StorageError::invalid_operation(format!(
243 "Property '{}' cannot have type Empty - properties must have valid types",
244 prop_name
245 ))),
246 DataType::Null => Err(linkrs_core::StorageError::invalid_operation(format!(
247 "Property '{}' cannot have type Null - use nullable=true instead",
248 prop_name
249 ))),
250 _ => Ok(()),
251 }
252 }
253}