use polars::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, thiserror::Error)]
pub enum DataError {
#[error("Polars error: {0}")]
Polars(#[from] PolarsError),
#[error("Data format error: {0}")]
Format(String),
#[error("Data validation error: {0}")]
Validation(String),
#[error("Data source error: {0}")]
Source(String),
#[error("Processing error: {0}")]
Processing(String),
}
#[derive(Debug, Clone)]
pub enum ProcessingStrategy {
CPU(RayonConfig),
GPU(ComputeConfig),
Streaming(StreamConfig),
Hybrid(HybridConfig),
}
#[derive(Debug, Clone)]
pub struct RayonConfig {
pub num_threads: Option<usize>,
pub chunk_size: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct ComputeConfig {
pub workgroup_size: u32,
pub memory_budget: usize,
}
#[derive(Debug, Clone)]
pub struct StreamConfig {
pub buffer_size: usize,
pub batch_size: usize,
pub enable_backpressure: bool,
}
#[derive(Debug, Clone)]
pub struct HybridConfig {
pub cpu_threshold: usize,
pub gpu_threshold: usize,
pub cpu_config: RayonConfig,
pub gpu_config: ComputeConfig,
}
pub struct StrategySelector {
benchmarks: HashMap<String, f64>,
device_capabilities: DeviceCapabilities,
}
impl StrategySelector {
pub fn new() -> Self {
Self {
benchmarks: HashMap::new(),
device_capabilities: DeviceCapabilities::detect(),
}
}
pub fn select(&self, spec: &DataSpec) -> ProcessingStrategy {
let data_size = spec.estimated_size();
let complexity = spec.complexity();
let is_streaming = spec.is_streaming();
if is_streaming {
return ProcessingStrategy::Streaming(StreamConfig {
buffer_size: 10_000,
batch_size: 1_000,
enable_backpressure: true,
});
}
if data_size > 1_000_000 && complexity > 0.7 && self.device_capabilities.gpu_available {
ProcessingStrategy::GPU(ComputeConfig {
workgroup_size: 64,
memory_budget: 100 * 1024 * 1024, })
} else {
ProcessingStrategy::CPU(RayonConfig {
num_threads: None, chunk_size: Some(10_000),
})
}
}
pub fn benchmark(&mut self, name: &str, duration: f64) {
self.benchmarks.insert(name.to_string(), duration);
}
}
#[derive(Debug, Clone)]
pub struct DeviceCapabilities {
pub gpu_available: bool,
pub cpu_cores: usize,
pub memory_gb: f64,
pub simd_available: bool,
}
impl DeviceCapabilities {
pub fn detect() -> Self {
Self {
gpu_available: Self::detect_gpu(),
cpu_cores: num_cpus::get(),
memory_gb: Self::detect_memory(),
simd_available: Self::detect_simd(),
}
}
fn detect_gpu() -> bool {
#[cfg(target_arch = "wasm32")]
{
false }
#[cfg(not(target_arch = "wasm32"))]
{
false }
}
fn detect_memory() -> f64 {
8.0 }
fn detect_simd() -> bool {
if cfg!(target_feature = "simd128") {
true
} else {
false
}
}
}
#[derive(Debug, Clone)]
pub struct DataSpec {
pub source: DataSource,
pub transforms: Vec<DataTransform>,
pub filters: Vec<Filter>,
pub aggregations: Vec<Aggregation>,
pub output_format: OutputFormat,
}
impl DataSpec {
pub fn estimated_size(&self) -> usize {
match &self.source {
DataSource::DataFrame(df) => df.height(),
DataSource::Url { .. } => 100_000, DataSource::Query { .. } => 1_000_000, DataSource::Stream { .. } => 10_000, }
}
pub fn complexity(&self) -> f64 {
let base_complexity = 1.0;
let transform_complexity = self.transforms.len() as f64 * 0.5;
let filter_complexity = self.filters.len() as f64 * 0.3;
let aggregation_complexity = self.aggregations.len() as f64 * 1.0;
base_complexity + transform_complexity + filter_complexity + aggregation_complexity
}
pub fn is_streaming(&self) -> bool {
matches!(self.source, DataSource::Stream { .. })
}
pub fn hash(&self) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.source.hash(&mut hasher);
self.transforms.hash(&mut hasher);
self.filters.hash(&mut hasher);
self.aggregations.hash(&mut hasher);
hasher.finish()
}
}
#[derive(Debug, Clone)]
pub enum DataSource {
DataFrame(crate::DataFrame),
Url { url: String, format: DataFormat },
Query { sql: String, dataset: String },
Stream { stream_id: String },
}
#[derive(Debug, Clone)]
pub enum DataTransform {
Select { columns: Vec<String> },
Rename { mappings: HashMap<String, String> },
Cast { column: String, data_type: DataType },
FillNull { column: String, value: FillValue },
DropNulls { columns: Option<Vec<String>> },
}
#[derive(Debug, Clone)]
pub enum Filter {
Expression {
expr: String,
},
Range {
column: String,
min: Option<f64>,
max: Option<f64>,
},
Values {
column: String,
values: Vec<serde_json::Value>,
},
Null {
column: String,
keep_nulls: bool,
},
}
#[derive(Debug, Clone, Hash)]
pub enum Aggregation {
GroupBy {
columns: Vec<String>,
},
Aggregate {
operations: Vec<AggOp>,
},
Pivot {
index: String,
columns: String,
values: String,
},
Window {
operations: Vec<WindowOp>,
},
}
#[derive(Debug, Clone, Hash)]
pub enum AggOp {
Sum {
column: String,
alias: Option<String>,
},
Mean {
column: String,
alias: Option<String>,
},
Count {
column: String,
alias: Option<String>,
},
Min {
column: String,
alias: Option<String>,
},
Max {
column: String,
alias: Option<String>,
},
Std {
column: String,
alias: Option<String>,
},
Var {
column: String,
alias: Option<String>,
},
}
#[derive(Debug, Clone, Hash)]
pub enum WindowOp {
RowNumber {
alias: String,
},
Rank {
column: String,
alias: String,
},
Lag {
column: String,
offset: i64,
alias: String,
},
Lead {
column: String,
offset: i64,
alias: String,
},
RollingMean {
column: String,
window: usize,
alias: String,
},
RollingSum {
column: String,
window: usize,
alias: String,
},
}
#[derive(Debug, Clone, Hash)]
pub enum OutputFormat {
DataFrame,
Json,
Csv,
Parquet,
Arrow,
}
#[derive(Debug, Clone, Hash)]
pub enum DataFormat {
Csv,
Json,
Parquet,
Arrow,
}
#[derive(Debug, Clone, Hash)]
pub enum DataType {
Int32,
Int64,
Float32,
Float64,
String,
Boolean,
Date,
DateTime,
}
#[derive(Debug, Clone, Hash)]
pub enum FillValue {
Zero,
Mean,
Median,
Mode,
Forward,
Backward,
Custom(serde_json::Value),
}
pub struct DataProcessor {
strategy_selector: StrategySelector,
cache: HashMap<u64, ProcessedData>,
stream_buffers: HashMap<String, StreamBuffer>,
}
impl DataProcessor {
pub fn new() -> Result<Self, DataError> {
Ok(Self {
strategy_selector: StrategySelector::new(),
cache: HashMap::new(),
stream_buffers: HashMap::new(),
})
}
pub async fn process(&mut self, spec: &DataSpec) -> Result<ProcessedData, DataError> {
let spec_hash = spec.hash();
if let Some(cached) = self.cache.get(&spec_hash) {
return Ok(cached.clone());
}
let strategy = self.strategy_selector.select(spec);
let result = match strategy {
ProcessingStrategy::CPU(config) => self.process_cpu(spec, &config).await,
ProcessingStrategy::GPU(config) => self.process_gpu(spec, &config).await,
ProcessingStrategy::Streaming(config) => self.process_streaming(spec, &config).await,
ProcessingStrategy::Hybrid(config) => self.process_hybrid(spec, &config).await,
}?;
self.cache.insert(spec_hash, result.clone());
Ok(result)
}
async fn process_cpu(
&self,
spec: &DataSpec,
config: &RayonConfig,
) -> Result<ProcessedData, DataError> {
let mut df = self.load_data(&spec.source).await?;
for filter in &spec.filters {
df = self.apply_filter(df, filter)?;
}
for transform in &spec.transforms {
df = self.apply_transform(df, transform)?;
}
for aggregation in &spec.aggregations {
df = self.apply_aggregation(df, aggregation)?;
}
Ok(ProcessedData {
data: df,
metadata: DataMetadata::from_dataframe(&df),
processing_time: std::time::Duration::from_millis(0), })
}
async fn process_gpu(
&self,
spec: &DataSpec,
config: &ComputeConfig,
) -> Result<ProcessedData, DataError> {
self.process_cpu(
spec,
&RayonConfig {
num_threads: None,
chunk_size: None,
},
)
.await
}
async fn process_streaming(
&mut self,
spec: &DataSpec,
config: &StreamConfig,
) -> Result<ProcessedData, DataError> {
if let DataSource::Stream { stream_id } = &spec.source {
let buffer = self
.stream_buffers
.entry(stream_id.clone())
.or_insert_with(|| StreamBuffer::new(config.buffer_size));
buffer.process_batch(config.batch_size)
} else {
Err(DataError::Processing(
"Streaming strategy requires stream source".to_string(),
))
}
}
async fn process_hybrid(
&self,
spec: &DataSpec,
config: &HybridConfig,
) -> Result<ProcessedData, DataError> {
let data_size = spec.estimated_size();
if data_size < config.cpu_threshold {
self.process_cpu(spec, &config.cpu_config).await
} else if data_size < config.gpu_threshold {
self.process_gpu(spec, &config.gpu_config).await
} else {
self.process_cpu(spec, &config.cpu_config).await
}
}
async fn load_data(&self, source: &DataSource) -> Result<DataFrame, DataError> {
match source {
DataSource::DataFrame(df) => Ok(df.clone()),
DataSource::Url { url, format } => match format {
DataFormat::Csv => {
let df = LazyFrame::scan_csv(url, ScanArgsIo::default())
.collect()
.map_err(DataError::Polars)?;
Ok(df)
}
DataFormat::Json => {
let df = LazyFrame::scan_ndjson(url, ScanArgsNdJson::default())
.collect()
.map_err(DataError::Polars)?;
Ok(df)
}
DataFormat::Parquet => {
let df = LazyFrame::scan_parquet(url, ScanArgsParquet::default())
.collect()
.map_err(DataError::Polars)?;
Ok(df)
}
_ => Err(DataError::Format("Unsupported format".to_string())),
},
DataSource::Query { sql, dataset } => {
Err(DataError::Processing(
"SQL queries not yet implemented".to_string(),
))
}
DataSource::Stream { stream_id } => {
Err(DataError::Processing(
"Stream processing not yet implemented".to_string(),
))
}
}
}
fn apply_filter(&self, df: DataFrame, filter: &Filter) -> Result<DataFrame, DataError> {
match filter {
Filter::Expression { expr } => {
let lazy_df = df.lazy().filter(col(expr));
lazy_df.collect().map_err(DataError::Polars)
}
Filter::Range { column, min, max } => {
let mut lazy_df = df.lazy();
if let Some(min_val) = min {
lazy_df = lazy_df.filter(col(column).gt(lit(*min_val)));
}
if let Some(max_val) = max {
lazy_df = lazy_df.filter(col(column).lt(lit(*max_val)));
}
lazy_df.collect().map_err(DataError::Polars)
}
Filter::Values { column, values } => {
let lazy_df = df.lazy().filter(col(column).is_in(values));
lazy_df.collect().map_err(DataError::Polars)
}
Filter::Null { column, keep_nulls } => {
if *keep_nulls {
df.lazy()
.filter(col(column).is_null())
.collect()
.map_err(DataError::Polars)
} else {
df.lazy()
.filter(col(column).is_not_null())
.collect()
.map_err(DataError::Polars)
}
}
}
}
fn apply_transform(
&self,
df: DataFrame,
transform: &DataTransform,
) -> Result<DataFrame, DataError> {
match transform {
DataTransform::Select { columns } => {
let lazy_df = df.lazy().select(columns.iter().map(|c| col(c)));
lazy_df.collect().map_err(DataError::Polars)
}
DataTransform::Rename { mappings } => {
let mut lazy_df = df.lazy();
for (old_name, new_name) in mappings {
lazy_df = lazy_df.rename([old_name], [new_name]);
}
lazy_df.collect().map_err(DataError::Polars)
}
DataTransform::Cast { column, data_type } => {
let polars_type = match data_type {
DataType::Int32 => DataType::Int32,
DataType::Int64 => DataType::Int64,
DataType::Float32 => DataType::Float32,
DataType::Float64 => DataType::Float64,
DataType::String => DataType::String,
DataType::Boolean => DataType::Boolean,
DataType::Date => DataType::Date,
DataType::DateTime => DataType::Datetime(TimeUnit::Milliseconds, None),
};
let lazy_df = df.lazy().with_columns([col(column).cast(polars_type)]);
lazy_df.collect().map_err(DataError::Polars)
}
DataTransform::FillNull { column, value } => {
let fill_value = match value {
FillValue::Zero => lit(0),
FillValue::Mean => col(column).mean(),
FillValue::Median => col(column).median(),
FillValue::Mode => col(column).mode().first(),
FillValue::Forward => col(column).forward_fill(),
FillValue::Backward => col(column).backward_fill(),
FillValue::Custom(val) => lit(val),
};
let lazy_df = df.lazy().with_columns([col(column).fill_null(fill_value)]);
lazy_df.collect().map_err(DataError::Polars)
}
DataTransform::DropNulls { columns } => {
let lazy_df = if let Some(cols) = columns {
df.lazy().drop_nulls(Some(cols))
} else {
df.lazy().drop_nulls(None)
};
lazy_df.collect().map_err(DataError::Polars)
}
}
}
fn apply_aggregation(
&self,
df: DataFrame,
aggregation: &Aggregation,
) -> Result<DataFrame, DataError> {
match aggregation {
Aggregation::GroupBy { columns } => {
let lazy_df = df.lazy().group_by(columns.iter().map(|c| col(c)));
lazy_df.collect().map_err(DataError::Polars)
}
Aggregation::Aggregate { operations } => {
let mut lazy_df = df.lazy();
let agg_exprs: Vec<Expr> = operations
.iter()
.map(|op| {
match op {
AggOp::Sum { column, alias } => {
let expr = col(column).sum();
if let Some(alias) = alias {
expr.alias(alias)
} else {
expr
}
}
AggOp::Mean { column, alias } => {
let expr = col(column).mean();
if let Some(alias) = alias {
expr.alias(alias)
} else {
expr
}
}
AggOp::Count { column, alias } => {
let expr = col(column).count();
if let Some(alias) = alias {
expr.alias(alias)
} else {
expr
}
}
_ => col(column).sum(), }
})
.collect();
lazy_df.agg(agg_exprs).collect().map_err(DataError::Polars)
}
_ => Ok(df), }
}
}
#[derive(Debug, Clone)]
pub struct ProcessedData {
pub data: DataFrame,
pub metadata: DataMetadata,
pub processing_time: std::time::Duration,
}
#[derive(Debug, Clone)]
pub struct DataMetadata {
pub row_count: usize,
pub column_count: usize,
pub column_types: HashMap<String, String>,
pub memory_usage: usize,
pub processing_stats: ProcessingStats,
}
impl DataMetadata {
pub fn from_dataframe(df: &DataFrame) -> Self {
let column_types: HashMap<String, String> = df
.get_column_names()
.iter()
.map(|name| {
let dtype = df.column(name).unwrap().dtype();
(name.clone(), format!("{:?}", dtype))
})
.collect();
Self {
row_count: df.height(),
column_count: df.width(),
column_types,
memory_usage: df.estimated_size(),
processing_stats: ProcessingStats::default(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ProcessingStats {
pub cpu_time: std::time::Duration,
pub gpu_time: std::time::Duration,
pub memory_peak: usize,
pub cache_hits: u64,
pub cache_misses: u64,
}
pub struct StreamBuffer {
buffer: Vec<DataFrame>,
max_size: usize,
dropped_count: u64,
}
impl StreamBuffer {
pub fn new(max_size: usize) -> Self {
Self {
buffer: Vec::new(),
max_size,
dropped_count: 0,
}
}
pub fn push(&mut self, data: DataFrame) {
if self.buffer.len() >= self.max_size {
self.buffer.remove(0);
self.dropped_count += 1;
}
self.buffer.push(data);
}
pub fn process_batch(&mut self, batch_size: usize) -> Result<ProcessedData, DataError> {
if self.buffer.is_empty() {
return Ok(ProcessedData {
data: DataFrame::empty(),
metadata: DataMetadata::from_dataframe(&DataFrame::empty()),
processing_time: std::time::Duration::from_millis(0),
});
}
let combined_df = self.buffer.iter().fold(DataFrame::empty(), |acc, df| {
if acc.is_empty() {
df.clone()
} else {
concat(&[acc, df.clone()], Default::default()).unwrap_or(df.clone())
}
});
Ok(ProcessedData {
data: combined_df,
metadata: DataMetadata::from_dataframe(&combined_df),
processing_time: std::time::Duration::from_millis(0),
})
}
pub fn health_metrics(&self) -> StreamHealth {
StreamHealth {
buffer_utilization: self.buffer.len() as f32 / self.max_size as f32,
dropped_messages: self.dropped_count,
current_size: self.buffer.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct StreamHealth {
pub buffer_utilization: f32,
pub dropped_messages: u64,
pub current_size: usize,
}