use once_cell::sync::Lazy;
use parking_lot::Mutex;
use rocksdb::DB;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use super::Transaction;
use crate::{Error, Result, Value};
static STATS_WRITE_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
pub(crate) fn stats_write_lock() -> parking_lot::MutexGuard<'static, ()> {
STATS_WRITE_LOCK.lock()
}
pub const BATCH_SIZE: usize = 1024;
#[derive(Debug, Clone)]
pub struct ColumnBatch {
pub column: String,
pub start_row_id: u64,
values: once_cell::sync::OnceCell<Vec<Value>>,
pub typed: Option<super::typed_batch::TypedBatch>,
}
impl Serialize for ColumnBatch {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut st = serializer.serialize_struct("ColumnBatch", 3)?;
st.serialize_field("column", &self.column)?;
st.serialize_field("start_row_id", &self.start_row_id)?;
st.serialize_field("values", self.values())?;
st.end()
}
}
impl<'de> Deserialize<'de> for ColumnBatch {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
#[derive(Deserialize)]
struct Wire {
column: String,
start_row_id: u64,
values: Vec<Value>,
}
let wire = Wire::deserialize(deserializer)?;
Ok(Self::from_values(wire.column, wire.start_row_id, wire.values))
}
}
impl ColumnBatch {
pub fn new(column: &str, start_row_id: u64) -> Self {
Self::from_values(column.to_string(), start_row_id, vec![Value::Null; BATCH_SIZE])
}
pub fn from_values(column: String, start_row_id: u64, values: Vec<Value>) -> Self {
Self {
column,
start_row_id,
values: once_cell::sync::OnceCell::with_value(values),
typed: None,
}
}
pub(crate) fn from_typed(column: String, start_row_id: u64, typed: super::typed_batch::TypedBatch) -> Self {
Self {
column,
start_row_id,
values: once_cell::sync::OnceCell::new(),
typed: Some(typed),
}
}
pub fn values(&self) -> &Vec<Value> {
self.values.get_or_init(|| {
self.typed
.as_ref()
.map(super::typed_batch::TypedBatch::materialize_values)
.unwrap_or_default()
})
}
fn values_mut(&mut self) -> &mut Vec<Value> {
self.values(); self.typed = None;
self.values
.get_mut()
.unwrap_or_else(|| unreachable!("values cell initialized above"))
}
pub fn slot_count(&self) -> usize {
match self.values.get() {
Some(values) => values.len(),
None => self.typed.as_ref().map_or(0, |typed| typed.slot_count()),
}
}
pub fn value_at(&self, offset: usize) -> Option<Value> {
if let Some(values) = self.values.get() {
return values.get(offset).cloned();
}
self.typed.as_ref().and_then(|typed| typed.value_at(offset))
}
pub fn get(&self, row_id: u64) -> Option<Value> {
if row_id < self.start_row_id {
return None;
}
let offset = (row_id - self.start_row_id) as usize;
self.value_at(offset)
}
pub fn set(&mut self, row_id: u64, value: Value) -> bool {
if row_id < self.start_row_id {
return false;
}
let offset = (row_id - self.start_row_id) as usize;
if offset >= BATCH_SIZE {
return false;
}
let values = self.values_mut();
while values.len() <= offset {
values.push(Value::Null);
}
if let Some(slot) = values.get_mut(offset) {
*slot = value;
}
true
}
pub fn count_non_null(&self) -> usize {
match (&self.typed, self.values.get()) {
(Some(typed), None) => typed.validity.iter().filter(|&&ok| ok != 0).count(),
_ => self.values().iter().filter(|v| !matches!(v, Value::Null)).count(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BatchStats {
pub min: Option<Value>,
pub max: Option<Value>,
pub null_count: u32,
pub row_count: u32,
}
impl BatchStats {
pub fn from_batch(batch: &ColumnBatch) -> Self {
let values = batch.values();
let slot_count = values.len().max(BATCH_SIZE);
let mut null_count = (slot_count - values.len()) as u32;
let mut min: Option<&Value> = None;
let mut max: Option<&Value> = None;
let mut orderable = true;
for value in values {
if matches!(value, Value::Null) {
null_count += 1;
continue;
}
if !orderable {
continue;
}
match (min, max) {
(None, _) => {
if zone_cmp(value, value).is_some() {
min = Some(value);
max = Some(value);
} else {
orderable = false;
}
}
(Some(current_min), Some(current_max)) => {
match zone_cmp(value, current_min) {
Some(std::cmp::Ordering::Less) => min = Some(value),
Some(_) => {}
None => {
orderable = false;
continue;
}
}
match zone_cmp(value, current_max) {
Some(std::cmp::Ordering::Greater) => max = Some(value),
Some(_) => {}
None => orderable = false,
}
}
_ => unreachable!("min/max always set together"),
}
}
let (min, max) = if orderable {
(min.cloned(), max.cloned())
} else {
(None, None)
};
Self {
min,
max,
null_count,
row_count: slot_count as u32,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BatchPresence {
pub live_count: u32,
pub bits: Vec<u8>,
}
impl BatchPresence {
pub fn new() -> Self {
Self {
live_count: 0,
bits: vec![0u8; BATCH_SIZE / 8],
}
}
pub fn set(&mut self, offset: usize) {
if offset >= BATCH_SIZE {
return;
}
if self.bits.len() < BATCH_SIZE / 8 {
self.bits.resize(BATCH_SIZE / 8, 0);
}
if let Some(byte) = self.bits.get_mut(offset / 8) {
let mask = 1u8 << (offset % 8);
if *byte & mask == 0 {
*byte |= mask;
self.live_count += 1;
}
}
}
pub fn clear(&mut self, offset: usize) {
if let Some(byte) = self.bits.get_mut(offset / 8) {
let mask = 1u8 << (offset % 8);
if *byte & mask != 0 {
*byte &= !mask;
self.live_count = self.live_count.saturating_sub(1);
}
}
}
pub fn is_live(&self, offset: usize) -> bool {
self.bits
.get(offset / 8)
.is_some_and(|byte| byte & (1u8 << (offset % 8)) != 0)
}
pub fn iter_live(&self) -> impl Iterator<Item = usize> + '_ {
self.bits.iter().enumerate().flat_map(|(byte_idx, &byte)| {
let mut remaining = byte;
std::iter::from_fn(move || {
if remaining == 0 {
return None;
}
let bit = remaining.trailing_zeros() as usize;
remaining &= remaining - 1;
Some(byte_idx * 8 + bit)
})
})
}
}
impl Default for BatchPresence {
fn default() -> Self {
Self::new()
}
}
fn zone_cmp(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
fn as_int(v: &Value) -> Option<i64> {
match v {
Value::Int2(x) => Some(*x as i64),
Value::Int4(x) => Some(*x as i64),
Value::Int8(x) => Some(*x),
_ => None,
}
}
fn as_float(v: &Value) -> Option<f64> {
match v {
Value::Float4(x) => Some(*x as f64),
Value::Float8(x) => Some(*x),
_ => None,
}
}
if let (Some(a), Some(b)) = (as_int(a), as_int(b)) {
return Some(a.cmp(&b));
}
if let (Some(a), Some(b)) = (as_float(a), as_float(b)) {
if a.is_finite() && b.is_finite() {
return a.partial_cmp(&b);
}
return None;
}
match (a, b) {
(Value::String(a), Value::String(b)) => Some(a.cmp(b)),
(Value::Timestamp(a), Value::Timestamp(b)) => Some(a.cmp(b)),
(Value::Date(a), Value::Date(b)) => Some(a.cmp(b)),
_ => None,
}
}
pub struct ColumnarStore;
impl ColumnarStore {
fn batch_key(table: &str, column: &str, batch_id: u64) -> Vec<u8> {
format!("col:{}:{}:{}", table, column, batch_id).into_bytes()
}
fn column_prefix(table: &str, column: &str) -> Vec<u8> {
format!("col:{}:{}:", table, column).into_bytes()
}
fn stats_key(table: &str, column: &str, batch_id: u64) -> Vec<u8> {
format!("colz:{}:{}:{}", table, column, batch_id).into_bytes()
}
fn stats_prefix(table: &str, column: &str) -> Vec<u8> {
format!("colz:{}:{}:", table, column).into_bytes()
}
fn stats_marker_key(table: &str, column: &str) -> Vec<u8> {
format!("colzm:{}:{}", table, column).into_bytes()
}
fn parse_batch_id(key: &[u8], prefix_len: usize) -> Option<u64> {
let digits = key.get(prefix_len..)?;
if digits.is_empty() {
return None;
}
std::str::from_utf8(digits).ok()?.parse().ok()
}
pub fn batch_location(row_id: u64) -> (u64, usize) {
let batch_id = row_id / BATCH_SIZE as u64;
let offset = (row_id % BATCH_SIZE as u64) as usize;
(batch_id, offset)
}
fn presence_key(table: &str, batch_id: u64) -> Vec<u8> {
format!("colp:{}:{}", table, batch_id).into_bytes()
}
fn presence_prefix(table: &str) -> Vec<u8> {
format!("colp:{}:", table).into_bytes()
}
fn presence_marker_key(table: &str) -> Vec<u8> {
format!("colpm:{}", table).into_bytes()
}
pub fn presence_manifest_complete(db: &DB, table: &str) -> bool {
db.get(Self::presence_marker_key(table)).ok().flatten().is_some()
}
fn load_presence_for_write(
db: &DB,
txn: Option<&Transaction>,
table: &str,
batch_id: u64,
) -> Result<BatchPresence> {
let key = Self::presence_key(table, batch_id);
let raw = match txn {
Some(txn) => match txn.get(&key)? {
Some(data) => Some(data),
None => db
.get(&key)
.map_err(|e| Error::storage(format!("Columnar presence load failed: {}", e)))?,
},
None => db
.get(&key)
.map_err(|e| Error::storage(format!("Columnar presence load failed: {}", e)))?,
};
match raw {
Some(data) => bincode::deserialize(&data)
.map_err(|e| Error::storage(format!("Columnar presence deserialize failed: {}", e))),
None => Ok(BatchPresence::new()),
}
}
pub fn load_presence_map(db: &DB, table: &str) -> Result<Vec<(u64, BatchPresence)>> {
let prefix = Self::presence_prefix(table);
let mut map = Vec::new();
let iter = db.prefix_iterator(&prefix);
for item in iter {
let (key, value) = item.map_err(|e| Error::storage(format!("Columnar presence iterator error: {}", e)))?;
if !key.starts_with(&prefix) {
break;
}
let Some(batch_id) = Self::parse_batch_id(&key, prefix.len()) else {
continue;
};
let presence: BatchPresence = bincode::deserialize(&value)
.map_err(|e| Error::storage(format!("Columnar presence deserialize failed: {}", e)))?;
map.push((batch_id, presence));
}
map.sort_by_key(|(batch_id, _)| *batch_id);
Ok(map)
}
pub fn backfill_presence(db: &DB, table: &str) -> Result<Vec<(u64, BatchPresence)>> {
let _guard = stats_write_lock();
Self::backfill_presence_locked(db, table)
}
fn backfill_presence_locked(db: &DB, table: &str) -> Result<Vec<(u64, BatchPresence)>> {
if Self::presence_manifest_complete(db, table) {
return Self::load_presence_map(db, table);
}
let prefix = format!("data:{}:", table).into_bytes();
let mut read_opts = rocksdb::ReadOptions::default();
read_opts.set_total_order_seek(true);
let iter = db.iterator_opt(
rocksdb::IteratorMode::From(&prefix, rocksdb::Direction::Forward),
read_opts,
);
let mut map: HashMap<u64, BatchPresence> = HashMap::new();
for item in iter {
let (key, _) = item.map_err(|e| Error::storage(format!("Iterator error: {}", e)))?;
if !key.starts_with(&prefix) {
break;
}
let Some(digits) = key.get(prefix.len()..) else {
continue;
};
let Some(row_id) = std::str::from_utf8(digits).ok().and_then(|s| s.parse::<u64>().ok()) else {
continue;
};
let (batch_id, offset) = Self::batch_location(row_id);
map.entry(batch_id).or_default().set(offset);
}
let mut write_batch = rocksdb::WriteBatch::default();
let presence_prefix = Self::presence_prefix(table);
let iter = db.prefix_iterator(&presence_prefix);
for item in iter {
let (key, _) = item.map_err(|e| Error::storage(format!("Columnar presence iterator error: {}", e)))?;
if !key.starts_with(&presence_prefix) {
break;
}
write_batch.delete(&key);
}
let mut built: Vec<(u64, BatchPresence)> = map.into_iter().collect();
built.sort_by_key(|(batch_id, _)| *batch_id);
for (batch_id, presence) in &built {
let data = bincode::serialize(presence)
.map_err(|e| Error::storage(format!("Columnar presence serialize failed: {}", e)))?;
write_batch.put(Self::presence_key(table, *batch_id), data);
}
write_batch.put(Self::presence_marker_key(table), [1u8]);
db.write(write_batch)
.map_err(|e| Error::storage(format!("Columnar presence backfill failed: {}", e)))?;
Ok(built)
}
pub fn presence_scan_map(db: &DB, table: &str) -> Result<Vec<(u64, BatchPresence)>> {
if Self::presence_manifest_complete(db, table) {
return Self::load_presence_map(db, table);
}
Self::backfill_presence(db, table)
}
pub fn purge_presence(db: &DB, table: &str) -> Result<()> {
let _guard = stats_write_lock();
let mut write_batch = rocksdb::WriteBatch::default();
let prefix = Self::presence_prefix(table);
let iter = db.prefix_iterator(&prefix);
for item in iter {
let (key, _) = item.map_err(|e| Error::storage(format!("Columnar presence iterator error: {}", e)))?;
if !key.starts_with(&prefix) {
break;
}
write_batch.delete(&key);
}
write_batch.delete(Self::presence_marker_key(table));
db.write(write_batch)
.map_err(|e| Error::storage(format!("Columnar presence purge failed: {}", e)))
}
pub fn purge_table_sidecars(db: &DB, table: &str) -> Result<()> {
let _guard = stats_write_lock();
let mut write_batch = rocksdb::WriteBatch::default();
for prefix in [
format!("col:{}:", table).into_bytes(),
format!("colz:{}:", table).into_bytes(),
format!("colzm:{}:", table).into_bytes(),
Self::presence_prefix(table),
] {
let iter = db.prefix_iterator(&prefix);
for item in iter {
let (key, _) = item.map_err(|e| Error::storage(format!("Columnar iterator error: {}", e)))?;
if !key.starts_with(&prefix) {
break;
}
write_batch.delete(&key);
}
}
write_batch.delete(Self::presence_marker_key(table));
db.write(write_batch)
.map_err(|e| Error::storage(format!("Columnar sidecar purge failed: {}", e)))
}
pub fn apply_batch_values(
db: &DB,
write_batch: &mut rocksdb::WriteBatch,
table: &str,
column: &str,
batch_id: u64,
values: &[(u64, Value)],
) -> Result<()> {
let key = Self::batch_key(table, column, batch_id);
let mut batch = match db
.get(&key)
.map_err(|e| Error::storage(format!("Columnar load failed: {}", e)))?
{
Some(data) => super::typed_batch::decode_column_batch(&data)?,
None => ColumnBatch::new(column, batch_id * BATCH_SIZE as u64),
};
for (row_id, value) in values {
if !batch.set(*row_id, value.clone()) {
return Err(Error::storage(format!(
"Invalid row_id {} for batch starting at {}",
row_id, batch.start_row_id
)));
}
}
let stats = BatchStats::from_batch(&batch);
let data = super::typed_batch::encode_column_batch(&batch, &stats)?;
let stats_data = bincode::serialize(&stats)
.map_err(|e| Error::storage(format!("Columnar stats serialize failed: {}", e)))?;
write_batch.put(&key, &data);
write_batch.put(Self::stats_key(table, column, batch_id), &stats_data);
Ok(())
}
pub fn apply_presence_updates(
db: &DB,
write_batch: &mut rocksdb::WriteBatch,
table: &str,
batch_id: u64,
set: &[u64],
clear: &[u64],
) -> Result<()> {
let mut presence = Self::load_presence_for_write(db, None, table, batch_id)?;
for row_id in set {
presence.set((row_id % BATCH_SIZE as u64) as usize);
}
for row_id in clear {
presence.clear((row_id % BATCH_SIZE as u64) as usize);
}
let data = bincode::serialize(&presence)
.map_err(|e| Error::storage(format!("Columnar presence serialize failed: {}", e)))?;
write_batch.put(Self::presence_key(table, batch_id), data);
Ok(())
}
pub fn store(db: &DB, table: &str, column: &str, row_id: u64, value: Value) -> Result<()> {
let (batch_id, _offset) = Self::batch_location(row_id);
let _guard = stats_write_lock();
let mut write_batch = rocksdb::WriteBatch::default();
Self::apply_batch_values(
db,
&mut write_batch,
table,
column,
batch_id,
std::slice::from_ref(&(row_id, value)),
)?;
db.write(write_batch)
.map_err(|e| Error::storage(format!("Columnar store failed: {}", e)))?;
Ok(())
}
pub fn stage_batch_values_in_transaction(
db: &DB,
txn: &Transaction,
table: &str,
column: &str,
batch_id: u64,
values: &[(u64, Value)],
) -> Result<()> {
let key = Self::batch_key(table, column, batch_id);
let mut batch = match txn.get(&key)? {
Some(data) => super::typed_batch::decode_column_batch(&data)?,
None => match db
.get(&key)
.map_err(|e| Error::storage(format!("Columnar load failed: {}", e)))?
{
Some(data) => super::typed_batch::decode_column_batch(&data)?,
None => ColumnBatch::new(column, batch_id * BATCH_SIZE as u64),
},
};
for (row_id, value) in values {
if !batch.set(*row_id, value.clone()) {
return Err(Error::storage(format!(
"Invalid row_id {} for batch starting at {}",
row_id, batch.start_row_id
)));
}
}
let stats = BatchStats::from_batch(&batch);
let data = super::typed_batch::encode_column_batch(&batch, &stats)?;
let stats_data = bincode::serialize(&stats)
.map_err(|e| Error::storage(format!("Columnar stats serialize failed: {}", e)))?;
txn.put(Self::stats_key(table, column, batch_id), stats_data)?;
txn.put(key, data)
}
pub fn stage_presence_updates_in_transaction(
db: &DB,
txn: &Transaction,
table: &str,
batch_id: u64,
set: &[u64],
clear: &[u64],
) -> Result<()> {
let mut presence = Self::load_presence_for_write(db, Some(txn), table, batch_id)?;
for row_id in set {
presence.set((row_id % BATCH_SIZE as u64) as usize);
}
for row_id in clear {
presence.clear((row_id % BATCH_SIZE as u64) as usize);
}
let data = bincode::serialize(&presence)
.map_err(|e| Error::storage(format!("Columnar presence serialize failed: {}", e)))?;
txn.put(Self::presence_key(table, batch_id), data)
}
pub fn store_in_transaction(
db: &DB,
txn: &Transaction,
table: &str,
column: &str,
row_id: u64,
value: Value,
) -> Result<()> {
let (batch_id, _offset) = Self::batch_location(row_id);
Self::stage_batch_values_in_transaction(
db,
txn,
table,
column,
batch_id,
std::slice::from_ref(&(row_id, value)),
)
}
pub fn get(db: &DB, table: &str, column: &str, row_id: u64) -> Result<Option<Value>> {
let (batch_id, _offset) = Self::batch_location(row_id);
let key = Self::batch_key(table, column, batch_id);
match db
.get(&key)
.map_err(|e| Error::storage(format!("Columnar load failed: {}", e)))?
{
Some(data) => {
let batch = super::typed_batch::decode_column_batch(&data)?;
Ok(batch.get(row_id))
}
None => Ok(None),
}
}
fn maybe_migrate_v1_batch(
db: &DB,
table: &str,
column: &str,
batch_id: u64,
original_raw: &[u8],
batch: &ColumnBatch,
) {
use super::typed_batch as tb;
if batch.typed.is_some() || !tb::batch_v2_migrate_enabled() || !tb::batch_v2_write_enabled() {
return;
}
if !tb::batch_is_v2_eligible(batch) {
return;
}
let key = Self::batch_key(table, column, batch_id);
let _guard = stats_write_lock();
match db.get(&key) {
Ok(Some(current)) if current.as_slice() == original_raw => {
let stats = BatchStats::from_batch(batch);
match tb::encode_column_batch(batch, &stats) {
Ok(encoded) => {
if let Err(e) = db.put(&key, encoded) {
tracing::warn!(table, column, batch_id, error = %e, "Batch v2 migration write failed");
}
}
Err(e) => {
tracing::warn!(table, column, batch_id, error = %e, "Batch v2 migration encode failed");
}
}
}
_ => {} }
}
pub fn scan_column(db: &DB, table: &str, column: &str) -> Result<Vec<(u64, Value)>> {
let prefix = Self::column_prefix(table, column);
let mut results = Vec::new();
let iter = db.prefix_iterator(&prefix);
for item in iter {
let (key, value) = item.map_err(|e| Error::storage(format!("Columnar iterator error: {}", e)))?;
if !key.starts_with(&prefix) {
break;
}
let batch = super::typed_batch::decode_column_batch(&value)?;
if super::typed_batch::batch_v2_migrate_enabled() {
if let Some(batch_id) = Self::parse_batch_id(&key, prefix.len()) {
Self::maybe_migrate_v1_batch(db, table, column, batch_id, &value, &batch);
}
}
for (i, val) in batch.values().iter().enumerate() {
if !matches!(val, Value::Null) {
results.push((batch.start_row_id + i as u64, val.clone()));
}
}
}
results.sort_by_key(|(row_id, _)| *row_id);
Ok(results)
}
pub fn scan_column_batches(db: &DB, table: &str, column: &str) -> Result<Vec<(u64, ColumnBatch)>> {
let prefix = Self::column_prefix(table, column);
let mut batches = Vec::new();
let iter = db.prefix_iterator(&prefix);
for item in iter {
let (key, value) = item.map_err(|e| Error::storage(format!("Columnar iterator error: {}", e)))?;
if !key.starts_with(&prefix) {
break;
}
let batch = super::typed_batch::decode_column_batch(&value)?;
let batch_id = batch.start_row_id / BATCH_SIZE as u64;
if super::typed_batch::batch_v2_migrate_enabled() {
Self::maybe_migrate_v1_batch(db, table, column, batch_id, &value, &batch);
}
batches.push((batch_id, batch));
}
batches.sort_by_key(|(batch_id, _)| *batch_id);
Ok(batches)
}
pub fn load_stats_map(db: &DB, table: &str, column: &str) -> Result<HashMap<u64, BatchStats>> {
let prefix = Self::stats_prefix(table, column);
let mut map = HashMap::new();
let iter = db.prefix_iterator(&prefix);
for item in iter {
let (key, value) = item.map_err(|e| Error::storage(format!("Columnar stats iterator error: {}", e)))?;
if !key.starts_with(&prefix) {
break;
}
let Some(batch_id) = Self::parse_batch_id(&key, prefix.len()) else {
continue;
};
let stats: BatchStats = bincode::deserialize(&value)
.map_err(|e| Error::storage(format!("Columnar stats deserialize failed: {}", e)))?;
map.insert(batch_id, stats);
}
Ok(map)
}
fn stats_manifest_complete(db: &DB, table: &str, column: &str) -> bool {
db.get(Self::stats_marker_key(table, column)).ok().flatten().is_some()
}
pub fn scan_column_batches_pruned(
db: &DB,
table: &str,
column: &str,
pruned: &HashSet<u64>,
) -> Result<Vec<(u64, ColumnBatch)>> {
let manifest_complete = Self::stats_manifest_complete(db, table, column);
if manifest_complete && pruned.is_empty() {
return Self::scan_column_batches(db, table, column);
}
if manifest_complete {
let stats_prefix = Self::stats_prefix(table, column);
let mut ids: Vec<u64> = Vec::new();
let iter = db.prefix_iterator(&stats_prefix);
for item in iter {
let (key, _) = item.map_err(|e| Error::storage(format!("Columnar stats iterator error: {}", e)))?;
if !key.starts_with(&stats_prefix) {
break;
}
if let Some(batch_id) = Self::parse_batch_id(&key, stats_prefix.len()) {
if !pruned.contains(&batch_id) {
ids.push(batch_id);
}
}
}
ids.sort_unstable();
let keys: Vec<Vec<u8>> = ids.iter().map(|id| Self::batch_key(table, column, *id)).collect();
let mut batches = Vec::with_capacity(ids.len());
for (batch_id, result) in ids.iter().zip(db.multi_get(&keys)) {
let data = result.map_err(|e| Error::storage(format!("Columnar load failed: {}", e)))?;
if let Some(data) = data {
let batch = super::typed_batch::decode_column_batch(&data)?;
if super::typed_batch::batch_v2_migrate_enabled() {
Self::maybe_migrate_v1_batch(db, table, column, *batch_id, &data, &batch);
}
batches.push((*batch_id, batch));
}
}
return Ok(batches);
}
let stats = Self::load_stats_map(db, table, column)?;
let prefix = Self::column_prefix(table, column);
let mut batches = Vec::new();
let mut pass_clean = true;
let iter = db.prefix_iterator(&prefix);
for item in iter {
let (key, value) = item.map_err(|e| Error::storage(format!("Columnar iterator error: {}", e)))?;
if !key.starts_with(&prefix) {
break;
}
let Some(batch_id) = Self::parse_batch_id(&key, prefix.len()) else {
pass_clean = false;
continue;
};
if stats.contains_key(&batch_id) {
if pruned.contains(&batch_id) {
continue; }
} else if let Err(e) = Self::backfill_batch_stats(db, table, column, batch_id) {
tracing::warn!(
table = table,
column = column,
batch_id = batch_id,
error = %e,
"Zone-stats backfill failed"
);
pass_clean = false;
}
if pruned.contains(&batch_id) {
continue;
}
let batch = super::typed_batch::decode_column_batch(&value)?;
if super::typed_batch::batch_v2_migrate_enabled() {
Self::maybe_migrate_v1_batch(db, table, column, batch_id, &value, &batch);
}
batches.push((batch_id, batch));
}
if pass_clean {
let _guard = stats_write_lock();
if let Err(e) = db.put(Self::stats_marker_key(table, column), [1u8]) {
tracing::warn!(table = table, column = column, error = %e, "Zone-stats marker write failed");
}
}
batches.sort_by_key(|(batch_id, _)| *batch_id);
Ok(batches)
}
fn backfill_batch_stats(db: &DB, table: &str, column: &str, batch_id: u64) -> Result<()> {
let stats_key = Self::stats_key(table, column, batch_id);
let _guard = stats_write_lock();
if db
.get(&stats_key)
.map_err(|e| Error::storage(format!("Columnar stats load failed: {}", e)))?
.is_some()
{
return Ok(()); }
let Some(data) = db
.get(Self::batch_key(table, column, batch_id))
.map_err(|e| Error::storage(format!("Columnar load failed: {}", e)))?
else {
return Ok(()); };
let batch = super::typed_batch::decode_column_batch(&data)?;
let stats_data = bincode::serialize(&BatchStats::from_batch(&batch))
.map_err(|e| Error::storage(format!("Columnar stats serialize failed: {}", e)))?;
db.put(&stats_key, &stats_data)
.map_err(|e| Error::storage(format!("Columnar stats store failed: {}", e)))
}
pub fn delete(db: &DB, table: &str, column: &str, row_id: u64) -> Result<()> {
Self::store(db, table, column, row_id, Value::Null)
}
pub fn drop_column(db: &DB, table: &str, column: &str) -> Result<usize> {
let prefix = Self::column_prefix(table, column);
let mut deleted = 0;
let _guard = stats_write_lock();
let iter = db.prefix_iterator(&prefix);
for item in iter {
let (key, _) = item.map_err(|e| Error::storage(format!("Columnar iterator error: {}", e)))?;
if !key.starts_with(&prefix) {
break;
}
db.delete(&key)
.map_err(|e| Error::storage(format!("Columnar delete failed: {}", e)))?;
deleted += 1;
}
let stats_prefix = Self::stats_prefix(table, column);
let iter = db.prefix_iterator(&stats_prefix);
for item in iter {
let (key, _) = item.map_err(|e| Error::storage(format!("Columnar stats iterator error: {}", e)))?;
if !key.starts_with(&stats_prefix) {
break;
}
db.delete(&key)
.map_err(|e| Error::storage(format!("Columnar stats delete failed: {}", e)))?;
}
db.delete(Self::stats_marker_key(table, column))
.map_err(|e| Error::storage(format!("Columnar stats marker delete failed: {}", e)))?;
Ok(deleted)
}
pub fn stats(db: &DB, table: &str, column: &str) -> Result<ColumnarStats> {
let prefix = Self::column_prefix(table, column);
let mut total_batches = 0;
let mut total_values = 0;
let mut non_null_values = 0;
let iter = db.prefix_iterator(&prefix);
for item in iter {
let (key, value) = item.map_err(|e| Error::storage(format!("Columnar iterator error: {}", e)))?;
if !key.starts_with(&prefix) {
break;
}
let batch = super::typed_batch::decode_column_batch(&value)?;
total_batches += 1;
total_values += batch.slot_count();
non_null_values += batch.count_non_null();
}
Ok(ColumnarStats {
batch_count: total_batches,
total_slots: total_values,
non_null_values,
batch_size: BATCH_SIZE,
})
}
}
#[derive(Debug, Clone)]
pub struct ColumnarStats {
pub batch_count: usize,
pub total_slots: usize,
pub non_null_values: usize,
pub batch_size: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn test_db() -> (TempDir, DB) {
let dir = TempDir::new().unwrap();
let db = DB::open_default(dir.path()).unwrap();
(dir, db)
}
#[test]
fn test_columnar_store_get() {
let (_dir, db) = test_db();
ColumnarStore::store(&db, "metrics", "value", 0, Value::Float8(1.5)).unwrap();
ColumnarStore::store(&db, "metrics", "value", 1, Value::Float8(2.5)).unwrap();
ColumnarStore::store(&db, "metrics", "value", 2, Value::Float8(3.5)).unwrap();
assert_eq!(
ColumnarStore::get(&db, "metrics", "value", 0).unwrap(),
Some(Value::Float8(1.5))
);
assert_eq!(
ColumnarStore::get(&db, "metrics", "value", 1).unwrap(),
Some(Value::Float8(2.5))
);
assert_eq!(
ColumnarStore::get(&db, "metrics", "value", 2).unwrap(),
Some(Value::Float8(3.5))
);
assert_eq!(
ColumnarStore::get(&db, "metrics", "value", 100).unwrap(),
Some(Value::Null)
);
}
#[test]
fn test_columnar_scan() {
let (_dir, db) = test_db();
ColumnarStore::store(&db, "test", "col", 0, Value::Int4(100)).unwrap();
ColumnarStore::store(&db, "test", "col", 5, Value::Int4(500)).unwrap();
ColumnarStore::store(&db, "test", "col", 10, Value::Int4(1000)).unwrap();
let results = ColumnarStore::scan_column(&db, "test", "col").unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0], (0, Value::Int4(100)));
assert_eq!(results[1], (5, Value::Int4(500)));
assert_eq!(results[2], (10, Value::Int4(1000)));
}
#[test]
fn test_columnar_multiple_batches() {
let (_dir, db) = test_db();
ColumnarStore::store(&db, "test", "col", 0, Value::Int4(1)).unwrap();
ColumnarStore::store(&db, "test", "col", 1023, Value::Int4(2)).unwrap(); ColumnarStore::store(&db, "test", "col", 1024, Value::Int4(3)).unwrap(); ColumnarStore::store(&db, "test", "col", 2048, Value::Int4(4)).unwrap();
assert_eq!(ColumnarStore::get(&db, "test", "col", 0).unwrap(), Some(Value::Int4(1)));
assert_eq!(
ColumnarStore::get(&db, "test", "col", 1023).unwrap(),
Some(Value::Int4(2))
);
assert_eq!(
ColumnarStore::get(&db, "test", "col", 1024).unwrap(),
Some(Value::Int4(3))
);
assert_eq!(
ColumnarStore::get(&db, "test", "col", 2048).unwrap(),
Some(Value::Int4(4))
);
let stats = ColumnarStore::stats(&db, "test", "col").unwrap();
assert_eq!(stats.batch_count, 3);
assert_eq!(stats.non_null_values, 4);
}
#[test]
fn test_columnar_scan_orders_decimal_batch_keys_by_row_id() {
let (_dir, db) = test_db();
ColumnarStore::store(&db, "test", "col", 10 * BATCH_SIZE as u64, Value::Int4(10)).unwrap();
ColumnarStore::store(&db, "test", "col", 2 * BATCH_SIZE as u64, Value::Int4(2)).unwrap();
let results = ColumnarStore::scan_column(&db, "test", "col").unwrap();
assert_eq!(
results,
vec![
(2 * BATCH_SIZE as u64, Value::Int4(2)),
(10 * BATCH_SIZE as u64, Value::Int4(10)),
]
);
}
#[test]
fn test_columnar_delete() {
let (_dir, db) = test_db();
ColumnarStore::store(&db, "test", "col", 5, Value::Int4(100)).unwrap();
ColumnarStore::delete(&db, "test", "col", 5).unwrap();
assert_eq!(ColumnarStore::get(&db, "test", "col", 5).unwrap(), Some(Value::Null));
}
#[test]
fn test_batch_location() {
assert_eq!(ColumnarStore::batch_location(0), (0, 0));
assert_eq!(ColumnarStore::batch_location(1023), (0, 1023));
assert_eq!(ColumnarStore::batch_location(1024), (1, 0));
assert_eq!(ColumnarStore::batch_location(2047), (1, 1023));
assert_eq!(ColumnarStore::batch_location(2048), (2, 0));
}
#[test]
fn test_batch_stats_int_min_max_and_nulls() {
let mut batch = ColumnBatch::new("v", 0);
batch.set(0, Value::Int4(7));
batch.set(1, Value::Int4(-3));
batch.set(2, Value::Int8(42));
let stats = BatchStats::from_batch(&batch);
assert_eq!(stats.min, Some(Value::Int4(-3)));
assert_eq!(stats.max, Some(Value::Int8(42)));
assert_eq!(stats.row_count, BATCH_SIZE as u32);
assert_eq!(stats.null_count, BATCH_SIZE as u32 - 3);
}
#[test]
fn test_batch_stats_unorderable_types_store_none() {
let mut batch = ColumnBatch::new("v", 0);
batch.set(0, Value::Boolean(true));
batch.set(1, Value::Boolean(false));
let stats = BatchStats::from_batch(&batch);
assert_eq!(stats.min, None);
assert_eq!(stats.max, None);
assert_eq!(stats.null_count, BATCH_SIZE as u32 - 2);
let mut batch = ColumnBatch::new("v", 0);
batch.set(0, Value::Int4(1));
batch.set(1, Value::String("x".into()));
let stats = BatchStats::from_batch(&batch);
assert_eq!(stats.min, None);
assert_eq!(stats.max, None);
let mut batch = ColumnBatch::new("v", 0);
batch.set(0, Value::Float8(1.0));
batch.set(1, Value::Float8(f64::NAN));
let stats = BatchStats::from_batch(&batch);
assert_eq!(stats.min, None);
assert_eq!(stats.max, None);
}
#[test]
fn test_batch_stats_all_null() {
let batch = ColumnBatch::new("v", 0);
let stats = BatchStats::from_batch(&batch);
assert_eq!(stats.min, None);
assert_eq!(stats.max, None);
assert_eq!(stats.null_count, BATCH_SIZE as u32);
assert_eq!(stats.row_count, BATCH_SIZE as u32);
}
#[test]
fn test_store_writes_stats_sidecar_atomically() {
let (_dir, db) = test_db();
ColumnarStore::store(&db, "t", "v", 5, Value::Int4(100)).unwrap();
let stats_map = ColumnarStore::load_stats_map(&db, "t", "v").unwrap();
assert_eq!(stats_map.len(), 1);
let stats = stats_map.get(&0).unwrap();
assert_eq!(stats.min, Some(Value::Int4(100)));
assert_eq!(stats.max, Some(Value::Int4(100)));
ColumnarStore::delete(&db, "t", "v", 5).unwrap();
let stats_map = ColumnarStore::load_stats_map(&db, "t", "v").unwrap();
let stats = stats_map.get(&0).unwrap();
assert_eq!(stats.min, None);
assert_eq!(stats.null_count, BATCH_SIZE as u32);
}
#[test]
fn test_lazy_backfill_writes_stats_and_marker() {
let (_dir, db) = test_db();
for batch_id in 0..3u64 {
let mut batch = ColumnBatch::new("v", batch_id * BATCH_SIZE as u64);
for offset in 0..BATCH_SIZE {
batch.set(
batch_id * BATCH_SIZE as u64 + offset as u64,
Value::Int8((batch_id * BATCH_SIZE as u64 + offset as u64) as i64),
);
}
let data = bincode::serialize(&batch).unwrap();
db.put(ColumnarStore::batch_key("t", "v", batch_id), data).unwrap();
}
assert!(ColumnarStore::load_stats_map(&db, "t", "v").unwrap().is_empty());
assert!(!ColumnarStore::stats_manifest_complete(&db, "t", "v"));
let empty = HashSet::new();
let batches = ColumnarStore::scan_column_batches_pruned(&db, "t", "v", &empty).unwrap();
assert_eq!(batches.len(), 3);
let stats_map = ColumnarStore::load_stats_map(&db, "t", "v").unwrap();
assert_eq!(stats_map.len(), 3);
assert!(ColumnarStore::stats_manifest_complete(&db, "t", "v"));
assert_eq!(stats_map.get(&1).unwrap().min, Some(Value::Int8(1024)));
assert_eq!(stats_map.get(&1).unwrap().max, Some(Value::Int8(2047)));
let pruned: HashSet<u64> = [0u64, 2u64].into_iter().collect();
let batches = ColumnarStore::scan_column_batches_pruned(&db, "t", "v", &pruned).unwrap();
assert_eq!(batches.len(), 1);
assert_eq!(batches.first().map(|(id, _)| *id), Some(1));
let pruned_none = HashSet::new();
let lazy = ColumnarStore::scan_column_batches_pruned(&db, "t", "v", &pruned_none).unwrap();
let eager = ColumnarStore::scan_column_batches(&db, "t", "v").unwrap();
assert_eq!(lazy.len(), eager.len());
for ((lid, lbatch), (eid, ebatch)) in lazy.iter().zip(eager.iter()) {
assert_eq!(lid, eid);
assert_eq!(lbatch.values(), ebatch.values());
}
}
#[test]
fn test_drop_column_purges_stats_and_marker() {
let (_dir, db) = test_db();
ColumnarStore::store(&db, "t", "v", 0, Value::Int4(1)).unwrap();
let empty = HashSet::new();
ColumnarStore::scan_column_batches_pruned(&db, "t", "v", &empty).unwrap();
assert!(ColumnarStore::stats_manifest_complete(&db, "t", "v"));
ColumnarStore::drop_column(&db, "t", "v").unwrap();
assert!(ColumnarStore::load_stats_map(&db, "t", "v").unwrap().is_empty());
assert!(!ColumnarStore::stats_manifest_complete(&db, "t", "v"));
}
}