1use std::collections::BTreeMap;
7
8use marsdb_storage::{ReadableMultimapTable, ReadableTable, Txn};
9use serde::{Deserialize, Serialize};
10
11use crate::error::GraphError;
12use crate::labels::lookup_label_id;
13use crate::model::{NodeId, PropertyValue};
14use crate::props::lookup_prop_id;
15use crate::write_ctx::WriteCtx;
16
17#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
18pub struct IndexDef {
19 pub unique: bool,
20}
21
22fn index_prefix(label_id: u32, prop_id: u32) -> [u8; 8] {
26 let mut out = [0u8; 8];
27 out[0..4].copy_from_slice(&label_id.to_be_bytes());
28 out[4..8].copy_from_slice(&prop_id.to_be_bytes());
29 out
30}
31
32pub(crate) fn encode_index_value(v: &PropertyValue) -> Vec<u8> {
41 match v {
42 PropertyValue::Null => vec![0x00],
43 PropertyValue::Bool(b) => vec![0x01, u8::from(*b)],
44 PropertyValue::Int(i) => {
49 let mut out = vec![0x02];
50 out.extend_from_slice(&((*i as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
51 out
52 }
53 PropertyValue::Float(f) => {
57 let bits = f.to_bits();
58 let sortable = if bits & 0x8000_0000_0000_0000 != 0 {
59 !bits
60 } else {
61 bits | 0x8000_0000_0000_0000
62 };
63 let mut out = vec![0x03];
64 out.extend_from_slice(&sortable.to_be_bytes());
65 out
66 }
67 PropertyValue::String(s) => {
71 let mut out = vec![0x04];
72 out.extend_from_slice(s.as_bytes());
73 out
74 }
75 PropertyValue::Date(days) => {
76 let mut out = vec![0x05];
77 out.extend_from_slice(&((*days as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
78 out
79 }
80 PropertyValue::Duration {
81 months,
82 days,
83 seconds,
84 nanos,
85 } => {
86 let mut out = vec![0x06];
87 out.extend_from_slice(&months.to_be_bytes());
88 out.extend_from_slice(&days.to_be_bytes());
89 out.extend_from_slice(&seconds.to_be_bytes());
90 out.extend_from_slice(&nanos.to_be_bytes());
91 out
92 }
93 PropertyValue::LocalTime(nanos_of_day) => {
96 let mut out = vec![0x07];
97 out.extend_from_slice(&nanos_of_day.to_be_bytes());
98 out
99 }
100 PropertyValue::Time {
106 nanos_of_day,
107 offset_seconds,
108 } => {
109 let instant = nanos_of_day - *offset_seconds as i64 * 1_000_000_000;
110 let mut out = vec![0x08];
111 out.extend_from_slice(&((instant as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
112 out
113 }
114 PropertyValue::LocalDateTime {
115 epoch_seconds,
116 nanos,
117 } => {
118 let mut out = vec![0x09];
119 out.extend_from_slice(&((*epoch_seconds as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
120 out.extend_from_slice(&nanos.to_be_bytes());
121 out
122 }
123 PropertyValue::DateTime {
127 epoch_seconds,
128 nanos,
129 ..
130 } => {
131 let mut out = vec![0x0A];
132 out.extend_from_slice(&((*epoch_seconds as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
133 out.extend_from_slice(&nanos.to_be_bytes());
134 out
135 }
136 PropertyValue::List(items) => {
146 let mut out = vec![0x0B];
147 for item in items {
148 let encoded = encode_index_value(item);
149 out.extend_from_slice(&(encoded.len() as u32).to_be_bytes());
150 out.extend_from_slice(&encoded);
151 }
152 out
153 }
154 PropertyValue::Map(_) => {
159 unreachable!("PropertyValue::Map is never a real stored/indexed property value")
160 }
161 }
162}
163
164fn index_key(label_id: u32, prop_id: u32, value: &PropertyValue) -> Vec<u8> {
165 let mut out = index_prefix(label_id, prop_id).to_vec();
166 out.extend_from_slice(&encode_index_value(value));
167 out
168}
169
170pub(crate) fn create_index(
177 ctx: &mut WriteCtx,
178 label: &str,
179 prop: &str,
180 unique: bool,
181) -> Result<(), GraphError> {
182 let label_id = crate::labels::intern_label(ctx, label)?;
183 let prop_id = crate::props::intern_prop(ctx, prop)?;
184 let prefix = index_prefix(label_id, prop_id);
185 if ctx.index_defs()?.get(prefix.as_slice())?.is_some() {
186 return Err(GraphError::CorruptData(format!(
187 "index on label {label:?} property {prop:?} already exists"
188 )));
189 }
190
191 let node_ids: Vec<u64> = ctx
198 .node_label_index()?
199 .get(label_id)?
200 .map(|entry| entry.map(|value| value.value()).map_err(GraphError::from))
201 .collect::<Result<Vec<_>, GraphError>>()?;
202 let mut entries: Vec<(Vec<u8>, u64)> = Vec::with_capacity(node_ids.len());
203 for node_id in &node_ids {
204 let Some(guard) = ctx.nodes()?.get(*node_id)? else {
205 continue;
206 };
207 if let Some(raw) = crate::encode::node_prop_raw(guard.value(), prop_id)? {
211 let value = crate::encode::decode_value(raw)?;
212 entries.push((index_key(label_id, prop_id, &value), *node_id));
213 }
214 }
215 if unique {
216 let mut seen = std::collections::HashSet::with_capacity(entries.len());
217 for (key, _) in &entries {
218 if !seen.insert(key.clone()) {
219 return Err(GraphError::UniqueConstraintViolation {
220 label: label.to_string(),
221 property: prop.to_string(),
222 });
223 }
224 }
225 }
226
227 let encoded = postcard::to_allocvec(&IndexDef { unique })?;
228 ctx.index_defs()?
229 .insert(prefix.as_slice(), encoded.as_slice())?;
230 for (key, node_id) in entries {
231 ctx.property_index()?.insert(key.as_slice(), node_id)?;
232 }
233 Ok(())
234}
235
236pub fn lookup_index_def(txn: Txn, label: &str, prop: &str) -> Result<Option<IndexDef>, GraphError> {
238 let Some(label_id) = lookup_label_id(txn, label)? else {
239 return Ok(None);
240 };
241 let Some(prop_id) = lookup_prop_id(txn, prop)? else {
242 return Ok(None);
243 };
244 let prefix = index_prefix(label_id, prop_id);
245 let defs = txn.open_table(marsdb_storage::tables::INDEX_DEFS)?;
246 let found = defs
247 .get(prefix.as_slice())?
248 .map(|guard| guard.value().to_vec());
249 drop(defs);
250 match found {
251 Some(bytes) => Ok(Some(postcard::from_bytes(&bytes)?)),
252 None => Ok(None),
253 }
254}
255
256pub fn lookup_exact(
269 txn: Txn,
270 label: &str,
271 prop: &str,
272 value: &PropertyValue,
273 limit: Option<usize>,
274) -> Result<Vec<NodeId>, GraphError> {
275 let Some(label_id) = lookup_label_id(txn, label)? else {
276 return Ok(Vec::new());
277 };
278 let Some(prop_id) = lookup_prop_id(txn, prop)? else {
279 return Ok(Vec::new());
280 };
281 let key = index_key(label_id, prop_id, value);
282 let index = txn.open_multimap_table(marsdb_storage::tables::PROPERTY_INDEX)?;
283 let iter = index.get(key.as_slice())?;
284 let ids: Vec<NodeId> = match limit {
285 Some(limit) => iter
286 .take(limit)
287 .map(|entry| {
288 entry
289 .map(|value| NodeId(value.value()))
290 .map_err(GraphError::from)
291 })
292 .collect::<Result<Vec<_>, GraphError>>()?,
293 None => iter
294 .map(|entry| {
295 entry
296 .map(|value| NodeId(value.value()))
297 .map_err(GraphError::from)
298 })
299 .collect::<Result<Vec<_>, GraphError>>()?,
300 };
301 drop(index);
302 Ok(ids)
303}
304
305pub fn match_count(
314 txn: Txn,
315 label: &str,
316 prop: &str,
317 value: &PropertyValue,
318) -> Result<u64, GraphError> {
319 let Some(label_id) = lookup_label_id(txn, label)? else {
320 return Ok(0);
321 };
322 let Some(prop_id) = lookup_prop_id(txn, prop)? else {
323 return Ok(0);
324 };
325 let key = index_key(label_id, prop_id, value);
326 let index = txn.open_multimap_table(marsdb_storage::tables::PROPERTY_INDEX)?;
327 let count = index.get(key.as_slice())?.len();
328 Ok(count)
329}
330
331fn indexes_for_labels(
337 ctx: &mut WriteCtx,
338 label_ids: &[u32],
339) -> Result<Vec<(u32, u32, String, IndexDef)>, GraphError> {
340 let raw: Vec<(u32, u32, IndexDef)> = {
346 let mut raw = Vec::new();
347 for entry in ctx.index_defs()?.iter()? {
348 let (key, value) = entry?;
349 let key_bytes = key.value();
350 let label_id = u32::from_be_bytes(
351 key_bytes[0..4]
352 .try_into()
353 .expect("index key prefix is 8 bytes"),
354 );
355 if !label_ids.contains(&label_id) {
356 continue;
357 }
358 let prop_id = u32::from_be_bytes(
359 key_bytes[4..8]
360 .try_into()
361 .expect("index key prefix is 8 bytes"),
362 );
363 let def: IndexDef = postcard::from_bytes(value.value())?;
364 raw.push((label_id, prop_id, def));
365 }
366 raw
367 };
368 raw.into_iter()
369 .map(|(label_id, prop_id, def)| {
370 let prop_name = resolve_prop_ctx(ctx, prop_id)?;
371 Ok((label_id, prop_id, prop_name, def))
372 })
373 .collect()
374}
375
376pub(crate) fn resolve_label_ctx(ctx: &mut WriteCtx, label_id: u32) -> Result<String, GraphError> {
384 let value = ctx.id_to_label()?.get(label_id)?.ok_or_else(|| {
385 GraphError::CorruptData(format!("label id {label_id} has no interned string"))
386 })?;
387 Ok(value.value().to_string())
388}
389
390pub(crate) fn resolve_prop_ctx(ctx: &mut WriteCtx, prop_id: u32) -> Result<String, GraphError> {
391 let value = ctx.id_to_prop()?.get(prop_id)?.ok_or_else(|| {
392 GraphError::CorruptData(format!("prop id {prop_id} has no interned string"))
393 })?;
394 Ok(value.value().to_string())
395}
396
397struct IndexTarget<'a> {
402 label_id: u32,
403 prop_id: u32,
404 label: &'a str,
405 prop: &'a str,
406}
407
408fn insert_entry(
409 ctx: &mut WriteCtx,
410 target: &IndexTarget<'_>,
411 value: &PropertyValue,
412 node_id: u64,
413 unique: bool,
414) -> Result<(), GraphError> {
415 let key = index_key(target.label_id, target.prop_id, value);
416 if unique && ctx.property_index()?.get(key.as_slice())?.next().is_some() {
417 return Err(GraphError::UniqueConstraintViolation {
418 label: target.label.to_string(),
419 property: target.prop.to_string(),
420 });
421 }
422 ctx.property_index()?.insert(key.as_slice(), node_id)?;
423 Ok(())
424}
425
426fn remove_entry(
427 ctx: &mut WriteCtx,
428 label_id: u32,
429 prop_id: u32,
430 value: &PropertyValue,
431 node_id: u64,
432) -> Result<(), GraphError> {
433 let key = index_key(label_id, prop_id, value);
434 ctx.property_index()?.remove(key.as_slice(), node_id)?;
435 Ok(())
436}
437
438pub(crate) fn on_node_created(
445 ctx: &mut WriteCtx,
446 node_id: u64,
447 label_ids: &[u32],
448 props: &BTreeMap<String, PropertyValue>,
449) -> Result<(), GraphError> {
450 for (label_id, prop_id, prop_name, def) in indexes_for_labels(ctx, label_ids)? {
451 if let Some(value) = props.get(&prop_name) {
452 let label = resolve_label_ctx(ctx, label_id)?;
453 let target = IndexTarget {
454 label_id,
455 prop_id,
456 label: &label,
457 prop: &prop_name,
458 };
459 insert_entry(ctx, &target, value, node_id, def.unique)?;
460 }
461 }
462 Ok(())
463}
464
465pub(crate) fn on_node_deleted(
471 ctx: &mut WriteCtx,
472 node_id: u64,
473 label_ids: &[u32],
474 props: &BTreeMap<String, PropertyValue>,
475) -> Result<(), GraphError> {
476 for (label_id, prop_id, prop_name, _def) in indexes_for_labels(ctx, label_ids)? {
477 if let Some(value) = props.get(&prop_name) {
478 remove_entry(ctx, label_id, prop_id, value, node_id)?;
479 }
480 }
481 Ok(())
482}
483
484pub(crate) fn on_node_prop_changed(
494 ctx: &mut WriteCtx,
495 node_id: u64,
496 label_ids: &[u32],
497 prop: &str,
498 old_value: Option<&PropertyValue>,
499 new_value: Option<&PropertyValue>,
500) -> Result<(), GraphError> {
501 for (label_id, prop_id, prop_name, def) in indexes_for_labels(ctx, label_ids)? {
502 if prop_name != prop {
503 continue;
504 }
505 if let Some(old) = old_value {
506 remove_entry(ctx, label_id, prop_id, old, node_id)?;
507 }
508 if let Some(new) = new_value {
509 let label = resolve_label_ctx(ctx, label_id)?;
510 let target = IndexTarget {
511 label_id,
512 prop_id,
513 label: &label,
514 prop: &prop_name,
515 };
516 insert_entry(ctx, &target, new, node_id, def.unique)?;
517 }
518 }
519 Ok(())
520}