use std::collections::BTreeMap;
use marsdb_storage::{ReadableMultimapTable, ReadableTable, Txn};
use serde::{Deserialize, Serialize};
use crate::error::GraphError;
use crate::labels::lookup_label_id;
use crate::model::{NodeId, PropertyValue};
use crate::props::lookup_prop_id;
use crate::write_ctx::WriteCtx;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct IndexDef {
pub unique: bool,
}
fn index_prefix(label_id: u32, prop_id: u32) -> [u8; 8] {
let mut out = [0u8; 8];
out[0..4].copy_from_slice(&label_id.to_be_bytes());
out[4..8].copy_from_slice(&prop_id.to_be_bytes());
out
}
pub(crate) fn encode_index_value(v: &PropertyValue) -> Vec<u8> {
match v {
PropertyValue::Null => vec![0x00],
PropertyValue::Bool(b) => vec![0x01, u8::from(*b)],
PropertyValue::Int(i) => {
let mut out = vec![0x02];
out.extend_from_slice(&((*i as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
out
}
PropertyValue::Float(f) => {
let bits = f.to_bits();
let sortable = if bits & 0x8000_0000_0000_0000 != 0 {
!bits
} else {
bits | 0x8000_0000_0000_0000
};
let mut out = vec![0x03];
out.extend_from_slice(&sortable.to_be_bytes());
out
}
PropertyValue::String(s) => {
let mut out = vec![0x04];
out.extend_from_slice(s.as_bytes());
out
}
PropertyValue::Date(days) => {
let mut out = vec![0x05];
out.extend_from_slice(&((*days as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
out
}
PropertyValue::Duration {
months,
days,
seconds,
nanos,
} => {
let mut out = vec![0x06];
out.extend_from_slice(&months.to_be_bytes());
out.extend_from_slice(&days.to_be_bytes());
out.extend_from_slice(&seconds.to_be_bytes());
out.extend_from_slice(&nanos.to_be_bytes());
out
}
PropertyValue::LocalTime(nanos_of_day) => {
let mut out = vec![0x07];
out.extend_from_slice(&nanos_of_day.to_be_bytes());
out
}
PropertyValue::Time {
nanos_of_day,
offset_seconds,
} => {
let instant = nanos_of_day - *offset_seconds as i64 * 1_000_000_000;
let mut out = vec![0x08];
out.extend_from_slice(&((instant as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
out
}
PropertyValue::LocalDateTime {
epoch_seconds,
nanos,
} => {
let mut out = vec![0x09];
out.extend_from_slice(&((*epoch_seconds as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
out.extend_from_slice(&nanos.to_be_bytes());
out
}
PropertyValue::DateTime {
epoch_seconds,
nanos,
..
} => {
let mut out = vec![0x0A];
out.extend_from_slice(&((*epoch_seconds as u64) ^ 0x8000_0000_0000_0000).to_be_bytes());
out.extend_from_slice(&nanos.to_be_bytes());
out
}
PropertyValue::List(items) => {
let mut out = vec![0x0B];
for item in items {
let encoded = encode_index_value(item);
out.extend_from_slice(&(encoded.len() as u32).to_be_bytes());
out.extend_from_slice(&encoded);
}
out
}
PropertyValue::Map(_) => {
unreachable!("PropertyValue::Map is never a real stored/indexed property value")
}
}
}
fn index_key(label_id: u32, prop_id: u32, value: &PropertyValue) -> Vec<u8> {
let mut out = index_prefix(label_id, prop_id).to_vec();
out.extend_from_slice(&encode_index_value(value));
out
}
pub(crate) fn create_index(
ctx: &mut WriteCtx,
label: &str,
prop: &str,
unique: bool,
) -> Result<(), GraphError> {
let label_id = crate::labels::intern_label(ctx, label)?;
let prop_id = crate::props::intern_prop(ctx, prop)?;
let prefix = index_prefix(label_id, prop_id);
if ctx.index_defs()?.get(prefix.as_slice())?.is_some() {
return Err(GraphError::CorruptData(format!(
"index on label {label:?} property {prop:?} already exists"
)));
}
let node_ids: Vec<u64> = ctx
.node_label_index()?
.get(label_id)?
.map(|entry| entry.map(|value| value.value()).map_err(GraphError::from))
.collect::<Result<Vec<_>, GraphError>>()?;
let mut entries: Vec<(Vec<u8>, u64)> = Vec::with_capacity(node_ids.len());
for node_id in &node_ids {
let Some(guard) = ctx.nodes()?.get(*node_id)? else {
continue;
};
if let Some(raw) = crate::encode::node_prop_raw(guard.value(), prop_id)? {
let value = crate::encode::decode_value(raw)?;
entries.push((index_key(label_id, prop_id, &value), *node_id));
}
}
if unique {
let mut seen = std::collections::HashSet::with_capacity(entries.len());
for (key, _) in &entries {
if !seen.insert(key.clone()) {
return Err(GraphError::UniqueConstraintViolation {
label: label.to_string(),
property: prop.to_string(),
});
}
}
}
let encoded = postcard::to_allocvec(&IndexDef { unique })?;
ctx.index_defs()?
.insert(prefix.as_slice(), encoded.as_slice())?;
for (key, node_id) in entries {
ctx.property_index()?.insert(key.as_slice(), node_id)?;
}
Ok(())
}
pub fn lookup_index_def(txn: Txn, label: &str, prop: &str) -> Result<Option<IndexDef>, GraphError> {
let Some(label_id) = lookup_label_id(txn, label)? else {
return Ok(None);
};
let Some(prop_id) = lookup_prop_id(txn, prop)? else {
return Ok(None);
};
let prefix = index_prefix(label_id, prop_id);
let defs = txn.open_table(marsdb_storage::tables::INDEX_DEFS)?;
let found = defs
.get(prefix.as_slice())?
.map(|guard| guard.value().to_vec());
drop(defs);
match found {
Some(bytes) => Ok(Some(postcard::from_bytes(&bytes)?)),
None => Ok(None),
}
}
pub fn lookup_exact(
txn: Txn,
label: &str,
prop: &str,
value: &PropertyValue,
limit: Option<usize>,
) -> Result<Vec<NodeId>, GraphError> {
let Some(label_id) = lookup_label_id(txn, label)? else {
return Ok(Vec::new());
};
let Some(prop_id) = lookup_prop_id(txn, prop)? else {
return Ok(Vec::new());
};
let key = index_key(label_id, prop_id, value);
let index = txn.open_multimap_table(marsdb_storage::tables::PROPERTY_INDEX)?;
let iter = index.get(key.as_slice())?;
let ids: Vec<NodeId> = match limit {
Some(limit) => iter
.take(limit)
.map(|entry| {
entry
.map(|value| NodeId(value.value()))
.map_err(GraphError::from)
})
.collect::<Result<Vec<_>, GraphError>>()?,
None => iter
.map(|entry| {
entry
.map(|value| NodeId(value.value()))
.map_err(GraphError::from)
})
.collect::<Result<Vec<_>, GraphError>>()?,
};
drop(index);
Ok(ids)
}
pub fn match_count(
txn: Txn,
label: &str,
prop: &str,
value: &PropertyValue,
) -> Result<u64, GraphError> {
let Some(label_id) = lookup_label_id(txn, label)? else {
return Ok(0);
};
let Some(prop_id) = lookup_prop_id(txn, prop)? else {
return Ok(0);
};
let key = index_key(label_id, prop_id, value);
let index = txn.open_multimap_table(marsdb_storage::tables::PROPERTY_INDEX)?;
let count = index.get(key.as_slice())?.len();
Ok(count)
}
fn indexes_for_labels(
ctx: &mut WriteCtx,
label_ids: &[u32],
) -> Result<Vec<(u32, u32, String, IndexDef)>, GraphError> {
let raw: Vec<(u32, u32, IndexDef)> = {
let mut raw = Vec::new();
for entry in ctx.index_defs()?.iter()? {
let (key, value) = entry?;
let key_bytes = key.value();
let label_id = u32::from_be_bytes(
key_bytes[0..4]
.try_into()
.expect("index key prefix is 8 bytes"),
);
if !label_ids.contains(&label_id) {
continue;
}
let prop_id = u32::from_be_bytes(
key_bytes[4..8]
.try_into()
.expect("index key prefix is 8 bytes"),
);
let def: IndexDef = postcard::from_bytes(value.value())?;
raw.push((label_id, prop_id, def));
}
raw
};
raw.into_iter()
.map(|(label_id, prop_id, def)| {
let prop_name = resolve_prop_ctx(ctx, prop_id)?;
Ok((label_id, prop_id, prop_name, def))
})
.collect()
}
pub(crate) fn resolve_label_ctx(ctx: &mut WriteCtx, label_id: u32) -> Result<String, GraphError> {
let value = ctx.id_to_label()?.get(label_id)?.ok_or_else(|| {
GraphError::CorruptData(format!("label id {label_id} has no interned string"))
})?;
Ok(value.value().to_string())
}
pub(crate) fn resolve_prop_ctx(ctx: &mut WriteCtx, prop_id: u32) -> Result<String, GraphError> {
let value = ctx.id_to_prop()?.get(prop_id)?.ok_or_else(|| {
GraphError::CorruptData(format!("prop id {prop_id} has no interned string"))
})?;
Ok(value.value().to_string())
}
struct IndexTarget<'a> {
label_id: u32,
prop_id: u32,
label: &'a str,
prop: &'a str,
}
fn insert_entry(
ctx: &mut WriteCtx,
target: &IndexTarget<'_>,
value: &PropertyValue,
node_id: u64,
unique: bool,
) -> Result<(), GraphError> {
let key = index_key(target.label_id, target.prop_id, value);
if unique && ctx.property_index()?.get(key.as_slice())?.next().is_some() {
return Err(GraphError::UniqueConstraintViolation {
label: target.label.to_string(),
property: target.prop.to_string(),
});
}
ctx.property_index()?.insert(key.as_slice(), node_id)?;
Ok(())
}
fn remove_entry(
ctx: &mut WriteCtx,
label_id: u32,
prop_id: u32,
value: &PropertyValue,
node_id: u64,
) -> Result<(), GraphError> {
let key = index_key(label_id, prop_id, value);
ctx.property_index()?.remove(key.as_slice(), node_id)?;
Ok(())
}
pub(crate) fn on_node_created(
ctx: &mut WriteCtx,
node_id: u64,
label_ids: &[u32],
props: &BTreeMap<String, PropertyValue>,
) -> Result<(), GraphError> {
for (label_id, prop_id, prop_name, def) in indexes_for_labels(ctx, label_ids)? {
if let Some(value) = props.get(&prop_name) {
let label = resolve_label_ctx(ctx, label_id)?;
let target = IndexTarget {
label_id,
prop_id,
label: &label,
prop: &prop_name,
};
insert_entry(ctx, &target, value, node_id, def.unique)?;
}
}
Ok(())
}
pub(crate) fn on_node_deleted(
ctx: &mut WriteCtx,
node_id: u64,
label_ids: &[u32],
props: &BTreeMap<String, PropertyValue>,
) -> Result<(), GraphError> {
for (label_id, prop_id, prop_name, _def) in indexes_for_labels(ctx, label_ids)? {
if let Some(value) = props.get(&prop_name) {
remove_entry(ctx, label_id, prop_id, value, node_id)?;
}
}
Ok(())
}
pub(crate) fn on_node_prop_changed(
ctx: &mut WriteCtx,
node_id: u64,
label_ids: &[u32],
prop: &str,
old_value: Option<&PropertyValue>,
new_value: Option<&PropertyValue>,
) -> Result<(), GraphError> {
for (label_id, prop_id, prop_name, def) in indexes_for_labels(ctx, label_ids)? {
if prop_name != prop {
continue;
}
if let Some(old) = old_value {
remove_entry(ctx, label_id, prop_id, old, node_id)?;
}
if let Some(new) = new_value {
let label = resolve_label_ctx(ctx, label_id)?;
let target = IndexTarget {
label_id,
prop_id,
label: &label,
prop: &prop_name,
};
insert_entry(ctx, &target, new, node_id, def.unique)?;
}
}
Ok(())
}