selene_graph/mutator/
composite_property_index.rs1use selene_core::{Change, DbString, SchemaChange, SchemaPropertyIndexKind};
4use smallvec::SmallVec;
5
6use crate::graph::{CompositePropertyIndexEntry, composite_property_key};
7use crate::schema_index_kind::schema_kind_from;
8use crate::{GraphError, GraphResult, Mutator, TypedIndexKind};
9
10impl<'tx, 'g> Mutator<'tx, 'g> {
11 pub fn create_composite_property_index_named(
18 &mut self,
19 label: DbString,
20 properties: SmallVec<[DbString; 4]>,
21 kinds: SmallVec<[TypedIndexKind; 4]>,
22 name: Option<DbString>,
23 ) -> GraphResult<()> {
24 validate_shape(&properties, &kinds)?;
25 let key = composite_property_key(&properties);
26 if self
27 .txn
28 .read()
29 .composite_property_index
30 .contains_key(&(label.clone(), key.clone()))
31 {
32 return Err(GraphError::CompositePropertyIndexAlreadyExists {
33 label,
34 properties: Box::new(properties),
35 });
36 }
37 let graph_id = self.txn.read().graph_id();
38 let index = crate::composite_property_index::build_composite_property_index(
39 self.txn.read(),
40 label.clone(),
41 properties.clone(),
42 kinds.clone(),
43 )?;
44 self.txn.guard_mut().composite_property_index.insert(
45 (label.clone(), key),
46 CompositePropertyIndexEntry::new(index, properties.clone(), name.clone()),
47 );
48 self.txn.changes.push(Change::SchemaChanged {
49 graph: graph_id,
50 change: SchemaChange::CompositePropertyIndexCreated {
51 label,
52 properties,
53 kinds: schema_kinds_from(&kinds),
54 name,
55 },
56 });
57 Ok(())
58 }
59
60 pub fn drop_composite_property_index(
65 &mut self,
66 label: DbString,
67 properties: SmallVec<[DbString; 4]>,
68 ) -> GraphResult<()> {
69 let key = composite_property_key(&properties);
70 if !self
71 .txn
72 .read()
73 .composite_property_index
74 .contains_key(&(label.clone(), key.clone()))
75 {
76 return Ok(());
77 }
78 let graph_id = self.txn.read().graph_id();
79 self.txn
80 .guard_mut()
81 .composite_property_index
82 .remove(&(label.clone(), key));
83 self.txn.changes.push(Change::SchemaChanged {
84 graph: graph_id,
85 change: SchemaChange::CompositePropertyIndexDropped { label, properties },
86 });
87 Ok(())
88 }
89}
90
91fn validate_shape(properties: &[DbString], kinds: &[TypedIndexKind]) -> Result<(), GraphError> {
92 if properties.len() < 2 {
93 return Err(GraphError::Inconsistent {
94 reason: "composite index requires at least two properties".to_owned(),
95 });
96 }
97 if properties.len() != kinds.len() {
98 return Err(GraphError::Inconsistent {
99 reason: format!(
100 "composite index has {} properties but {} kinds",
101 properties.len(),
102 kinds.len()
103 ),
104 });
105 }
106 let mut key = properties.to_vec();
107 key.sort();
108 key.dedup();
109 if key.len() != properties.len() {
110 return Err(GraphError::Inconsistent {
111 reason: "composite index property list contains duplicates".to_owned(),
112 });
113 }
114 Ok(())
115}
116
117fn schema_kinds_from(kinds: &[TypedIndexKind]) -> SmallVec<[SchemaPropertyIndexKind; 4]> {
118 kinds.iter().copied().map(schema_kind_from).collect()
119}
120
121#[cfg(test)]
122mod tests {
123 use selene_core::{GraphId, db_string};
124 use smallvec::smallvec;
125
126 use crate::{GraphError, SharedGraph, TypedIndexKind};
127
128 #[test]
129 fn create_composite_property_index_rejects_empty_property_list() {
130 let shared = SharedGraph::new(GraphId::new(140_201));
131 let mut txn = shared.begin_write();
132 let err = txn
133 .mutator()
134 .create_composite_property_index_named(
135 db_string("CompositeShape").unwrap(),
136 smallvec![],
137 smallvec![],
138 None,
139 )
140 .unwrap_err();
141
142 assert!(matches!(
143 err,
144 GraphError::Inconsistent { reason }
145 if reason == "composite index requires at least two properties"
146 ));
147 }
148
149 #[test]
150 fn create_composite_property_index_rejects_single_property_list() {
151 let shared = SharedGraph::new(GraphId::new(140_202));
152 let mut txn = shared.begin_write();
153 let err = txn
154 .mutator()
155 .create_composite_property_index_named(
156 db_string("CompositeShape").unwrap(),
157 smallvec![db_string("only").unwrap()],
158 smallvec![TypedIndexKind::I64],
159 None,
160 )
161 .unwrap_err();
162
163 assert!(matches!(
164 err,
165 GraphError::Inconsistent { reason }
166 if reason == "composite index requires at least two properties"
167 ));
168 }
169}