#![allow(dead_code)]
use std::sync::Arc;
use arrow::{
array::{
Array, ArrayRef, BooleanArray, BooleanBuilder, Date32Array, Float64Array, Int64Array,
PrimitiveBuilder, StringArray, StringBuilder, TimestampMicrosecondArray,
},
datatypes::{
DataType, Date32Type, Field, Float64Type, Int64Type, Schema, TimeUnit,
TimestampMicrosecondType,
},
record_batch::RecordBatch,
};
use vibesql_storage::Row;
use vibesql_types::{Date, SqlValue, Time, Timestamp};
use crate::errors::ExecutorError;
pub const DEFAULT_BATCH_SIZE: usize = 1024;
pub const SCAN_BATCH_SIZE: usize = 4096;
pub const JOIN_BATCH_SIZE: usize = 512;
pub const L1_CACHE_BATCH_SIZE: usize = 1024;
pub const SMALL_BATCH_SIZE: usize = 256;
pub const L2_CACHE_BATCH_SIZE: usize = 2048;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryContext {
Scan,
Join,
GroupBy,
Default,
}
impl QueryContext {
pub fn recommended_batch_size(&self) -> usize {
match self {
QueryContext::Scan => SCAN_BATCH_SIZE,
QueryContext::Join => JOIN_BATCH_SIZE,
QueryContext::GroupBy => L1_CACHE_BATCH_SIZE,
QueryContext::Default => DEFAULT_BATCH_SIZE,
}
}
}
pub fn rows_to_record_batch(
rows: &[Row],
column_names: &[String],
) -> Result<RecordBatch, ExecutorError> {
rows_to_record_batch_with_columns(rows, column_names, None)
}
pub fn rows_to_record_batch_with_columns(
rows: &[Row],
column_names: &[String],
column_indices: Option<&[usize]>,
) -> Result<RecordBatch, ExecutorError> {
if rows.is_empty() {
return Err(ExecutorError::ColumnarLengthMismatch {
expected: 1,
actual: 0,
context: "Cannot create RecordBatch from empty rows".to_string(),
});
}
let indices: Vec<usize> = match column_indices {
Some(cols) => cols.to_vec(),
None => (0..rows[0].values.len()).collect(),
};
if indices.is_empty() {
return Err(ExecutorError::ColumnarColumnNotFound { column_index: 0, batch_columns: 0 });
}
let mut schema_fields = Vec::with_capacity(indices.len());
for &col_idx in &indices {
let name = column_names.get(col_idx).cloned().unwrap_or_else(|| format!("col_{}", col_idx));
let data_type = match &rows[0].values.get(col_idx) {
Some(SqlValue::Integer(_))
| Some(SqlValue::Bigint(_))
| Some(SqlValue::Smallint(_)) => DataType::Int64,
Some(SqlValue::Float(_))
| Some(SqlValue::Real(_))
| Some(SqlValue::Double(_))
| Some(SqlValue::Numeric(_)) => DataType::Float64,
Some(SqlValue::Character(_)) | Some(SqlValue::Varchar(_)) => DataType::Utf8,
Some(SqlValue::Boolean(_)) => DataType::Boolean,
Some(SqlValue::Date(_)) => DataType::Date32,
Some(SqlValue::Timestamp(_)) => DataType::Timestamp(TimeUnit::Microsecond, None),
Some(SqlValue::Null) => DataType::Int64,
_ => {
return Err(ExecutorError::UnsupportedArrayType {
operation: format!("SIMD conversion at column {}", col_idx),
array_type: format!("{:?}", rows[0].values.get(col_idx)),
})
}
};
schema_fields.push(Field::new(name, data_type, true));
}
let schema = Schema::new(schema_fields);
let mut columns: Vec<ArrayRef> = Vec::with_capacity(indices.len());
for (field_idx, &col_idx) in indices.iter().enumerate() {
let field = schema.field(field_idx);
let array = build_column_array(rows, col_idx, field.data_type())?;
columns.push(array);
}
RecordBatch::try_new(Arc::new(schema), columns).map_err(|e| {
ExecutorError::SimdOperationFailed {
operation: "RecordBatch creation".to_string(),
reason: e.to_string(),
}
})
}
pub fn record_batch_to_rows(batch: &RecordBatch) -> Result<Vec<Row>, ExecutorError> {
let num_rows = batch.num_rows();
let num_columns = batch.num_columns();
let mut rows = Vec::with_capacity(num_rows);
for row_idx in 0..num_rows {
let mut values = Vec::with_capacity(num_columns);
for col_idx in 0..num_columns {
let array = batch.column(col_idx);
let value = arrow_value_to_sql(array, row_idx)?;
values.push(value);
}
rows.push(Row::from_vec(values));
}
Ok(rows)
}
pub(super) fn date_to_days_since_epoch(date: &Date) -> i32 {
let year_days = (date.year - 1970) * 365;
let leap_years =
((date.year - 1969) / 4) - ((date.year - 1901) / 100) + ((date.year - 1601) / 400);
let month_days: i32 = match date.month {
1 => 0,
2 => 31,
3 => 59,
4 => 90,
5 => 120,
6 => 151,
7 => 181,
8 => 212,
9 => 243,
10 => 273,
11 => 304,
12 => 334,
_ => 0,
};
let is_leap = date.year % 4 == 0 && (date.year % 100 != 0 || date.year % 400 == 0);
let leap_adjustment = if is_leap && date.month > 2 { 1 } else { 0 };
year_days + leap_years + month_days + leap_adjustment + (date.day as i32) - 1
}
pub(super) fn days_since_epoch_to_date(days: i32) -> Date {
let mut year = 1970;
let mut remaining_days = days;
loop {
let year_days =
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) { 366 } else { 365 };
if remaining_days < year_days {
break;
}
remaining_days -= year_days;
year += 1;
}
let is_leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
let month_lengths = if is_leap {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut month = 1;
for &days_in_month in &month_lengths {
if remaining_days < days_in_month {
break;
}
remaining_days -= days_in_month;
month += 1;
}
let day = remaining_days + 1;
Date::new(year, month as u8, day as u8).unwrap_or_else(|_| Date::new(1970, 1, 1).unwrap())
}
pub(super) fn timestamp_to_microseconds(ts: &Timestamp) -> i64 {
let days = date_to_days_since_epoch(&ts.date);
let day_micros = days as i64 * 86_400_000_000; let time_micros = (ts.time.hour as i64) * 3_600_000_000
+ (ts.time.minute as i64) * 60_000_000
+ (ts.time.second as i64) * 1_000_000
+ (ts.time.nanosecond as i64) / 1_000; day_micros + time_micros
}
pub(super) fn microseconds_to_timestamp(micros: i64) -> Timestamp {
let days = (micros / 86_400_000_000) as i32;
let remaining_micros = micros % 86_400_000_000;
let date = days_since_epoch_to_date(days);
let hours = (remaining_micros / 3_600_000_000) as u8;
let remaining_micros = remaining_micros % 3_600_000_000;
let minutes = (remaining_micros / 60_000_000) as u8;
let remaining_micros = remaining_micros % 60_000_000;
let seconds = (remaining_micros / 1_000_000) as u8;
let nanoseconds = ((remaining_micros % 1_000_000) * 1_000) as u32;
let time = Time::new(hours, minutes, seconds, nanoseconds)
.unwrap_or_else(|_| Time::new(0, 0, 0, 0).unwrap());
Timestamp::new(date, time)
}
fn infer_schema_from_row(row: &Row, column_names: &[String]) -> Result<Schema, ExecutorError> {
let fields: Result<Vec<_>, _> = row
.values
.iter()
.enumerate()
.map(|(idx, value)| {
let name = column_names.get(idx).cloned().unwrap_or_else(|| format!("col_{}", idx));
let data_type = match value {
SqlValue::Integer(_) | SqlValue::Bigint(_) | SqlValue::Smallint(_) => {
DataType::Int64
}
SqlValue::Float(_)
| SqlValue::Real(_)
| SqlValue::Double(_)
| SqlValue::Numeric(_) => DataType::Float64,
SqlValue::Character(_) | SqlValue::Varchar(_) => DataType::Utf8,
SqlValue::Boolean(_) => DataType::Boolean,
SqlValue::Date(_) => DataType::Date32,
SqlValue::Timestamp(_) => DataType::Timestamp(TimeUnit::Microsecond, None),
SqlValue::Null => DataType::Int64, _ => {
return Err(ExecutorError::UnsupportedArrayType {
operation: "SIMD schema inference".to_string(),
array_type: format!("{:?}", value),
})
}
};
Ok(Field::new(name, data_type, true))
})
.collect();
Ok(Schema::new(fields?))
}
fn build_column_array(
rows: &[Row],
col_idx: usize,
data_type: &DataType,
) -> Result<ArrayRef, ExecutorError> {
match data_type {
DataType::Int64 => {
let mut builder = PrimitiveBuilder::<Int64Type>::new();
for row in rows {
match &row.values[col_idx] {
SqlValue::Integer(v) => builder.append_value(*v),
SqlValue::Bigint(v) => builder.append_value(*v),
SqlValue::Smallint(v) => builder.append_value(*v as i64),
SqlValue::Null => builder.append_null(),
_ => builder.append_null(),
}
}
Ok(Arc::new(builder.finish()))
}
DataType::Float64 => {
let mut builder = PrimitiveBuilder::<Float64Type>::new();
for row in rows {
match &row.values[col_idx] {
SqlValue::Float(v) => builder.append_value(*v as f64),
SqlValue::Real(v) => builder.append_value(*v as f64),
SqlValue::Double(v) => builder.append_value(*v),
SqlValue::Numeric(v) => builder.append_value(*v),
SqlValue::Integer(v) => builder.append_value(*v as f64),
SqlValue::Null => builder.append_null(),
_ => builder.append_null(),
}
}
Ok(Arc::new(builder.finish()))
}
DataType::Utf8 => {
let mut builder = StringBuilder::new();
for row in rows {
match &row.values[col_idx] {
SqlValue::Character(s) | SqlValue::Varchar(s) => builder.append_value(s),
SqlValue::Null => builder.append_null(),
_ => builder.append_null(),
}
}
Ok(Arc::new(builder.finish()))
}
DataType::Boolean => {
let mut builder = BooleanBuilder::new();
for row in rows {
match &row.values[col_idx] {
SqlValue::Boolean(b) => builder.append_value(*b),
SqlValue::Null => builder.append_null(),
_ => builder.append_null(),
}
}
Ok(Arc::new(builder.finish()))
}
DataType::Date32 => {
let mut builder = PrimitiveBuilder::<Date32Type>::new();
for row in rows {
match &row.values[col_idx] {
SqlValue::Date(d) => builder.append_value(date_to_days_since_epoch(d)),
SqlValue::Null => builder.append_null(),
_ => builder.append_null(),
}
}
Ok(Arc::new(builder.finish()))
}
DataType::Timestamp(TimeUnit::Microsecond, None) => {
let mut builder = PrimitiveBuilder::<TimestampMicrosecondType>::new();
for row in rows {
match &row.values[col_idx] {
SqlValue::Timestamp(ts) => builder.append_value(timestamp_to_microseconds(ts)),
SqlValue::Null => builder.append_null(),
_ => builder.append_null(),
}
}
Ok(Arc::new(builder.finish()))
}
_ => Err(ExecutorError::UnsupportedArrayType {
operation: "build_column_array".to_string(),
array_type: format!("{:?}", data_type),
}),
}
}
fn arrow_value_to_sql(array: &dyn Array, idx: usize) -> Result<SqlValue, ExecutorError> {
if array.is_null(idx) {
return Ok(SqlValue::Null);
}
match array.data_type() {
DataType::Int64 => {
let arr = array.as_any().downcast_ref::<Int64Array>().ok_or_else(|| {
ExecutorError::ArrowDowncastError {
expected_type: "Int64Array".to_string(),
context: "arrow_value_to_sql".to_string(),
}
})?;
Ok(SqlValue::Integer(arr.value(idx)))
}
DataType::Float64 => {
let arr = array.as_any().downcast_ref::<Float64Array>().ok_or_else(|| {
ExecutorError::ArrowDowncastError {
expected_type: "Float64Array".to_string(),
context: "arrow_value_to_sql".to_string(),
}
})?;
Ok(SqlValue::Double(arr.value(idx)))
}
DataType::Utf8 => {
let arr = array.as_any().downcast_ref::<StringArray>().ok_or_else(|| {
ExecutorError::ArrowDowncastError {
expected_type: "StringArray".to_string(),
context: "arrow_value_to_sql".to_string(),
}
})?;
Ok(SqlValue::Varchar(arr.value(idx).into()))
}
DataType::Boolean => {
let arr = array.as_any().downcast_ref::<BooleanArray>().ok_or_else(|| {
ExecutorError::ArrowDowncastError {
expected_type: "BooleanArray".to_string(),
context: "arrow_value_to_sql".to_string(),
}
})?;
Ok(SqlValue::Boolean(arr.value(idx)))
}
DataType::Date32 => {
let arr = array.as_any().downcast_ref::<Date32Array>().ok_or_else(|| {
ExecutorError::ArrowDowncastError {
expected_type: "Date32Array".to_string(),
context: "arrow_value_to_sql".to_string(),
}
})?;
let days = arr.value(idx);
Ok(SqlValue::Date(days_since_epoch_to_date(days)))
}
DataType::Timestamp(TimeUnit::Microsecond, None) => {
let arr =
array.as_any().downcast_ref::<TimestampMicrosecondArray>().ok_or_else(|| {
ExecutorError::ArrowDowncastError {
expected_type: "TimestampMicrosecondArray".to_string(),
context: "arrow_value_to_sql".to_string(),
}
})?;
let micros = arr.value(idx);
Ok(SqlValue::Timestamp(microseconds_to_timestamp(micros)))
}
_ => Err(ExecutorError::UnsupportedArrayType {
operation: "arrow_value_to_sql conversion".to_string(),
array_type: format!("{:?}", array.data_type()),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rows_to_record_batch_basic() {
let rows = vec![
Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("hello")),
]),
Row::from_vec(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("world")),
]),
];
let column_names = vec!["id".to_string(), "name".to_string()];
let batch = rows_to_record_batch(&rows, &column_names).unwrap();
assert_eq!(batch.num_rows(), 2);
assert_eq!(batch.num_columns(), 2);
}
#[test]
fn test_round_trip_conversion() {
let original_rows = vec![
Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Double(3.5),
SqlValue::Varchar(arcstr::ArcStr::from("test")),
SqlValue::Boolean(true),
]),
Row::from_vec(vec![
SqlValue::Integer(2),
SqlValue::Double(2.5),
SqlValue::Varchar(arcstr::ArcStr::from("data")),
SqlValue::Boolean(false),
]),
];
let column_names =
vec!["id".to_string(), "value".to_string(), "name".to_string(), "flag".to_string()];
let batch = rows_to_record_batch(&original_rows, &column_names).unwrap();
let converted_rows = record_batch_to_rows(&batch).unwrap();
assert_eq!(original_rows.len(), converted_rows.len());
assert_eq!(original_rows[0].values.len(), converted_rows[0].values.len());
}
#[test]
fn test_column_pruning() {
let rows = vec![
Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Double(3.5),
SqlValue::Varchar(arcstr::ArcStr::from("test")),
SqlValue::Boolean(true),
]),
Row::from_vec(vec![
SqlValue::Integer(2),
SqlValue::Double(2.5),
SqlValue::Varchar(arcstr::ArcStr::from("data")),
SqlValue::Boolean(false),
]),
];
let column_names =
vec!["id".to_string(), "value".to_string(), "name".to_string(), "flag".to_string()];
let column_indices = vec![0, 2];
let batch =
rows_to_record_batch_with_columns(&rows, &column_names, Some(&column_indices)).unwrap();
assert_eq!(batch.num_columns(), 2);
assert_eq!(batch.num_rows(), 2);
let schema = batch.schema();
assert_eq!(schema.field(0).name(), "id");
assert_eq!(schema.field(1).name(), "name");
}
#[test]
#[allow(clippy::assertions_on_constants)]
fn test_adaptive_batch_sizing() {
assert_eq!(QueryContext::Scan.recommended_batch_size(), SCAN_BATCH_SIZE);
assert_eq!(QueryContext::Join.recommended_batch_size(), JOIN_BATCH_SIZE);
assert_eq!(QueryContext::GroupBy.recommended_batch_size(), L1_CACHE_BATCH_SIZE);
assert_eq!(QueryContext::Default.recommended_batch_size(), DEFAULT_BATCH_SIZE);
assert!(SCAN_BATCH_SIZE > DEFAULT_BATCH_SIZE);
assert!(JOIN_BATCH_SIZE < DEFAULT_BATCH_SIZE);
}
#[test]
fn test_date_conversion() {
use vibesql_types::Date;
let date1 = Date::new(2024, 1, 15).unwrap();
let date2 = Date::new(2024, 12, 25).unwrap();
let rows = vec![
Row::from_vec(vec![SqlValue::Date(date1), SqlValue::Integer(1)]),
Row::from_vec(vec![SqlValue::Date(date2), SqlValue::Integer(2)]),
];
let column_names = vec!["date_col".to_string(), "id".to_string()];
let batch = rows_to_record_batch(&rows, &column_names).unwrap();
assert_eq!(batch.num_rows(), 2);
assert_eq!(batch.num_columns(), 2);
let converted_rows = record_batch_to_rows(&batch).unwrap();
assert_eq!(rows.len(), converted_rows.len());
}
#[test]
fn test_timestamp_conversion() {
use vibesql_types::{Date, Time, Timestamp};
let date = Date::new(2024, 6, 15).unwrap();
let time = Time::new(14, 30, 45, 123456000).unwrap(); let ts = Timestamp::new(date, time);
let rows = vec![Row::from_vec(vec![SqlValue::Timestamp(ts), SqlValue::Integer(1)])];
let column_names = vec!["ts_col".to_string(), "id".to_string()];
let batch = rows_to_record_batch(&rows, &column_names).unwrap();
assert_eq!(batch.num_rows(), 1);
assert_eq!(batch.num_columns(), 2);
let converted_rows = record_batch_to_rows(&batch).unwrap();
assert_eq!(rows.len(), converted_rows.len());
}
#[test]
fn test_date_timestamp_round_trip() {
use vibesql_types::{Date, Time, Timestamp};
let date1 = Date::new(2020, 1, 1).unwrap();
let date2 = Date::new(2025, 12, 31).unwrap();
let ts_date = Date::new(2024, 7, 4).unwrap();
let ts_time = Time::new(10, 20, 30, 500000000).unwrap(); let ts = Timestamp::new(ts_date, ts_time);
let original_rows = vec![
Row::from_vec(vec![
SqlValue::Date(date1),
SqlValue::Timestamp(ts),
SqlValue::Integer(100),
]),
Row::from_vec(vec![
SqlValue::Date(date2),
SqlValue::Timestamp(ts),
SqlValue::Integer(200),
]),
];
let column_names = vec!["date_col".to_string(), "ts_col".to_string(), "id".to_string()];
let batch = rows_to_record_batch(&original_rows, &column_names).unwrap();
let converted_rows = record_batch_to_rows(&batch).unwrap();
assert_eq!(original_rows.len(), converted_rows.len());
assert_eq!(original_rows[0].values.len(), converted_rows[0].values.len());
}
#[test]
fn test_null_values_in_numeric_columns() {
let original_rows = vec![
Row::from_vec(vec![SqlValue::Integer(100), SqlValue::Double(3.5)]),
Row::from_vec(vec![
SqlValue::Null, SqlValue::Double(2.5),
]),
Row::from_vec(vec![
SqlValue::Integer(200),
SqlValue::Null, ]),
Row::from_vec(vec![
SqlValue::Null, SqlValue::Null, ]),
];
let column_names = vec!["int_col".to_string(), "float_col".to_string()];
let batch = rows_to_record_batch(&original_rows, &column_names).unwrap();
assert_eq!(batch.num_rows(), 4);
assert_eq!(batch.num_columns(), 2);
let int_array = batch.column(0).as_any().downcast_ref::<Int64Array>().unwrap();
assert!(!int_array.is_null(0)); assert!(int_array.is_null(1)); assert!(!int_array.is_null(2)); assert!(int_array.is_null(3));
let float_array = batch.column(1).as_any().downcast_ref::<Float64Array>().unwrap();
assert!(!float_array.is_null(0)); assert!(!float_array.is_null(1)); assert!(float_array.is_null(2)); assert!(float_array.is_null(3));
let converted_rows = record_batch_to_rows(&batch).unwrap();
assert_eq!(original_rows.len(), converted_rows.len());
assert!(matches!(converted_rows[1].values[0], SqlValue::Null));
assert!(matches!(converted_rows[2].values[1], SqlValue::Null));
assert!(matches!(converted_rows[3].values[0], SqlValue::Null));
assert!(matches!(converted_rows[3].values[1], SqlValue::Null));
}
#[test]
fn test_null_values_in_all_column_types() {
let original_rows = vec![
Row::from_vec(vec![
SqlValue::Integer(1),
SqlValue::Double(1.1),
SqlValue::Varchar(arcstr::ArcStr::from("test")),
SqlValue::Boolean(true),
]),
Row::from_vec(vec![SqlValue::Null, SqlValue::Null, SqlValue::Null, SqlValue::Null]),
Row::from_vec(vec![
SqlValue::Integer(3),
SqlValue::Double(3.3),
SqlValue::Varchar(arcstr::ArcStr::from("data")),
SqlValue::Boolean(false),
]),
];
let column_names = vec![
"int_col".to_string(),
"float_col".to_string(),
"str_col".to_string(),
"bool_col".to_string(),
];
let batch = rows_to_record_batch(&original_rows, &column_names).unwrap();
assert!(!batch.column(0).is_null(0));
assert!(batch.column(0).is_null(1)); assert!(!batch.column(0).is_null(2));
assert!(!batch.column(1).is_null(0));
assert!(batch.column(1).is_null(1)); assert!(!batch.column(1).is_null(2));
assert!(!batch.column(2).is_null(0));
assert!(batch.column(2).is_null(1)); assert!(!batch.column(2).is_null(2));
assert!(!batch.column(3).is_null(0));
assert!(batch.column(3).is_null(1)); assert!(!batch.column(3).is_null(2));
let converted_rows = record_batch_to_rows(&batch).unwrap();
assert_eq!(original_rows.len(), converted_rows.len());
assert!(matches!(converted_rows[1].values[0], SqlValue::Null));
assert!(matches!(converted_rows[1].values[1], SqlValue::Null));
assert!(matches!(converted_rows[1].values[2], SqlValue::Null));
assert!(matches!(converted_rows[1].values[3], SqlValue::Null));
}
#[test]
fn test_mixed_null_non_null_numeric() {
let original_rows = vec![
Row::from_vec(vec![SqlValue::Integer(10), SqlValue::Double(0.1)]),
Row::from_vec(vec![
SqlValue::Integer(20),
SqlValue::Null, ]),
Row::from_vec(vec![SqlValue::Integer(30), SqlValue::Double(0.2)]),
Row::from_vec(vec![
SqlValue::Integer(40),
SqlValue::Null, ]),
];
let column_names = vec!["quantity".to_string(), "discount".to_string()];
let batch = rows_to_record_batch(&original_rows, &column_names).unwrap();
let discount_array = batch.column(1).as_any().downcast_ref::<Float64Array>().unwrap();
assert!(!discount_array.is_null(0));
assert!(discount_array.is_null(1));
assert!(!discount_array.is_null(2));
assert!(discount_array.is_null(3));
let converted_rows = record_batch_to_rows(&batch).unwrap();
assert!(matches!(converted_rows[1].values[1], SqlValue::Null));
assert!(matches!(converted_rows[3].values[1], SqlValue::Null));
match &converted_rows[0].values[1] {
SqlValue::Double(v) => assert!((v - 0.1).abs() < 0.001),
_ => panic!("Expected Double value"),
}
match &converted_rows[2].values[1] {
SqlValue::Double(v) => assert!((v - 0.2).abs() < 0.001),
_ => panic!("Expected Double value"),
}
}
}