use std::collections::BTreeMap;
use std::collections::btree_map::Entry;
use crate::meta::{Column, ColumnId, ColumnStore, ColumnType, DocBitmap};
use super::{AdmittedDocumentRead, QueryError, Store, StoreError};
pub const MAX_DOCUMENT_GROUP_LIMIT: usize = 1 << 16;
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum DocumentGroupValue {
U64(u64),
I64(i64),
String(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DocumentGroup {
pub value: DocumentGroupValue,
pub count: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DocumentGroupCounts {
pub groups: Vec<DocumentGroup>,
pub missing: u64,
pub count: u64,
pub generation: u64,
}
enum Tally {
U64(BTreeMap<u64, u64>),
I64(BTreeMap<i64, u64>),
String(BTreeMap<String, u64>),
}
impl Store {
pub fn count_documents_grouped(
&self,
predicate: Option<&crate::meta::Predicate>,
timestamp_range: Option<(i64, i64)>,
column: ColumnId,
limit: usize,
) -> Result<DocumentGroupCounts, QueryError> {
let invalid = |detail: String| QueryError::Store(StoreError::InvalidScan { detail });
if limit == 0 || limit > MAX_DOCUMENT_GROUP_LIMIT {
return Err(invalid(format!(
"group limit must be in 1..={MAX_DOCUMENT_GROUP_LIMIT}, received {limit}"
)));
}
let definition = self.schema.column(column).ok_or_else(|| {
invalid(format!(
"group-by attribute {} is not in the schema",
column.get()
))
})?;
let nullable = definition.is_nullable();
let mut tally = match definition.column_type() {
ColumnType::U64 => Tally::U64(BTreeMap::new()),
ColumnType::I64 => Tally::I64(BTreeMap::new()),
ColumnType::DictionaryString | ColumnType::RawString => Tally::String(BTreeMap::new()),
other @ (ColumnType::Id128 | ColumnType::F64 | ColumnType::Bool) => {
return Err(invalid(format!(
"group-by attribute {} has type {other:?}; grouping supports \
U64, I64, DictionaryString and RawString",
column.get()
)));
}
};
let predicate = super::document_scan_predicate(predicate, timestamp_range)?;
if let Some(predicate) = &predicate {
crate::planner::validate_predicate(predicate, &self.schema)
.map_err(|error| invalid(error.to_string()))?;
}
let AdmittedDocumentRead {
generation,
active,
snapshot,
active_query,
} = self.admit_document_read().map_err(QueryError::Store)?;
let mut missing = 0_u64;
for segment in snapshot.segments() {
let rows = super::sealed_scan_rows(segment, predicate.as_ref())?;
if rows.is_empty() {
continue;
}
let columns = segment.query_columns().map_err(QueryError::Store)?;
missing = add(
missing,
tally.add_rows(&columns, &rows, column, nullable, limit)?,
)?;
}
let columns = super::active_scan_columns(&active, &self.schema)?;
let alive = active.alive().map_err(QueryError::Store)?;
let rows = match &predicate {
Some(predicate) => {
crate::meta::evaluate(predicate, &columns, &alive).map_err(|error| {
QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Columns(
error.to_string(),
)))
})?
}
None => alive.alive_bitmap().clone(),
};
missing = add(
missing,
tally.add_rows(&columns, &rows, column, nullable, limit)?,
)?;
drop(active_query);
let groups = tally.into_groups()?;
let count = groups
.iter()
.try_fold(missing, |total, group| add(total, group.count))?;
Ok(DocumentGroupCounts {
groups,
missing,
count,
generation,
})
}
}
impl Tally {
fn add_rows(
&mut self,
columns: &ColumnStore,
rows: &DocBitmap,
column: ColumnId,
nullable: bool,
limit: usize,
) -> Result<u64, QueryError> {
let values = columns.column(column).ok_or_else(|| {
corrupt(format!(
"group-by attribute {} is missing from a segment",
column.get()
))
})?;
let mut missing = 0_u64;
for row in rows.iter() {
let present = match (&mut *self, values) {
(Self::U64(map), Column::U64(values)) => values
.get(row)
.map(|value| bump(map, value, limit))
.transpose()?,
(Self::I64(map), Column::I64(values)) => values
.get(row)
.map(|value| bump(map, value, limit))
.transpose()?,
(Self::String(map), Column::DictionaryString(values)) => values
.get(row)
.map(|value| bump_string(map, value, limit))
.transpose()?,
(Self::String(map), Column::RawString(values)) => values
.get(row)
.map(|value| bump_string(map, value, limit))
.transpose()?,
(_, values) => {
return Err(corrupt(format!(
"group-by attribute {} is stored as {:?}",
column.get(),
values.column_type()
)));
}
};
if present.is_none() {
if !nullable {
return Err(corrupt(format!(
"required group-by attribute {} is null at row {row}",
column.get()
)));
}
missing = add(missing, 1)?;
}
}
Ok(missing)
}
fn into_groups(self) -> Result<Vec<DocumentGroup>, QueryError> {
fn collect<K>(
map: BTreeMap<K, u64>,
value: impl Fn(K) -> DocumentGroupValue,
) -> Result<Vec<DocumentGroup>, QueryError> {
let mut groups = Vec::new();
groups.try_reserve_exact(map.len()).map_err(|_| {
QueryError::Store(StoreError::AllocationFailed {
needed: super::allocation_bytes::<DocumentGroup>(map.len()),
component: "grouped count groups",
})
})?;
groups.extend(map.into_iter().map(|(key, count)| DocumentGroup {
value: value(key),
count,
}));
Ok(groups)
}
match self {
Self::U64(map) => collect(map, DocumentGroupValue::U64),
Self::I64(map) => collect(map, DocumentGroupValue::I64),
Self::String(map) => collect(map, DocumentGroupValue::String),
}
}
}
fn bump<K: Ord>(map: &mut BTreeMap<K, u64>, key: K, limit: usize) -> Result<(), QueryError> {
let len = map.len();
match map.entry(key) {
Entry::Occupied(mut entry) => {
*entry.get_mut() = add(*entry.get(), 1)?;
}
Entry::Vacant(_) if len >= limit => {
return Err(QueryError::Store(StoreError::GroupLimitExceeded { limit }));
}
Entry::Vacant(entry) => {
entry.insert(1);
}
}
Ok(())
}
fn bump_string(map: &mut BTreeMap<String, u64>, key: &str, limit: usize) -> Result<(), QueryError> {
if let Some(count) = map.get_mut(key) {
*count = add(*count, 1)?;
return Ok(());
}
if map.len() >= limit {
return Err(QueryError::Store(StoreError::GroupLimitExceeded { limit }));
}
let key = super::copy_string(key, "grouped count value").map_err(QueryError::Store)?;
map.insert(key, 1);
Ok(())
}
fn add(total: u64, count: u64) -> Result<u64, QueryError> {
total
.checked_add(count)
.ok_or(QueryError::Scan(crate::scan::ScanError::ArithmeticOverflow))
}
fn corrupt(detail: String) -> QueryError {
QueryError::Store(StoreError::Segment(crate::segment::SegmentError::Geometry(
detail,
)))
}