use crate::span_record::SpanRecord;
use anyhow::Result;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub struct StorageConfig {
pub row_group_size: usize,
pub bloom_filter_trace_id: bool,
pub composite_index_trace_time: bool,
pub predicate_pushdown: bool,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
row_group_size: 10_000,
bloom_filter_trace_id: true,
composite_index_trace_time: true,
predicate_pushdown: true,
}
}
}
pub struct TruenoDbStorage {
path: PathBuf,
config: StorageConfig,
_db: Arc<Mutex<PlaceholderDb>>,
}
struct PlaceholderDb {
}
impl TruenoDbStorage {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::with_config(path, StorageConfig::default())
}
pub fn with_config<P: AsRef<Path>>(path: P, config: StorageConfig) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let _db = Arc::new(Mutex::new(PlaceholderDb {}));
eprintln!("INFO: TruenoDbStorage initialized at {path:?}");
eprintln!(" - Row group size: {}", config.row_group_size);
eprintln!(" - Bloom filter (trace_id): {}", config.bloom_filter_trace_id);
eprintln!(
" - Composite index (trace_id, timestamp): {}",
config.composite_index_trace_time
);
eprintln!(" - Predicate pushdown: {}", config.predicate_pushdown);
eprintln!("TODO Sprint 43: Apply configuration to trueno-db");
Ok(Self { path, config, _db })
}
pub fn insert_batch(&self, spans: &[SpanRecord]) -> Result<()> {
if spans.is_empty() {
return Ok(());
}
eprintln!("DEBUG: TruenoDbStorage::insert_batch() - {} spans (placeholder)", spans.len());
eprintln!("TODO Sprint 40: Write to Parquet via trueno-db");
Ok(())
}
pub fn query_by_trace_id(&self, trace_id: &[u8; 16]) -> Result<Vec<SpanRecord>> {
eprintln!(
"DEBUG: TruenoDbStorage::query_by_trace_id({}) (placeholder)",
hex::encode(trace_id)
);
eprintln!("TODO Sprint 40: Query Parquet via trueno-db");
Ok(vec![])
}
pub fn query_by_trace_id_and_time(
&self,
trace_id: &[u8; 16],
start_time_nanos: u64,
end_time_nanos: u64,
) -> Result<Vec<SpanRecord>> {
eprintln!(
"DEBUG: TruenoDbStorage::query_by_trace_id_and_time({}, {}-{}) (placeholder)",
hex::encode(trace_id),
start_time_nanos,
end_time_nanos
);
Ok(vec![])
}
pub fn query_by_process_id(&self, process_id: u32) -> Result<Vec<SpanRecord>> {
eprintln!("DEBUG: TruenoDbStorage::query_by_process_id({process_id}) (placeholder)");
Ok(vec![])
}
pub fn query_errors(&self) -> Result<Vec<SpanRecord>> {
eprintln!("DEBUG: TruenoDbStorage::query_errors() (placeholder)");
Ok(vec![])
}
pub fn stats(&self) -> Result<StorageStats> {
eprintln!("DEBUG: TruenoDbStorage::stats() (placeholder)");
Ok(StorageStats {
total_spans: 0,
file_size_bytes: 0,
row_groups: 0,
compression_ratio: 1.0,
})
}
pub fn flush(&self) -> Result<()> {
eprintln!("DEBUG: TruenoDbStorage::flush() (placeholder)");
Ok(())
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn config(&self) -> &StorageConfig {
&self.config
}
pub fn query_optimized(
&self,
trace_id: Option<&[u8; 16]>,
start_time_min: Option<u64>,
start_time_max: Option<u64>,
process_id: Option<u32>,
) -> Result<Vec<SpanRecord>> {
eprintln!("DEBUG: TruenoDbStorage::query_optimized() (placeholder)");
eprintln!(" - trace_id: {:?}", trace_id.map(hex::encode));
eprintln!(" - start_time_min: {start_time_min:?}");
eprintln!(" - start_time_max: {start_time_max:?}");
eprintln!(" - process_id: {process_id:?}");
eprintln!(" - predicate_pushdown: {}", self.config.predicate_pushdown);
eprintln!("TODO Sprint 43: Implement optimized query with predicate pushdown");
Ok(vec![])
}
}
#[derive(Debug, Clone, Copy)]
pub struct StorageStats {
pub total_spans: u64,
pub file_size_bytes: u64,
pub row_groups: usize,
pub compression_ratio: f64,
}
impl StorageStats {
pub fn avg_span_size_bytes(&self) -> f64 {
if self.total_spans == 0 {
0.0
} else {
(self.file_size_bytes as f64 * self.compression_ratio) / self.total_spans as f64
}
}
pub fn avg_compressed_span_size_bytes(&self) -> f64 {
if self.total_spans == 0 {
0.0
} else {
self.file_size_bytes as f64 / self.total_spans as f64
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::span_record::{SpanKind, StatusCode};
use std::collections::HashMap;
use tempfile::TempDir;
fn create_test_span(i: u64) -> SpanRecord {
SpanRecord::new(
[1; 16],
[(i as u8); 8],
None,
format!("span_{}", i),
SpanKind::Internal,
i * 1000,
i * 2000,
i,
StatusCode::Ok,
String::new(),
HashMap::new(),
HashMap::new(),
1234,
5678,
)
}
#[test]
fn test_storage_creation() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
assert_eq!(storage.path(), path);
}
#[test]
fn test_insert_batch_empty() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
storage.insert_batch(&[]).expect("test");
}
#[test]
fn test_insert_batch_single() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let spans = vec![create_test_span(0)];
storage.insert_batch(&spans).expect("test");
}
#[test]
fn test_insert_batch_multiple() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let spans: Vec<_> = (0..100).map(create_test_span).collect();
storage.insert_batch(&spans).expect("test");
}
#[test]
fn test_query_by_trace_id() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let trace_id = [0x4b; 16];
let result = storage.query_by_trace_id(&trace_id).expect("test");
assert_eq!(result.len(), 0);
}
#[test]
fn test_storage_stats() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let stats = storage.stats().expect("test");
assert_eq!(stats.total_spans, 0);
}
#[test]
fn test_stats_calculations() {
let stats = StorageStats {
total_spans: 1000,
file_size_bytes: 10_000,
row_groups: 10,
compression_ratio: 5.0,
};
assert_eq!(stats.avg_compressed_span_size_bytes(), 10.0);
assert_eq!(stats.avg_span_size_bytes(), 50.0);
}
#[test]
fn test_flush() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
storage.flush().expect("test");
}
#[test]
fn test_query_by_process_id() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let result = storage.query_by_process_id(1234).expect("test");
assert!(result.is_empty());
}
#[test]
fn test_query_errors() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let result = storage.query_errors().expect("test");
assert!(result.is_empty());
}
#[test]
fn test_query_optimized_all_none() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let result = storage.query_optimized(None, None, None, None).expect("test");
assert!(result.is_empty());
}
#[test]
fn test_query_optimized_with_trace_id() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let trace_id = [0x4b; 16];
let result = storage.query_optimized(Some(&trace_id), None, None, None).expect("test");
assert!(result.is_empty());
}
#[test]
fn test_query_optimized_with_time_range() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let result = storage.query_optimized(None, Some(1000), Some(2000), None).expect("test");
assert!(result.is_empty());
}
#[test]
fn test_query_optimized_with_process_id() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let result = storage.query_optimized(None, None, None, Some(1234)).expect("test");
assert!(result.is_empty());
}
#[test]
fn test_query_optimized_all_filters() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let trace_id = [0x4b; 16];
let result = storage
.query_optimized(Some(&trace_id), Some(1000), Some(2000), Some(5678))
.expect("test");
assert!(result.is_empty());
}
#[test]
fn test_storage_config_default() {
let config = StorageConfig::default();
assert_eq!(config.row_group_size, 10_000);
assert!(config.bloom_filter_trace_id);
assert!(config.composite_index_trace_time);
assert!(config.predicate_pushdown);
}
#[test]
fn test_storage_config() {
let tmp_dir = TempDir::new().expect("test");
let path = tmp_dir.path().join("test.parquet");
let storage = TruenoDbStorage::new(&path).expect("test");
let config = storage.config();
assert_eq!(config.row_group_size, 10_000);
}
#[test]
fn test_stats_zero_spans() {
let stats = StorageStats {
total_spans: 0,
file_size_bytes: 0,
row_groups: 0,
compression_ratio: 1.0,
};
assert!((stats.avg_compressed_span_size_bytes() - 0.0).abs() < f64::EPSILON);
assert!((stats.avg_span_size_bytes() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_stats_debug() {
let stats = StorageStats {
total_spans: 100,
file_size_bytes: 1000,
row_groups: 1,
compression_ratio: 2.0,
};
let debug = format!("{:?}", stats);
assert!(debug.contains("StorageStats"));
assert!(debug.contains("total_spans"));
}
#[test]
fn test_storage_config_clone() {
let config = StorageConfig::default();
let cloned = config.clone();
assert_eq!(cloned.row_group_size, config.row_group_size);
}
#[test]
fn test_storage_config_debug() {
let config = StorageConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("StorageConfig"));
}
}