1use crate::core::edge_type::{
5 MAX_SCHEMA_TYPE_ID, VIRTUAL_EDGE_TYPE_ID_SENTINEL, VIRTUAL_EDGE_TYPE_ID_START,
6 is_schemaless_edge_type, make_schemaless_id,
7};
8use crate::sync::{acquire_read, acquire_write};
9use anyhow::{Result, anyhow};
10use chrono::{DateTime, Utc};
11use object_store::ObjectStore;
12use object_store::ObjectStoreExt;
13use object_store::local::LocalFileSystem;
14use object_store::path::Path as ObjectStorePath;
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::path::Path;
18use std::sync::{Arc, RwLock};
19
20#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
21#[non_exhaustive]
22pub enum SchemaElementState {
23 Active,
24 Hidden {
25 since: DateTime<Utc>,
26 last_active_snapshot: String, },
28 Tombstone {
29 since: DateTime<Utc>,
30 },
31}
32
33use arrow_schema::{DataType as ArrowDataType, Field, Fields, TimeUnit};
34
35pub fn datetime_struct_fields() -> Fields {
42 Fields::from(vec![
43 Field::new(
44 "nanos_since_epoch",
45 ArrowDataType::Timestamp(TimeUnit::Nanosecond, None),
46 true,
47 ),
48 Field::new("offset_seconds", ArrowDataType::Int32, true),
49 Field::new("timezone_name", ArrowDataType::Utf8, true),
50 ])
51}
52
53pub fn time_struct_fields() -> Fields {
59 Fields::from(vec![
60 Field::new(
61 "nanos_since_midnight",
62 ArrowDataType::Time64(TimeUnit::Nanosecond),
63 true,
64 ),
65 Field::new("offset_seconds", ArrowDataType::Int32, true),
66 ])
67}
68
69pub fn is_datetime_struct(arrow_dt: &ArrowDataType) -> bool {
71 matches!(arrow_dt, ArrowDataType::Struct(fields) if *fields == datetime_struct_fields())
72}
73
74pub fn is_time_struct(arrow_dt: &ArrowDataType) -> bool {
76 matches!(arrow_dt, ArrowDataType::Struct(fields) if *fields == time_struct_fields())
77}
78
79pub fn sparse_vector_struct_fields() -> Fields {
86 Fields::from(vec![
87 Field::new(
88 "indices",
89 ArrowDataType::List(Arc::new(Field::new("item", ArrowDataType::UInt32, true))),
90 false,
91 ),
92 Field::new(
93 "values",
94 ArrowDataType::List(Arc::new(Field::new("item", ArrowDataType::Float32, true))),
95 false,
96 ),
97 ])
98}
99
100pub fn is_sparse_vector_struct(arrow_dt: &ArrowDataType) -> bool {
102 matches!(arrow_dt, ArrowDataType::Struct(fields) if *fields == sparse_vector_struct_fields())
103}
104
105pub fn raw_bytes_field_metadata() -> HashMap<String, String> {
113 HashMap::from([("uni_raw_bytes".to_string(), "true".to_string())])
114}
115
116#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
117#[non_exhaustive]
118pub enum CrdtType {
119 GCounter,
120 GSet,
121 ORSet,
122 LWWRegister,
123 LWWMap,
124 Rga,
125 VectorClock,
126 VCRegister,
127}
128
129impl CrdtType {
130 #[must_use]
142 pub fn type_name(&self) -> &'static str {
143 match self {
144 CrdtType::GCounter => "GCounter",
145 CrdtType::GSet => "GSet",
146 CrdtType::ORSet => "ORSet",
147 CrdtType::LWWRegister => "LWWRegister",
148 CrdtType::LWWMap => "LWWMap",
149 CrdtType::Rga => "Rga",
150 CrdtType::VectorClock => "VectorClock",
151 CrdtType::VCRegister => "VCRegister",
152 }
153 }
154}
155
156#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
157pub enum PointType {
158 Geographic, Cartesian2D, Cartesian3D, }
162
163#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
164#[non_exhaustive]
165pub enum DataType {
166 String,
167 Int32,
168 Int64,
169 Float32,
170 Float64,
171 Bool,
172 Timestamp,
173 Date,
174 Time,
175 DateTime,
176 Duration,
177 CypherValue,
178 Bytes,
179 Point(PointType),
180 Vector {
181 dimensions: usize,
182 },
183 SparseVector {
186 dimensions: usize,
187 },
188 BinaryVector {
193 dimensions: usize,
194 },
195 Btic,
196 Crdt(CrdtType),
197 List(Box<DataType>),
198 Map(Box<DataType>, Box<DataType>),
199}
200
201impl DataType {
202 #[allow(non_upper_case_globals)]
204 pub const Float: DataType = DataType::Float64;
205 #[allow(non_upper_case_globals)]
206 pub const Int: DataType = DataType::Int64;
207
208 pub fn to_arrow(&self) -> ArrowDataType {
209 match self {
210 DataType::String => ArrowDataType::Utf8,
211 DataType::Int32 => ArrowDataType::Int32,
212 DataType::Int64 => ArrowDataType::Int64,
213 DataType::Float32 => ArrowDataType::Float32,
214 DataType::Float64 => ArrowDataType::Float64,
215 DataType::Bool => ArrowDataType::Boolean,
216 DataType::Timestamp => {
217 ArrowDataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into()))
218 }
219 DataType::Date => ArrowDataType::Date32,
220 DataType::Time => ArrowDataType::Struct(time_struct_fields()),
221 DataType::DateTime => ArrowDataType::Struct(datetime_struct_fields()),
222 DataType::Duration => ArrowDataType::LargeBinary, DataType::CypherValue => ArrowDataType::LargeBinary, DataType::Bytes => ArrowDataType::LargeBinary, DataType::Point(pt) => match pt {
226 PointType::Geographic => ArrowDataType::Struct(Fields::from(vec![
227 Field::new("latitude", ArrowDataType::Float64, false),
228 Field::new("longitude", ArrowDataType::Float64, false),
229 Field::new("crs", ArrowDataType::Utf8, false),
230 ])),
231 PointType::Cartesian2D => ArrowDataType::Struct(Fields::from(vec![
232 Field::new("x", ArrowDataType::Float64, false),
233 Field::new("y", ArrowDataType::Float64, false),
234 Field::new("crs", ArrowDataType::Utf8, false),
235 ])),
236 PointType::Cartesian3D => ArrowDataType::Struct(Fields::from(vec![
237 Field::new("x", ArrowDataType::Float64, false),
238 Field::new("y", ArrowDataType::Float64, false),
239 Field::new("z", ArrowDataType::Float64, false),
240 Field::new("crs", ArrowDataType::Utf8, false),
241 ])),
242 },
243 DataType::Vector { dimensions } => ArrowDataType::FixedSizeList(
244 Arc::new(Field::new("item", ArrowDataType::Float32, true)),
245 *dimensions as i32,
246 ),
247 DataType::SparseVector { .. } => ArrowDataType::Struct(sparse_vector_struct_fields()),
248 DataType::BinaryVector { dimensions } => ArrowDataType::FixedSizeList(
249 Arc::new(Field::new("item", ArrowDataType::UInt8, true)),
250 *dimensions as i32,
251 ),
252 DataType::Btic => ArrowDataType::FixedSizeBinary(24),
253 DataType::Crdt(_) => ArrowDataType::Binary, DataType::List(inner) => {
255 let item = Field::new("item", inner.to_arrow(), true);
259 let item = if matches!(**inner, DataType::Bytes) {
260 item.with_metadata(raw_bytes_field_metadata())
261 } else {
262 item
263 };
264 ArrowDataType::List(Arc::new(item))
265 }
266 DataType::Map(key, value) => {
267 let value_field = if value.map_value_is_typed() {
274 let f = Field::new("value", value.to_arrow(), true);
275 if matches!(**value, DataType::Bytes) {
276 f.with_metadata(raw_bytes_field_metadata())
277 } else {
278 f
279 }
280 } else {
281 Field::new("value", ArrowDataType::LargeBinary, true)
282 };
283 ArrowDataType::List(Arc::new(Field::new(
284 "item",
285 ArrowDataType::Struct(Fields::from(vec![
286 Field::new("key", key.to_arrow(), false),
287 value_field,
288 ])),
289 true,
290 )))
291 }
292 }
293 }
294
295 pub fn map_value_is_typed(&self) -> bool {
300 matches!(
301 self,
302 DataType::String
303 | DataType::Int64
304 | DataType::Int32
305 | DataType::Float64
306 | DataType::Float32
307 | DataType::Bool
308 | DataType::Bytes
309 )
310 }
311
312 pub fn accepts(&self, value: &crate::value::Value) -> bool {
338 use crate::value::{TemporalValue, Value};
339
340 if matches!(value, Value::Null) {
342 return true;
343 }
344
345 match self {
346 DataType::CypherValue | DataType::Crdt(_) | DataType::Point(_) => true,
348
349 DataType::String => matches!(value, Value::String(_)),
350 DataType::Int32 | DataType::Int64 => matches!(value, Value::Int(_)),
351 DataType::Float32 | DataType::Float64 => {
353 matches!(value, Value::Int(_) | Value::Float(_))
354 }
355 DataType::Bool => matches!(value, Value::Bool(_)),
356
357 DataType::Timestamp => matches!(
360 value,
361 Value::String(_)
362 | Value::Int(_)
363 | Value::Temporal(
364 TemporalValue::DateTime { .. } | TemporalValue::LocalDateTime { .. }
365 )
366 ),
367 DataType::DateTime => matches!(
368 value,
369 Value::Temporal(
370 TemporalValue::DateTime { .. } | TemporalValue::LocalDateTime { .. }
371 )
372 ),
373 DataType::Date => {
374 matches!(
375 value,
376 Value::Int(_) | Value::Temporal(TemporalValue::Date { .. })
377 )
378 }
379 DataType::Time => matches!(
380 value,
381 Value::Int(_)
382 | Value::Temporal(TemporalValue::Time { .. } | TemporalValue::LocalTime { .. })
383 ),
384 DataType::Duration => {
385 matches!(value, Value::Temporal(TemporalValue::Duration { .. }))
386 }
387 DataType::Bytes => matches!(value, Value::Bytes(_)),
388 DataType::Btic => matches!(
390 value,
391 Value::String(_) | Value::List(_) | Value::Temporal(TemporalValue::Btic { .. })
392 ),
393 DataType::Vector { .. } => matches!(value, Value::Vector(_) | Value::List(_)),
396 DataType::SparseVector { .. } => {
401 matches!(value, Value::SparseVector { .. } | Value::Map(_))
402 }
403 DataType::BinaryVector { .. } => {
407 matches!(value, Value::BinaryVector(_) | Value::List(_))
408 }
409 DataType::List(_) => matches!(value, Value::List(_)),
412 DataType::Map(_, _) => matches!(value, Value::Map(_)),
413 }
414 }
415
416 pub fn check_vector_dims(&self, value: &crate::value::Value) -> Result<(), VectorDimError> {
434 use crate::value::Value;
435
436 if matches!(value, Value::Null) {
437 return Ok(());
438 }
439
440 match self {
441 DataType::Vector { dimensions } => check_dense_vector_value(value, *dimensions),
442 DataType::BinaryVector { dimensions } => check_binary_vector_value(value, *dimensions),
443 DataType::List(inner) => {
444 let DataType::Vector { dimensions } = inner.as_ref() else {
445 return Ok(());
446 };
447 let Value::List(tokens) = value else {
448 return Err(VectorDimError::NotATokenList {
449 actual: value_variant_name(value),
450 });
451 };
452 for (token, token_value) in tokens.iter().enumerate() {
453 check_dense_vector_value(token_value, *dimensions)
454 .map_err(|e| e.for_token(token))?;
455 }
456 Ok(())
457 }
458 _ => Ok(()),
459 }
460 }
461}
462
463#[derive(Debug, Clone, PartialEq, Eq)]
469pub enum VectorDimError {
470 WrongLength {
472 expected: usize,
474 actual: usize,
476 },
477 NonNumericElement {
479 index: usize,
481 },
482 NotAVector {
484 actual: &'static str,
486 },
487 TokenWrongLength {
489 token: usize,
491 expected: usize,
493 actual: usize,
495 },
496 TokenNonNumericElement {
498 token: usize,
500 index: usize,
502 },
503 TokenNotAVector {
505 token: usize,
507 actual: &'static str,
509 },
510 NotATokenList {
512 actual: &'static str,
514 },
515}
516
517impl VectorDimError {
518 fn for_token(self, token: usize) -> Self {
520 match self {
521 Self::WrongLength { expected, actual } => Self::TokenWrongLength {
522 token,
523 expected,
524 actual,
525 },
526 Self::NonNumericElement { index } => Self::TokenNonNumericElement { token, index },
527 Self::NotAVector { actual } => Self::TokenNotAVector { token, actual },
528 other => other,
529 }
530 }
531}
532
533impl std::fmt::Display for VectorDimError {
534 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
535 match self {
536 Self::WrongLength { expected, actual } => write!(
537 f,
538 "got a vector of length {actual}, expected {expected} dimensions"
539 ),
540 Self::NonNumericElement { index } => {
541 write!(f, "element {index} is not numeric")
542 }
543 Self::NotAVector { actual } => {
544 write!(f, "got a non-vector value of type {actual}")
545 }
546 Self::TokenWrongLength {
547 token,
548 expected,
549 actual,
550 } => write!(
551 f,
552 "token {token} has {actual} dimensions, expected {expected}"
553 ),
554 Self::TokenNonNumericElement { token, index } => {
555 write!(f, "token {token} element {index} is not numeric")
556 }
557 Self::TokenNotAVector { token, actual } => {
558 write!(f, "token {token} is not a vector (got {actual})")
559 }
560 Self::NotATokenList { actual } => write!(
561 f,
562 "got a non-list value of type {actual} for a multi-vector column"
563 ),
564 }
565 }
566}
567
568impl std::error::Error for VectorDimError {}
569
570pub fn check_dense_vector_value(
584 value: &crate::value::Value,
585 dimensions: usize,
586) -> Result<(), VectorDimError> {
587 use crate::value::Value;
588
589 match value {
590 Value::Null => Ok(()),
591 Value::Vector(v) => {
592 if v.len() == dimensions {
593 Ok(())
594 } else {
595 Err(VectorDimError::WrongLength {
596 expected: dimensions,
597 actual: v.len(),
598 })
599 }
600 }
601 Value::List(items) => {
602 if items.len() != dimensions {
603 return Err(VectorDimError::WrongLength {
604 expected: dimensions,
605 actual: items.len(),
606 });
607 }
608 if let Some(index) = items.iter().position(|e| !e.is_number()) {
609 return Err(VectorDimError::NonNumericElement { index });
610 }
611 Ok(())
612 }
613 other => Err(VectorDimError::NotAVector {
614 actual: value_variant_name(other),
615 }),
616 }
617}
618
619pub fn check_binary_vector_value(
633 value: &crate::value::Value,
634 dimensions: usize,
635) -> Result<(), VectorDimError> {
636 use crate::value::Value;
637
638 match value {
639 Value::Null => Ok(()),
640 Value::BinaryVector(bytes) => {
641 if bytes.len() == dimensions {
642 Ok(())
643 } else {
644 Err(VectorDimError::WrongLength {
645 expected: dimensions,
646 actual: bytes.len(),
647 })
648 }
649 }
650 Value::List(items) => {
651 if items.len() != dimensions {
652 return Err(VectorDimError::WrongLength {
653 expected: dimensions,
654 actual: items.len(),
655 });
656 }
657 if let Some(index) = items
658 .iter()
659 .position(|e| !matches!(e.as_i64(), Some(0..=255)))
660 {
661 return Err(VectorDimError::NonNumericElement { index });
662 }
663 Ok(())
664 }
665 other => Err(VectorDimError::NotAVector {
666 actual: value_variant_name(other),
667 }),
668 }
669}
670
671fn value_variant_name(value: &crate::value::Value) -> &'static str {
673 use crate::value::Value;
674
675 match value {
676 Value::Null => "Null",
677 Value::Bool(_) => "Bool",
678 Value::Int(_) => "Int",
679 Value::Float(_) => "Float",
680 Value::String(_) => "String",
681 Value::Bytes(_) => "Bytes",
682 Value::List(_) => "List",
683 Value::Map(_) => "Map",
684 Value::Node(_) => "Node",
685 Value::Edge(_) => "Edge",
686 Value::Path(_) => "Path",
687 Value::Vector(_) => "Vector",
688 Value::SparseVector { .. } => "SparseVector",
689 Value::BinaryVector(_) => "BinaryVector",
690 Value::Temporal(_) => "Temporal",
691 }
692}
693
694fn default_created_at() -> DateTime<Utc> {
695 Utc::now()
696}
697
698fn default_state() -> SchemaElementState {
699 SchemaElementState::Active
700}
701
702fn default_version_1() -> u32 {
703 1
704}
705
706#[derive(Clone, Debug, Serialize, Deserialize)]
707pub struct PropertyMeta {
708 pub r#type: DataType,
709 pub nullable: bool,
710 #[serde(default = "default_version_1")]
711 pub added_in: u32, #[serde(default = "default_state")]
713 pub state: SchemaElementState,
714 #[serde(default)]
715 pub generation_expression: Option<String>,
716 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub description: Option<String>,
718}
719
720#[derive(Clone, Debug, Serialize, Deserialize)]
721pub struct LabelMeta {
722 pub id: u16, #[serde(default = "default_created_at")]
724 pub created_at: DateTime<Utc>,
725 #[serde(default = "default_state")]
726 pub state: SchemaElementState,
727 #[serde(default, skip_serializing_if = "Option::is_none")]
728 pub description: Option<String>,
729}
730
731#[derive(Clone, Debug, Serialize, Deserialize)]
732pub struct EdgeTypeMeta {
733 pub id: u32,
735 pub src_labels: Vec<String>,
736 pub dst_labels: Vec<String>,
737 #[serde(default = "default_state")]
738 pub state: SchemaElementState,
739 #[serde(default, skip_serializing_if = "Option::is_none")]
740 pub description: Option<String>,
741}
742
743#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
744#[non_exhaustive]
745pub enum ConstraintType {
746 Unique {
747 properties: Vec<String>,
748 },
749 Exists {
750 property: String,
751 },
752 Check {
753 expression: String,
754 },
755 NodeKey {
759 properties: Vec<String>,
760 },
761}
762
763impl ConstraintType {
764 #[must_use]
770 pub fn unique_properties(&self) -> Option<&[String]> {
771 match self {
772 ConstraintType::Unique { properties } | ConstraintType::NodeKey { properties } => {
773 Some(properties)
774 }
775 _ => None,
776 }
777 }
778}
779
780#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
781#[non_exhaustive]
782pub enum ConstraintTarget {
783 Label(String),
784 EdgeType(String),
785}
786
787#[derive(Clone, Debug, Serialize, Deserialize)]
788pub struct Constraint {
789 pub name: String,
790 pub constraint_type: ConstraintType,
791 pub target: ConstraintTarget,
792 pub enabled: bool,
793}
794
795#[derive(Clone, Debug, Serialize, Deserialize)]
801pub struct SchemalessEdgeTypeRegistry {
802 name_to_id: HashMap<String, u32>,
803 id_to_name: HashMap<u32, String>,
804 next_local_id: u32,
806}
807
808impl SchemalessEdgeTypeRegistry {
809 pub fn new() -> Self {
810 Self {
811 name_to_id: HashMap::new(),
812 id_to_name: HashMap::new(),
813 next_local_id: 1,
814 }
815 }
816
817 pub fn get_or_assign_id(&mut self, type_name: &str) -> u32 {
819 if let Some(&id) = self.name_to_id.get(type_name) {
820 return id;
821 }
822
823 let id = make_schemaless_id(self.next_local_id);
824 self.next_local_id += 1;
825
826 self.name_to_id.insert(type_name.to_string(), id);
827 self.id_to_name.insert(id, type_name.to_string());
828
829 id
830 }
831
832 pub fn type_name_by_id(&self, type_id: u32) -> Option<&str> {
834 self.id_to_name.get(&type_id).map(String::as_str)
835 }
836
837 pub fn contains(&self, type_name: &str) -> bool {
839 self.name_to_id.contains_key(type_name)
840 }
841
842 pub fn id_by_name(&self, type_name: &str) -> Option<u32> {
844 self.name_to_id.get(type_name).copied()
845 }
846
847 pub fn id_by_name_case_insensitive(&self, type_name: &str) -> Option<u32> {
849 self.name_to_id
850 .iter()
851 .find(|(k, _)| k.eq_ignore_ascii_case(type_name))
852 .map(|(_, &id)| id)
853 }
854
855 pub fn all_type_ids(&self) -> Vec<u32> {
857 self.id_to_name.keys().copied().collect()
858 }
859
860 pub fn is_empty(&self) -> bool {
862 self.name_to_id.is_empty()
863 }
864}
865
866impl Default for SchemalessEdgeTypeRegistry {
867 fn default() -> Self {
868 Self::new()
869 }
870}
871
872pub const VIRTUAL_LABEL_ID_START: u16 = 0xFF00;
878pub const VIRTUAL_LABEL_ID_SENTINEL: u16 = 0xFFFF;
880
881const MAX_SCHEMA_NAME_LEN: usize = 255;
886
887#[inline]
889pub fn is_virtual_label_id(id: u16) -> bool {
890 (VIRTUAL_LABEL_ID_START..VIRTUAL_LABEL_ID_SENTINEL).contains(&id)
891}
892
893#[derive(Clone, Debug, Serialize, Deserialize)]
894pub struct Schema {
895 pub schema_version: u32,
896 pub labels: HashMap<String, LabelMeta>,
897 pub edge_types: HashMap<String, EdgeTypeMeta>,
898 pub properties: HashMap<String, HashMap<String, PropertyMeta>>,
899 #[serde(default)]
900 pub indexes: Vec<IndexDefinition>,
901 #[serde(default)]
902 pub constraints: Vec<Constraint>,
903 #[serde(default)]
905 pub schemaless_registry: SchemalessEdgeTypeRegistry,
906}
907
908impl Default for Schema {
909 fn default() -> Self {
910 Self {
911 schema_version: 1,
912 labels: HashMap::new(),
913 edge_types: HashMap::new(),
914 properties: HashMap::new(),
915 indexes: Vec::new(),
916 constraints: Vec::new(),
917 schemaless_registry: SchemalessEdgeTypeRegistry::new(),
918 }
919 }
920}
921
922impl Schema {
923 fn bump_version(&mut self) {
932 self.schema_version = self.schema_version.wrapping_add(1);
933 }
934
935 pub fn label_name_by_id(&self, label_id: u16) -> Option<&str> {
940 self.labels
941 .iter()
942 .find(|(_, meta)| meta.id == label_id)
943 .map(|(name, _)| name.as_str())
944 }
945
946 pub fn label_id_by_name(&self, label_name: &str) -> Option<u16> {
948 self.labels.get(label_name).map(|meta| meta.id)
949 }
950
951 pub fn edge_type_name_by_id(&self, type_id: u32) -> Option<&str> {
956 self.edge_types
957 .iter()
958 .find(|(_, meta)| meta.id == type_id)
959 .map(|(name, _)| name.as_str())
960 }
961
962 pub fn edge_type_id_by_name(&self, type_name: &str) -> Option<u32> {
964 self.edge_types.get(type_name).map(|meta| meta.id)
965 }
966
967 pub fn vector_index_for_property(
972 &self,
973 label: &str,
974 property: &str,
975 ) -> Option<&VectorIndexConfig> {
976 self.indexes.iter().find_map(|idx| {
977 if let IndexDefinition::Vector(config) = idx
978 && config.label == label
979 && config.property == property
980 && config.metadata.status == IndexStatus::Online
981 {
982 return Some(config);
983 }
984 None
985 })
986 }
987
988 pub fn sparse_index_for_property(
990 &self,
991 label: &str,
992 property: &str,
993 ) -> Option<&SparseVectorIndexConfig> {
994 self.indexes.iter().find_map(|idx| {
995 if let IndexDefinition::Sparse(config) = idx
996 && config.label == label
997 && config.property == property
998 && config.metadata.status == IndexStatus::Online
999 {
1000 return Some(config);
1001 }
1002 None
1003 })
1004 }
1005
1006 pub fn fulltext_index_for_property(
1011 &self,
1012 label: &str,
1013 property: &str,
1014 ) -> Option<&FullTextIndexConfig> {
1015 self.indexes.iter().find_map(|idx| {
1016 if let IndexDefinition::FullText(config) = idx
1017 && config.label == label
1018 && config.properties.iter().any(|p| p == property)
1019 && config.metadata.status == IndexStatus::Online
1020 {
1021 return Some(config);
1022 }
1023 None
1024 })
1025 }
1026
1027 pub fn get_label_case_insensitive(&self, name: &str) -> Option<&LabelMeta> {
1032 self.labels
1033 .iter()
1034 .find(|(k, _)| k.eq_ignore_ascii_case(name))
1035 .map(|(_, v)| v)
1036 }
1037
1038 pub fn canonical_label_name(&self, name: &str) -> Option<String> {
1045 self.labels
1046 .iter()
1047 .find(|(k, _)| k.eq_ignore_ascii_case(name))
1048 .map(|(k, _)| k.clone())
1049 }
1050
1051 pub fn label_id_by_name_case_insensitive(&self, label_name: &str) -> Option<u16> {
1053 self.get_label_case_insensitive(label_name)
1054 .map(|meta| meta.id)
1055 }
1056
1057 pub fn get_edge_type_case_insensitive(&self, name: &str) -> Option<&EdgeTypeMeta> {
1062 self.edge_types
1063 .iter()
1064 .find(|(k, _)| k.eq_ignore_ascii_case(name))
1065 .map(|(_, v)| v)
1066 }
1067
1068 pub fn edge_type_id_by_name_case_insensitive(&self, type_name: &str) -> Option<u32> {
1070 self.get_edge_type_case_insensitive(type_name)
1071 .map(|meta| meta.id)
1072 }
1073
1074 pub fn edge_type_id_unified_case_insensitive(&self, type_name: &str) -> Option<u32> {
1077 self.edge_type_id_by_name_case_insensitive(type_name)
1078 .or_else(|| {
1079 self.schemaless_registry
1080 .id_by_name_case_insensitive(type_name)
1081 })
1082 }
1083
1084 pub fn get_or_assign_edge_type_id(&mut self, type_name: &str) -> u32 {
1090 if let Some(id) = self.edge_type_id_unified(type_name) {
1091 return id;
1092 }
1093 let id = self.schemaless_registry.get_or_assign_id(type_name);
1101 self.bump_version();
1102 id
1103 }
1104
1105 pub fn edge_type_id_unified(&self, type_name: &str) -> Option<u32> {
1112 self.edge_type_id_by_name(type_name)
1113 .or_else(|| self.schemaless_registry.id_by_name(type_name))
1114 }
1115
1116 pub fn edge_type_name_by_id_unified(&self, type_id: u32) -> Option<String> {
1120 if is_schemaless_edge_type(type_id) {
1121 self.schemaless_registry
1122 .type_name_by_id(type_id)
1123 .map(str::to_owned)
1124 } else {
1125 self.edge_type_name_by_id(type_id).map(str::to_owned)
1126 }
1127 }
1128
1129 pub fn all_edge_type_ids(&self) -> Vec<u32> {
1132 let mut ids: Vec<u32> = self.edge_types.values().map(|m| m.id).collect();
1133 ids.extend(self.schemaless_registry.all_type_ids());
1134 ids.sort_unstable();
1135 ids
1136 }
1137}
1138
1139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1141pub enum IndexStatus {
1142 #[default]
1144 Online,
1145 Building,
1147 Stale,
1149 Failed,
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1155pub struct IndexMetadata {
1156 #[serde(default)]
1158 pub status: IndexStatus,
1159 #[serde(default)]
1161 pub last_built_at: Option<DateTime<Utc>>,
1162 #[serde(default)]
1164 pub row_count_at_build: Option<u64>,
1165}
1166
1167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1168#[serde(tag = "type")]
1169#[non_exhaustive]
1170pub enum IndexDefinition {
1171 Vector(VectorIndexConfig),
1172 FullText(FullTextIndexConfig),
1173 Scalar(ScalarIndexConfig),
1174 Inverted(InvertedIndexConfig),
1175 JsonFullText(JsonFtsIndexConfig),
1176 Sparse(SparseVectorIndexConfig),
1178}
1179
1180macro_rules! for_each_index_variant {
1186 ($mac:ident) => {
1187 $mac! { Vector, FullText, Scalar, Inverted, JsonFullText, Sparse }
1188 };
1189}
1190
1191macro_rules! index_field_accessor {
1194 ($($variant:ident),*) => {
1195 impl IndexDefinition {
1196 pub fn name(&self) -> &str {
1198 match self { $(IndexDefinition::$variant(c) => &c.name,)* }
1199 }
1200
1201 pub fn label(&self) -> &str {
1203 match self { $(IndexDefinition::$variant(c) => &c.label,)* }
1204 }
1205
1206 pub fn metadata(&self) -> &IndexMetadata {
1208 match self { $(IndexDefinition::$variant(c) => &c.metadata,)* }
1209 }
1210
1211 pub fn metadata_mut(&mut self) -> &mut IndexMetadata {
1213 match self { $(IndexDefinition::$variant(c) => &mut c.metadata,)* }
1214 }
1215 }
1216 };
1217}
1218
1219for_each_index_variant!(index_field_accessor);
1220
1221impl IndexDefinition {}
1222
1223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1224pub struct InvertedIndexConfig {
1225 pub name: String,
1226 pub label: String,
1227 pub property: String,
1228 #[serde(default = "default_normalize")]
1229 pub normalize: bool,
1230 #[serde(default = "default_max_terms_per_doc")]
1231 pub max_terms_per_doc: usize,
1232 #[serde(default)]
1233 pub metadata: IndexMetadata,
1234}
1235
1236fn default_normalize() -> bool {
1237 true
1238}
1239
1240fn default_max_terms_per_doc() -> usize {
1241 10_000
1242}
1243
1244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1251pub struct SparseVectorIndexConfig {
1252 pub name: String,
1253 pub label: String,
1254 pub property: String,
1255 pub dimensions: usize,
1257 #[serde(default = "default_sparse_quantize")]
1259 pub quantize: bool,
1260 #[serde(default)]
1264 pub embedding_config: Option<EmbeddingConfig>,
1265 #[serde(default)]
1266 pub metadata: IndexMetadata,
1267}
1268
1269fn default_sparse_quantize() -> bool {
1270 true
1271}
1272
1273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1274pub struct VectorIndexConfig {
1275 pub name: String,
1276 pub label: String,
1277 pub property: String,
1278 pub index_type: VectorIndexType,
1279 pub metric: DistanceMetric,
1280 pub embedding_config: Option<EmbeddingConfig>,
1281 #[serde(default)]
1282 pub metadata: IndexMetadata,
1283}
1284
1285#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1286pub struct EmbeddingConfig {
1287 pub alias: String,
1289 pub source_properties: Vec<String>,
1290 pub batch_size: usize,
1291 #[serde(default)]
1294 pub document_prefix: Option<String>,
1295 #[serde(default)]
1298 pub query_prefix: Option<String>,
1299}
1300
1301#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1302#[non_exhaustive]
1303pub enum VectorIndexType {
1304 Flat,
1305 IvfFlat {
1306 num_partitions: u32,
1307 },
1308 IvfPq {
1309 num_partitions: u32,
1310 num_sub_vectors: u32,
1311 bits_per_subvector: u8,
1312 },
1313 IvfSq {
1314 num_partitions: u32,
1315 },
1316 IvfRq {
1317 num_partitions: u32,
1318 #[serde(default)]
1319 num_bits: Option<u8>,
1320 },
1321 HnswFlat {
1322 m: u32,
1323 ef_construction: u32,
1324 #[serde(default)]
1325 num_partitions: Option<u32>,
1326 },
1327 HnswSq {
1328 m: u32,
1329 ef_construction: u32,
1330 #[serde(default)]
1331 num_partitions: Option<u32>,
1332 },
1333 HnswPq {
1334 m: u32,
1335 ef_construction: u32,
1336 num_sub_vectors: u32,
1337 #[serde(default)]
1338 num_partitions: Option<u32>,
1339 },
1340 Muvera {
1347 k_sim: u32,
1349 reps: u32,
1351 d_proj: u32,
1353 seed: u64,
1355 inner: Box<VectorIndexType>,
1357 },
1358}
1359
1360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1361#[non_exhaustive]
1362pub enum DistanceMetric {
1363 Cosine,
1364 L2,
1365 Dot,
1366 L1,
1369 Hamming,
1374 Jaccard,
1379}
1380
1381impl DistanceMetric {
1382 pub fn compute_distance(&self, a: &[f32], b: &[f32]) -> f32 {
1395 assert_eq!(a.len(), b.len(), "vector dimension mismatch");
1396 match self {
1397 DistanceMetric::L2 => a.iter().zip(b).map(|(x, y)| (x - y).powi(2)).sum(),
1398 DistanceMetric::L1 => a.iter().zip(b).map(|(x, y)| (x - y).abs()).sum(),
1399 DistanceMetric::Cosine => {
1400 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
1401 let norm_a: f32 = a.iter().map(|x| x.powi(2)).sum::<f32>().sqrt();
1402 let norm_b: f32 = b.iter().map(|x| x.powi(2)).sum::<f32>().sqrt();
1403 let denom = norm_a * norm_b;
1404 if denom == 0.0 { 1.0 } else { 1.0 - dot / denom }
1405 }
1406 DistanceMetric::Dot => {
1407 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
1408 -dot
1409 }
1410 DistanceMetric::Hamming | DistanceMetric::Jaccard => {
1414 panic!("{self:?} is a binary-vector metric; use compute_distance_binary")
1415 }
1416 }
1417 }
1418
1419 pub fn is_binary(&self) -> bool {
1427 matches!(self, DistanceMetric::Hamming | DistanceMetric::Jaccard)
1428 }
1429
1430 pub fn compute_distance_binary(&self, a: &[u8], b: &[u8]) -> f32 {
1443 assert_eq!(a.len(), b.len(), "binary vector dimension mismatch");
1444 match self {
1445 DistanceMetric::Hamming => a
1446 .iter()
1447 .zip(b)
1448 .map(|(x, y)| (x ^ y).count_ones())
1449 .sum::<u32>() as f32,
1450 DistanceMetric::Jaccard => {
1451 let mut inter: u32 = 0;
1452 let mut union: u32 = 0;
1453 for (x, y) in a.iter().zip(b) {
1454 inter += (x & y).count_ones();
1455 union += (x | y).count_ones();
1456 }
1457 if union == 0 {
1458 0.0
1459 } else {
1460 1.0 - (inter as f32) / (union as f32)
1461 }
1462 }
1463 other => panic!("{other:?} is a float-vector metric; use compute_distance"),
1464 }
1465 }
1466}
1467
1468#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1469pub struct FullTextIndexConfig {
1470 pub name: String,
1471 pub label: String,
1472 pub properties: Vec<String>,
1473 pub tokenizer: TokenizerConfig,
1474 pub with_positions: bool,
1475 #[serde(default)]
1476 pub metadata: IndexMetadata,
1477}
1478
1479#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1480#[non_exhaustive]
1481pub enum TokenizerConfig {
1482 Standard,
1483 Whitespace,
1484 Ngram {
1485 min: u8,
1486 max: u8,
1487 },
1488 Custom {
1489 name: String,
1490 },
1491 Analyzer(AnalyzerConfig),
1497}
1498
1499#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1508pub struct AnalyzerConfig {
1509 #[serde(default)]
1511 pub base: BaseTokenizer,
1512 #[serde(default)]
1514 pub language: FtsLanguage,
1515 #[serde(default = "default_true")]
1517 pub lower_case: bool,
1518 #[serde(default = "default_true")]
1520 pub stem: bool,
1521 #[serde(default = "default_true")]
1523 pub remove_stop_words: bool,
1524 #[serde(default)]
1526 pub custom_stop_words: Option<Vec<String>>,
1527 #[serde(default = "default_true")]
1529 pub ascii_folding: bool,
1530 #[serde(default)]
1532 pub max_token_length: Option<u32>,
1533}
1534
1535impl Default for AnalyzerConfig {
1536 fn default() -> Self {
1537 Self {
1538 base: BaseTokenizer::default(),
1539 language: FtsLanguage::default(),
1540 lower_case: true,
1541 stem: true,
1542 remove_stop_words: true,
1543 custom_stop_words: None,
1544 ascii_folding: true,
1545 max_token_length: None,
1546 }
1547 }
1548}
1549
1550#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1555#[non_exhaustive]
1556pub enum BaseTokenizer {
1557 #[default]
1559 Simple,
1560 Whitespace,
1562 Raw,
1564 Ngram {
1566 min: u32,
1568 max: u32,
1570 },
1571 Custom(String),
1573}
1574
1575#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1581#[non_exhaustive]
1582pub enum FtsLanguage {
1583 Arabic,
1585 Danish,
1587 Dutch,
1589 #[default]
1591 English,
1592 Finnish,
1594 French,
1596 German,
1598 Greek,
1600 Hungarian,
1602 Italian,
1604 Norwegian,
1606 Portuguese,
1608 Romanian,
1610 Russian,
1612 Spanish,
1614 Swedish,
1616 Tamil,
1618 Turkish,
1620}
1621
1622fn default_true() -> bool {
1624 true
1625}
1626
1627#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1628pub struct JsonFtsIndexConfig {
1629 pub name: String,
1630 pub label: String,
1631 pub column: String,
1632 #[serde(default)]
1633 pub paths: Vec<String>,
1634 #[serde(default)]
1635 pub with_positions: bool,
1636 #[serde(default)]
1637 pub metadata: IndexMetadata,
1638}
1639
1640#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1641pub struct ScalarIndexConfig {
1642 pub name: String,
1643 pub label: String,
1644 pub properties: Vec<String>,
1645 pub index_type: ScalarIndexType,
1646 pub where_clause: Option<String>,
1647 #[serde(default)]
1648 pub metadata: IndexMetadata,
1649}
1650
1651#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1652#[non_exhaustive]
1653pub enum ScalarIndexType {
1654 BTree,
1655 Hash,
1656 Bitmap,
1657 LabelList,
1658}
1659
1660pub struct SchemaManager {
1661 store: Arc<dyn ObjectStore>,
1662 path: ObjectStorePath,
1663 schema: RwLock<Arc<Schema>>,
1664}
1665
1666impl SchemaManager {
1667 pub async fn load(path: impl AsRef<Path>) -> Result<Self> {
1668 let path = path.as_ref();
1669 let parent = path
1670 .parent()
1671 .ok_or_else(|| anyhow!("Invalid schema path"))?;
1672 let filename = path
1673 .file_name()
1674 .ok_or_else(|| anyhow!("Invalid schema filename"))?
1675 .to_str()
1676 .ok_or_else(|| anyhow!("Invalid utf8 filename"))?;
1677
1678 let store = Arc::new(LocalFileSystem::new_with_prefix(parent)?);
1679 let obj_path = ObjectStorePath::from(filename);
1680
1681 Self::load_from_store(store, &obj_path).await
1682 }
1683
1684 pub async fn load_from_store(
1685 store: Arc<dyn ObjectStore>,
1686 path: &ObjectStorePath,
1687 ) -> Result<Self> {
1688 match store.get(path).await {
1689 Ok(result) => {
1690 let bytes = result.bytes().await?;
1691 let content = String::from_utf8(bytes.to_vec())?;
1692 let mut schema: Schema = serde_json::from_str(&content)?;
1693 let original_len = schema.indexes.len();
1701 if original_len > 0 {
1702 let mut seen: std::collections::HashSet<String> =
1703 std::collections::HashSet::with_capacity(original_len);
1704 let mut dedup: Vec<IndexDefinition> = schema
1705 .indexes
1706 .iter()
1707 .rev()
1708 .filter(|idx| seen.insert(idx.name().to_string()))
1709 .cloned()
1710 .collect();
1711 dedup.reverse();
1712 if dedup.len() != original_len {
1713 tracing::warn!(
1714 collapsed = original_len - dedup.len(),
1715 kept = dedup.len(),
1716 "schema.indexes: collapsed duplicate entries on load (issue #63)"
1717 );
1718 schema.indexes = dedup;
1719 }
1720 }
1721 Ok(Self {
1722 store,
1723 path: path.clone(),
1724 schema: RwLock::new(Arc::new(schema)),
1725 })
1726 }
1727 Err(object_store::Error::NotFound { .. }) => Ok(Self {
1728 store,
1729 path: path.clone(),
1730 schema: RwLock::new(Arc::new(Schema::default())),
1731 }),
1732 Err(e) => Err(anyhow::Error::from(e)),
1733 }
1734 }
1735
1736 pub async fn save(&self) -> Result<()> {
1737 let content = {
1738 let schema_guard = acquire_read(&self.schema, "schema")?;
1739 serde_json::to_string_pretty(&**schema_guard)?
1740 };
1741 self.store
1742 .put(&self.path, content.into())
1743 .await
1744 .map_err(anyhow::Error::from)?;
1745 Ok(())
1746 }
1747
1748 pub fn path(&self) -> &ObjectStorePath {
1749 &self.path
1750 }
1751
1752 pub fn schema(&self) -> Arc<Schema> {
1753 self.schema
1754 .read()
1755 .expect("Schema lock poisoned - a thread panicked while holding it")
1756 .clone()
1757 }
1758
1759 fn normalize_function_names(expr: &str) -> String {
1762 let mut result = String::with_capacity(expr.len());
1763 let mut chars = expr.chars().peekable();
1764
1765 while let Some(ch) = chars.next() {
1766 if ch.is_alphabetic() {
1767 let mut ident = String::new();
1769 ident.push(ch);
1770
1771 while let Some(&next) = chars.peek() {
1772 if next.is_alphanumeric() || next == '_' {
1773 ident.push(chars.next().unwrap());
1774 } else {
1775 break;
1776 }
1777 }
1778
1779 if chars.peek() == Some(&'(') {
1781 result.push_str(&ident.to_uppercase());
1782 } else {
1783 result.push_str(&ident); }
1785 } else {
1786 result.push(ch);
1787 }
1788 }
1789
1790 result
1791 }
1792
1793 pub fn generated_column_name(expr: &str) -> String {
1801 let normalized = Self::normalize_function_names(expr);
1803
1804 let sanitized = normalized
1805 .replace(|c: char| !c.is_alphanumeric(), "_")
1806 .trim_matches('_')
1807 .to_string();
1808
1809 const FNV_OFFSET_BASIS: u64 = 14695981039346656037;
1811 const FNV_PRIME: u64 = 1099511628211;
1812
1813 let mut hash = FNV_OFFSET_BASIS;
1814 for byte in normalized.as_bytes() {
1815 hash ^= *byte as u64;
1816 hash = hash.wrapping_mul(FNV_PRIME);
1817 }
1818
1819 format!("_gen_{}_{:x}", sanitized, hash)
1820 }
1821
1822 pub fn replace_schema(&self, new_schema: Schema) {
1823 let mut schema = self
1824 .schema
1825 .write()
1826 .expect("Schema lock poisoned - a thread panicked while holding it");
1827 *schema = Arc::new(new_schema);
1828 }
1829
1830 #[must_use]
1843 pub fn with_overlay(&self, overlay: &crate::core::fork::SchemaDelta) -> Arc<Self> {
1844 let primary = self.schema();
1845 let merged = if overlay.is_empty() {
1846 (*primary).clone()
1847 } else {
1848 let mut merged = (*primary).clone();
1849 for (name, label) in &overlay.added_labels {
1850 merged.labels.insert(name.clone(), label.clone());
1851 }
1852 for (name, edge_type) in &overlay.added_edge_types {
1853 merged.edge_types.insert(name.clone(), edge_type.clone());
1854 }
1855 for addition in &overlay.added_properties {
1856 let props = merged.properties.entry(addition.owner.clone()).or_default();
1857 props.insert(
1858 addition.property.clone(),
1859 PropertyMeta {
1860 r#type: addition.data_type.clone(),
1861 nullable: addition.nullable,
1862 added_in: merged.schema_version,
1863 state: SchemaElementState::Active,
1864 generation_expression: None,
1865 description: None,
1866 },
1867 );
1868 }
1869 merged
1870 };
1871
1872 Arc::new(Self {
1873 store: self.store.clone(),
1874 path: self.path.clone(),
1875 schema: RwLock::new(Arc::new(merged)),
1876 })
1877 }
1878
1879 pub fn next_label_id(&self) -> u16 {
1880 self.schema()
1881 .labels
1882 .values()
1883 .map(|l| l.id)
1884 .max()
1885 .unwrap_or(0)
1886 + 1
1887 }
1888
1889 pub fn next_type_id(&self) -> u32 {
1890 let max_schema_id = self
1891 .schema()
1892 .edge_types
1893 .values()
1894 .map(|t| t.id)
1895 .max()
1896 .unwrap_or(0);
1897
1898 if max_schema_id >= MAX_SCHEMA_TYPE_ID {
1900 panic!("Schema edge type ID exhaustion");
1901 }
1902
1903 max_schema_id + 1
1904 }
1905
1906 pub fn validate_schema_element_name(kind: &str, name: &str) -> Result<()> {
1924 if name.is_empty() || name.chars().all(char::is_whitespace) {
1925 return Err(anyhow!(
1926 "{kind} name must be non-empty and not all whitespace"
1927 ));
1928 }
1929 if name.len() > MAX_SCHEMA_NAME_LEN {
1930 return Err(anyhow!("{kind} name exceeds {MAX_SCHEMA_NAME_LEN} bytes"));
1931 }
1932 if let Some(c) = name
1933 .chars()
1934 .find(|c| c.is_control() || c.is_whitespace() || matches!(c, '/' | '\\'))
1935 {
1936 return Err(anyhow!(
1937 "{kind} name '{name}' contains an unsafe character ({c:?})"
1938 ));
1939 }
1940 Ok(())
1941 }
1942
1943 pub fn add_label(&self, name: &str) -> Result<u16> {
1944 self.add_label_with_desc(name, None)
1945 }
1946
1947 pub fn add_label_with_desc(&self, name: &str, description: Option<String>) -> Result<u16> {
1948 Self::validate_schema_element_name("Label", name)?;
1949 let mut guard = acquire_write(&self.schema, "schema")?;
1950 let schema = Arc::make_mut(&mut *guard);
1951 if schema.labels.contains_key(name) {
1952 return Err(anyhow!("Label '{}' already exists", name));
1953 }
1954
1955 let id = schema.labels.values().map(|l| l.id).max().unwrap_or(0) + 1;
1956 if id >= VIRTUAL_LABEL_ID_START {
1957 return Err(anyhow!(
1958 "Native label space exhausted (next id {id:#x} would enter the \
1959 virtual range {VIRTUAL_LABEL_ID_START:#x}..{VIRTUAL_LABEL_ID_SENTINEL:#x} \
1960 reserved for catalog-resolved labels)"
1961 ));
1962 }
1963 schema.labels.insert(
1964 name.to_string(),
1965 LabelMeta {
1966 id,
1967 created_at: Utc::now(),
1968 state: SchemaElementState::Active,
1969 description,
1970 },
1971 );
1972 schema.bump_version();
1973 Ok(id)
1974 }
1975
1976 pub fn add_edge_type(
1977 &self,
1978 name: &str,
1979 src_labels: Vec<String>,
1980 dst_labels: Vec<String>,
1981 ) -> Result<u32> {
1982 self.add_edge_type_with_desc(name, src_labels, dst_labels, None)
1983 }
1984
1985 pub fn add_edge_type_with_desc(
1986 &self,
1987 name: &str,
1988 src_labels: Vec<String>,
1989 dst_labels: Vec<String>,
1990 description: Option<String>,
1991 ) -> Result<u32> {
1992 Self::validate_schema_element_name("Edge type", name)?;
1993 let mut guard = acquire_write(&self.schema, "schema")?;
1994 let schema = Arc::make_mut(&mut *guard);
1995 if schema.edge_types.contains_key(name) {
1996 return Err(anyhow!("Edge type '{}' already exists", name));
1997 }
1998
1999 let id = schema.edge_types.values().map(|t| t.id).max().unwrap_or(0) + 1;
2000
2001 if id >= VIRTUAL_EDGE_TYPE_ID_START {
2006 return Err(anyhow!(
2007 "Native edge type space exhausted (next id {id:#x} would enter the \
2008 virtual range {VIRTUAL_EDGE_TYPE_ID_START:#x}..{VIRTUAL_EDGE_TYPE_ID_SENTINEL:#x} \
2009 reserved for catalog-resolved edge types)"
2010 ));
2011 }
2012
2013 schema.edge_types.insert(
2014 name.to_string(),
2015 EdgeTypeMeta {
2016 id,
2017 src_labels,
2018 dst_labels,
2019 state: SchemaElementState::Active,
2020 description,
2021 },
2022 );
2023 schema.bump_version();
2024 Ok(id)
2025 }
2026
2027 pub fn get_or_assign_edge_type_id(&self, type_name: &str) -> u32 {
2036 {
2037 let guard = acquire_read(&self.schema, "schema")
2038 .expect("Schema lock poisoned - a thread panicked while holding it");
2039 if let Some(id) = guard.edge_type_id_unified(type_name) {
2040 return id;
2041 }
2042 }
2043 let mut guard = acquire_write(&self.schema, "schema")
2044 .expect("Schema lock poisoned - a thread panicked while holding it");
2045 let schema = Arc::make_mut(&mut *guard);
2046 schema.get_or_assign_edge_type_id(type_name)
2047 }
2048
2049 pub fn edge_type_name_by_id_unified(&self, type_id: u32) -> Option<String> {
2051 let schema = acquire_read(&self.schema, "schema")
2052 .expect("Schema lock poisoned - a thread panicked while holding it");
2053 schema.edge_type_name_by_id_unified(type_id)
2054 }
2055
2056 pub fn add_property(
2057 &self,
2058 label_or_type: &str,
2059 prop_name: &str,
2060 data_type: DataType,
2061 nullable: bool,
2062 ) -> Result<()> {
2063 self.add_property_with_desc(label_or_type, prop_name, data_type, nullable, None)
2064 }
2065
2066 pub fn add_property_with_desc(
2067 &self,
2068 label_or_type: &str,
2069 prop_name: &str,
2070 data_type: DataType,
2071 nullable: bool,
2072 description: Option<String>,
2073 ) -> Result<()> {
2074 validate_property_name(prop_name)?;
2075 let mut guard = acquire_write(&self.schema, "schema")?;
2076 let schema = Arc::make_mut(&mut *guard);
2077 let version = schema.schema_version;
2078 let props = schema
2079 .properties
2080 .entry(label_or_type.to_string())
2081 .or_default();
2082
2083 if props.contains_key(prop_name) {
2084 return Err(anyhow!(
2085 "Property '{}' already exists for '{}'",
2086 prop_name,
2087 label_or_type
2088 ));
2089 }
2090
2091 props.insert(
2092 prop_name.to_string(),
2093 PropertyMeta {
2094 r#type: data_type,
2095 nullable,
2096 added_in: version,
2097 state: SchemaElementState::Active,
2098 generation_expression: None,
2099 description,
2100 },
2101 );
2102 schema.bump_version();
2104 Ok(())
2105 }
2106
2107 pub fn declare_property(
2125 &self,
2126 label_or_type: &str,
2127 prop_name: &str,
2128 data_type: DataType,
2129 nullable: bool,
2130 description: Option<String>,
2131 ) -> Result<bool> {
2132 validate_property_name(prop_name)?;
2133 let mut guard = acquire_write(&self.schema, "schema")?;
2134 let schema = Arc::make_mut(&mut *guard);
2135 let version = schema.schema_version;
2136 let props = schema
2137 .properties
2138 .entry(label_or_type.to_string())
2139 .or_default();
2140
2141 if let Some(existing) = props.get(prop_name) {
2142 if existing.r#type == data_type && existing.nullable == nullable {
2143 return Ok(false); }
2145 return Err(anyhow!(
2146 "Property '{}' on '{}' is declared as {:?} (nullable: {}); cannot re-declare \
2147 as {:?} (nullable: {}). Property types are immutable — use a new property \
2148 name or migrate the data",
2149 prop_name,
2150 label_or_type,
2151 existing.r#type,
2152 existing.nullable,
2153 data_type,
2154 nullable
2155 ));
2156 }
2157
2158 props.insert(
2159 prop_name.to_string(),
2160 PropertyMeta {
2161 r#type: data_type,
2162 nullable,
2163 added_in: version,
2164 state: SchemaElementState::Active,
2165 generation_expression: None,
2166 description,
2167 },
2168 );
2169 schema.bump_version();
2171 Ok(true)
2172 }
2173
2174 pub fn add_internal_property(
2185 &self,
2186 label_or_type: &str,
2187 prop_name: &str,
2188 data_type: DataType,
2189 nullable: bool,
2190 ) -> Result<bool> {
2191 validate_reserved_property_name(prop_name)?;
2192 let mut guard = acquire_write(&self.schema, "schema")?;
2193 let schema = Arc::make_mut(&mut *guard);
2194 let version = schema.schema_version;
2195 let props = schema
2196 .properties
2197 .entry(label_or_type.to_string())
2198 .or_default();
2199
2200 if let Some(existing) = props.get(prop_name) {
2201 if existing.r#type == data_type {
2202 return Ok(false); }
2204 return Err(anyhow!(
2205 "Internal property '{}' already exists for '{}' with a different type",
2206 prop_name,
2207 label_or_type
2208 ));
2209 }
2210
2211 props.insert(
2212 prop_name.to_string(),
2213 PropertyMeta {
2214 r#type: data_type,
2215 nullable,
2216 added_in: version,
2217 state: SchemaElementState::Active,
2218 generation_expression: None,
2219 description: None,
2220 },
2221 );
2222 schema.bump_version();
2223 Ok(true)
2224 }
2225
2226 pub fn add_generated_property(
2227 &self,
2228 label_or_type: &str,
2229 prop_name: &str,
2230 data_type: DataType,
2231 expr: String,
2232 ) -> Result<()> {
2233 validate_reserved_property_name(prop_name)?;
2236 let mut guard = acquire_write(&self.schema, "schema")?;
2237 let schema = Arc::make_mut(&mut *guard);
2238 let version = schema.schema_version;
2239 let props = schema
2240 .properties
2241 .entry(label_or_type.to_string())
2242 .or_default();
2243
2244 if props.contains_key(prop_name) {
2245 return Err(anyhow!("Property '{}' already exists", prop_name));
2246 }
2247
2248 props.insert(
2249 prop_name.to_string(),
2250 PropertyMeta {
2251 r#type: data_type,
2252 nullable: true,
2253 added_in: version,
2254 state: SchemaElementState::Active,
2255 generation_expression: Some(expr),
2256 description: None,
2257 },
2258 );
2259 schema.bump_version();
2261 Ok(())
2262 }
2263
2264 pub fn set_label_description(&self, name: &str, description: Option<String>) -> Result<()> {
2265 let mut guard = acquire_write(&self.schema, "schema")?;
2266 let schema = Arc::make_mut(&mut *guard);
2267 let meta = schema
2268 .labels
2269 .get_mut(name)
2270 .ok_or_else(|| anyhow!("Label '{}' does not exist", name))?;
2271 meta.description = description;
2272 Ok(())
2273 }
2274
2275 pub fn set_edge_type_description(&self, name: &str, description: Option<String>) -> Result<()> {
2276 let mut guard = acquire_write(&self.schema, "schema")?;
2277 let schema = Arc::make_mut(&mut *guard);
2278 let meta = schema
2279 .edge_types
2280 .get_mut(name)
2281 .ok_or_else(|| anyhow!("Edge type '{}' does not exist", name))?;
2282 meta.description = description;
2283 Ok(())
2284 }
2285
2286 pub fn set_property_description(
2287 &self,
2288 entity: &str,
2289 prop_name: &str,
2290 description: Option<String>,
2291 ) -> Result<()> {
2292 let mut guard = acquire_write(&self.schema, "schema")?;
2293 let schema = Arc::make_mut(&mut *guard);
2294 let props = schema
2295 .properties
2296 .get_mut(entity)
2297 .ok_or_else(|| anyhow!("Entity '{}' does not exist", entity))?;
2298 let meta = props
2299 .get_mut(prop_name)
2300 .ok_or_else(|| anyhow!("Property '{}' does not exist on '{}'", prop_name, entity))?;
2301 meta.description = description;
2302 Ok(())
2303 }
2304
2305 pub fn add_index(&self, index_def: IndexDefinition) -> Result<()> {
2314 let mut guard = acquire_write(&self.schema, "schema")?;
2315 let schema = Arc::make_mut(&mut *guard);
2316 if let Some(existing) = schema
2317 .indexes
2318 .iter_mut()
2319 .find(|i| i.name() == index_def.name())
2320 {
2321 *existing = index_def;
2322 } else {
2323 schema.indexes.push(index_def);
2324 }
2325 schema.bump_version();
2326 Ok(())
2327 }
2328
2329 pub fn get_index(&self, name: &str) -> Option<IndexDefinition> {
2330 let schema = self.schema.read().expect("Schema lock poisoned");
2331 schema.indexes.iter().find(|i| i.name() == name).cloned()
2332 }
2333
2334 pub fn update_index_metadata(
2339 &self,
2340 index_name: &str,
2341 f: impl FnOnce(&mut IndexMetadata),
2342 ) -> Result<()> {
2343 let mut guard = acquire_write(&self.schema, "schema")?;
2344 let schema = Arc::make_mut(&mut *guard);
2345 let idx = schema
2346 .indexes
2347 .iter_mut()
2348 .find(|i| i.name() == index_name)
2349 .ok_or_else(|| anyhow!("Index '{}' not found", index_name))?;
2350 f(idx.metadata_mut());
2351 Ok(())
2352 }
2353
2354 pub fn remove_index(&self, name: &str) -> Result<()> {
2355 let mut guard = acquire_write(&self.schema, "schema")?;
2356 let schema = Arc::make_mut(&mut *guard);
2357 if let Some(pos) = schema.indexes.iter().position(|i| i.name() == name) {
2358 schema.indexes.remove(pos);
2359 schema.bump_version();
2360 Ok(())
2361 } else {
2362 Err(anyhow!("Index '{}' not found", name))
2363 }
2364 }
2365
2366 pub fn add_constraint(&self, constraint: Constraint) -> Result<()> {
2367 let mut guard = acquire_write(&self.schema, "schema")?;
2368 let schema = Arc::make_mut(&mut *guard);
2369 if schema.constraints.iter().any(|c| c.name == constraint.name) {
2370 return Err(anyhow!("Constraint '{}' already exists", constraint.name));
2371 }
2372 schema.constraints.push(constraint);
2373 schema.bump_version();
2374 Ok(())
2375 }
2376
2377 pub fn drop_constraint(&self, name: &str, if_exists: bool) -> Result<()> {
2378 let mut guard = acquire_write(&self.schema, "schema")?;
2379 let schema = Arc::make_mut(&mut *guard);
2380 if let Some(pos) = schema.constraints.iter().position(|c| c.name == name) {
2381 schema.constraints.remove(pos);
2382 schema.bump_version();
2383 Ok(())
2384 } else if if_exists {
2385 Ok(())
2386 } else {
2387 Err(anyhow!("Constraint '{}' not found", name))
2388 }
2389 }
2390
2391 pub fn drop_property(&self, label_or_type: &str, prop_name: &str) -> Result<()> {
2392 let mut guard = acquire_write(&self.schema, "schema")?;
2393 let schema = Arc::make_mut(&mut *guard);
2394 let Some(props) = schema.properties.get_mut(label_or_type) else {
2395 return Err(anyhow!("Label or Edge Type '{}' not found", label_or_type));
2396 };
2397 if props.remove(prop_name).is_none() {
2398 return Err(anyhow!(
2399 "Property '{}' not found for '{}'",
2400 prop_name,
2401 label_or_type
2402 ));
2403 }
2404 schema.bump_version();
2405 Ok(())
2406 }
2407
2408 pub fn rename_property(
2409 &self,
2410 label_or_type: &str,
2411 old_name: &str,
2412 new_name: &str,
2413 ) -> Result<()> {
2414 validate_property_name(new_name)?;
2419 let mut guard = acquire_write(&self.schema, "schema")?;
2420 let schema = Arc::make_mut(&mut *guard);
2421 let Some(props) = schema.properties.get_mut(label_or_type) else {
2422 return Err(anyhow!("Label or Edge Type '{}' not found", label_or_type));
2423 };
2424 let Some(meta) = props.remove(old_name) else {
2425 return Err(anyhow!(
2426 "Property '{}' not found for '{}'",
2427 old_name,
2428 label_or_type
2429 ));
2430 };
2431 if props.contains_key(new_name) {
2432 props.insert(old_name.to_string(), meta); return Err(anyhow!("Property '{}' already exists", new_name));
2435 }
2436 props.insert(new_name.to_string(), meta);
2437 schema.bump_version();
2438 Ok(())
2439 }
2440
2441 pub fn drop_label(&self, name: &str, if_exists: bool) -> Result<()> {
2442 let mut guard = acquire_write(&self.schema, "schema")?;
2443 let schema = Arc::make_mut(&mut *guard);
2444 if let Some(label_meta) = schema.labels.get_mut(name) {
2445 label_meta.state = SchemaElementState::Tombstone { since: Utc::now() };
2446 schema.bump_version();
2448 Ok(())
2449 } else if if_exists {
2450 Ok(())
2451 } else {
2452 Err(anyhow!("Label '{}' not found", name))
2453 }
2454 }
2455
2456 pub fn drop_edge_type(&self, name: &str, if_exists: bool) -> Result<()> {
2457 let mut guard = acquire_write(&self.schema, "schema")?;
2458 let schema = Arc::make_mut(&mut *guard);
2459 if let Some(edge_meta) = schema.edge_types.get_mut(name) {
2460 edge_meta.state = SchemaElementState::Tombstone { since: Utc::now() };
2461 schema.bump_version();
2463 Ok(())
2464 } else if if_exists {
2465 Ok(())
2466 } else {
2467 Err(anyhow!("Edge Type '{}' not found", name))
2468 }
2469 }
2470}
2471
2472pub fn validate_identifier(name: &str) -> Result<()> {
2474 if name.is_empty() || name.len() > 64 {
2476 return Err(anyhow!("Identifier '{}' must be 1-64 characters", name));
2477 }
2478
2479 let first = name.chars().next().unwrap();
2481 if !first.is_alphabetic() && first != '_' {
2482 return Err(anyhow!(
2483 "Identifier '{}' must start with letter or underscore",
2484 name
2485 ));
2486 }
2487
2488 if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
2490 return Err(anyhow!(
2491 "Identifier '{}' must contain only alphanumeric and underscore",
2492 name
2493 ));
2494 }
2495
2496 const RESERVED: &[&str] = &[
2498 "MATCH", "CREATE", "DELETE", "SET", "RETURN", "WHERE", "MERGE", "CALL", "YIELD", "WITH",
2499 "UNION", "ORDER", "LIMIT",
2500 ];
2501 if RESERVED.contains(&name.to_uppercase().as_str()) {
2502 return Err(anyhow!("Identifier '{}' cannot be a reserved word", name));
2503 }
2504
2505 Ok(())
2506}
2507
2508pub fn validate_property_name(name: &str) -> Result<()> {
2515 if name.starts_with('_') {
2516 return Err(anyhow!(
2517 "Property name '{}' is reserved: names starting with '_' are reserved by the storage layer",
2518 name
2519 ));
2520 }
2521 validate_reserved_property_name(name)
2522}
2523
2524fn validate_reserved_property_name(name: &str) -> Result<()> {
2531 const RESERVED_PROPS: &[&str] = &[
2540 "ext_id",
2541 "overflow_json",
2542 "eid",
2543 "src_vid",
2544 "dst_vid",
2545 "op",
2546 "__set_struct__",
2554 ];
2555 if RESERVED_PROPS.contains(&name) {
2556 return Err(anyhow!(
2557 "Property name '{}' is reserved by the storage layer; please choose a different name",
2558 name
2559 ));
2560 }
2561 Ok(())
2562}
2563
2564#[cfg(test)]
2565mod tests {
2566 use super::*;
2567 use crate::value::{TemporalValue, Value};
2568 use object_store::local::LocalFileSystem;
2569 use tempfile::tempdir;
2570
2571 #[test]
2572 fn binary_vector_metrics_exact() {
2573 assert_eq!(
2576 DistanceMetric::Hamming.compute_distance_binary(&[0x00], &[0xFF]),
2577 8.0
2578 );
2579 assert_eq!(
2580 DistanceMetric::Hamming.compute_distance_binary(&[0xA5, 0x0F], &[0xA5, 0x00]),
2581 4.0
2582 );
2583 assert_eq!(
2584 DistanceMetric::Hamming.compute_distance_binary(&[0xA5], &[0xA5]),
2585 0.0
2586 );
2587
2588 let j = DistanceMetric::Jaccard.compute_distance_binary(&[0b1100], &[0b1010]);
2591 assert!((j - (2.0 / 3.0)).abs() < 1e-6, "got {j}");
2592 assert_eq!(
2594 DistanceMetric::Jaccard.compute_distance_binary(&[0xFF], &[0xFF]),
2595 0.0
2596 );
2597 assert_eq!(
2599 DistanceMetric::Jaccard.compute_distance_binary(&[0x00, 0x00], &[0x00, 0x00]),
2600 0.0
2601 );
2602 }
2603
2604 #[test]
2605 fn binary_metrics_are_binary_and_route_correctly() {
2606 assert!(DistanceMetric::Hamming.is_binary());
2607 assert!(DistanceMetric::Jaccard.is_binary());
2608 assert!(!DistanceMetric::L2.is_binary());
2609 assert!(!DistanceMetric::L1.is_binary());
2610 }
2611
2612 #[test]
2613 #[should_panic(expected = "binary-vector metric")]
2614 fn float_compute_distance_rejects_binary_metric() {
2615 DistanceMetric::Hamming.compute_distance(&[1.0], &[0.0]);
2616 }
2617
2618 #[test]
2619 fn check_binary_vector_value_guards() {
2620 let ty = DataType::BinaryVector { dimensions: 3 };
2621 assert!(
2622 ty.check_vector_dims(&Value::BinaryVector(vec![1, 2, 3]))
2623 .is_ok()
2624 );
2625 assert!(ty.check_vector_dims(&Value::Null).is_ok());
2626 assert!(
2628 ty.check_vector_dims(&Value::BinaryVector(vec![1, 2]))
2629 .is_err()
2630 );
2631 assert!(
2633 ty.check_vector_dims(&Value::List(vec![
2634 Value::Int(0),
2635 Value::Int(255),
2636 Value::Int(128)
2637 ]))
2638 .is_ok()
2639 );
2640 assert!(
2642 ty.check_vector_dims(&Value::List(vec![
2643 Value::Int(0),
2644 Value::Int(256),
2645 Value::Int(1)
2646 ]))
2647 .is_err()
2648 );
2649 }
2650
2651 #[test]
2652 fn test_datatype_accepts_matrix() {
2653 let dt = || TemporalValue::DateTime {
2654 nanos_since_epoch: 0,
2655 offset_seconds: 0,
2656 timezone_name: None,
2657 };
2658
2659 for ty in [
2661 DataType::String,
2662 DataType::Int64,
2663 DataType::Bool,
2664 DataType::DateTime,
2665 DataType::Float64,
2666 ] {
2667 assert!(ty.accepts(&Value::Null), "{ty:?} must accept Null");
2668 }
2669
2670 assert!(DataType::String.accepts(&Value::String("x".into())));
2672 assert!(DataType::Int64.accepts(&Value::Int(1)));
2673 assert!(DataType::Bool.accepts(&Value::Bool(true)));
2674 assert!(DataType::DateTime.accepts(&Value::Temporal(dt())));
2675
2676 assert!(
2678 DataType::Float64.accepts(&Value::Int(3)),
2679 "Int widens to Float"
2680 );
2681 assert!(DataType::Int32.accepts(&Value::Int(3)), "Int fits Int32");
2682 assert!(DataType::Timestamp.accepts(&Value::Temporal(dt())));
2683 assert!(
2684 DataType::Timestamp.accepts(&Value::String("2026-01-01T00:00:00Z".into())),
2685 "storage parses strings for non-struct Timestamp columns"
2686 );
2687
2688 assert!(
2690 !DataType::DateTime.accepts(&Value::String("2026-01-01T00:00:00Z".into())),
2691 "String into a DateTime struct column nulls silently — reject here"
2692 );
2693 assert!(!DataType::Bool.accepts(&Value::Int(1)));
2694 assert!(!DataType::Int64.accepts(&Value::Bool(true)));
2695 assert!(!DataType::Int64.accepts(&Value::Float(1.5)));
2696 assert!(
2697 !DataType::String.accepts(&Value::Int(10)),
2698 "no implicit stringification"
2699 );
2700 assert!(!DataType::Duration.accepts(&Value::String("P1D".into())));
2701
2702 assert!(DataType::CypherValue.accepts(&Value::Map(Default::default())));
2704 }
2705
2706 #[test]
2707 fn test_check_vector_dims_matrix() {
2708 let vec3 = DataType::Vector { dimensions: 3 };
2709 let multi2 = DataType::List(Box::new(DataType::Vector { dimensions: 2 }));
2710 let flist = |vals: &[f64]| Value::List(vals.iter().map(|f| Value::Float(*f)).collect());
2711
2712 assert!(vec3.check_vector_dims(&Value::Null).is_ok());
2714 assert!(multi2.check_vector_dims(&Value::Null).is_ok());
2715
2716 assert!(
2718 vec3.check_vector_dims(&Value::Vector(vec![1.0, 2.0, 3.0]))
2719 .is_ok()
2720 );
2721 assert!(vec3.check_vector_dims(&flist(&[1.0, 2.0, 3.0])).is_ok());
2722 assert!(
2723 vec3.check_vector_dims(&Value::List(vec![
2724 Value::Int(1),
2725 Value::Float(2.0),
2726 Value::Int(3)
2727 ]))
2728 .is_ok()
2729 );
2730
2731 assert_eq!(
2733 vec3.check_vector_dims(&Value::Vector(vec![1.0, 2.0])),
2734 Err(VectorDimError::WrongLength {
2735 expected: 3,
2736 actual: 2
2737 })
2738 );
2739 assert_eq!(
2740 vec3.check_vector_dims(&flist(&[1.0, 2.0, 3.0, 4.0, 5.0])),
2741 Err(VectorDimError::WrongLength {
2742 expected: 3,
2743 actual: 5
2744 })
2745 );
2746 assert_eq!(
2747 vec3.check_vector_dims(&Value::List(vec![])),
2748 Err(VectorDimError::WrongLength {
2749 expected: 3,
2750 actual: 0
2751 })
2752 );
2753 assert_eq!(
2754 vec3.check_vector_dims(&Value::List(vec![
2755 Value::Float(1.0),
2756 Value::String("x".into()),
2757 Value::Float(3.0),
2758 ])),
2759 Err(VectorDimError::NonNumericElement { index: 1 })
2760 );
2761 assert_eq!(
2762 vec3.check_vector_dims(&Value::List(vec![
2763 Value::Float(1.0),
2764 Value::Null,
2765 Value::Float(3.0)
2766 ])),
2767 Err(VectorDimError::NonNumericElement { index: 1 })
2768 );
2769 assert_eq!(
2770 vec3.check_vector_dims(&Value::String("not a vector".into())),
2771 Err(VectorDimError::NotAVector { actual: "String" })
2772 );
2773
2774 assert!(multi2.check_vector_dims(&Value::List(vec![])).is_ok());
2777 assert!(
2778 multi2
2779 .check_vector_dims(&Value::List(vec![flist(&[1.0, 2.0]), flist(&[3.0, 4.0])]))
2780 .is_ok()
2781 );
2782 assert_eq!(
2783 multi2.check_vector_dims(&Value::List(vec![
2784 flist(&[1.0, 2.0]),
2785 flist(&[9.0, 9.0, 9.0])
2786 ])),
2787 Err(VectorDimError::TokenWrongLength {
2788 token: 1,
2789 expected: 2,
2790 actual: 3
2791 })
2792 );
2793 assert_eq!(
2794 multi2.check_vector_dims(&Value::List(vec![Value::String("tok".into())])),
2795 Err(VectorDimError::TokenNotAVector {
2796 token: 0,
2797 actual: "String"
2798 })
2799 );
2800 assert_eq!(
2801 multi2.check_vector_dims(&Value::Vector(vec![1.0, 2.0])),
2802 Err(VectorDimError::NotATokenList { actual: "Vector" })
2803 );
2804
2805 assert!(
2807 DataType::Int64
2808 .check_vector_dims(&Value::String("x".into()))
2809 .is_ok()
2810 );
2811 assert!(
2812 DataType::List(Box::new(DataType::Float64))
2813 .check_vector_dims(&Value::List(vec![Value::String("x".into())]))
2814 .is_ok()
2815 );
2816 assert!(
2817 DataType::SparseVector { dimensions: 8 }
2818 .check_vector_dims(&Value::Map(Default::default()))
2819 .is_ok()
2820 );
2821
2822 let msg = VectorDimError::WrongLength {
2824 expected: 4,
2825 actual: 5,
2826 }
2827 .to_string();
2828 assert!(msg.contains('4') && msg.contains('5'), "message: {msg}");
2829 }
2830
2831 #[tokio::test]
2832 async fn test_declare_property_idempotent_and_conflicting() -> Result<()> {
2833 let dir = tempdir()?;
2834 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path())?);
2835 let path = ObjectStorePath::from("schema.json");
2836 let manager = SchemaManager::load_from_store(store.clone(), &path).await?;
2837
2838 manager.add_label("Doc")?;
2839 let vec4 = DataType::Vector { dimensions: 4 };
2840
2841 assert!(manager.declare_property("Doc", "embedding", vec4.clone(), true, None)?);
2843
2844 assert!(!manager.declare_property("Doc", "embedding", vec4.clone(), true, None)?);
2847 assert!(!manager.declare_property(
2848 "Doc",
2849 "embedding",
2850 vec4.clone(),
2851 true,
2852 Some("new docs".into())
2853 )?);
2854
2855 let err = manager
2858 .declare_property(
2859 "Doc",
2860 "embedding",
2861 DataType::Vector { dimensions: 8 },
2862 true,
2863 None,
2864 )
2865 .unwrap_err()
2866 .to_string();
2867 assert!(err.contains('4') && err.contains('8'), "message: {err}");
2868 assert!(!err.contains("already exists"), "message: {err}");
2869
2870 assert!(
2872 manager
2873 .declare_property("Doc", "embedding", vec4.clone(), false, None)
2874 .is_err()
2875 );
2876
2877 let schema = manager.schema();
2879 let meta = &schema.properties["Doc"]["embedding"];
2880 assert_eq!(meta.r#type, vec4);
2881 assert!(meta.nullable);
2882 Ok(())
2883 }
2884
2885 #[tokio::test]
2886 async fn test_schema_management() -> Result<()> {
2887 let dir = tempdir()?;
2888 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path())?);
2889 let path = ObjectStorePath::from("schema.json");
2890 let manager = SchemaManager::load_from_store(store.clone(), &path).await?;
2891
2892 let lid = manager.add_label("Person")?;
2894 assert_eq!(lid, 1);
2895 assert!(manager.add_label("Person").is_err());
2896
2897 manager.add_property("Person", "name", DataType::String, false)?;
2899 assert!(
2900 manager
2901 .add_property("Person", "name", DataType::String, false)
2902 .is_err()
2903 );
2904
2905 let tid = manager.add_edge_type("knows", vec!["Person".into()], vec!["Person".into()])?;
2907 assert_eq!(tid, 1);
2908
2909 manager.save().await?;
2910 assert!(store.get(&path).await.is_ok());
2912
2913 let manager2 = SchemaManager::load_from_store(store, &path).await?;
2914 assert!(manager2.schema().labels.contains_key("Person"));
2915 assert!(
2916 manager2
2917 .schema()
2918 .properties
2919 .get("Person")
2920 .unwrap()
2921 .contains_key("name")
2922 );
2923
2924 Ok(())
2925 }
2926
2927 #[tokio::test]
2928 async fn test_reserved_property_names_rejected() -> Result<()> {
2929 let dir = tempdir()?;
2930 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path())?);
2931 let path = ObjectStorePath::from("schema.json");
2932 let manager = SchemaManager::load_from_store(store, &path).await?;
2933
2934 manager.add_label("Tiny")?;
2935
2936 for reserved in &["ext_id", "overflow_json", "eid", "src_vid", "dst_vid", "op"] {
2940 let err = manager
2941 .add_property("Tiny", reserved, DataType::String, true)
2942 .expect_err(&format!("expected '{reserved}' to be rejected"));
2943 assert!(
2944 err.to_string().contains("reserved"),
2945 "error for '{reserved}' should mention 'reserved', got: {err}"
2946 );
2947 }
2948
2949 let err = manager
2954 .add_property("Tiny", "__set_struct__", DataType::String, true)
2955 .expect_err("expected '__set_struct__' to be rejected");
2956 assert!(
2957 err.to_string().contains("reserved"),
2958 "__set_struct__ rejection should mention 'reserved', got: {err}"
2959 );
2960
2961 for reserved in &["_vid", "_uid", "_eid", "_version", "_created_at"] {
2963 assert!(
2964 manager
2965 .add_property("Tiny", reserved, DataType::String, true)
2966 .is_err(),
2967 "expected '{reserved}' to be rejected"
2968 );
2969 }
2970
2971 manager.add_property("Tiny", "ext_id_foo", DataType::String, true)?;
2974 manager.add_property("Tiny", "user_op", DataType::String, true)?;
2975 manager.add_property("Tiny", "type_name", DataType::String, true)?;
2976
2977 manager.add_edge_type("knows", vec!["Tiny".into()], vec!["Tiny".into()])?;
2979 assert!(
2980 manager
2981 .add_property("knows", "src_vid", DataType::Int64, true)
2982 .is_err()
2983 );
2984
2985 assert!(
2987 manager
2988 .add_generated_property(
2989 "Tiny",
2990 "ext_id",
2991 DataType::String,
2992 "concat('x', name)".into()
2993 )
2994 .is_err()
2995 );
2996
2997 Ok(())
2998 }
2999
3000 #[test]
3001 fn test_normalize_function_names() {
3002 assert_eq!(
3003 SchemaManager::normalize_function_names("lower(email)"),
3004 "LOWER(email)"
3005 );
3006 assert_eq!(
3007 SchemaManager::normalize_function_names("LOWER(email)"),
3008 "LOWER(email)"
3009 );
3010 assert_eq!(
3011 SchemaManager::normalize_function_names("Lower(email)"),
3012 "LOWER(email)"
3013 );
3014 assert_eq!(
3015 SchemaManager::normalize_function_names("trim(lower(email))"),
3016 "TRIM(LOWER(email))"
3017 );
3018 }
3019
3020 #[test]
3021 fn test_generated_column_name_case_insensitive() {
3022 let col1 = SchemaManager::generated_column_name("lower(email)");
3023 let col2 = SchemaManager::generated_column_name("LOWER(email)");
3024 let col3 = SchemaManager::generated_column_name("Lower(email)");
3025 assert_eq!(col1, col2);
3026 assert_eq!(col2, col3);
3027 assert!(col1.starts_with("_gen_LOWER_email_"));
3028 }
3029
3030 #[test]
3031 fn test_index_metadata_serde_backward_compat() {
3032 let json = r#"{
3034 "type": "Scalar",
3035 "name": "idx_person_name",
3036 "label": "Person",
3037 "properties": ["name"],
3038 "index_type": "BTree",
3039 "where_clause": null
3040 }"#;
3041 let def: IndexDefinition = serde_json::from_str(json).unwrap();
3042 let meta = def.metadata();
3043 assert_eq!(meta.status, IndexStatus::Online);
3044 assert!(meta.last_built_at.is_none());
3045 assert!(meta.row_count_at_build.is_none());
3046 }
3047
3048 #[test]
3049 fn test_index_metadata_serde_roundtrip() {
3050 let now = Utc::now();
3051 let def = IndexDefinition::Scalar(ScalarIndexConfig {
3052 name: "idx_test".to_string(),
3053 label: "Test".to_string(),
3054 properties: vec!["prop".to_string()],
3055 index_type: ScalarIndexType::BTree,
3056 where_clause: None,
3057 metadata: IndexMetadata {
3058 status: IndexStatus::Building,
3059 last_built_at: Some(now),
3060 row_count_at_build: Some(42),
3061 },
3062 });
3063
3064 let json = serde_json::to_string(&def).unwrap();
3065 let parsed: IndexDefinition = serde_json::from_str(&json).unwrap();
3066 assert_eq!(parsed.metadata().status, IndexStatus::Building);
3067 assert_eq!(parsed.metadata().row_count_at_build, Some(42));
3068 assert!(parsed.metadata().last_built_at.is_some());
3069 }
3070
3071 #[tokio::test]
3072 async fn test_update_index_metadata() -> Result<()> {
3073 let dir = tempdir()?;
3074 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path())?);
3075 let path = ObjectStorePath::from("schema.json");
3076 let manager = SchemaManager::load_from_store(store, &path).await?;
3077
3078 manager.add_label("Person")?;
3079 let idx = IndexDefinition::Scalar(ScalarIndexConfig {
3080 name: "idx_test".to_string(),
3081 label: "Person".to_string(),
3082 properties: vec!["name".to_string()],
3083 index_type: ScalarIndexType::BTree,
3084 where_clause: None,
3085 metadata: Default::default(),
3086 });
3087 manager.add_index(idx)?;
3088
3089 let initial = manager.get_index("idx_test").unwrap();
3091 assert_eq!(initial.metadata().status, IndexStatus::Online);
3092
3093 manager.update_index_metadata("idx_test", |m| {
3095 m.status = IndexStatus::Building;
3096 m.row_count_at_build = Some(100);
3097 })?;
3098
3099 let updated = manager.get_index("idx_test").unwrap();
3100 assert_eq!(updated.metadata().status, IndexStatus::Building);
3101 assert_eq!(updated.metadata().row_count_at_build, Some(100));
3102
3103 assert!(manager.update_index_metadata("nope", |_| {}).is_err());
3105
3106 Ok(())
3107 }
3108
3109 #[tokio::test]
3114 async fn add_internal_property_reports_newly_added() -> Result<()> {
3115 let dir = tempdir()?;
3116 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path())?);
3117 let path = ObjectStorePath::from("schema.json");
3118 let manager = SchemaManager::load_from_store(store, &path).await?;
3119 manager.add_label("Doc")?;
3120
3121 let dt = DataType::Vector { dimensions: 16 };
3122 assert!(manager.add_internal_property("Doc", "__fde_x", dt.clone(), true)?);
3124 assert!(!manager.add_internal_property("Doc", "__fde_x", dt.clone(), true)?);
3126 assert!(
3128 manager
3129 .add_internal_property("Doc", "__fde_x", DataType::Vector { dimensions: 8 }, true)
3130 .is_err()
3131 );
3132 Ok(())
3133 }
3134
3135 #[tokio::test]
3140 async fn test_add_index_is_upsert_by_name() -> Result<()> {
3141 let dir = tempdir()?;
3142 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path())?);
3143 let path = ObjectStorePath::from("schema.json");
3144 let manager = SchemaManager::load_from_store(store, &path).await?;
3145 manager.add_label("Person")?;
3146
3147 let initial = IndexDefinition::Scalar(ScalarIndexConfig {
3148 name: "idx_test".to_string(),
3149 label: "Person".to_string(),
3150 properties: vec!["name".to_string()],
3151 index_type: ScalarIndexType::BTree,
3152 where_clause: None,
3153 metadata: IndexMetadata {
3154 status: IndexStatus::Building,
3155 ..Default::default()
3156 },
3157 });
3158 manager.add_index(initial.clone())?;
3159 assert_eq!(manager.schema().indexes.len(), 1);
3160
3161 manager.add_index(initial.clone())?;
3163 assert_eq!(
3164 manager.schema().indexes.len(),
3165 1,
3166 "duplicate add_index by name must not append"
3167 );
3168
3169 let mut updated_cfg = match initial {
3171 IndexDefinition::Scalar(c) => c,
3172 _ => unreachable!(),
3173 };
3174 updated_cfg.metadata.status = IndexStatus::Online;
3175 updated_cfg.metadata.row_count_at_build = Some(42);
3176 manager.add_index(IndexDefinition::Scalar(updated_cfg))?;
3177 assert_eq!(manager.schema().indexes.len(), 1);
3178 let stored = manager.get_index("idx_test").unwrap();
3179 assert_eq!(stored.metadata().status, IndexStatus::Online);
3180 assert_eq!(stored.metadata().row_count_at_build, Some(42));
3181
3182 let other = IndexDefinition::Scalar(ScalarIndexConfig {
3184 name: "idx_other".to_string(),
3185 label: "Person".to_string(),
3186 properties: vec!["age".to_string()],
3187 index_type: ScalarIndexType::BTree,
3188 where_clause: None,
3189 metadata: IndexMetadata::default(),
3190 });
3191 manager.add_index(other)?;
3192 assert_eq!(manager.schema().indexes.len(), 2);
3193
3194 Ok(())
3195 }
3196
3197 #[tokio::test]
3200 async fn test_load_dedups_bloated_indexes() -> Result<()> {
3201 let dir = tempdir()?;
3202 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path())?);
3203 let path = ObjectStorePath::from("schema.json");
3204
3205 let mut schema = Schema::default();
3209 schema.labels.insert(
3210 "Person".to_string(),
3211 LabelMeta {
3212 id: 1,
3213 created_at: chrono::Utc::now(),
3214 state: SchemaElementState::Active,
3215 description: None,
3216 },
3217 );
3218 let make = |status: IndexStatus, count: Option<u64>| {
3219 IndexDefinition::Scalar(ScalarIndexConfig {
3220 name: "idx_dup".to_string(),
3221 label: "Person".to_string(),
3222 properties: vec!["name".to_string()],
3223 index_type: ScalarIndexType::BTree,
3224 where_clause: None,
3225 metadata: IndexMetadata {
3226 status,
3227 row_count_at_build: count,
3228 ..Default::default()
3229 },
3230 })
3231 };
3232 for _ in 0..49 {
3233 schema.indexes.push(make(IndexStatus::Building, None));
3234 }
3235 schema.indexes.push(make(IndexStatus::Online, Some(123)));
3236 let json = serde_json::to_string_pretty(&schema)?;
3237 store.put(&path, json.into()).await?;
3238
3239 let manager = SchemaManager::load_from_store(store, &path).await?;
3240 let schema = manager.schema();
3241 assert_eq!(
3242 schema.indexes.len(),
3243 1,
3244 "load() must collapse 50 duplicates by name to 1"
3245 );
3246 assert_eq!(schema.indexes[0].metadata().status, IndexStatus::Online);
3248 assert_eq!(schema.indexes[0].metadata().row_count_at_build, Some(123));
3249
3250 Ok(())
3251 }
3252
3253 #[test]
3254 fn test_vector_index_for_property_skips_non_online() {
3255 let mut schema = Schema::default();
3256 schema.labels.insert(
3257 "Document".to_string(),
3258 LabelMeta {
3259 id: 1,
3260 created_at: chrono::Utc::now(),
3261 state: SchemaElementState::Active,
3262 description: None,
3263 },
3264 );
3265
3266 schema
3268 .indexes
3269 .push(IndexDefinition::Vector(VectorIndexConfig {
3270 name: "vec_doc_embedding".to_string(),
3271 label: "Document".to_string(),
3272 property: "embedding".to_string(),
3273 index_type: VectorIndexType::Flat,
3274 metric: DistanceMetric::Cosine,
3275 embedding_config: None,
3276 metadata: IndexMetadata {
3277 status: IndexStatus::Stale,
3278 ..Default::default()
3279 },
3280 }));
3281
3282 assert!(
3284 schema
3285 .vector_index_for_property("Document", "embedding")
3286 .is_none()
3287 );
3288
3289 if let IndexDefinition::Vector(cfg) = &mut schema.indexes[0] {
3291 cfg.metadata.status = IndexStatus::Online;
3292 }
3293 let result = schema.vector_index_for_property("Document", "embedding");
3294 assert!(result.is_some());
3295 assert_eq!(result.unwrap().metric, DistanceMetric::Cosine);
3296 }
3297
3298 #[tokio::test]
3299 async fn with_overlay_empty_clones_primary_in_isolation() -> Result<()> {
3300 use crate::core::fork::SchemaDelta;
3301
3302 let dir = tempdir()?;
3303 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path())?);
3304 let path = ObjectStorePath::from("schema.json");
3305 let primary = SchemaManager::load_from_store(store, &path).await?;
3306 primary.add_label("Person")?;
3307
3308 let overlay = primary.with_overlay(&SchemaDelta::empty());
3309 assert_eq!(overlay.schema().labels.len(), 1);
3310
3311 overlay.add_label("Forked")?;
3314 assert!(overlay.schema().labels.contains_key("Forked"));
3315 assert!(!primary.schema().labels.contains_key("Forked"));
3316
3317 Ok(())
3318 }
3319
3320 #[tokio::test]
3321 async fn with_overlay_merges_added_labels_and_edge_types() -> Result<()> {
3322 use crate::core::fork::SchemaDelta;
3323 use chrono::Utc;
3324
3325 let dir = tempdir()?;
3326 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path())?);
3327 let path = ObjectStorePath::from("schema.json");
3328 let primary = SchemaManager::load_from_store(store, &path).await?;
3329 primary.add_label("Existing")?;
3330
3331 let label_meta = LabelMeta {
3332 id: 99,
3333 created_at: Utc::now(),
3334 state: SchemaElementState::Active,
3335 description: None,
3336 };
3337 let edge_meta = EdgeTypeMeta {
3338 id: 99,
3339 src_labels: vec!["NewLabel".into()],
3340 dst_labels: vec!["NewLabel".into()],
3341 state: SchemaElementState::Active,
3342 description: None,
3343 };
3344 let delta = SchemaDelta {
3345 added_labels: vec![("NewLabel".to_string(), label_meta)],
3346 added_edge_types: vec![("NewEdge".to_string(), edge_meta)],
3347 added_properties: vec![],
3348 };
3349
3350 let overlay = primary.with_overlay(&delta);
3351 let merged = overlay.schema();
3352 assert!(merged.labels.contains_key("Existing"));
3353 assert!(merged.labels.contains_key("NewLabel"));
3354 assert!(merged.edge_types.contains_key("NewEdge"));
3355
3356 assert!(!primary.schema().labels.contains_key("NewLabel"));
3358 Ok(())
3359 }
3360
3361 #[tokio::test]
3366 async fn test_get_or_assign_edge_type_id_concurrent() -> Result<()> {
3367 let dir = tempdir()?;
3368 let store = Arc::new(LocalFileSystem::new_with_prefix(dir.path())?);
3369 let path = ObjectStorePath::from("schema.json");
3370 let manager = Arc::new(SchemaManager::load_from_store(store, &path).await?);
3371
3372 let mut handles = Vec::new();
3373 for _ in 0..16 {
3374 let m = manager.clone();
3375 handles.push(std::thread::spawn(move || {
3376 m.get_or_assign_edge_type_id("RACED")
3377 }));
3378 }
3379 let ids: Vec<u32> = handles.into_iter().map(|h| h.join().unwrap()).collect();
3380 assert!(
3381 ids.iter().all(|&id| id == ids[0]),
3382 "all racers must observe one id, got {ids:?}"
3383 );
3384 assert_eq!(manager.get_or_assign_edge_type_id("RACED"), ids[0]);
3386
3387 manager.add_label("A")?;
3389 let declared = manager.add_edge_type("DECLARED", vec!["A".into()], vec!["A".into()])?;
3390 assert_eq!(manager.get_or_assign_edge_type_id("DECLARED"), declared);
3391 Ok(())
3392 }
3393
3394 #[test]
3399 fn test_new_schemaless_edge_type_bumps_schema_version() {
3400 let mut schema = Schema::default();
3401 let v0 = schema.schema_version;
3402
3403 let id1 = schema.get_or_assign_edge_type_id("FRESH");
3404 assert_eq!(
3405 schema.schema_version,
3406 v0.wrapping_add(1),
3407 "minting a new edge type must bump schema_version"
3408 );
3409
3410 let id1_again = schema.get_or_assign_edge_type_id("FRESH");
3412 assert_eq!(id1, id1_again);
3413 assert_eq!(
3414 schema.schema_version,
3415 v0.wrapping_add(1),
3416 "resolving an existing edge type must not bump schema_version"
3417 );
3418
3419 let _id2 = schema.get_or_assign_edge_type_id("OTHER");
3421 assert_eq!(
3422 schema.schema_version,
3423 v0.wrapping_add(2),
3424 "a second new edge type must bump schema_version again"
3425 );
3426 }
3427
3428 #[test]
3432 fn validate_schema_element_name_rejects_unsafe() {
3433 for bad in ["", " ", "a/b", "a b", "a\nb", "a\\b", "x\0y"] {
3434 assert!(
3435 SchemaManager::validate_schema_element_name("Label", bad).is_err(),
3436 "expected {bad:?} to be rejected"
3437 );
3438 }
3439 for good in ["Person", "My.Label", "edge_2", "KNOWS"] {
3440 assert!(
3441 SchemaManager::validate_schema_element_name("Label", good).is_ok(),
3442 "expected {good:?} to be accepted"
3443 );
3444 }
3445 let long = "x".repeat(MAX_SCHEMA_NAME_LEN + 1);
3447 assert!(SchemaManager::validate_schema_element_name("Label", &long).is_err());
3448 }
3449}