use crate::compression::column_store::{
ColumnCompressionType, ColumnDataType, ColumnDefinition, ColumnStoreCompressor,
};
use crate::dictionary::NodeId;
use crate::error::{Result, TdbError};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct ColumnarStore {
base_dir: PathBuf,
column_groups: Arc<RwLock<Vec<ColumnGroup>>>,
compressor: ColumnStoreCompressor,
stripe_size: usize,
stats: Arc<RwLock<ColumnarStats>>,
}
#[derive(Debug, Clone)]
struct ColumnGroup {
name: String,
columns: Vec<String>,
stripes: Vec<Stripe>,
}
#[derive(Debug, Clone)]
struct Stripe {
id: usize,
row_count: usize,
column_data: HashMap<String, Vec<u8>>,
statistics: StripeStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct StripeStats {
row_count: usize,
min_values: HashMap<String, u64>,
max_values: HashMap<String, u64>,
null_counts: HashMap<String, usize>,
compressed_size: usize,
uncompressed_size: usize,
}
impl StripeStats {
fn new() -> Self {
Self {
row_count: 0,
min_values: HashMap::new(),
max_values: HashMap::new(),
null_counts: HashMap::new(),
compressed_size: 0,
uncompressed_size: 0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ColumnarStats {
pub total_rows: usize,
pub total_stripes: usize,
pub total_compressed_bytes: usize,
pub total_uncompressed_bytes: usize,
pub compression_ratio: f64,
pub queries_executed: usize,
pub rows_scanned: usize,
pub rows_pruned: usize,
}
impl ColumnarStore {
pub fn new<P: AsRef<Path>>(base_dir: P) -> Result<Self> {
let base_dir = base_dir.as_ref().to_path_buf();
std::fs::create_dir_all(&base_dir).map_err(TdbError::Io)?;
Ok(Self {
base_dir,
column_groups: Arc::new(RwLock::new(Vec::new())),
compressor: ColumnStoreCompressor::with_analytics(),
stripe_size: 10000, stats: Arc::new(RwLock::new(ColumnarStats::default())),
})
}
pub fn create_rdf_column_group(&mut self) -> Result<()> {
let columns = vec![
"subject".to_string(),
"predicate".to_string(),
"object".to_string(),
];
let group = ColumnGroup {
name: "rdf_triples".to_string(),
columns,
stripes: Vec::new(),
};
self.column_groups.write().push(group);
Ok(())
}
pub fn insert_batch(
&mut self,
subjects: &[NodeId],
predicates: &[NodeId],
objects: &[NodeId],
) -> Result<()> {
if subjects.len() != predicates.len() || subjects.len() != objects.len() {
return Err(TdbError::Other("Mismatched array lengths".to_string()));
}
let row_count = subjects.len();
let subject_bytes: Vec<Vec<u8>> = subjects
.iter()
.map(|id| id.as_u64().to_le_bytes().to_vec())
.collect();
let predicate_bytes: Vec<Vec<u8>> = predicates
.iter()
.map(|id| id.as_u64().to_le_bytes().to_vec())
.collect();
let object_bytes: Vec<Vec<u8>> = objects
.iter()
.map(|id| id.as_u64().to_le_bytes().to_vec())
.collect();
let mut column_data = HashMap::new();
column_data.insert("subject".to_string(), self.compress_column(&subject_bytes)?);
column_data.insert(
"predicate".to_string(),
self.compress_column(&predicate_bytes)?,
);
column_data.insert("object".to_string(), self.compress_column(&object_bytes)?);
let compressed_size: usize = column_data.values().map(|v| v.len()).sum();
let uncompressed_size = row_count * 24;
let mut stats = StripeStats::new();
stats.row_count = row_count;
stats.min_values.insert(
"subject".to_string(),
subjects.iter().map(|id| id.as_u64()).min().unwrap_or(0),
);
stats.max_values.insert(
"subject".to_string(),
subjects.iter().map(|id| id.as_u64()).max().unwrap_or(0),
);
stats.min_values.insert(
"predicate".to_string(),
predicates.iter().map(|id| id.as_u64()).min().unwrap_or(0),
);
stats.max_values.insert(
"predicate".to_string(),
predicates.iter().map(|id| id.as_u64()).max().unwrap_or(0),
);
stats.min_values.insert(
"object".to_string(),
objects.iter().map(|id| id.as_u64()).min().unwrap_or(0),
);
stats.max_values.insert(
"object".to_string(),
objects.iter().map(|id| id.as_u64()).max().unwrap_or(0),
);
stats.compressed_size = compressed_size;
stats.uncompressed_size = uncompressed_size;
let stripe_id = {
let groups = self.column_groups.read();
groups.first().map(|g| g.stripes.len()).unwrap_or(0)
};
let stripe = Stripe {
id: stripe_id,
row_count,
column_data,
statistics: stats,
};
{
let mut groups = self.column_groups.write();
if let Some(group) = groups.first_mut() {
group.stripes.push(stripe);
}
}
{
let mut global_stats = self.stats.write();
global_stats.total_rows += row_count;
global_stats.total_stripes += 1;
global_stats.total_compressed_bytes += compressed_size;
global_stats.total_uncompressed_bytes += uncompressed_size;
global_stats.compression_ratio = global_stats.total_uncompressed_bytes as f64
/ global_stats.total_compressed_bytes.max(1) as f64;
}
Ok(())
}
fn compress_column(&mut self, values: &[Vec<u8>]) -> Result<Vec<u8>> {
let mut result = Vec::new();
let count = values.len() as u32;
result.extend_from_slice(&count.to_le_bytes());
for value in values {
let len = value.len() as u32;
result.extend_from_slice(&len.to_le_bytes());
result.extend_from_slice(value);
}
Ok(result)
}
fn decompress_column(&self, compressed: &[u8]) -> Result<Vec<Vec<u8>>> {
let mut cursor = 0;
if compressed.len() < 4 {
return Ok(Vec::new());
}
let count = u32::from_le_bytes([
compressed[cursor],
compressed[cursor + 1],
compressed[cursor + 2],
compressed[cursor + 3],
]) as usize;
cursor += 4;
let mut result = Vec::with_capacity(count);
for _ in 0..count {
if cursor + 4 > compressed.len() {
break;
}
let len = u32::from_le_bytes([
compressed[cursor],
compressed[cursor + 1],
compressed[cursor + 2],
compressed[cursor + 3],
]) as usize;
cursor += 4;
if cursor + len > compressed.len() {
break;
}
let value = compressed[cursor..cursor + len].to_vec();
result.push(value);
cursor += len;
}
Ok(result)
}
pub fn scan_column(
&self,
column_name: &str,
predicate: Option<Box<dyn Fn(NodeId) -> bool>>,
) -> Result<Vec<NodeId>> {
let groups = self.column_groups.read();
let group = groups
.first()
.ok_or_else(|| TdbError::Other("No column groups".to_string()))?;
let mut results = Vec::new();
let mut rows_scanned = 0;
let mut rows_pruned = 0;
for stripe in &group.stripes {
if let Some(ref pred) = predicate {
if let (Some(&min), Some(&max)) = (
stripe.statistics.min_values.get(column_name),
stripe.statistics.max_values.get(column_name),
) {
let min_id = NodeId::new(min);
let max_id = NodeId::new(max);
if !pred(min_id) && !pred(max_id) {
rows_pruned += stripe.row_count;
continue;
}
}
}
if let Some(compressed) = stripe.column_data.get(column_name) {
let values = self.decompress_column(compressed)?;
rows_scanned += values.len();
for value_bytes in values {
if value_bytes.len() >= 8 {
let id_val = u64::from_le_bytes([
value_bytes[0],
value_bytes[1],
value_bytes[2],
value_bytes[3],
value_bytes[4],
value_bytes[5],
value_bytes[6],
value_bytes[7],
]);
let node_id = NodeId::new(id_val);
if let Some(ref pred) = predicate {
if pred(node_id) {
results.push(node_id);
}
} else {
results.push(node_id);
}
}
}
}
}
{
let mut stats = self.stats.write();
stats.queries_executed += 1;
stats.rows_scanned += rows_scanned;
stats.rows_pruned += rows_pruned;
}
Ok(results)
}
pub fn aggregate(
&self,
column_name: &str,
agg_func: AggregateFunction,
) -> Result<AggregateResult> {
let groups = self.column_groups.read();
let group = groups
.first()
.ok_or_else(|| TdbError::Other("No column groups".to_string()))?;
let mut count = 0u64;
let mut sum = 0u64;
let mut min = u64::MAX;
let mut max = 0u64;
for stripe in &group.stripes {
if let Some(compressed) = stripe.column_data.get(column_name) {
let values = self.decompress_column(compressed)?;
for value_bytes in values {
if value_bytes.len() >= 8 {
let value = u64::from_le_bytes([
value_bytes[0],
value_bytes[1],
value_bytes[2],
value_bytes[3],
value_bytes[4],
value_bytes[5],
value_bytes[6],
value_bytes[7],
]);
count += 1;
sum += value;
min = min.min(value);
max = max.max(value);
}
}
}
}
match agg_func {
AggregateFunction::Count => Ok(AggregateResult::Count(count)),
AggregateFunction::Sum => Ok(AggregateResult::Sum(sum)),
AggregateFunction::Min => Ok(AggregateResult::Min(min)),
AggregateFunction::Max => Ok(AggregateResult::Max(max)),
AggregateFunction::Avg => Ok(AggregateResult::Avg(if count > 0 {
sum as f64 / count as f64
} else {
0.0
})),
}
}
pub fn statistics(&self) -> ColumnarStats {
self.stats.read().clone()
}
pub fn stripe_count(&self) -> usize {
self.column_groups
.read()
.first()
.map(|g| g.stripes.len())
.unwrap_or(0)
}
pub fn clear(&mut self) {
self.column_groups.write().clear();
*self.stats.write() = ColumnarStats::default();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AggregateFunction {
Count,
Sum,
Min,
Max,
Avg,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AggregateResult {
Count(u64),
Sum(u64),
Min(u64),
Max(u64),
Avg(f64),
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_columnar_store_creation() {
let temp_dir = env::temp_dir().join("oxirs_columnar_creation");
std::fs::remove_dir_all(&temp_dir).ok();
let mut store = ColumnarStore::new(&temp_dir).unwrap();
store.create_rdf_column_group().unwrap();
assert_eq!(store.stripe_count(), 0);
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_insert_batch() {
let temp_dir = env::temp_dir().join("oxirs_columnar_insert");
std::fs::remove_dir_all(&temp_dir).ok();
let mut store = ColumnarStore::new(&temp_dir).unwrap();
store.create_rdf_column_group().unwrap();
let subjects = vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)];
let predicates = vec![NodeId::new(10), NodeId::new(10), NodeId::new(10)];
let objects = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
store
.insert_batch(&subjects, &predicates, &objects)
.unwrap();
let stats = store.statistics();
assert_eq!(stats.total_rows, 3);
assert_eq!(stats.total_stripes, 1);
assert!(stats.compression_ratio > 0.0);
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_scan_column() {
let temp_dir = env::temp_dir().join("oxirs_columnar_scan");
std::fs::remove_dir_all(&temp_dir).ok();
let mut store = ColumnarStore::new(&temp_dir).unwrap();
store.create_rdf_column_group().unwrap();
let subjects = vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)];
let predicates = vec![NodeId::new(10), NodeId::new(10), NodeId::new(10)];
let objects = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
store
.insert_batch(&subjects, &predicates, &objects)
.unwrap();
let results = store.scan_column("subject", None).unwrap();
assert_eq!(results.len(), 3);
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_scan_with_predicate() {
let temp_dir = env::temp_dir().join("oxirs_columnar_predicate");
std::fs::remove_dir_all(&temp_dir).ok();
let mut store = ColumnarStore::new(&temp_dir).unwrap();
store.create_rdf_column_group().unwrap();
let subjects = vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)];
let predicates = vec![NodeId::new(10), NodeId::new(10), NodeId::new(10)];
let objects = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
store
.insert_batch(&subjects, &predicates, &objects)
.unwrap();
let results = store
.scan_column("subject", Some(Box::new(|id| id.as_u64() > 1)))
.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].as_u64(), 2);
assert_eq!(results[1].as_u64(), 3);
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_aggregate_count() {
let temp_dir = env::temp_dir().join("oxirs_columnar_agg_count");
std::fs::remove_dir_all(&temp_dir).ok();
let mut store = ColumnarStore::new(&temp_dir).unwrap();
store.create_rdf_column_group().unwrap();
let subjects = vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)];
let predicates = vec![NodeId::new(10), NodeId::new(10), NodeId::new(10)];
let objects = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
store
.insert_batch(&subjects, &predicates, &objects)
.unwrap();
let result = store
.aggregate("subject", AggregateFunction::Count)
.unwrap();
assert_eq!(result, AggregateResult::Count(3));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_aggregate_sum() {
let temp_dir = env::temp_dir().join("oxirs_columnar_agg_sum");
std::fs::remove_dir_all(&temp_dir).ok();
let mut store = ColumnarStore::new(&temp_dir).unwrap();
store.create_rdf_column_group().unwrap();
let subjects = vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)];
let predicates = vec![NodeId::new(10), NodeId::new(10), NodeId::new(10)];
let objects = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
store
.insert_batch(&subjects, &predicates, &objects)
.unwrap();
let result = store.aggregate("subject", AggregateFunction::Sum).unwrap();
assert_eq!(result, AggregateResult::Sum(6));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_aggregate_min_max() {
let temp_dir = env::temp_dir().join("oxirs_columnar_agg_minmax");
std::fs::remove_dir_all(&temp_dir).ok();
let mut store = ColumnarStore::new(&temp_dir).unwrap();
store.create_rdf_column_group().unwrap();
let subjects = vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)];
let predicates = vec![NodeId::new(10), NodeId::new(10), NodeId::new(10)];
let objects = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
store
.insert_batch(&subjects, &predicates, &objects)
.unwrap();
let min_result = store.aggregate("subject", AggregateFunction::Min).unwrap();
assert_eq!(min_result, AggregateResult::Min(1));
let max_result = store.aggregate("subject", AggregateFunction::Max).unwrap();
assert_eq!(max_result, AggregateResult::Max(3));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_aggregate_avg() {
let temp_dir = env::temp_dir().join("oxirs_columnar_agg_avg");
std::fs::remove_dir_all(&temp_dir).ok();
let mut store = ColumnarStore::new(&temp_dir).unwrap();
store.create_rdf_column_group().unwrap();
let subjects = vec![NodeId::new(2), NodeId::new(4), NodeId::new(6)];
let predicates = vec![NodeId::new(10), NodeId::new(10), NodeId::new(10)];
let objects = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
store
.insert_batch(&subjects, &predicates, &objects)
.unwrap();
let result = store.aggregate("subject", AggregateFunction::Avg).unwrap();
assert_eq!(result, AggregateResult::Avg(4.0));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_multiple_stripes() {
let temp_dir = env::temp_dir().join("oxirs_columnar_multi_stripe");
std::fs::remove_dir_all(&temp_dir).ok();
let mut store = ColumnarStore::new(&temp_dir).unwrap();
store.create_rdf_column_group().unwrap();
for i in 0..3 {
let subjects = vec![
NodeId::new(i * 10 + 1),
NodeId::new(i * 10 + 2),
NodeId::new(i * 10 + 3),
];
let predicates = vec![NodeId::new(10), NodeId::new(10), NodeId::new(10)];
let objects = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
store
.insert_batch(&subjects, &predicates, &objects)
.unwrap();
}
assert_eq!(store.stripe_count(), 3);
let stats = store.statistics();
assert_eq!(stats.total_rows, 9);
assert_eq!(stats.total_stripes, 3);
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_statistics_pruning() {
let temp_dir = env::temp_dir().join("oxirs_columnar_pruning");
std::fs::remove_dir_all(&temp_dir).ok();
let mut store = ColumnarStore::new(&temp_dir).unwrap();
store.create_rdf_column_group().unwrap();
let subjects1 = vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)];
let predicates1 = vec![NodeId::new(10), NodeId::new(10), NodeId::new(10)];
let objects1 = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
store
.insert_batch(&subjects1, &predicates1, &objects1)
.unwrap();
let subjects2 = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
let predicates2 = vec![NodeId::new(10), NodeId::new(10), NodeId::new(10)];
let objects2 = vec![NodeId::new(100), NodeId::new(200), NodeId::new(300)];
store
.insert_batch(&subjects2, &predicates2, &objects2)
.unwrap();
let results = store
.scan_column("subject", Some(Box::new(|id| id.as_u64() < 10)))
.unwrap();
assert_eq!(results.len(), 3);
let stats = store.statistics();
assert!(stats.rows_pruned > 0);
std::fs::remove_dir_all(&temp_dir).ok();
}
}