use super::resources::{MemoryError, QueryArena, QueryReservation};
use super::runtime::{RuntimeContext, RuntimeError, WorkKind};
use super::{MAX_LIST_ELEMENTS, QueryError, QueryValue, QueryView};
use crate::property_graph::NodeId;
pub enum Eligibility<'a, 'v, 'm, 'g> {
AllIndexed,
Set(&'a EligibleNodeSet<'v, 'm, 'g>),
}
pub struct EligibleNodeSet<'v, 'm, 'g> {
view: &'v QueryView,
ids: QueryArena<'m, 'g, NodeId>,
_control: QueryReservation<'m, 'g>,
}
impl<'v, 'm, 'g> EligibleNodeSet<'v, 'm, 'g> {
pub fn build<'a>(
context: &mut RuntimeContext<'v, 'm, 'g>,
capacity: usize,
values: impl IntoIterator<Item = QueryValue<'a>>,
) -> Result<Self, RuntimeError> {
context.checkpoint()?;
if capacity > MAX_LIST_ELEMENTS {
return Err(QueryError::ListLimit.into());
}
let control = context.memory().reserve(
std::mem::size_of::<Self>() - std::mem::size_of::<QueryArena<'_, '_, NodeId>>(),
)?;
let mut ids = QueryArena::<NodeId>::new(context.memory(), capacity)?;
let mut ordered = true;
let table_capacity = capacity
.max(1)
.checked_mul(2)
.and_then(usize::checked_next_power_of_two)
.ok_or(MemoryError::Limit)?;
let mut table = QueryArena::new(context.memory(), table_capacity)?;
for _ in 0..table_capacity {
context.checkpoint()?;
table.push(usize::MAX)?;
}
for value in values {
context.charge(WorkKind::OperatorRows, 1)?;
context.charge(WorkKind::EligibilityEntries, 1)?;
value.validate(context.values())?;
let QueryValue::NodeRef(node) = value else {
return Err(QueryError::Type.into());
};
let id = node.id();
let mixed = xxhash_rust::xxh3::xxh3_64(&id.get().to_le_bytes());
let mut bucket = mixed as usize & (table_capacity - 1);
loop {
context.charge(WorkKind::HashProbes, 1)?;
let entry = *table.as_slice().get(bucket).ok_or(RuntimeError::Batch)?;
if entry == usize::MAX {
if ids.len() == capacity {
return Err(MemoryError::Limit.into());
}
if let Some(previous) = ids.as_slice().last() {
context.values().step()?;
ordered &= *previous < id;
}
context.charge(WorkKind::CopiedBytes, 16)?;
*table
.as_mut_slice()
.get_mut(bucket)
.ok_or(RuntimeError::Batch)? = ids.len();
context.check_work(WorkKind::EligibilityUniqueEntries, 1)?;
ids.push(id)?;
context.charge(WorkKind::EligibilityUniqueEntries, 1)?;
break;
}
if ids.as_slice().get(entry) == Some(&id) {
break;
}
bucket = (bucket + 1) & (table_capacity - 1);
}
}
drop(table);
let length = ids.len();
if !ordered {
for root in (0..length / 2).rev() {
sift(ids.as_mut_slice(), root, length, context)?;
}
for end in (1..length).rev() {
swap(ids.as_mut_slice(), 0, end, context)?;
sift(ids.as_mut_slice(), 0, end, context)?;
}
}
context.checkpoint()?;
Ok(Self {
view: context.view(),
ids,
_control: control,
})
}
pub fn ids_for(&self, view: &QueryView) -> Result<&[NodeId], QueryError> {
if !std::ptr::eq(self.view, view) {
return Err(QueryError::ForeignView);
}
Ok(self.ids.as_slice())
}
pub fn capacity(&self) -> usize {
self.ids.capacity()
}
}
fn sift(
ids: &mut [NodeId],
mut root: usize,
length: usize,
context: &mut RuntimeContext<'_, '_, '_>,
) -> Result<(), RuntimeError> {
loop {
let left = root
.checked_mul(2)
.and_then(|n| n.checked_add(1))
.ok_or(RuntimeError::Batch)?;
if left >= length {
break;
}
let mut child = left;
if left + 1 < length {
context.values().step()?;
if ids.get(left).ok_or(RuntimeError::Batch)?
< ids.get(left + 1).ok_or(RuntimeError::Batch)?
{
child = left + 1;
}
}
context.values().step()?;
if ids.get(root).ok_or(RuntimeError::Batch)? >= ids.get(child).ok_or(RuntimeError::Batch)? {
break;
}
swap(ids, root, child, context)?;
root = child;
}
Ok(())
}
fn swap(
ids: &mut [NodeId],
a: usize,
b: usize,
context: &mut RuntimeContext<'_, '_, '_>,
) -> Result<(), RuntimeError> {
let av = *ids.get(a).ok_or(RuntimeError::Batch)?;
let bv = *ids.get(b).ok_or(RuntimeError::Batch)?;
context.charge(WorkKind::CopiedBytes, 32)?;
*ids.get_mut(a).ok_or(RuntimeError::Batch)? = bv;
*ids.get_mut(b).ok_or(RuntimeError::Batch)? = av;
Ok(())
}