1use std::collections::BTreeMap;
11
12use smol_str::SmolStr;
13
14use crate::{
15 Cardinality, EndpointDecl, FunctionSignature, ParamDecl, ProcedureSignature, PropertyDecl,
16 SchemaProvider,
17};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct RelDecl {
24 pub name: SmolStr,
26 pub start_labels: Vec<SmolStr>,
28 pub end_labels: Vec<SmolStr>,
30 pub properties: Vec<PropertyDecl>,
32}
33
34#[derive(Debug, Default, Clone)]
41pub struct InMemorySchema {
42 pub(crate) labels: BTreeMap<SmolStr, Vec<PropertyDecl>>,
43 pub(crate) rel_types: BTreeMap<SmolStr, RelDecl>,
44 pub(crate) parameters: BTreeMap<SmolStr, ParamDecl>,
45 pub(crate) schema_name: Option<SmolStr>,
46 pub(crate) description: Option<String>,
47}
48
49impl InMemorySchema {
50 #[must_use]
53 pub fn builder() -> InMemorySchemaBuilder {
54 InMemorySchemaBuilder::default()
55 }
56
57 #[must_use]
59 pub fn label_names(&self) -> Vec<SmolStr> {
60 self.labels.keys().cloned().collect()
61 }
62
63 #[must_use]
65 pub fn rel_type_names(&self) -> Vec<SmolStr> {
66 self.rel_types.keys().cloned().collect()
67 }
68
69 pub fn rel_types(&self) -> impl Iterator<Item = &RelDecl> {
71 self.rel_types.values()
72 }
73
74 pub fn parameters(&self) -> impl Iterator<Item = &ParamDecl> {
76 self.parameters.values()
77 }
78
79 #[must_use]
81 pub fn schema_name(&self) -> Option<&str> {
82 self.schema_name.as_deref()
83 }
84
85 #[must_use]
87 pub fn description(&self) -> Option<&str> {
88 self.description.as_deref()
89 }
90
91 #[must_use]
93 pub fn label_count(&self) -> usize {
94 self.labels.len()
95 }
96
97 #[must_use]
99 pub fn rel_type_count(&self) -> usize {
100 self.rel_types.len()
101 }
102
103 #[must_use]
105 pub fn parameter_count(&self) -> usize {
106 self.parameters.len()
107 }
108}
109
110impl SchemaProvider for InMemorySchema {
111 fn labels(&self) -> Vec<SmolStr> {
112 self.label_names()
113 }
114
115 fn relationship_types(&self) -> Vec<SmolStr> {
116 self.rel_type_names()
117 }
118
119 fn node_properties(&self, label: &str) -> Option<Vec<PropertyDecl>> {
120 self.labels.get(label).cloned()
121 }
122
123 fn relationship_properties(&self, rel_type: &str) -> Option<Vec<PropertyDecl>> {
124 self.rel_types.get(rel_type).map(|r| r.properties.clone())
125 }
126
127 fn relationship_endpoints(&self, rel_type: &str) -> Vec<EndpointDecl> {
128 let Some(r) = self.rel_types.get(rel_type) else {
129 return Vec::new();
130 };
131 if r.start_labels.is_empty() || r.end_labels.is_empty() {
132 return Vec::new();
133 }
134 let mut out = Vec::with_capacity(r.start_labels.len() * r.end_labels.len());
135 for from in &r.start_labels {
136 for to in &r.end_labels {
137 out.push(EndpointDecl {
138 from: from.clone(),
139 to: to.clone(),
140 cardinality: Cardinality::ManyToMany,
141 });
142 }
143 }
144 out
145 }
146
147 fn inverse_of(&self, _rel_type: &str) -> Option<SmolStr> {
148 None
149 }
150
151 fn function(&self, _name: &str) -> Option<FunctionSignature> {
152 None
153 }
154
155 fn procedure(&self, _name: &str) -> Option<ProcedureSignature> {
156 None
157 }
158
159 fn schema_digest(&self) -> [u8; 32] {
160 let mut acc: [u8; 32] = [0; 32];
164 let mut h = fnv1a_64_init();
165 for (name, props) in &self.labels {
166 fnv1a_64_str(&mut h, "L:");
167 fnv1a_64_str(&mut h, name);
168 for p in props {
169 fnv1a_64_str(&mut h, "p:");
170 fnv1a_64_str(&mut h, &p.name);
171 fnv1a_64_str(&mut h, if p.required { "!" } else { "?" });
172 }
173 }
174 for (name, r) in &self.rel_types {
175 fnv1a_64_str(&mut h, "R:");
176 fnv1a_64_str(&mut h, name);
177 for l in &r.start_labels {
178 fnv1a_64_str(&mut h, "s:");
179 fnv1a_64_str(&mut h, l);
180 }
181 for l in &r.end_labels {
182 fnv1a_64_str(&mut h, "e:");
183 fnv1a_64_str(&mut h, l);
184 }
185 for p in &r.properties {
186 fnv1a_64_str(&mut h, "p:");
187 fnv1a_64_str(&mut h, &p.name);
188 }
189 }
190 for (name, p) in &self.parameters {
191 fnv1a_64_str(&mut h, "P:");
192 fnv1a_64_str(&mut h, name);
193 if let Some(d) = &p.default {
194 fnv1a_64_str(&mut h, "=");
195 fnv1a_64_str(&mut h, d);
196 }
197 }
198 for (i, byte) in acc.iter_mut().enumerate() {
200 let rot = u32::try_from(i).unwrap_or(0).wrapping_mul(7);
201 *byte = u8::try_from(h.rotate_left(rot) & 0xff).unwrap_or(0);
202 }
203 acc
204 }
205}
206
207fn fnv1a_64_init() -> u64 {
208 0xcbf2_9ce4_8422_2325
209}
210
211fn fnv1a_64_str(h: &mut u64, s: &str) {
212 for b in s.as_bytes() {
213 *h ^= u64::from(*b);
214 *h = h.wrapping_mul(0x100_0000_01b3);
215 }
216}
217
218#[derive(Debug, Default)]
228pub struct InMemorySchemaBuilder {
229 inner: InMemorySchema,
230 duplicate_label: Option<SmolStr>,
231 duplicate_rel_type: Option<SmolStr>,
232 duplicate_parameter: Option<SmolStr>,
233}
234
235impl InMemorySchemaBuilder {
236 #[must_use]
240 pub fn add_label(mut self, name: SmolStr, properties: Vec<PropertyDecl>) -> Self {
241 if self.inner.labels.contains_key(&name) {
242 self.duplicate_label.get_or_insert(name);
243 return self;
244 }
245 self.inner.labels.insert(name, properties);
246 self
247 }
248
249 #[must_use]
253 pub fn add_rel_type(mut self, rel: RelDecl) -> Self {
254 if self.inner.rel_types.contains_key(&rel.name) {
255 self.duplicate_rel_type.get_or_insert(rel.name.clone());
256 return self;
257 }
258 self.inner.rel_types.insert(rel.name.clone(), rel);
259 self
260 }
261
262 #[must_use]
264 pub fn add_parameter(mut self, param: ParamDecl) -> Self {
265 if self.inner.parameters.contains_key(¶m.name) {
266 self.duplicate_parameter.get_or_insert(param.name.clone());
267 return self;
268 }
269 self.inner.parameters.insert(param.name.clone(), param);
270 self
271 }
272
273 #[must_use]
275 pub fn schema_name(mut self, name: Option<SmolStr>) -> Self {
276 self.inner.schema_name = name;
277 self
278 }
279
280 #[must_use]
282 pub fn description(mut self, desc: Option<String>) -> Self {
283 self.inner.description = desc;
284 self
285 }
286
287 pub fn build(self) -> Result<InMemorySchema, BuilderError> {
295 if let Some(n) = self.duplicate_label {
296 return Err(BuilderError::DuplicateLabel(n));
297 }
298 if let Some(n) = self.duplicate_rel_type {
299 return Err(BuilderError::DuplicateRelType(n));
300 }
301 if let Some(n) = self.duplicate_parameter {
302 return Err(BuilderError::DuplicateParameter(n));
303 }
304 Ok(self.inner)
305 }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
310#[allow(clippy::enum_variant_names)] pub enum BuilderError {
312 DuplicateLabel(SmolStr),
314 DuplicateRelType(SmolStr),
316 DuplicateParameter(SmolStr),
318}
319
320impl core::fmt::Display for BuilderError {
321 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
322 match self {
323 Self::DuplicateLabel(n) => write!(f, "duplicate label `{n}`"),
324 Self::DuplicateRelType(n) => write!(f, "duplicate rel type `{n}`"),
325 Self::DuplicateParameter(n) => write!(f, "duplicate parameter `{n}`"),
326 }
327 }
328}
329
330impl std::error::Error for BuilderError {}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn empty_builder_builds_empty_schema() {
338 let s = InMemorySchema::builder().build().expect("builds");
339 assert_eq!(s.label_count(), 0);
340 assert_eq!(s.rel_type_count(), 0);
341 assert_eq!(s.parameter_count(), 0);
342 }
343
344 #[test]
345 fn builder_preserves_sorted_iteration() {
346 let s = InMemorySchema::builder()
347 .add_label(SmolStr::new("Zebra"), vec![])
348 .add_label(SmolStr::new("Apple"), vec![])
349 .add_label(SmolStr::new("Mango"), vec![])
350 .build()
351 .expect("builds");
352 assert_eq!(
353 s.label_names(),
354 vec![
355 SmolStr::new("Apple"),
356 SmolStr::new("Mango"),
357 SmolStr::new("Zebra"),
358 ]
359 );
360 }
361
362 #[test]
363 fn duplicate_label_surfaces_as_error() {
364 let err = InMemorySchema::builder()
365 .add_label(SmolStr::new("X"), vec![])
366 .add_label(SmolStr::new("X"), vec![])
367 .build()
368 .expect_err("duplicate");
369 assert_eq!(err, BuilderError::DuplicateLabel(SmolStr::new("X")));
370 }
371
372 #[test]
373 fn rel_endpoints_cross_product_for_declared_labels() {
374 let s = InMemorySchema::builder()
375 .add_label(SmolStr::new("A"), vec![])
376 .add_label(SmolStr::new("B"), vec![])
377 .add_rel_type(RelDecl {
378 name: SmolStr::new("R"),
379 start_labels: vec![SmolStr::new("A")],
380 end_labels: vec![SmolStr::new("A"), SmolStr::new("B")],
381 properties: vec![],
382 })
383 .build()
384 .expect("builds");
385 let ends = s.relationship_endpoints("R");
386 assert_eq!(ends.len(), 2);
387 assert!(
388 ends.iter()
389 .all(|e| e.cardinality == Cardinality::ManyToMany)
390 );
391 }
392
393 #[test]
394 fn rel_endpoints_empty_when_polymorphic() {
395 let s = InMemorySchema::builder()
396 .add_rel_type(RelDecl {
397 name: SmolStr::new("R"),
398 start_labels: vec![],
399 end_labels: vec![],
400 properties: vec![],
401 })
402 .build()
403 .expect("builds");
404 assert!(s.relationship_endpoints("R").is_empty());
405 }
406
407 #[test]
408 fn schema_digest_is_deterministic() {
409 let build = || {
410 InMemorySchema::builder()
411 .add_label(
412 SmolStr::new("Person"),
413 vec![PropertyDecl {
414 name: SmolStr::new("name"),
415 ty: crate::PropertyType::String,
416 required: true,
417 }],
418 )
419 .build()
420 .expect("builds")
421 };
422 assert_eq!(build().schema_digest(), build().schema_digest());
423 }
424
425 #[test]
426 fn schema_digest_changes_on_observable_change() {
427 let a = InMemorySchema::builder()
428 .add_label(SmolStr::new("A"), vec![])
429 .build()
430 .expect("builds");
431 let b = InMemorySchema::builder()
432 .add_label(SmolStr::new("B"), vec![])
433 .build()
434 .expect("builds");
435 assert_ne!(a.schema_digest(), b.schema_digest());
436 }
437}