pub mod sip_hash;
use std::collections::HashSet;
use crate::content::node::NodeState;
use crate::index::definition::{DEFAULT_COUNTER_RESOLUTION, IndexDefinition};
use crate::index::property::mirror::APPROXIMATE_COUNT_PREFIX;
use crate::index::{IndexError, IndexResult, converting_long, values_of};
use crate::segment::record::RecordIdentifier;
pub use sip_hash::{SipHash, hash_for_path, narrowed_seed};
pub const COUNT_HASH_PROPERTY_NAME: &str = ":cnt";
pub const COUNT_PROPERTY_NAME: &str = ":count";
pub const APPROXIMATE_COUNT_RESOLUTION: i64 = 100;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CountBound {
Expected,
Maximum,
}
impl CountBound {
const fn addend(self) -> i64 {
match self {
CountBound::Expected => 0,
CountBound::Maximum => APPROXIMATE_COUNT_RESOLUTION,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum NodeCountEstimate {
Unknown,
Fallback,
Count(u64),
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct CounterEntry {
pub path: String,
pub count: Option<i64>,
}
pub struct CounterIndex<'provider> {
definition_node: NodeState<'provider>,
definition_path: String,
resolution: i64,
seed: i64,
}
impl<'provider> CounterIndex<'provider> {
#[must_use]
pub fn open(definition_node: NodeState<'provider>, definition: &IndexDefinition) -> Self {
Self {
definition_node,
definition_path: definition.path.clone(),
resolution: definition.resolution.unwrap_or(DEFAULT_COUNTER_RESOLUTION),
seed: narrowed_seed(definition.seed.unwrap_or(0)),
}
}
#[must_use]
pub const fn resolution(&self) -> i64 {
self.resolution
}
#[must_use]
pub const fn seed(&self) -> i64 {
self.seed
}
#[must_use]
pub const fn bit_mask(&self) -> i32 {
let resolution = self.resolution as i32;
(highest_one_bit(resolution).wrapping_mul(2)).wrapping_sub(1)
}
#[must_use]
pub fn is_sampled(&self, path: &str) -> bool {
hash_for_path(self.seed, path).hash_code() & self.bit_mask() == 0
}
pub fn entries(&self) -> IndexResult<Vec<CounterEntry>> {
let mut entries = Vec::new();
for (name, data_node) in self.definition_node.child_node_entries()? {
if !is_data_node_name(&name) {
continue;
}
let mut records_on_path = HashSet::new();
self.walk(&data_node, "/", &mut records_on_path, &mut entries)?;
}
entries.sort();
Ok(entries)
}
fn walk(
&self,
node: &NodeState<'provider>,
path: &str,
records_on_path: &mut HashSet<RecordIdentifier>,
entries: &mut Vec<CounterEntry>,
) -> IndexResult<()> {
let record = node.record_identifier();
if !records_on_path.insert(record) {
return Err(IndexError::Record(crate::Error::InvalidFormat {
details: format!(
"the counter storage of {} contains node record {record} in its own \
subtree; the node records form a cycle",
self.definition_path
),
}));
}
entries.push(CounterEntry {
path: path.to_owned(),
count: combined_count(node)?,
});
for (name, child) in node.child_node_entries()? {
let child_path = if path == "/" {
format!("/{name}")
} else {
format!("{path}/{name}")
};
self.walk(&child, &child_path, records_on_path, entries)?;
}
records_on_path.remove(&record);
Ok(())
}
pub fn has_data_node(&self) -> IndexResult<bool> {
Ok(self
.definition_node
.child_node_entries()?
.iter()
.any(|(name, _)| is_data_node_name(name)))
}
}
pub fn estimated_node_count(
content_root: &NodeState<'_>,
path: &str,
bound: CountBound,
) -> IndexResult<NodeCountEstimate> {
let Some(target) = descend(content_root, path)? else {
return Ok(NodeCountEstimate::Count(0));
};
if bound == CountBound::Expected
&& let Some(approximate) = approximate_count(&target)?
{
return Ok(non_negative(approximate));
}
if let Some(combined) = combined_count(&target)? {
return Ok(non_negative(combined + bound.addend()));
}
let counter = content_root
.child_node(crate::index::INDEX_DEFINITIONS_NAME)?
.and_then(|index| index.child_node("counter").transpose())
.transpose()?;
let Some(counter) = counter else {
return Ok(NodeCountEstimate::Unknown);
};
let data_children: Vec<NodeState<'_>> = counter
.child_node_entries()?
.into_iter()
.filter(|(name, _)| is_data_node_name(name))
.map(|(_, node)| node)
.collect();
if data_children.is_empty() {
return Ok(NodeCountEstimate::Unknown);
}
let mut sum = 0i64;
for data_node in data_children {
if let Some(node) = descend(&data_node, path)?
&& let Some(combined) = combined_count(&node)?
{
sum += combined;
}
}
if sum == 0 {
return Ok(NodeCountEstimate::Fallback);
}
Ok(non_negative(sum + bound.addend()))
}
fn non_negative(value: i64) -> NodeCountEstimate {
NodeCountEstimate::Count(u64::try_from(value).unwrap_or(0))
}
fn combined_count(node: &NodeState<'_>) -> IndexResult<Option<i64>> {
let hashed = converting_long(node.property(COUNT_HASH_PROPERTY_NAME)?.as_ref());
let old = converting_long(node.property(COUNT_PROPERTY_NAME)?.as_ref());
Ok(match (hashed, old) {
(None, None) => None,
(hashed, old) => Some(hashed.unwrap_or(0) + old.unwrap_or(0)),
})
}
fn approximate_count(node: &NodeState<'_>) -> IndexResult<Option<i64>> {
let mut found = false;
let mut added = 0i64;
let mut removed = 0i64;
for property in node.properties()? {
if !property.name.starts_with(APPROXIMATE_COUNT_PREFIX) {
continue;
}
found = true;
let value = values_of(&property)
.first()
.and_then(crate::content::property::PropertyValue::as_text)
.and_then(|text| text.parse::<i64>().ok())
.unwrap_or(0);
if value > 0 {
added += value;
} else {
removed -= value;
}
}
Ok(found.then(|| (added / 2).max(added - removed)))
}
fn is_data_node_name(name: &str) -> bool {
name == crate::index::INDEX_CONTENT_NODE_NAME
|| (name.starts_with(':') && name.ends_with("-index"))
}
fn descend<'provider>(
node: &NodeState<'provider>,
path: &str,
) -> IndexResult<Option<NodeState<'provider>>> {
let mut current = *node;
for element in path.split('/').filter(|element| !element.is_empty()) {
match current.child_node(element)? {
Some(child) => current = child,
None => return Ok(None),
}
}
Ok(Some(current))
}
const fn highest_one_bit(value: i32) -> i32 {
if value <= 0 {
return 0;
}
1i32 << (value as u32).ilog2()
}
#[cfg(test)]
mod tests {
use super::{highest_one_bit, narrowed_seed};
#[test]
fn the_bit_mask_of_the_default_resolution_is_one_thousand_and_twenty_three() {
assert_eq!(highest_one_bit(1000), 512);
assert_eq!(highest_one_bit(1000) * 2 - 1, 1023);
}
#[test]
fn the_highest_one_bit_of_a_power_of_two_is_itself() {
assert_eq!(highest_one_bit(1024), 1024);
assert_eq!(highest_one_bit(1), 1);
}
#[test]
fn a_non_positive_resolution_has_no_highest_bit() {
assert_eq!(highest_one_bit(0), 0);
assert_eq!(highest_one_bit(-1), 0);
}
#[test]
fn the_stored_seed_is_narrowed_to_thirty_two_bits_sign_extended() {
assert_eq!(narrowed_seed(-7_610_761_686_379_641_542), -584_039_110);
assert_eq!(narrowed_seed(1), 1);
assert_eq!(narrowed_seed(-1), -1);
assert_eq!(narrowed_seed(i64::from(i32::MIN) - 1), i64::from(i32::MAX));
}
}