1use std::collections::BTreeMap;
7
8use marsdb_storage::{ReadableMultimapTable, ReadableTable, Txn, WriteTransaction};
9use serde::{Deserialize, Serialize};
10
11use crate::error::GraphError;
12use crate::labels::{lookup_label_id, resolve_label};
13use crate::model::{NodeId, PropertyValue};
14use crate::props::{lookup_prop_id, resolve_prop};
15
16#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
17pub struct IndexDef {
18 pub unique: bool,
19}
20
21fn index_prefix(label_id: u32, prop_id: u32) -> [u8; 8] {
25 let mut out = [0u8; 8];
26 out[0..4].copy_from_slice(&label_id.to_be_bytes());
27 out[4..8].copy_from_slice(&prop_id.to_be_bytes());
28 out
29}
30
31pub(crate) fn encode_index_value(v: &PropertyValue) -> Vec<u8> {
40 match v {
41 PropertyValue::Null => vec![0x00],
42 PropertyValue::Bool(b) => vec![0x01, u8::from(*b)],
43 PropertyValue::Int(i) => {
48 let mut out = vec![0x02];
49 out.extend_from_slice(&((*i as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
50 out
51 }
52 PropertyValue::Float(f) => {
56 let bits = f.to_bits();
57 let sortable = if bits & 0x8000_0000_0000_0000 != 0 {
58 !bits
59 } else {
60 bits | 0x8000_0000_0000_0000
61 };
62 let mut out = vec![0x03];
63 out.extend_from_slice(&sortable.to_be_bytes());
64 out
65 }
66 PropertyValue::String(s) => {
70 let mut out = vec![0x04];
71 out.extend_from_slice(s.as_bytes());
72 out
73 }
74 PropertyValue::Date(days) => {
75 let mut out = vec![0x05];
76 out.extend_from_slice(&((*days as i64 as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
77 out
78 }
79 PropertyValue::Duration {
80 months,
81 days,
82 seconds,
83 nanos,
84 } => {
85 let mut out = vec![0x06];
86 out.extend_from_slice(&months.to_be_bytes());
87 out.extend_from_slice(&days.to_be_bytes());
88 out.extend_from_slice(&seconds.to_be_bytes());
89 out.extend_from_slice(&nanos.to_be_bytes());
90 out
91 }
92 PropertyValue::LocalTime(nanos_of_day) => {
95 let mut out = vec![0x07];
96 out.extend_from_slice(&nanos_of_day.to_be_bytes());
97 out
98 }
99 PropertyValue::Time {
105 nanos_of_day,
106 offset_seconds,
107 } => {
108 let instant = nanos_of_day - *offset_seconds as i64 * 1_000_000_000;
109 let mut out = vec![0x08];
110 out.extend_from_slice(&((instant as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
111 out
112 }
113 PropertyValue::LocalDateTime {
114 epoch_seconds,
115 nanos,
116 } => {
117 let mut out = vec![0x09];
118 out.extend_from_slice(&((*epoch_seconds as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
119 out.extend_from_slice(&nanos.to_be_bytes());
120 out
121 }
122 PropertyValue::DateTime {
126 epoch_seconds,
127 nanos,
128 ..
129 } => {
130 let mut out = vec![0x0A];
131 out.extend_from_slice(&((*epoch_seconds as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
132 out.extend_from_slice(&nanos.to_be_bytes());
133 out
134 }
135 PropertyValue::List(items) => {
145 let mut out = vec![0x0B];
146 for item in items {
147 let encoded = encode_index_value(item);
148 out.extend_from_slice(&(encoded.len() as u32).to_be_bytes());
149 out.extend_from_slice(&encoded);
150 }
151 out
152 }
153 PropertyValue::Map(_) => {
158 unreachable!("PropertyValue::Map is never a real stored/indexed property value")
159 }
160 }
161}
162
163fn index_key(label_id: u32, prop_id: u32, value: &PropertyValue) -> Vec<u8> {
164 let mut out = index_prefix(label_id, prop_id).to_vec();
165 out.extend_from_slice(&encode_index_value(value));
166 out
167}
168
169pub fn create_index(
176 write_txn: &WriteTransaction,
177 label: &str,
178 prop: &str,
179 unique: bool,
180) -> Result<(), GraphError> {
181 let label_id = crate::labels::intern_label(write_txn, label)?;
182 let prop_id = crate::props::intern_prop(write_txn, prop)?;
183 let prefix = index_prefix(label_id, prop_id);
184 {
185 let defs = write_txn.open_table(marsdb_storage::tables::INDEX_DEFS)?;
186 if defs.get(prefix.as_slice())?.is_some() {
187 return Err(GraphError::CorruptData(format!(
188 "index on label {label:?} property {prop:?} already exists"
189 )));
190 }
191 }
192
193 let label_index = write_txn.open_multimap_table(marsdb_storage::tables::NODE_LABEL_INDEX)?;
200 let node_ids: Vec<u64> = label_index
201 .get(label_id)?
202 .map(|entry| entry.map(|value| value.value()).map_err(GraphError::from))
203 .collect::<Result<Vec<_>, GraphError>>()?;
204 drop(label_index);
205 let mut entries: Vec<(Vec<u8>, u64)> = Vec::with_capacity(node_ids.len());
206 {
207 let nodes = write_txn.open_table(marsdb_storage::tables::NODES)?;
208 for node_id in &node_ids {
209 let Some(guard) = nodes.get(*node_id)? else {
210 continue;
211 };
212 let record: crate::encode::NodeRecord = crate::encode::decode(guard.value())?;
213 if let Some(value) = record.props.get(prop) {
214 entries.push((index_key(label_id, prop_id, value), *node_id));
215 }
216 }
217 }
218 if unique {
219 let mut seen = std::collections::HashSet::with_capacity(entries.len());
220 for (key, _) in &entries {
221 if !seen.insert(key.clone()) {
222 return Err(GraphError::UniqueConstraintViolation {
223 label: label.to_string(),
224 property: prop.to_string(),
225 });
226 }
227 }
228 }
229
230 {
231 let mut defs = write_txn.open_table(marsdb_storage::tables::INDEX_DEFS)?;
232 let encoded = postcard::to_allocvec(&IndexDef { unique })?;
233 defs.insert(prefix.as_slice(), encoded.as_slice())?;
234 }
235 {
236 let mut index = write_txn.open_multimap_table(marsdb_storage::tables::PROPERTY_INDEX)?;
237 for (key, node_id) in entries {
238 index.insert(key.as_slice(), node_id)?;
239 }
240 }
241 Ok(())
242}
243
244pub fn lookup_index_def(txn: Txn, label: &str, prop: &str) -> Result<Option<IndexDef>, GraphError> {
246 let Some(label_id) = lookup_label_id(txn, label)? else {
247 return Ok(None);
248 };
249 let Some(prop_id) = lookup_prop_id(txn, prop)? else {
250 return Ok(None);
251 };
252 let prefix = index_prefix(label_id, prop_id);
253 let defs = txn.open_table(marsdb_storage::tables::INDEX_DEFS)?;
254 let found = defs
255 .get(prefix.as_slice())?
256 .map(|guard| guard.value().to_vec());
257 drop(defs);
258 match found {
259 Some(bytes) => Ok(Some(postcard::from_bytes(&bytes)?)),
260 None => Ok(None),
261 }
262}
263
264pub fn lookup_exact(
277 txn: Txn,
278 label: &str,
279 prop: &str,
280 value: &PropertyValue,
281 limit: Option<usize>,
282) -> Result<Vec<NodeId>, GraphError> {
283 let Some(label_id) = lookup_label_id(txn, label)? else {
284 return Ok(Vec::new());
285 };
286 let Some(prop_id) = lookup_prop_id(txn, prop)? else {
287 return Ok(Vec::new());
288 };
289 let key = index_key(label_id, prop_id, value);
290 let index = txn.open_multimap_table(marsdb_storage::tables::PROPERTY_INDEX)?;
291 let iter = index.get(key.as_slice())?;
292 let ids: Vec<NodeId> = match limit {
293 Some(limit) => iter
294 .take(limit)
295 .map(|entry| {
296 entry
297 .map(|value| NodeId(value.value()))
298 .map_err(GraphError::from)
299 })
300 .collect::<Result<Vec<_>, GraphError>>()?,
301 None => iter
302 .map(|entry| {
303 entry
304 .map(|value| NodeId(value.value()))
305 .map_err(GraphError::from)
306 })
307 .collect::<Result<Vec<_>, GraphError>>()?,
308 };
309 drop(index);
310 Ok(ids)
311}
312
313pub fn match_count(
322 txn: Txn,
323 label: &str,
324 prop: &str,
325 value: &PropertyValue,
326) -> Result<u64, GraphError> {
327 let Some(label_id) = lookup_label_id(txn, label)? else {
328 return Ok(0);
329 };
330 let Some(prop_id) = lookup_prop_id(txn, prop)? else {
331 return Ok(0);
332 };
333 let key = index_key(label_id, prop_id, value);
334 let index = txn.open_multimap_table(marsdb_storage::tables::PROPERTY_INDEX)?;
335 let count = index.get(key.as_slice())?.len();
336 Ok(count)
337}
338
339fn indexes_for_labels(
345 txn: Txn,
346 label_ids: &[u32],
347) -> Result<Vec<(u32, u32, String, IndexDef)>, GraphError> {
348 let defs = match txn.open_table(marsdb_storage::tables::INDEX_DEFS) {
349 Ok(table) => table,
350 Err(marsdb_storage::StorageError::Table(redb::TableError::TableDoesNotExist(_))) => {
351 return Ok(Vec::new())
352 }
353 Err(e) => return Err(e.into()),
354 };
355 let mut out = Vec::new();
356 for entry in defs.iter()? {
357 let (key, value) = entry?;
358 let key_bytes = key.value();
359 let label_id = u32::from_be_bytes(
360 key_bytes[0..4]
361 .try_into()
362 .expect("index key prefix is 8 bytes"),
363 );
364 if !label_ids.contains(&label_id) {
365 continue;
366 }
367 let prop_id = u32::from_be_bytes(
368 key_bytes[4..8]
369 .try_into()
370 .expect("index key prefix is 8 bytes"),
371 );
372 let def: IndexDef = postcard::from_bytes(value.value())?;
373 let prop_name = resolve_prop(txn, prop_id)?;
374 out.push((label_id, prop_id, prop_name, def));
375 }
376 Ok(out)
377}
378
379struct IndexTarget<'a> {
384 label_id: u32,
385 prop_id: u32,
386 label: &'a str,
387 prop: &'a str,
388}
389
390fn insert_entry(
391 write_txn: &WriteTransaction,
392 target: &IndexTarget<'_>,
393 value: &PropertyValue,
394 node_id: u64,
395 unique: bool,
396) -> Result<(), GraphError> {
397 let key = index_key(target.label_id, target.prop_id, value);
398 if unique {
399 let index = write_txn.open_multimap_table(marsdb_storage::tables::PROPERTY_INDEX)?;
400 let exists = index.get(key.as_slice())?.next().is_some();
401 drop(index);
402 if exists {
403 return Err(GraphError::UniqueConstraintViolation {
404 label: target.label.to_string(),
405 property: target.prop.to_string(),
406 });
407 }
408 }
409 let mut index = write_txn.open_multimap_table(marsdb_storage::tables::PROPERTY_INDEX)?;
410 index.insert(key.as_slice(), node_id)?;
411 Ok(())
412}
413
414fn remove_entry(
415 write_txn: &WriteTransaction,
416 label_id: u32,
417 prop_id: u32,
418 value: &PropertyValue,
419 node_id: u64,
420) -> Result<(), GraphError> {
421 let key = index_key(label_id, prop_id, value);
422 let mut index = write_txn.open_multimap_table(marsdb_storage::tables::PROPERTY_INDEX)?;
423 index.remove(key.as_slice(), node_id)?;
424 Ok(())
425}
426
427pub fn on_node_created(
434 write_txn: &WriteTransaction,
435 node_id: u64,
436 label_ids: &[u32],
437 props: &BTreeMap<String, PropertyValue>,
438) -> Result<(), GraphError> {
439 for (label_id, prop_id, prop_name, def) in indexes_for_labels(Txn::Write(write_txn), label_ids)?
440 {
441 if let Some(value) = props.get(&prop_name) {
442 let label = resolve_label(Txn::Write(write_txn), label_id)?;
443 let target = IndexTarget {
444 label_id,
445 prop_id,
446 label: &label,
447 prop: &prop_name,
448 };
449 insert_entry(write_txn, &target, value, node_id, def.unique)?;
450 }
451 }
452 Ok(())
453}
454
455pub fn on_node_deleted(
461 write_txn: &WriteTransaction,
462 node_id: u64,
463 label_ids: &[u32],
464 props: &BTreeMap<String, PropertyValue>,
465) -> Result<(), GraphError> {
466 for (label_id, prop_id, prop_name, _def) in
467 indexes_for_labels(Txn::Write(write_txn), label_ids)?
468 {
469 if let Some(value) = props.get(&prop_name) {
470 remove_entry(write_txn, label_id, prop_id, value, node_id)?;
471 }
472 }
473 Ok(())
474}
475
476pub fn on_node_prop_changed(
486 write_txn: &WriteTransaction,
487 node_id: u64,
488 label_ids: &[u32],
489 prop: &str,
490 old_value: Option<&PropertyValue>,
491 new_value: Option<&PropertyValue>,
492) -> Result<(), GraphError> {
493 for (label_id, prop_id, prop_name, def) in indexes_for_labels(Txn::Write(write_txn), label_ids)?
494 {
495 if prop_name != prop {
496 continue;
497 }
498 if let Some(old) = old_value {
499 remove_entry(write_txn, label_id, prop_id, old, node_id)?;
500 }
501 if let Some(new) = new_value {
502 let label = resolve_label(Txn::Write(write_txn), label_id)?;
503 let target = IndexTarget {
504 label_id,
505 prop_id,
506 label: &label,
507 prop: &prop_name,
508 };
509 insert_entry(write_txn, &target, new, node_id, def.unique)?;
510 }
511 }
512 Ok(())
513}