#[cfg(not(target_endian = "little"))]
compile_error!("core-storage v8 seam: zero-copy f64 transmute requires a little-endian target");
use crate::columns::{ColumnHandle, ColumnStore};
use crate::edge_props::EdgeProps;
use crate::topology::{Direction, Topology};
use crate::types::Value;
use crate::v8::layout::{ArchivedColumnData, ArchivedColumns, ArchivedCsr, ArchivedEdgeProps};
use std::borrow::Cow;
use std::collections::{BTreeSet, HashMap};
pub struct TopologyView<'a> {
pub overlay: &'a Topology,
pub base: Option<&'a ArchivedCsr>,
}
impl<'a> TopologyView<'a> {
pub fn owned(overlay: &'a Topology) -> Self {
Self {
overlay,
base: None,
}
}
pub fn with_base(overlay: &'a Topology, base: &'a ArchivedCsr) -> Self {
Self {
overlay,
base: Some(base),
}
}
pub fn neighbors(&self, etype: u32, dir: Direction, v: u32) -> Cow<'a, [u32]> {
let overlay_nbrs = self.overlay.neighbors(etype, dir, v);
let base = match self.base {
None => return overlay_nbrs,
Some(b) => b,
};
let base_nbrs = base_neighbors_from_archived(base, etype, dir, v);
if base_nbrs.is_empty() {
return overlay_nbrs;
}
let filtered_base_nbrs = subtract_tombstones(base_nbrs, etype, dir, v, self.overlay);
if filtered_base_nbrs.is_empty() {
return overlay_nbrs;
}
if overlay_nbrs.is_empty() {
return Cow::Owned(filtered_base_nbrs);
}
Cow::Owned(merge_sorted_unique(
overlay_nbrs.as_ref(),
&filtered_base_nbrs,
))
}
pub fn edge_count(&self) -> u64 {
let ov = self.overlay.edge_count();
match self.base {
None => ov,
Some(b) => {
let bv = u64::from(b.edge_count);
let tombstones: u64 = self
.overlay
.out_tombstones
.values()
.flat_map(|m| m.values())
.map(|s| s.len() as u64)
.sum();
bv.saturating_sub(tombstones) + ov
}
}
}
pub fn etypes(&self) -> std::vec::IntoIter<u32> {
match self.base {
None => {
self.overlay.etypes().collect::<Vec<_>>().into_iter()
}
Some(base) => {
let mut set: BTreeSet<u32> = self.overlay.etypes().collect();
for et in base.etypes.iter() {
set.insert(u32::from(et.etype));
}
set.into_iter().collect::<Vec<_>>().into_iter()
}
}
}
}
fn base_neighbors_from_archived(
base: &ArchivedCsr,
etype: u32,
dir: Direction,
v: u32,
) -> Vec<u32> {
let et_pos = base
.etypes
.binary_search_by_key(&etype, |e| u32::from(e.etype));
let et_entry = match et_pos {
Ok(i) => &base.etypes[i],
Err(_) => return Vec::new(),
};
let adj = match dir {
Direction::Out => &et_entry.out_adj,
Direction::In => &et_entry.in_adj,
};
let row_pos = adj.rows.binary_search_by_key(&v, |r| u32::from(r.vertex));
let row = match row_pos {
Ok(i) => &adj.rows[i],
Err(_) => return Vec::new(),
};
row.neighbors.iter().map(|n| u32::from(*n)).collect()
}
fn merge_sorted_unique(a: &[u32], b: &[u32]) -> Vec<u32> {
let mut out = Vec::with_capacity(a.len() + b.len());
let mut ai = 0;
let mut bi = 0;
while ai < a.len() && bi < b.len() {
match a[ai].cmp(&b[bi]) {
std::cmp::Ordering::Less => {
out.push(a[ai]);
ai += 1;
}
std::cmp::Ordering::Greater => {
out.push(b[bi]);
bi += 1;
}
std::cmp::Ordering::Equal => {
out.push(a[ai]);
ai += 1;
bi += 1;
}
}
}
out.extend_from_slice(&a[ai..]);
out.extend_from_slice(&b[bi..]);
out
}
fn subtract_tombstones(
nbrs: Vec<u32>,
etype: u32,
dir: Direction,
v: u32,
overlay: &Topology,
) -> Vec<u32> {
let tombstones = match dir {
Direction::Out => overlay.out_tombstones_for(etype, v),
Direction::In => overlay.in_tombstones_for(etype, v),
};
match tombstones {
None => nbrs,
Some(t) if t.is_empty() => nbrs,
Some(t) => nbrs.into_iter().filter(|n| !t.contains(n)).collect(),
}
}
pub type MergedNeighbors<'a> = Cow<'a, [u32]>;
#[cfg(test)]
mod tests {
use super::*;
use crate::columns::ColumnStore;
use crate::idmap::IdMap;
use crate::interner::Interner;
use crate::topology::{RemoveEdgeOutcome, Topology};
use crate::v8::encode::{encode_v8, V8Meta};
use crate::v8::MappedBase;
use std::collections::BTreeMap;
#[test]
fn a_pre_v9_column_resolves_through_its_own_table() {
use crate::v8::layout::{ColumnData, ColumnsData, FieldEntry};
let legacy = ColumnsData {
fields: vec![FieldEntry {
name: "tag".to_string(),
col: ColumnData::Str {
ids: vec![2, 0, 1],
present: vec![0b111],
strings: vec!["alpha".into(), "beta".into(), "gamma".into()],
},
}],
};
let bytes = rkyv::api::high::to_bytes::<rkyv::rancor::Error>(&legacy).expect("rkyv encode");
let archived =
rkyv::access::<crate::v8::layout::ArchivedColumnsData, rkyv::rancor::Error>(&bytes)
.expect("rkyv access");
let overlay = ColumnStore::new();
let view = ColumnsView::with_base(&overlay, archived);
assert!(view.strings.is_none(), "a pre-V9 base has no shared table");
for (id, want) in [(0u32, "gamma"), (1, "alpha"), (2, "beta")] {
match view.get(id, "tag") {
Some(ValueRef::Owned(Value::Str(got))) => assert_eq!(got, want, "node {id}"),
other => panic!("node {id}: expected Str({want}), got {other:?}"),
}
}
let store = crate::v8::encode::archived_to_columnstore(archived, None);
assert_eq!(store.get(0, "tag"), Some(&Value::Str("gamma".into())));
assert_eq!(store.get(1, "tag"), Some(&Value::Str("alpha".into())));
assert_eq!(store.get(2, "tag"), Some(&Value::Str("beta".into())));
}
fn tiny_meta() -> V8Meta {
use std::collections::HashMap;
V8Meta {
labels: vec![],
edge_props: crate::edge_props::EdgeProps::new(),
rule_defs: vec![],
provenance: BTreeMap::new(),
rule_tripped: BTreeMap::new(),
rule_fires: BTreeMap::new(),
ivf_bytes: Vec::new(),
view_defs: vec![],
wal_truncated: false,
hnsw: BTreeMap::new(),
last_change: HashMap::new(),
}
}
#[test]
fn neighbors_with_deletions_subtracts_from_base() {
let etype = 7u32;
let a = 0u32;
let b = 1u32;
let mut base_topo = Topology::new();
base_topo.add_edge(etype, a, b);
let mut ids = IdMap::new();
ids.get_or_insert("A");
ids.get_or_insert("B");
let meta = tiny_meta();
let mut snap_bytes = Vec::new();
encode_v8(
None,
None,
None,
None,
None,
&base_topo,
&ColumnStore::new(),
&ids,
&Interner::new(),
&meta,
&mut snap_bytes,
)
.expect("encode_v8");
let mapped = MappedBase::from_bytes(snap_bytes).expect("from_bytes");
let archived_csr = mapped.topology().expect("topology section");
let mut overlay = Topology::new();
let outcome = overlay.remove_edge(etype, a, b);
assert_eq!(
outcome,
RemoveEdgeOutcome::TombstonedBase,
"edge was base-only, not in overlay"
);
let view = TopologyView {
overlay: &overlay,
base: Some(archived_csr),
};
let out_nbrs = view.neighbors(etype, Direction::Out, a);
assert!(
!out_nbrs.contains(&b),
"tombstoned edge A→B must not appear in Out neighbors; got {out_nbrs:?}"
);
let in_nbrs = view.neighbors(etype, Direction::In, b);
assert!(
!in_nbrs.contains(&a),
"tombstoned edge A→B must not appear in In neighbors of B; got {in_nbrs:?}"
);
let c = 2u32;
let mut base_topo2 = Topology::new();
base_topo2.add_edge(etype, a, b);
base_topo2.add_edge(etype, a, c);
let mut snap2 = Vec::new();
let mut ids2 = IdMap::new();
ids2.get_or_insert("A");
ids2.get_or_insert("B");
ids2.get_or_insert("C");
encode_v8(
None,
None,
None,
None,
None,
&base_topo2,
&ColumnStore::new(),
&ids2,
&Interner::new(),
&tiny_meta(),
&mut snap2,
)
.expect("encode_v8 2");
let mapped2 = MappedBase::from_bytes(snap2).expect("from_bytes 2");
let archived_csr2 = mapped2.topology().expect("topology section 2");
let mut overlay2 = Topology::new();
overlay2.remove_edge(etype, a, b); let view2 = TopologyView {
overlay: &overlay2,
base: Some(archived_csr2),
};
let out_nbrs2 = view2.neighbors(etype, Direction::Out, a);
assert!(
!out_nbrs2.contains(&b),
"B must be hidden by tombstone; got {out_nbrs2:?}"
);
assert!(
out_nbrs2.contains(&c),
"C must still be visible (not tombstoned); got {out_nbrs2:?}"
);
}
}
#[derive(Debug)]
pub enum ValueRef<'a> {
Borrowed(&'a Value),
Owned(Value),
}
impl<'a> ValueRef<'a> {
pub fn into_value(self) -> Value {
match self {
Self::Borrowed(v) => v.clone(),
Self::Owned(v) => v,
}
}
pub fn as_value(&self) -> &Value {
match self {
Self::Borrowed(v) => v,
Self::Owned(v) => v,
}
}
}
impl<'a> PartialEq<Value> for ValueRef<'a> {
fn eq(&self, other: &Value) -> bool {
self.as_value() == other
}
}
impl<'a> PartialEq for ValueRef<'a> {
fn eq(&self, other: &Self) -> bool {
self.as_value() == other.as_value()
}
}
#[derive(Default)]
pub struct MixedCache {
decoded: std::sync::RwLock<HashMap<String, std::sync::Arc<HashMap<u32, Value>>>>,
}
impl MixedCache {
pub fn get_or_decode(&self, field: &str, blob: &[u8]) -> std::sync::Arc<HashMap<u32, Value>> {
{
let guard = self.decoded.read().expect("mixed column cache poisoned");
if let Some(hit) = guard.get(field) {
return std::sync::Arc::clone(hit);
}
}
let decoded: std::sync::Arc<HashMap<u32, Value>> =
std::sync::Arc::new(bincode::deserialize(blob).unwrap_or_default());
let mut guard = self.decoded.write().expect("mixed column cache poisoned");
std::sync::Arc::clone(
guard
.entry(field.to_string())
.or_insert_with(|| std::sync::Arc::clone(&decoded)),
)
}
}
#[derive(Copy, Clone)]
pub struct BaseColumns<'a> {
pub cols: &'a ArchivedColumns,
pub strings: Option<&'a crate::v8::layout::ArchivedStringTable>,
}
#[derive(Copy, Clone)]
pub struct ColumnsView<'a> {
pub overlay: &'a ColumnStore,
pub base: Option<&'a crate::v8::layout::ArchivedColumns>,
pub mixed: Option<&'a MixedCache>,
pub strings: Option<&'a crate::v8::layout::ArchivedStringTable>,
}
impl<'a> ColumnsView<'a> {
pub fn owned(overlay: &'a ColumnStore) -> Self {
Self {
overlay,
base: None,
mixed: None,
strings: None,
}
}
pub fn with_base(overlay: &'a ColumnStore, base: &'a ArchivedColumns) -> Self {
Self {
overlay,
base: Some(base),
mixed: None,
strings: None,
}
}
pub fn with_base_cached(
overlay: &'a ColumnStore,
base: &'a ArchivedColumns,
mixed: &'a MixedCache,
) -> Self {
Self {
overlay,
base: Some(base),
mixed: Some(mixed),
strings: None,
}
}
pub fn with_shared_strings(
mut self,
strings: Option<&'a crate::v8::layout::ArchivedStringTable>,
) -> Self {
self.strings = strings;
self
}
pub fn get(&self, id: u32, field: &str) -> Option<ValueRef<'_>> {
if let Some(v) = self.overlay.get(id, field) {
return Some(ValueRef::Borrowed(v));
}
if self.overlay.is_tombstoned(id, field) {
return None;
}
let base = self.base?;
let field_entry = base.fields.iter().find(|e| e.name.as_str() == field)?;
match &field_entry.col {
ArchivedColumnData::Int { data, present } => {
if !archived_bitmap_test(present.as_slice(), id) {
return None;
}
let idx = id as usize;
if idx >= data.len() {
return None;
}
Some(ValueRef::Owned(Value::Int(i64::from(data[idx]))))
}
ArchivedColumnData::Float { data, present } => {
if !archived_bitmap_test(present.as_slice(), id) {
return None;
}
let idx = id as usize;
if idx >= data.len() {
return None;
}
Some(ValueRef::Owned(Value::Float(f64::from(data[idx]))))
}
ArchivedColumnData::Bool { data, present } => {
if !archived_bitmap_test(present.as_slice(), id) {
return None;
}
let idx = id as usize;
if idx >= data.len() {
return None;
}
Some(ValueRef::Owned(Value::Bool(data[idx] != 0)))
}
ArchivedColumnData::Str {
ids,
present,
strings,
} => {
if !archived_bitmap_test(present.as_slice(), id) {
return None;
}
let idx = id as usize;
if idx >= ids.len() {
return None;
}
let sid = u32::from(ids[idx]) as usize;
let table = match self.strings {
Some(shared) => &shared.strings,
None => strings,
};
if sid >= table.len() {
return None;
}
Some(ValueRef::Owned(Value::Str(table[sid].as_str().to_string())))
}
ArchivedColumnData::Mixed(blob) => match self.mixed {
Some(cache) => cache
.get_or_decode(field, blob.as_slice())
.get(&id)
.cloned()
.map(ValueRef::Owned),
None => {
let map: HashMap<u32, Value> = bincode::deserialize(blob.as_slice()).ok()?;
map.get(&id).cloned().map(ValueRef::Owned)
}
},
ArchivedColumnData::Vector { dim, data, present } => {
if !archived_bitmap_test(present.as_slice(), id) {
return None;
}
let dim_val = u32::from(*dim) as usize;
let start = id as usize * dim_val;
let end = start + dim_val;
if end > data.len() {
return None;
}
let floats: Vec<Value> = data[start..end]
.iter()
.map(|f| Value::Float(f64::from(*f)))
.collect();
Some(ValueRef::Owned(Value::List(floats)))
}
}
}
pub fn vector(&self, id: u32, field: &str) -> Option<Cow<'_, [f64]>> {
if self.overlay.get(id, field).is_some() {
return None;
}
if self.overlay.is_tombstoned(id, field) {
return None;
}
let base = self.base?;
let field_entry = base.fields.iter().find(|e| e.name.as_str() == field)?;
let (dim, data, present) = match &field_entry.col {
ArchivedColumnData::Vector { dim, data, present } => (dim, data, present),
_ => return None,
};
if !archived_bitmap_test(present.as_slice(), id) {
return None;
}
let dim_val = u32::from(*dim) as usize;
if dim_val == 0 {
return None;
}
let start = id as usize * dim_val;
let end = start + dim_val;
let archived_slice = data.as_slice();
if end > archived_slice.len() {
return None;
}
let chunk = &archived_slice[start..end];
let ptr = chunk.as_ptr() as *const f64;
let result = if ptr.align_offset(std::mem::align_of::<f64>()) == 0 {
Cow::Borrowed(unsafe { std::slice::from_raw_parts(ptr, chunk.len()) })
} else {
let vec: Vec<f64> = (0..chunk.len())
.map(|i|
unsafe { std::ptr::read_unaligned(ptr.add(i)) })
.collect();
Cow::Owned(vec)
};
Some(result)
}
pub fn field_names(&self) -> Vec<String> {
let mut seen = BTreeSet::new();
for f in self.overlay.fields() {
seen.insert(f.to_string());
}
if let Some(base) = self.base {
for e in base.fields.iter() {
seen.insert(e.name.as_str().to_string());
}
}
seen.into_iter().collect()
}
pub fn column(&self, field: &str) -> ColumnHandle<'_> {
self.overlay.column(field)
}
}
#[derive(Copy, Clone)]
pub struct EdgePropsView<'a> {
pub overlay: &'a EdgeProps,
pub base: Option<&'a ArchivedEdgeProps>,
}
impl<'a> EdgePropsView<'a> {
pub fn owned(overlay: &'a EdgeProps) -> Self {
Self {
overlay,
base: None,
}
}
pub fn with_base(overlay: &'a EdgeProps, base: &'a ArchivedEdgeProps) -> Self {
Self {
overlay,
base: Some(base),
}
}
pub fn get(&self, etype: u32, src: u32, dst: u32, field: &str) -> Option<Value> {
if self.overlay.is_tombstoned(etype, src, dst) {
return None;
}
if let Some(v) = self.overlay.get(etype, src, dst, field) {
return Some(v.clone());
}
let base = self.base?;
let entry = base.entries.binary_search_by(|e| {
let ke = u32::from(e.etype);
let ks = u32::from(e.src);
let kd = u32::from(e.dst);
(ke, ks, kd).cmp(&(etype, src, dst))
});
let entry = match entry {
Ok(i) => &base.entries[i],
Err(_) => return None,
};
let props: std::collections::BTreeMap<String, Value> =
bincode::deserialize(entry.props_blob.as_slice()).ok()?;
props.get(field).cloned()
}
}
fn archived_bitmap_test(words: &[rkyv::Archived<u64>], id: u32) -> bool {
let word = id as usize / 64;
let bit = id as usize % 64;
if word >= words.len() {
return false;
}
(u64::from(words[word]) >> bit) & 1 == 1
}
#[cfg(test)]
mod value_ref_tests {
use super::ValueRef;
use crate::types::Value;
fn borrowed(v: &Value) -> ValueRef<'_> {
ValueRef::Borrowed(v)
}
fn owned(v: Value) -> ValueRef<'static> {
ValueRef::Owned(v)
}
#[test]
fn value_ref_cross_type_equivalence_battery() {
use std::collections::BTreeMap;
let cases: Vec<Value> = vec![
Value::Int(0),
Value::Int(i64::MIN),
Value::Int(i64::MAX),
Value::Float(0.0),
Value::Float(f64::NAN), Value::Float(1.5),
Value::Bool(true),
Value::Bool(false),
Value::Str("hello".into()),
Value::Str("".into()),
Value::List(vec![Value::Float(1.0), Value::Float(2.0)]),
Value::List(vec![]),
Value::Map(BTreeMap::new()),
Value::Map({
let mut m = BTreeMap::new();
m.insert("k".to_string(), Value::Int(1));
m
}),
];
for val in &cases {
if let Value::Float(f) = val {
if f.is_nan() {
let b = borrowed(val);
let o = owned(val.clone());
assert_ne!(b, *val, "NaN: Borrowed(v) must not equal v");
assert_ne!(o, *val, "NaN: Owned(v) must not equal v");
assert_ne!(b, borrowed(val), "NaN: Borrowed == Borrowed must be false");
assert_ne!(o, owned(val.clone()), "NaN: Owned == Owned must be false");
continue;
}
}
let b = borrowed(val);
let o = owned(val.clone());
assert_eq!(b, *val, "Borrowed(v) == v failed for {val:?}");
assert_eq!(o, *val, "Owned(v) == v failed for {val:?}");
assert_eq!(b, borrowed(val), "Borrowed == Borrowed failed for {val:?}");
assert_eq!(o, owned(val.clone()), "Owned == Owned failed for {val:?}");
assert_eq!(b, o, "Borrowed == Owned failed for {val:?}");
}
}
#[test]
fn value_ref_cross_type_not_equal() {
let int_val = Value::Int(1);
let float_val = Value::Float(1.0);
assert_ne!(
borrowed(&int_val),
borrowed(&float_val),
"Int(1) must not equal Float(1.0)"
);
assert_ne!(
owned(Value::Bool(true)),
owned(Value::Int(1)),
"Bool(true) must not equal Int(1)"
);
assert_ne!(
owned(Value::Map(std::collections::BTreeMap::new())),
owned(Value::Str("".into())),
"Map(empty) must not equal Str(empty)"
);
}
#[test]
fn value_ref_into_value_roundtrip() {
let val = Value::Str("round-trip".into());
let b = borrowed(&val);
assert_eq!(b.into_value(), val);
let o = owned(Value::Int(42));
assert_eq!(o.into_value(), Value::Int(42));
}
}