use vibesql_ast::{Expression, SelectItem};
use vibesql_storage::Row;
use super::{ExecutionContext, ExecutionPipeline, PipelineInput, PipelineOutput};
use crate::{
errors::ExecutorError,
select::columnar::{
extract_aggregates, extract_column_predicates, simd_filter_batch, AggregateOp,
AggregateSource, AggregateSpec, ColumnarBatch,
},
};
pub struct NativeColumnarPipeline {
#[allow(dead_code)]
has_columnar_storage: bool,
}
impl NativeColumnarPipeline {
#[inline]
pub fn new() -> Self {
Self { has_columnar_storage: true }
}
#[inline]
#[allow(dead_code)]
pub fn with_storage(has_columnar_storage: bool) -> Self {
Self { has_columnar_storage }
}
}
impl Default for NativeColumnarPipeline {
fn default() -> Self {
Self::new()
}
}
impl ExecutionPipeline for NativeColumnarPipeline {
#[inline]
fn create_evaluator<'a>(
&self,
ctx: &'a ExecutionContext<'a>,
) -> crate::evaluator::CombinedExpressionEvaluator<'a> {
ctx.create_evaluator()
}
fn apply_filter(
&self,
input: PipelineInput<'_>,
predicate: Option<&Expression>,
ctx: &ExecutionContext<'_>,
) -> Result<PipelineOutput, ExecutorError> {
if let PipelineInput::NativeColumnar { table_name, column_indices: _ } = &input {
let columnar_table = match ctx.database.get_columnar(table_name) {
Ok(Some(ct)) => ct,
Ok(None) | Err(_) => {
return self.fallback_filter(input.into_rows(), predicate, ctx);
}
};
if columnar_table.row_count() == 0 {
return Ok(PipelineOutput::Empty);
}
let batch = ColumnarBatch::from_storage_columnar(&columnar_table)?;
if predicate.is_none() {
let rows = batch.to_rows()?;
return Ok(PipelineOutput::from_rows(rows));
}
let predicate = predicate.unwrap();
let predicates = match extract_column_predicates(predicate, ctx.schema, ctx.database.case_sensitive_like()) {
Some(preds) => preds,
None => {
let rows = batch.to_rows()?;
return self.fallback_filter(rows, Some(predicate), ctx);
}
};
let filtered_batch =
if predicates.is_empty() { batch } else { simd_filter_batch(&batch, &predicates)? };
return Ok(PipelineOutput::from_batch(filtered_batch));
}
let rows = input.into_rows();
if predicate.is_none() {
return Ok(PipelineOutput::from_rows(rows));
}
let predicate = predicate.unwrap();
let predicates = match extract_column_predicates(predicate, ctx.schema, ctx.database.case_sensitive_like()) {
Some(preds) => preds,
None => {
return self.fallback_filter(rows, Some(predicate), ctx);
}
};
if rows.is_empty() {
return Ok(PipelineOutput::Empty);
}
let batch = ColumnarBatch::from_rows(&rows)?;
let filtered_batch =
if predicates.is_empty() { batch } else { simd_filter_batch(&batch, &predicates)? };
Ok(PipelineOutput::from_batch(filtered_batch))
}
fn apply_projection(
&self,
input: PipelineInput<'_>,
select_items: &[SelectItem],
ctx: &ExecutionContext<'_>,
) -> Result<PipelineOutput, ExecutorError> {
let rows = match input {
PipelineInput::Batch(batch) => {
if batch.row_count() == 0 {
return Ok(PipelineOutput::Empty);
}
batch.to_rows()?
}
PipelineInput::Rows(rows) => {
if rows.is_empty() {
return Ok(PipelineOutput::Empty);
}
rows.to_vec()
}
PipelineInput::RowsOwned(rows) => {
if rows.is_empty() {
return Ok(PipelineOutput::Empty);
}
rows
}
PipelineInput::Empty => return Ok(PipelineOutput::Empty),
PipelineInput::NativeColumnar { .. } => {
return Ok(PipelineOutput::Empty);
}
};
let evaluator = ctx.create_evaluator();
let buffer_pool = vibesql_storage::QueryBufferPool::new();
let mut projected_rows = Vec::with_capacity(rows.len());
for row in rows {
let projected = crate::select::projection::project_row_combined(
&row,
select_items,
&evaluator,
ctx.schema,
&None,
&buffer_pool,
)?;
projected_rows.push(projected);
}
Ok(PipelineOutput::from_rows(projected_rows))
}
fn apply_aggregation(
&self,
input: PipelineInput<'_>,
select_items: &[SelectItem],
group_by: Option<&[Expression]>,
having: Option<&Expression>,
ctx: &ExecutionContext<'_>,
) -> Result<PipelineOutput, ExecutorError> {
if having.is_some() {
return Err(ExecutorError::UnsupportedFeature(
"HAVING not supported in native columnar pipeline".to_string(),
));
}
let agg_exprs: Vec<Expression> = select_items
.iter()
.filter_map(|item| {
if let SelectItem::Expression { expr, .. } = item {
Some(expr.clone())
} else {
None
}
})
.collect();
let agg_specs = match extract_aggregates(&agg_exprs, ctx.schema) {
Some(specs) => specs,
None => {
return Err(ExecutorError::UnsupportedFeature(
"Complex aggregates not supported in native columnar pipeline".to_string(),
));
}
};
let has_group_by = group_by.is_some_and(|exprs| !exprs.is_empty());
let group_by_count = group_by.map_or(0, |exprs| exprs.len());
let select_non_agg_count = select_items
.iter()
.filter(|item| {
matches!(item, SelectItem::Expression { expr, .. }
if !matches!(expr, Expression::AggregateFunction { .. }))
})
.count();
if has_group_by && select_non_agg_count != group_by_count {
return Err(ExecutorError::UnsupportedFeature(format!(
"GROUP BY with SELECT list that doesn't include all group keys not supported in native columnar (SELECT has {} non-aggs, GROUP BY has {} keys)",
select_non_agg_count, group_by_count
)));
}
if has_group_by {
let mut first_agg_pos = None;
let mut last_non_agg_pos = None;
for (i, item) in select_items.iter().enumerate() {
if let SelectItem::Expression { expr, .. } = item {
if matches!(expr, Expression::AggregateFunction { .. }) {
if first_agg_pos.is_none() {
first_agg_pos = Some(i);
}
} else {
last_non_agg_pos = Some(i);
}
}
}
if let (Some(first_agg), Some(last_non_agg)) = (first_agg_pos, last_non_agg_pos) {
if first_agg < last_non_agg {
log::debug!(
"Native columnar fallback: aggregate at {} before non-aggregate at {}",
first_agg,
last_non_agg
);
return Err(ExecutorError::UnsupportedFeature(
"SELECT list with aggregates before GROUP BY columns not supported in native columnar".to_string(),
));
}
}
}
let return_empty_result = |has_gb: bool, specs: &[AggregateSpec]| -> PipelineOutput {
if has_gb {
PipelineOutput::from_rows(vec![])
} else {
let values: Vec<vibesql_types::SqlValue> = specs
.iter()
.map(|spec| match spec.op {
AggregateOp::Count => vibesql_types::SqlValue::Integer(0),
_ => vibesql_types::SqlValue::Null,
})
.collect();
PipelineOutput::from_rows(vec![Row::new(values)])
}
};
let batch = match input {
PipelineInput::Batch(batch) => batch,
PipelineInput::Rows(rows) => {
if rows.is_empty() {
return Ok(return_empty_result(has_group_by, &agg_specs));
}
ColumnarBatch::from_rows(rows)?
}
PipelineInput::RowsOwned(rows) => {
if rows.is_empty() {
return Ok(return_empty_result(has_group_by, &agg_specs));
}
ColumnarBatch::from_rows(&rows)?
}
PipelineInput::Empty => {
return Ok(return_empty_result(has_group_by, &agg_specs));
}
PipelineInput::NativeColumnar { table_name, .. } => {
let columnar_table = match ctx.database.get_columnar(&table_name) {
Ok(Some(ct)) => ct,
Ok(None) | Err(_) => {
return Err(ExecutorError::Other(format!(
"Table '{}' not found for columnar aggregation",
table_name
)));
}
};
ColumnarBatch::from_storage_columnar(&columnar_table)?
}
};
if batch.row_count() == 0 {
return Ok(return_empty_result(has_group_by, &agg_specs));
}
if let Some(group_exprs) = group_by {
if !group_exprs.is_empty() {
return self.execute_group_by(&batch, group_exprs, &agg_specs, ctx);
}
}
let needs_schema =
agg_specs.iter().any(|spec| matches!(spec.source, AggregateSource::Expression(_)));
let schema_ref = if needs_schema { Some(ctx.schema) } else { None };
let results =
crate::select::columnar::compute_aggregates_from_batch(&batch, &agg_specs, schema_ref)?;
Ok(PipelineOutput::from_rows(vec![Row::new(results)]))
}
fn supports_query_pattern(
&self,
has_aggregation: bool,
_has_group_by: bool,
has_joins: bool,
) -> bool {
has_aggregation && !has_joins
}
#[inline]
fn name(&self) -> &'static str {
"NativeColumnarPipeline (SIMD)"
}
}
impl NativeColumnarPipeline {
fn fallback_filter(
&self,
rows: Vec<Row>,
predicate: Option<&Expression>,
ctx: &ExecutionContext<'_>,
) -> Result<PipelineOutput, ExecutorError> {
if predicate.is_none() {
return Ok(PipelineOutput::from_rows(rows));
}
let predicate = predicate.unwrap();
let evaluator = ctx.create_evaluator();
let mut filtered = Vec::with_capacity(rows.len());
for row in rows {
let value = evaluator.eval(predicate, &row)?;
let include = match value {
vibesql_types::SqlValue::Boolean(true) => true,
vibesql_types::SqlValue::Boolean(false) | vibesql_types::SqlValue::Null => false,
vibesql_types::SqlValue::Integer(0) => false,
vibesql_types::SqlValue::Integer(_) => true,
_ => false,
};
if include {
filtered.push(row);
}
}
Ok(PipelineOutput::from_rows(filtered))
}
fn execute_group_by(
&self,
batch: &ColumnarBatch,
group_exprs: &[Expression],
agg_specs: &[AggregateSpec],
ctx: &ExecutionContext<'_>,
) -> Result<PipelineOutput, ExecutorError> {
let group_cols: Vec<usize> = group_exprs
.iter()
.filter_map(|expr| match expr {
Expression::ColumnRef(col_id) => {
ctx.schema.get_column_index(col_id.table_canonical(), col_id.column_canonical())
}
_ => None,
})
.collect();
if group_cols.len() != group_exprs.len() {
return Err(ExecutorError::UnsupportedFeature(
"GROUP BY with non-column expressions not supported in native columnar".to_string(),
));
}
let agg_cols: Vec<(usize, AggregateOp)> = agg_specs
.iter()
.filter_map(|spec| match &spec.source {
AggregateSource::Column(idx) => Some((*idx, spec.op)),
AggregateSource::CountStar => Some((0, AggregateOp::Count)),
AggregateSource::Expression(_) => None,
})
.collect();
if agg_cols.len() != agg_specs.len() {
return Err(ExecutorError::UnsupportedFeature(
"Expression aggregates not supported in native columnar GROUP BY".to_string(),
));
}
let result =
crate::select::columnar::columnar_group_by_batch(batch, &group_cols, &agg_cols)?;
Ok(PipelineOutput::from_rows(result))
}
}
#[cfg(test)]
mod tests {
use vibesql_catalog::TableSchema;
use vibesql_types::SqlValue;
use super::*;
use crate::schema::CombinedSchema;
fn create_test_setup() -> (vibesql_storage::Database, CombinedSchema) {
let database = vibesql_storage::Database::new();
let table_schema = TableSchema::new("test".to_string(), vec![]);
let schema = CombinedSchema::from_table("test".to_string(), table_schema);
(database, schema)
}
fn make_test_row(values: Vec<i64>) -> Row {
Row::new(values.into_iter().map(SqlValue::Integer).collect::<Vec<_>>())
}
#[test]
fn test_native_columnar_pipeline_name() {
let pipeline = NativeColumnarPipeline::new();
let name = pipeline.name();
assert!(name.starts_with("NativeColumnarPipeline"));
}
#[test]
fn test_native_columnar_pipeline_supports_aggregates() {
let pipeline = NativeColumnarPipeline::new();
let supports_simple = pipeline.supports_query_pattern(true, false, false);
assert!(supports_simple);
assert!(!pipeline.supports_query_pattern(true, false, true));
}
#[test]
fn test_native_columnar_pipeline_filter_no_predicate() {
let (database, schema) = create_test_setup();
let pipeline = NativeColumnarPipeline::new();
let rows = vec![make_test_row(vec![1, 2]), make_test_row(vec![3, 4])];
let input = PipelineInput::from_rows_owned(rows);
let ctx = ExecutionContext::new(&schema, &database);
let result = pipeline.apply_filter(input, None, &ctx).unwrap();
assert_eq!(result.row_count(), 2);
}
#[test]
fn test_native_columnar_pipeline_default() {
let pipeline = NativeColumnarPipeline::default();
assert!(pipeline.name().starts_with("NativeColumnarPipeline"));
}
#[test]
fn test_native_columnar_pipeline_limit_offset() {
let pipeline = NativeColumnarPipeline::new();
let rows = vec![
make_test_row(vec![1]),
make_test_row(vec![2]),
make_test_row(vec![3]),
make_test_row(vec![4]),
make_test_row(vec![5]),
];
let output = PipelineOutput::from_rows(rows);
let result = pipeline.apply_limit_offset(output, Some(3), None).unwrap();
assert_eq!(result.len(), 3);
}
}