#![deny(clippy::unused_async)]
mod aggregation;
mod execute;
mod lookup;
mod predicate;
mod row_build;
mod value_ops;
mod writetime_ttl;
#[cfg(test)]
pub(crate) mod test_support;
use super::{
access_path::{AccessPath, FallbackReason},
result::{
cql_type_to_data_type, ColumnInfo, ProjectionFlags, QueryMetadata, QueryResult,
QueryResultIterator, QueryRow, StreamingConfig,
},
select_ast::*,
select_optimizer::{AggregationPlan, ExecutionStep, OptimizedQueryPlan, SSTablePredicate},
};
use crate::{
schema::{CqlType, SchemaManager, TableSchema},
storage::StorageEngine,
types::{RowKey, Value},
Error, Result, TableId,
};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::mpsc;
use aggregation::{
build_group_key, finalize_group, find_or_init_group, update_aggregate, AggregationState,
};
use lookup::{
classify_partition_lookup, honest_targeted_path, sort_rows_by_token, PartitionLookupOutcome,
};
use row_build::{column_info_from_type_str, parse_cql_type_str, parse_table_id};
use value_ops::{
compare_values_ordering, const_arithmetic, eval_arithmetic, try_compare_values, values_equal,
};
use writetime_ttl::{
evaluate_writetime_ttl, like_pattern_to_regex, select_has_writetime_ttl,
writetime_ttl_column_name, SystemClock,
};
pub use predicate::{evaluate_leaf, evaluate_predicates, LeafOutcome};
pub use row_build::build_row_from_scan;
pub use writetime_ttl::{FixedClock, NowSeconds};
use predicate::validate_token_predicates;
#[cfg(test)]
thread_local! {
pub(crate) static PROJECTION_NAME_DERIVATIONS: std::cell::Cell<usize> =
const { std::cell::Cell::new(0) };
}
#[cfg(test)]
thread_local! {
pub(crate) static SORT_KEY_EVALUATIONS: std::cell::Cell<usize> =
const { std::cell::Cell::new(0) };
}
pub(super) fn partition_key_digest(key_bytes: &[u8]) -> u128 {
let (h1, h2) = crate::util::cassandra_murmur3::cassandra_murmur3_x64_128(key_bytes);
((h1 as u64 as u128) << 64) | (h2 as u64 as u128)
}
type PartitionCounts = HashMap<u128, Vec<(Vec<u8>, u64)>>;
fn admit_partition_row(
counts: &mut PartitionCounts,
digest: u128,
key_bytes: &[u8],
count: u64,
) -> bool {
let chain = counts.entry(digest).or_default();
let slot = match chain
.iter()
.position(|(bytes, _)| bytes.as_slice() == key_bytes)
{
Some(i) => &mut chain[i],
None => {
chain.push((key_bytes.to_vec(), 0));
let last = chain.len() - 1;
&mut chain[last]
}
};
if slot.1 < count {
slot.1 += 1;
true
} else {
false
}
}
fn projected_column_name(expr: &SelectExpression, index: usize) -> std::sync::Arc<str> {
#[cfg(test)]
PROJECTION_NAME_DERIVATIONS.with(|c| c.set(c.get() + 1));
match expr {
SelectExpression::Column(col_ref) => col_ref.column.as_str().into(),
SelectExpression::Aliased(_, alias) => alias.as_str().into(),
SelectExpression::WriteTimeTtl(call) => writetime_ttl_column_name(call).into(),
_ => format!("col_{index}").into(),
}
}
fn project_expr_reshapes_row(expr: &SelectExpression) -> bool {
!matches!(expr, SelectExpression::Column(_))
}
fn project_trims_scan_columns(
columns: &[SelectExpression],
scan_projection: Option<&[String]>,
) -> bool {
let Some(scan) = scan_projection else {
return false;
};
let output: std::collections::HashSet<&str> = columns
.iter()
.filter_map(|e| match e {
SelectExpression::Column(c) => Some(c.column.as_str()),
_ => None,
})
.collect();
let scan_set: std::collections::HashSet<&str> = scan.iter().map(|s| s.as_str()).collect();
output != scan_set
}
const DEFAULT_MAX_RESULT_ROWS: usize = 1_000_000;
pub struct SelectExecutor {
_schema: Arc<SchemaManager>,
storage: Arc<StorageEngine>,
clock: Arc<dyn NowSeconds>,
max_result_bytes: usize,
max_result_rows: usize,
}
impl std::fmt::Debug for SelectExecutor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SelectExecutor")
.field("_schema", &self._schema)
.field("storage", &self.storage)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
struct ExecutionContext {
pub table_id: TableId,
pub columns: Vec<ColumnInfo>,
pub rows_processed: u64,
pub scan_rows: u64,
pub projection_flags: ProjectionFlags,
pub access_path: Option<AccessPath>,
pub reverse_served: bool, }
impl SelectExecutor {
pub fn new(schema: Arc<SchemaManager>, storage: Arc<StorageEngine>) -> Self {
Self {
_schema: schema,
storage,
clock: Arc::new(SystemClock),
max_result_bytes: usize::try_from(crate::config::DEFAULT_MAX_RESULT_BYTES)
.unwrap_or(usize::MAX),
max_result_rows: DEFAULT_MAX_RESULT_ROWS,
}
}
pub fn with_max_result_bytes(mut self, max_result_bytes: usize) -> Self {
self.max_result_bytes = max_result_bytes;
self
}
pub fn with_max_result_rows(mut self, max_result_rows: usize) -> Self {
self.max_result_rows = max_result_rows;
self
}
#[cfg(test)]
pub fn with_clock(
schema: Arc<SchemaManager>,
storage: Arc<StorageEngine>,
clock: Arc<dyn NowSeconds>,
) -> Self {
Self {
_schema: schema,
storage,
clock,
max_result_bytes: usize::try_from(crate::config::DEFAULT_MAX_RESULT_BYTES)
.unwrap_or(usize::MAX),
max_result_rows: DEFAULT_MAX_RESULT_ROWS,
}
}
fn select_plan_family(plan: &OptimizedQueryPlan, access_path: Option<&AccessPath>) -> String {
if plan.aggregation_plan.is_some() {
return "Aggregation".to_string();
}
match access_path {
Some(
AccessPath::PartitionLookup
| AccessPath::MultiPartitionLookup
| AccessPath::MetadataPartitionLookup
| AccessPath::StreamingPartitionLookup,
) => "PointLookup".to_string(),
Some(AccessPath::ClusteringSlice) => "RangeScan".to_string(),
Some(AccessPath::FullScan | AccessPath::FallbackFullScan { .. }) | None => {
"TableScan".to_string()
}
}
}
fn select_plan_info(
plan: &OptimizedQueryPlan,
access_path: Option<&AccessPath>,
) -> crate::query::result::PlanInfo {
crate::query::result::PlanInfo {
plan_type: Self::select_plan_family(plan, access_path),
estimated_cost: 0.0,
actual_cost: 0.0,
indexes_used: access_path
.map(|p| vec![p.label().to_string()])
.unwrap_or_default(),
steps: vec![],
parallelization: None,
}
}
pub async fn execute_streaming(
&self,
plan: OptimizedQueryPlan,
config: StreamingConfig,
) -> Result<QueryResultIterator> {
crate::query::access_path::reset();
if self.requires_materialization(&plan) {
log::info!("Query requires materialization (ORDER BY/GROUP BY/aggregates), using execute-then-stream");
return self.execute_and_stream(plan, config).await;
}
let table_id = if let Some(ref from_clause) = plan.statement.from_clause {
self.extract_table_id(from_clause)?
} else {
return self.execute_and_stream(plan, config).await;
};
let query_schema: Option<Arc<TableSchema>> = self.resolve_table_schema(&table_id).await;
let columns = self.get_result_columns(&plan.statement, query_schema.as_deref())?;
let (tx, rx) = mpsc::channel(config.buffer_size);
let execution_steps = if plan.execution_steps.is_empty() {
vec![ExecutionStep::SSTableScan {
table: table_id.clone(),
predicates: vec![],
projection: columns.iter().map(|c| c.name.clone()).collect(),
}]
} else {
plan.execution_steps.clone()
};
for step in &execution_steps {
if let ExecutionStep::SSTableScan { predicates, .. } = step {
validate_token_predicates(predicates, query_schema.as_deref())?;
}
}
let storage = Arc::clone(&self.storage);
let buffer_size = config.buffer_size;
tokio::spawn(async move {
if let Err(e) = Self::execute_streaming_background(
storage,
query_schema,
table_id,
execution_steps,
tx,
buffer_size,
)
.await
{
log::error!("Streaming execution error: {}", e);
}
});
let metadata = QueryMetadata {
columns,
total_rows: None, plan_info: None,
performance: Default::default(),
warnings: vec![],
access_path: None,
};
Ok(QueryResultIterator::new(rx, metadata))
}
fn requires_materialization(&self, plan: &OptimizedQueryPlan) -> bool {
let scan_projection: Option<&[String]> =
plan.execution_steps.iter().find_map(|step| match step {
ExecutionStep::SSTableScan { projection, .. } => Some(projection.as_slice()),
_ => None,
});
for step in &plan.execution_steps {
match step {
ExecutionStep::Sort { .. } => return true,
ExecutionStep::Aggregate { .. } => return true,
ExecutionStep::Project { columns }
if columns.iter().any(project_expr_reshapes_row)
|| project_trims_scan_columns(columns, scan_projection) =>
{
return true
}
_ => {}
}
}
if matches!(plan.statement.select_clause, SelectClause::Distinct(_)) {
return true;
}
select_has_writetime_ttl(&plan.statement)
}
async fn execute_and_stream(
&self,
plan: OptimizedQueryPlan,
config: StreamingConfig,
) -> Result<QueryResultIterator> {
let result = self.execute(plan).await?;
let (tx, rx) = mpsc::channel(config.buffer_size);
tokio::spawn(async move {
for row in result.rows {
if tx.send(Ok(row)).await.is_err() {
break; }
}
});
Ok(QueryResultIterator::new(rx, result.metadata))
}
fn execute_filter(
&self,
rows: Vec<QueryRow>,
filter_expr: &WhereExpression,
context: &mut ExecutionContext,
) -> Result<Vec<QueryRow>> {
let mut filtered_rows = Vec::new();
for row in rows {
if self.evaluate_where_expression(filter_expr, &row)? {
filtered_rows.push(row);
}
context.rows_processed += 1;
}
Ok(filtered_rows)
}
fn evaluate_where_expression(&self, expr: &WhereExpression, row: &QueryRow) -> Result<bool> {
match expr {
WhereExpression::Comparison(comp) => self.evaluate_comparison(comp, row),
WhereExpression::And(exprs) => {
for expr in exprs {
if !self.evaluate_where_expression(expr, row)? {
return Ok(false);
}
}
Ok(true)
}
WhereExpression::Or(exprs) => {
for expr in exprs {
if self.evaluate_where_expression(expr, row)? {
return Ok(true);
}
}
Ok(false)
}
WhereExpression::Not(expr) => Ok(!self.evaluate_where_expression(expr, row)?),
WhereExpression::Parentheses(expr) => self.evaluate_where_expression(expr, row),
}
}
fn evaluate_comparison(&self, comp: &ComparisonExpression, row: &QueryRow) -> Result<bool> {
use ComparisonOperator::*;
let left_value = self.evaluate_select_expression(&comp.left, row)?;
match comp.operator {
IsNull => return Ok(left_value.is_null()),
IsNotNull => return Ok(!left_value.is_null()),
_ => {}
}
match (&comp.operator, &comp.right) {
(
op @ (Equal | NotEqual | LessThan | LessThanOrEqual | GreaterThan
| GreaterThanOrEqual),
ComparisonRightSide::Value(right_expr),
) => {
let right_value = self.evaluate_select_expression(right_expr, row)?;
let result = match op {
Equal => values_equal(&left_value, &right_value),
NotEqual => !values_equal(&left_value, &right_value),
LessThan => try_compare_values(&left_value, &right_value)?.is_lt(),
LessThanOrEqual => try_compare_values(&left_value, &right_value)?.is_le(),
GreaterThan => try_compare_values(&left_value, &right_value)?.is_gt(),
GreaterThanOrEqual => try_compare_values(&left_value, &right_value)?.is_ge(),
_ => unreachable!("guarded by outer match"),
};
Ok(result)
}
(In, ComparisonRightSide::ValueList(value_exprs)) => {
for value_expr in value_exprs {
let value = self.evaluate_select_expression(value_expr, row)?;
if left_value == value {
return Ok(true);
}
}
Ok(false)
}
(Like, ComparisonRightSide::Value(pattern_expr)) => {
let pattern = self.evaluate_select_expression(pattern_expr, row)?;
if let (Value::Text(text), Value::Text(pattern_str)) = (&left_value, &pattern) {
Ok(self.match_like_pattern(text, pattern_str))
} else {
Ok(false)
}
}
_ => Err(Error::query_execution(
"Unsupported comparison operator".to_string(),
)),
}
}
fn evaluate_select_expression(&self, expr: &SelectExpression, row: &QueryRow) -> Result<Value> {
match expr {
SelectExpression::Column(col_ref) => row
.values
.get(col_ref.column.as_str())
.cloned()
.ok_or_else(|| {
Error::query_execution(format!("Column not found: {}", col_ref.column))
}),
SelectExpression::Literal(value) => Ok(value.clone()),
SelectExpression::BindMarker(idx) => Err(Error::query_execution(format!(
"Unbound parameter placeholder ?{idx} reached execution; \
parameters must be bound before the query runs"
))),
SelectExpression::CollectionAccess(access) => {
self.evaluate_collection_access(access, row)
}
SelectExpression::Arithmetic(arith) => {
let left = self.evaluate_select_expression(&arith.left, row)?;
let right = self.evaluate_select_expression(&arith.right, row)?;
self.evaluate_arithmetic(&arith.operator, left, right)
}
SelectExpression::Aliased(expr, _) => self.evaluate_select_expression(expr, row),
SelectExpression::Aggregate(_) => {
Err(Error::query_execution(
"Aggregate expressions should be processed during aggregation step, not row evaluation".to_string(),
))
}
SelectExpression::Function(_) => {
Err(Error::query_execution(
"Function expressions not yet implemented".to_string(),
))
}
SelectExpression::WriteTimeTtl(call) => {
let now_secs = self.clock.now_seconds();
Ok(evaluate_writetime_ttl(call, row, now_secs))
}
}
}
fn evaluate_collection_access(
&self,
access: &CollectionAccessExpression,
row: &QueryRow,
) -> Result<Value> {
let lookup_column = |col: &ColumnRef| -> Result<&Value> {
row.values
.get(col.column.as_str())
.ok_or_else(|| Error::query_execution(format!("Column not found: {}", col.column)))
};
match access {
CollectionAccessExpression::ListIndex(col_ref, index_expr) => {
let list_value = lookup_column(col_ref)?;
let index_value = self.evaluate_select_expression(index_expr, row)?;
let (Value::List(list), Value::Integer(index)) = (list_value, &index_value) else {
return Err(Error::query_execution("Invalid list access".to_string()));
};
if *index >= 0 && (*index as usize) < list.len() {
Ok(list[*index as usize].clone())
} else {
Ok(Value::Null)
}
}
CollectionAccessExpression::MapKey(col_ref, key_expr) => {
let map_value = lookup_column(col_ref)?;
let key_value = self.evaluate_select_expression(key_expr, row)?;
let Value::Map(map) = map_value else {
return Err(Error::query_execution("Invalid map access".to_string()));
};
Ok(map
.iter()
.find(|(k, _)| *k == key_value)
.map(|(_, v)| v.clone())
.unwrap_or(Value::Null))
}
CollectionAccessExpression::SetContains(col_ref, value_expr) => {
let set_value = lookup_column(col_ref)?;
let test_value = self.evaluate_select_expression(value_expr, row)?;
let Value::Set(set) = set_value else {
return Err(Error::query_execution(
"Invalid set contains operation".to_string(),
));
};
Ok(Value::Boolean(set.contains(&test_value)))
}
}
}
fn evaluate_arithmetic(
&self,
op: &ArithmeticOperator,
left: Value,
right: Value,
) -> Result<Value> {
match (&left, &right) {
(Value::Integer(_), Value::Integer(_)) | (Value::Float(_), Value::Float(_)) => {
eval_arithmetic(op, left, right)
}
_ => Err(Error::query_execution(
"Incompatible types for arithmetic".to_string(),
)),
}
}
fn match_like_pattern(&self, text: &str, pattern: &str) -> bool {
regex::Regex::new(&like_pattern_to_regex(pattern))
.map(|re| re.is_match(text))
.unwrap_or(false)
}
fn execute_sort(
&self,
mut rows: Vec<QueryRow>,
order_by: &OrderByClause,
_context: &mut ExecutionContext,
) -> Result<Vec<QueryRow>> {
let mut decorated: Vec<(Vec<Value>, QueryRow)> = Vec::with_capacity(rows.len());
for row in rows.drain(..) {
let mut keys = Vec::with_capacity(order_by.items.len());
for item in &order_by.items {
#[cfg(test)]
SORT_KEY_EVALUATIONS.with(|c| c.set(c.get() + 1));
keys.push(
self.evaluate_select_expression(&item.expression, &row)
.unwrap_or(Value::Null),
);
}
decorated.push((keys, row));
}
decorated.sort_by(|(a_keys, _), (b_keys, _)| {
for (idx, item) in order_by.items.iter().enumerate() {
let ordering = match item.direction {
SortDirection::Ascending => compare_values_ordering(&a_keys[idx], &b_keys[idx]),
SortDirection::Descending => {
compare_values_ordering(&b_keys[idx], &a_keys[idx])
}
};
if !ordering.is_eq() {
return ordering;
}
}
std::cmp::Ordering::Equal
});
Ok(decorated.into_iter().map(|(_, row)| row).collect())
}
fn execute_aggregation(
&self,
rows: Vec<QueryRow>,
agg_plan: &AggregationPlan,
_context: &mut ExecutionContext,
) -> Result<Vec<QueryRow>> {
const PER_ROW_MEMORY_ESTIMATE_BYTES: usize = 100;
const DEFAULT_AGGREGATION_MEMORY_LIMIT: usize = 512 * 1024 * 1024;
let mut agg_state = AggregationState {
groups: Vec::new(),
group_index: rustc_hash::FxHashMap::default(),
memory_usage_bytes: 0,
memory_limit_bytes: DEFAULT_AGGREGATION_MEMORY_LIMIT,
};
for row in rows {
let group_key = build_group_key(&row, &agg_plan.group_by_columns);
let group_index = find_or_init_group(&mut agg_state, group_key, &agg_plan.aggregates);
let group_aggregates = &mut agg_state.groups[group_index].1;
for (i, agg_comp) in agg_plan.aggregates.iter().enumerate() {
update_aggregate(&mut group_aggregates[i], agg_comp, &row);
}
agg_state.memory_usage_bytes += PER_ROW_MEMORY_ESTIMATE_BYTES;
if agg_state.memory_usage_bytes > agg_state.memory_limit_bytes {
return Err(Error::query_execution(
"Aggregation memory limit exceeded".to_string(),
));
}
}
let result_rows = agg_state
.groups
.into_iter()
.map(|(group_key, group_aggregates)| {
finalize_group(group_key, group_aggregates, agg_plan)
})
.collect();
Ok(result_rows)
}
fn execute_per_partition_limit(rows: Vec<QueryRow>, count: u64) -> Vec<QueryRow> {
let mut out = Vec::with_capacity(rows.len());
let mut counts: PartitionCounts = HashMap::new();
for row in rows {
let digest = partition_key_digest(&row.key.0);
if admit_partition_row(&mut counts, digest, &row.key.0, count) {
out.push(row);
}
}
out
}
fn execute_limit(
&self,
mut rows: Vec<QueryRow>,
count: u64,
offset: Option<u64>,
_context: &mut ExecutionContext,
) -> Result<Vec<QueryRow>> {
let start_index = offset.unwrap_or(0) as usize;
let limit = count as usize;
if start_index == 0 {
rows.truncate(limit);
return Ok(rows);
}
Ok(rows.into_iter().skip(start_index).take(limit).collect())
}
fn execute_projection(
&self,
rows: Vec<QueryRow>,
columns: &[SelectExpression],
_context: &mut ExecutionContext,
) -> Result<Vec<QueryRow>> {
let column_names: Vec<std::sync::Arc<str>> = columns
.iter()
.enumerate()
.map(|(i, expr)| projected_column_name(expr, i))
.collect();
let mut projected_rows = Vec::with_capacity(rows.len());
for row in rows {
let mut projected_values: HashMap<std::sync::Arc<str>, Value> =
HashMap::with_capacity(columns.len());
for (i, expr) in columns.iter().enumerate() {
let value = self.evaluate_select_expression(expr, &row)?;
projected_values.insert(column_names[i].clone(), value);
}
projected_rows.push(QueryRow {
values: projected_values,
key: RowKey::new(vec![]),
metadata: Default::default(),
cell_metadata: None,
});
}
Ok(projected_rows)
}
fn trim_projection(
&self,
mut rows: Vec<QueryRow>,
columns: &[SelectExpression],
) -> Vec<QueryRow> {
let selected: std::collections::HashSet<&str> = columns
.iter()
.filter_map(|e| match e {
SelectExpression::Column(c) => Some(c.column.as_str()),
_ => None,
})
.collect();
for row in &mut rows {
row.values
.retain(|name, _| selected.contains(name.as_ref()));
}
rows
}
fn execute_constant_query(
&self,
statement: &SelectStatement,
_context: &ExecutionContext,
) -> Result<QueryResult> {
let mut values = HashMap::new();
let mut columns = Vec::new();
match &statement.select_clause {
SelectClause::All => {
return Err(Error::query_execution(
"SELECT * requires a FROM clause".to_string(),
));
}
SelectClause::Columns(expressions) | SelectClause::Distinct(expressions) => {
for (i, expr) in expressions.iter().enumerate() {
let (value, column_name) = self.evaluate_constant_expression(expr)?;
let key = column_name.unwrap_or_else(|| format!("column_{}", i));
values.insert(key.clone(), value);
columns.push(ColumnInfo {
name: key,
data_type: crate::types::DataType::Text, nullable: true,
position: i,
table_name: None, cql_type: None,
});
}
}
}
let row = QueryRow::with_values(RowKey::new(vec![1]), values);
Ok(QueryResult {
rows: vec![row],
rows_affected: 1, execution_time_ms: 0,
metadata: crate::query::result::QueryMetadata {
columns,
total_rows: Some(1),
plan_info: None,
performance: crate::query::result::PerformanceMetrics::default(),
warnings: Vec::new(),
access_path: None,
},
})
}
#[allow(clippy::only_used_in_recursion)]
fn evaluate_constant_expression(
&self,
expr: &SelectExpression,
) -> Result<(Value, Option<String>)> {
match expr {
SelectExpression::Literal(value) => Ok((value.clone(), None)),
SelectExpression::Aliased(inner_expr, alias) => {
let (value, _) = self.evaluate_constant_expression(inner_expr)?;
Ok((value, Some(alias.clone())))
}
SelectExpression::Arithmetic(arith) => {
let (left_val, _) = self.evaluate_constant_expression(&arith.left)?;
let (right_val, _) = self.evaluate_constant_expression(&arith.right)?;
let result = const_arithmetic(&arith.operator, left_val, right_val)?;
Ok((result, None))
}
_ => Err(Error::query_execution(
"Expression type not supported in constant queries".to_string(),
)),
}
}
fn extract_table_id(&self, from_clause: &FromClause) -> Result<TableId> {
match from_clause {
FromClause::Table(table_id) | FromClause::TableAlias(table_id, _) => {
Ok(table_id.clone())
}
}
}
async fn resolve_table_schema(&self, table: &TableId) -> Option<Arc<TableSchema>> {
let (keyspace, table_name) = parse_table_id(table);
self._schema
.find_schema_by_table(&keyspace, &table_name)
.await
.ok()
.flatten()
.map(Arc::new)
}
fn get_result_columns(
&self,
statement: &SelectStatement,
schema: Option<&TableSchema>,
) -> Result<Vec<ColumnInfo>> {
let mut columns = Vec::new();
match &statement.select_clause {
SelectClause::All => {
if let Some(ref from_clause) = statement.from_clause {
let table_id = self.extract_table_id(from_clause)?;
let (keyspace_opt, table_name) = parse_table_id(&table_id);
if let Some(schema) = schema {
let mut schema_cols: Vec<&crate::schema::Column> =
schema.columns.iter().collect();
schema_cols.sort_by_key(|c| c.name.as_str());
let keyspace_str = keyspace_opt.as_deref().unwrap_or("");
let table_name_str = format!("{}.{}", keyspace_str, table_name);
for (idx, schema_col) in schema_cols.iter().enumerate() {
columns.push(column_info_from_type_str(
schema_col.name.clone(),
&schema_col.data_type,
idx,
Some(table_name_str.clone()),
));
}
log::debug!(
"SELECT * resolved {} columns from schema for {:?}.{}",
columns.len(),
keyspace_opt,
table_name
);
}
}
}
SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) => {
let schema_opt = schema;
for (i, expr) in exprs.iter().enumerate() {
if let SelectExpression::WriteTimeTtl(call) = expr {
let col_name = writetime_ttl_column_name(call);
let (data_type, cql_type) = match call.function {
WriteTimeTtlFunction::WriteTime => {
(crate::types::DataType::BigInt, Some(CqlType::BigInt))
}
WriteTimeTtlFunction::Ttl => {
(crate::types::DataType::Integer, Some(CqlType::Int))
}
};
let mut col_info = ColumnInfo {
name: col_name,
data_type,
nullable: true, position: i,
table_name: None,
cql_type: None,
};
if let Some(ct) = cql_type {
col_info = col_info.with_cql_type(ct);
}
columns.push(col_info);
continue;
}
let column_name = crate::query::select_naming::result_column_name(expr, i);
let cql_type_opt = schema_opt.and_then(|schema| {
schema
.columns
.iter()
.find(|c| c.name == column_name)
.and_then(|c| parse_cql_type_str(&c.data_type))
});
let data_type = cql_type_opt
.as_ref()
.map(cql_type_to_data_type)
.unwrap_or(crate::types::DataType::Text);
let mut col_info = ColumnInfo {
name: column_name,
data_type,
nullable: true,
position: i,
table_name: None,
cql_type: None,
};
if let Some(cql_type) = cql_type_opt {
col_info = col_info.with_cql_type(cql_type);
}
columns.push(col_info);
}
}
}
Ok(columns)
}
}
#[cfg(test)]
mod tests {
use super::test_support::row_with_key;
use super::*;
use crate::query::result::{CellExpiration, CellWriteMetadata};
use crate::{platform::Platform, Config};
use tempfile::TempDir;
#[tokio::test]
async fn execute_resolves_schema_once_per_query() {
use crate::schema::TABLE_SCHEMA_CLONES;
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(Platform::new(&config).await.unwrap());
let storage = Arc::new(
StorageEngine::open(
temp_dir.path(),
&config,
platform.clone(),
#[cfg(feature = "state_machine")]
None,
)
.await
.unwrap(),
);
let schema = Arc::new(SchemaManager::new(temp_dir.path()).await.unwrap());
schema
.parse_and_register_cql_schema(
"CREATE TABLE ks.t (id int PRIMARY KEY, name text, age int)",
)
.await
.expect("schema registers");
let executor = SelectExecutor::new(schema.clone(), storage.clone());
let optimizer =
crate::query::select_optimizer::SelectOptimizer::new(schema.clone(), storage.clone());
let statement = crate::query::select_parser::parse_select("SELECT * FROM ks.t").unwrap();
let plan = optimizer.optimize(statement).await.unwrap();
TABLE_SCHEMA_CLONES.with(|c| c.set(0));
let _ = executor.execute(plan).await.expect("query executes");
let clones = TABLE_SCHEMA_CLONES.with(|c| c.get());
assert_eq!(
clones, 1,
"issue #1587: a query must deep-clone its schema out of the registry once \
(was 2–4: column-metadata + scan + fallback each re-resolved), got {clones}"
);
}
async fn create_test_executor() -> SelectExecutor {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(Platform::new(&config).await.unwrap());
let storage = Arc::new(
StorageEngine::open(
temp_dir.path(),
&config,
platform.clone(),
#[cfg(feature = "state_machine")]
None,
)
.await
.unwrap(),
);
let schema = Arc::new(SchemaManager::new(temp_dir.path()).await.unwrap());
SelectExecutor::new(schema, storage)
}
async fn create_test_executor_with_clock(now_secs: i64) -> SelectExecutor {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(Platform::new(&config).await.unwrap());
let storage = Arc::new(
StorageEngine::open(
temp_dir.path(),
&config,
platform.clone(),
#[cfg(feature = "state_machine")]
None,
)
.await
.unwrap(),
);
let schema = Arc::new(SchemaManager::new(temp_dir.path()).await.unwrap());
SelectExecutor::with_clock(schema, storage, Arc::new(FixedClock(now_secs)))
}
fn row_with_cell_meta(column: &str, value: Value, meta: Option<CellWriteMetadata>) -> QueryRow {
let mut row = QueryRow::new(RowKey::new(vec![1]));
row.set(column.to_string(), value);
if let Some(m) = meta {
row.insert_cell_metadata(column.to_string(), m);
}
row
}
#[tokio::test]
async fn test_like_pattern_matching() {
let executor = create_test_executor().await;
assert!(executor.match_like_pattern("hello", "h%"));
assert!(executor.match_like_pattern("hello", "%lo"));
assert!(executor.match_like_pattern("hello", "h_llo"));
assert!(!executor.match_like_pattern("hello", "h_l"));
}
#[test]
fn per_partition_limit_caps_interleaved_partitions() {
let a = b"A".as_slice();
let b = b"B".as_slice();
let rows = vec![
row_with_key(a),
row_with_key(b),
row_with_key(a),
row_with_key(a),
row_with_key(b),
];
let out = SelectExecutor::execute_per_partition_limit(rows, 2);
let count = |p: &[u8]| out.iter().filter(|r| r.key.0 == p).count();
assert_eq!(
count(a),
2,
"partition A must be capped at 2 despite interleaving"
);
assert_eq!(count(b), 2, "partition B has 2 rows, all kept");
assert_eq!(out.len(), 4);
}
#[tokio::test]
async fn execute_projection_derives_names_once_per_query() {
let executor = create_test_executor().await;
let columns = vec![
SelectExpression::Column(ColumnRef {
table: None,
column: "a".to_string(),
}),
SelectExpression::Column(ColumnRef {
table: None,
column: "b".to_string(),
}),
SelectExpression::Column(ColumnRef {
table: None,
column: "c".to_string(),
}),
];
let num_cols = columns.len();
let num_rows = 100usize;
let rows: Vec<QueryRow> = (0..num_rows)
.map(|r| {
let mut row = QueryRow::new(RowKey::new(vec![r as u8]));
row.set("a", Value::Integer(r as i32));
row.set("b", Value::Integer(r as i32));
row.set("c", Value::Integer(r as i32));
row
})
.collect();
let mut ctx = ExecutionContext {
table_id: TableId::new("ks.t"),
columns: Vec::new(),
rows_processed: 0,
scan_rows: 0,
projection_flags: ProjectionFlags::default(),
access_path: None,
reverse_served: false,
};
PROJECTION_NAME_DERIVATIONS.with(|c| c.set(0));
let projected = executor
.execute_projection(rows, &columns, &mut ctx)
.expect("projection must succeed");
let derivations = PROJECTION_NAME_DERIVATIONS.with(|c| c.get());
assert_eq!(projected.len(), num_rows, "one projected row per input row");
assert_eq!(
derivations,
num_cols,
"issue #1584: column names must be derived once per query \
(O(cols) = {num_cols}), not per row (would be {})",
num_rows * num_cols
);
assert_eq!(projected[0].values.get("a"), Some(&Value::Integer(0)));
assert_eq!(projected[0].values.get("b"), Some(&Value::Integer(0)));
assert_eq!(projected[0].values.get("c"), Some(&Value::Integer(0)));
assert_eq!(projected[99].values.get("a"), Some(&Value::Integer(99)));
}
#[tokio::test]
async fn trim_projection_preserves_key_and_trims_helpers() {
let executor = create_test_executor().await;
let columns = vec![
SelectExpression::Column(ColumnRef {
table: None,
column: "a".to_string(),
}),
SelectExpression::Column(ColumnRef {
table: None,
column: "b".to_string(),
}),
];
let mut row = QueryRow::new(RowKey::new(vec![7, 8, 9]));
row.set("a", Value::Integer(1));
row.set("b", Value::Integer(2));
row.set("helper", Value::Integer(3));
row.set_metadata(crate::query::result::RowMetadata {
version: Some(42),
ttl: None,
tags: Default::default(),
});
let out = executor.trim_projection(vec![row], &columns);
assert_eq!(out.len(), 1);
let r = &out[0];
assert_eq!(
r.key.0,
vec![7, 8, 9],
"trim must preserve the real RowKey, not destroy it to vec![]"
);
assert_eq!(
r.metadata.version,
Some(42),
"row metadata must be preserved"
);
assert_eq!(r.values.get("a"), Some(&Value::Integer(1)));
assert_eq!(r.values.get("b"), Some(&Value::Integer(2)));
assert!(
!r.values.contains_key("helper"),
"the unselected helper column must be trimmed"
);
let mut keys: Vec<&str> = r.values.keys().map(|k| k.as_ref()).collect();
keys.sort_unstable();
assert_eq!(keys, vec!["a", "b"], "only the selected columns remain");
}
#[tokio::test]
async fn trim_projection_tolerates_absent_selected_cell() {
let executor = create_test_executor().await;
let columns = vec![
SelectExpression::Column(ColumnRef {
table: None,
column: "a".to_string(),
}),
SelectExpression::Column(ColumnRef {
table: None,
column: "b".to_string(),
}),
];
let build_row = || {
let mut row = QueryRow::new(RowKey::new(vec![1]));
row.set("a", Value::Integer(10));
row.set("helper", Value::Integer(99));
row
};
let out = executor.trim_projection(vec![build_row()], &columns);
assert_eq!(out.len(), 1);
let r = &out[0];
assert_eq!(r.key.0, vec![1], "sparse row keeps its real key");
assert_eq!(r.values.get("a"), Some(&Value::Integer(10)));
assert!(
!r.values.contains_key("b"),
"absent selected cell `b` is omitted, not defaulted"
);
assert!(!r.values.contains_key("helper"), "helper trimmed");
let mut ctx = ExecutionContext {
table_id: TableId::new("ks.t"),
columns: Vec::new(),
rows_processed: 0,
scan_rows: 0,
projection_flags: ProjectionFlags::default(),
access_path: None,
reverse_served: false,
};
let err = executor
.execute_projection(vec![build_row()], &columns, &mut ctx)
.expect_err("pre-fix execute_projection must error on the absent cell");
assert!(
err.to_string().contains("Column not found"),
"the pre-fix defect is a 'Column not found' error on a sparse row; got: {err}"
);
}
#[tokio::test]
async fn execute_limit_offset_matches_drain_truncate_reference() {
let executor = create_test_executor().await;
let mut ctx = ExecutionContext {
table_id: TableId::new("ks.t"),
columns: Vec::new(),
rows_processed: 0,
scan_rows: 0,
projection_flags: ProjectionFlags::default(),
access_path: None,
reverse_served: false,
};
let make = |len: usize| -> Vec<QueryRow> {
(0..len)
.map(|r| {
let mut row = QueryRow::new(RowKey::new(vec![r as u8]));
row.set("a", Value::Integer(r as i32));
row
})
.collect()
};
let reference = |mut rows: Vec<QueryRow>, count: u64, offset: Option<u64>| {
let start_index = offset.unwrap_or(0) as usize;
if start_index >= rows.len() {
return Vec::new();
}
rows.drain(..start_index);
rows.truncate(count as usize);
rows
};
let tags = |rows: &[QueryRow]| -> Vec<i32> {
rows.iter()
.map(|r| match r.values.get("a") {
Some(Value::Integer(v)) => *v,
_ => -1,
})
.collect()
};
for len in [0usize, 1, 5, 10] {
for offset in [
None,
Some(0u64),
Some(1),
Some(3),
Some(9),
Some(10),
Some(50),
] {
for count in [0u64, 1, 3, 10, 1000] {
let got = executor
.execute_limit(make(len), count, offset, &mut ctx)
.expect("limit must succeed");
let want = reference(make(len), count, offset);
assert_eq!(
tags(&got),
tags(&want),
"len={len} offset={offset:?} count={count}: skip/take must \
equal drain/truncate"
);
}
}
}
}
#[test]
fn per_partition_limit_digest_matches_raw_bytes_reference() {
let spec: [(&[u8], i32); 9] = [
(b"pk-a", 0),
(b"pk-b", 1),
(b"pk-a", 2),
(b"pk-c", 3),
(b"pk-b", 4),
(b"pk-a", 5),
(b"pk-a", 6),
(b"pk-c", 7),
(b"pk-b", 8),
];
let make = || -> Vec<QueryRow> {
spec.iter()
.map(|(pk, tag)| {
let mut row = QueryRow::new(RowKey::new(pk.to_vec()));
row.set("a", Value::Integer(*tag));
row
})
.collect()
};
let reference = |rows: Vec<QueryRow>, count: u64| -> Vec<QueryRow> {
let mut out = Vec::with_capacity(rows.len());
let mut counts: HashMap<Vec<u8>, u64> = HashMap::new();
for row in rows {
let seen = counts.entry(row.key.0.clone()).or_insert(0);
if *seen < count {
*seen += 1;
out.push(row);
}
}
out
};
let tags = |rows: &[QueryRow]| -> Vec<i32> {
rows.iter()
.map(|r| match r.values.get("a") {
Some(Value::Integer(v)) => *v,
_ => -1,
})
.collect()
};
for count in [0u64, 1, 2, 3, 100] {
let got = SelectExecutor::execute_per_partition_limit(make(), count);
let want = reference(make(), count);
assert_eq!(
tags(&got),
tags(&want),
"count={count}: digest-keyed per-partition-limit must equal raw-bytes reference"
);
}
}
#[test]
fn per_partition_limit_exact_confirm_survives_digest_collision() {
let mut counts: PartitionCounts = HashMap::new();
const COLLIDING: u128 = 0xDEAD_BEEF;
let a = b"partition-a".as_slice();
let b = b"partition-b".as_slice();
assert!(
admit_partition_row(&mut counts, COLLIDING, a, 1),
"A's first row is admitted"
);
assert!(
admit_partition_row(&mut counts, COLLIDING, b, 1),
"B's first row is admitted despite the colliding digest (separate counter)"
);
assert!(
!admit_partition_row(&mut counts, COLLIDING, a, 1),
"A's second row is capped"
);
assert!(
!admit_partition_row(&mut counts, COLLIDING, b, 1),
"B's second row is capped"
);
assert_eq!(
counts.get(&COLLIDING).map(Vec::len),
Some(2),
"both distinct keys are chained under the colliding digest"
);
}
#[tokio::test]
async fn execute_sort_evaluates_keys_once_per_row() {
let executor = create_test_executor().await;
let num_rows = 64usize;
let rows: Vec<QueryRow> = (0..num_rows)
.rev()
.map(|r| {
let mut row = QueryRow::new(RowKey::new(vec![r as u8]));
row.set("k", Value::Integer(r as i32));
row
})
.collect();
let order_by = OrderByClause {
items: vec![OrderByItem {
expression: SelectExpression::Column(ColumnRef {
table: None,
column: "k".to_string(),
}),
direction: SortDirection::Ascending,
}],
};
let mut ctx = ExecutionContext {
table_id: TableId::new("ks.t"),
columns: Vec::new(),
rows_processed: 0,
scan_rows: 0,
projection_flags: ProjectionFlags::default(),
access_path: None,
reverse_served: false,
};
SORT_KEY_EVALUATIONS.with(|c| c.set(0));
let sorted = executor
.execute_sort(rows, &order_by, &mut ctx)
.expect("sort must succeed");
let evaluations = SORT_KEY_EVALUATIONS.with(|c| c.get());
assert_eq!(
evaluations, num_rows,
"issue #1587: sort keys must be evaluated once per row (n = {num_rows}), \
not O(n log n) times inside the comparator"
);
assert_eq!(sorted.len(), num_rows);
for (i, row) in sorted.iter().enumerate() {
assert_eq!(
row.values.get("k"),
Some(&Value::Integer(i as i32)),
"row {i} must sort into ascending position"
);
}
}
#[tokio::test]
async fn execute_sort_preserves_float_nan_signed_zero_ordering() {
let executor = create_test_executor().await;
let make_rows = |inputs: &[(u8, f64)]| -> Vec<QueryRow> {
inputs
.iter()
.map(|(tag, f)| {
let mut row = QueryRow::new(RowKey::new(vec![*tag]));
row.set("f", Value::Float(*f));
row
})
.collect()
};
let order_by = |dir: &SortDirection| OrderByClause {
items: vec![OrderByItem {
expression: SelectExpression::Column(ColumnRef {
table: None,
column: "f".to_string(),
}),
direction: dir.clone(),
}],
};
let mut ctx = ExecutionContext {
table_id: TableId::new("ks.t"),
columns: Vec::new(),
rows_processed: 0,
scan_rows: 0,
projection_flags: ProjectionFlags::default(),
access_path: None,
reverse_served: false,
};
let tags = |rows: &[QueryRow]| -> Vec<u8> { rows.iter().map(|r| r.key.0[0]).collect() };
let with_nan: [(u8, f64); 7] = [
(0, 2.0),
(1, f64::NAN),
(2, -0.0),
(3, 1.0),
(4, 0.0),
(5, -1.0),
(6, f64::NAN),
];
for dir in [SortDirection::Ascending, SortDirection::Descending] {
let sorted = executor
.execute_sort(make_rows(&with_nan), &order_by(&dir), &mut ctx)
.expect("sort must succeed");
let mut reference = make_rows(&with_nan);
reference.sort_by(|a, b| {
let (ka, kb) = (
a.values.get("f").expect("key"),
b.values.get("f").expect("key"),
);
match dir {
SortDirection::Ascending => compare_values_ordering(ka, kb),
SortDirection::Descending => compare_values_ordering(kb, ka),
}
});
assert_eq!(
tags(&sorted),
tags(&reference),
"issue #1587: decorate-sort must be order-identical to the reference \
comparator sort for float/NaN keys ({dir:?})"
);
}
let no_nan: [(u8, f64); 5] = [(0, 2.0), (2, -0.0), (3, 1.0), (4, 0.0), (5, -1.0)];
let asc = executor
.execute_sort(
make_rows(&no_nan),
&order_by(&SortDirection::Ascending),
&mut ctx,
)
.expect("sort must succeed");
assert_eq!(
tags(&asc),
vec![5, 2, 4, 3, 0],
"ascending float order with stable signed zeros"
);
let desc = executor
.execute_sort(
make_rows(&no_nan),
&order_by(&SortDirection::Descending),
&mut ctx,
)
.expect("sort must succeed");
assert_eq!(
tags(&desc),
vec![0, 3, 2, 4, 5],
"descending float order with stable signed zeros"
);
}
#[tokio::test]
async fn test_executor_evaluate_writetime_reads_cell_metadata() {
let executor = create_test_executor_with_clock(0).await;
let write_ts = 1_700_000_000_000_000_i64;
let row = row_with_cell_meta(
"name",
Value::Text("Carol".to_string()),
Some(CellWriteMetadata {
write_timestamp_micros: write_ts,
expiration: None,
}),
);
let expr = SelectExpression::WriteTimeTtl(WriteTimeTtlCall {
function: WriteTimeTtlFunction::WriteTime,
column: "name".to_string(),
alias: None,
});
let result = executor.evaluate_select_expression(&expr, &row).unwrap();
assert_eq!(result, Value::BigInt(write_ts));
}
#[tokio::test]
async fn test_executor_evaluate_writetime_null_when_no_metadata() {
let executor = create_test_executor_with_clock(0).await;
let row = row_with_cell_meta("name", Value::Text("Dave".to_string()), None);
let expr = SelectExpression::WriteTimeTtl(WriteTimeTtlCall {
function: WriteTimeTtlFunction::WriteTime,
column: "name".to_string(),
alias: None,
});
let result = executor.evaluate_select_expression(&expr, &row).unwrap();
assert_eq!(result, Value::Null);
}
#[tokio::test]
async fn test_executor_evaluate_ttl_with_injected_clock() {
let now_secs: i64 = 1000;
let executor = create_test_executor_with_clock(now_secs).await;
let row = row_with_cell_meta(
"session",
Value::Text("tok".to_string()),
Some(CellWriteMetadata {
write_timestamp_micros: 0,
expiration: Some(CellExpiration {
ttl_seconds: 5000,
expires_at_seconds: 5000,
}),
}),
);
let expr = SelectExpression::WriteTimeTtl(WriteTimeTtlCall {
function: WriteTimeTtlFunction::Ttl,
column: "session".to_string(),
alias: None,
});
let result = executor.evaluate_select_expression(&expr, &row).unwrap();
assert_eq!(
result,
Value::Integer(4000),
"TTL must use the injected clock, not the wall clock"
);
}
#[tokio::test]
async fn test_executor_evaluate_ttl_expired_cell_returns_null() {
let executor = create_test_executor_with_clock(9999).await;
let row = row_with_cell_meta(
"cache",
Value::Text("val".to_string()),
Some(CellWriteMetadata {
write_timestamp_micros: 0,
expiration: Some(CellExpiration {
ttl_seconds: 100,
expires_at_seconds: 100,
}),
}),
);
let expr = SelectExpression::WriteTimeTtl(WriteTimeTtlCall {
function: WriteTimeTtlFunction::Ttl,
column: "cache".to_string(),
alias: None,
});
let result = executor.evaluate_select_expression(&expr, &row).unwrap();
assert_eq!(result, Value::Null, "Expired TTL cell must produce NULL");
}
#[tokio::test]
async fn test_get_result_columns_writetime_has_bigint_type() {
let executor = create_test_executor().await;
let stmt = SelectStatement {
select_clause: SelectClause::Columns(vec![SelectExpression::WriteTimeTtl(
WriteTimeTtlCall {
function: WriteTimeTtlFunction::WriteTime,
column: "name".to_string(),
alias: None,
},
)]),
from_clause: None,
where_clause: None,
group_by: None,
having_clause: None,
order_by: None,
limit: None,
per_partition_limit: None,
offset: None,
allow_filtering: false,
};
let cols = executor.get_result_columns(&stmt, None).unwrap();
assert_eq!(cols.len(), 1);
assert_eq!(cols[0].name, "writetime(name)");
assert_eq!(cols[0].data_type, crate::types::DataType::BigInt);
assert!(cols[0].nullable, "WRITETIME column must be nullable");
assert_eq!(cols[0].cql_type, Some(CqlType::BigInt));
}
#[tokio::test]
async fn test_get_result_columns_ttl_has_int_type() {
let executor = create_test_executor().await;
let stmt = SelectStatement {
select_clause: SelectClause::Columns(vec![SelectExpression::WriteTimeTtl(
WriteTimeTtlCall {
function: WriteTimeTtlFunction::Ttl,
column: "score".to_string(),
alias: None,
},
)]),
from_clause: None,
where_clause: None,
group_by: None,
having_clause: None,
order_by: None,
limit: None,
per_partition_limit: None,
offset: None,
allow_filtering: false,
};
let cols = executor.get_result_columns(&stmt, None).unwrap();
assert_eq!(cols.len(), 1);
assert_eq!(cols[0].name, "ttl(score)");
assert_eq!(cols[0].data_type, crate::types::DataType::Integer);
assert!(cols[0].nullable, "TTL column must be nullable");
assert_eq!(cols[0].cql_type, Some(CqlType::Int));
}
#[tokio::test]
async fn test_get_result_columns_writetime_with_alias() {
let executor = create_test_executor().await;
let stmt = SelectStatement {
select_clause: SelectClause::Columns(vec![SelectExpression::WriteTimeTtl(
WriteTimeTtlCall {
function: WriteTimeTtlFunction::WriteTime,
column: "name".to_string(),
alias: Some("wt".to_string()),
},
)]),
from_clause: None,
where_clause: None,
group_by: None,
having_clause: None,
order_by: None,
limit: None,
per_partition_limit: None,
offset: None,
allow_filtering: false,
};
let cols = executor.get_result_columns(&stmt, None).unwrap();
assert_eq!(cols.len(), 1);
assert_eq!(
cols[0].name, "wt",
"Alias must override Cassandra convention"
);
}
async fn plan_for(sql: &str) -> OptimizedQueryPlan {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(Platform::new(&config).await.unwrap());
let storage = Arc::new(
StorageEngine::open(
temp_dir.path(),
&config,
platform.clone(),
#[cfg(feature = "state_machine")]
None,
)
.await
.unwrap(),
);
let schema = Arc::new(SchemaManager::new(temp_dir.path()).await.unwrap());
schema
.parse_and_register_cql_schema(
"CREATE TABLE ks.t (id int PRIMARY KEY, a int, b int, c int)",
)
.await
.expect("schema registers");
let optimizer =
crate::query::select_optimizer::SelectOptimizer::new(schema.clone(), storage.clone());
let statement = crate::query::select_parser::parse_select(sql).unwrap();
optimizer.optimize(statement).await.unwrap()
}
#[tokio::test]
async fn requires_materialization_for_where_helper_trim() {
let plan = plan_for("SELECT a FROM ks.t WHERE b = 1").await;
let executor = create_test_executor().await;
assert!(
executor.requires_materialization(&plan),
"SELECT a WHERE b=1 scans [a,b] but outputs [a]; the Project TRIM of \
the helper column `b` must force materialization (streaming ignores \
Project and would leak `b`)"
);
}
#[tokio::test]
async fn requires_materialization_for_order_by_helper_trim() {
let plan = plan_for("SELECT a FROM ks.t WHERE b = 1 ORDER BY c").await;
let executor = create_test_executor().await;
assert!(
executor.requires_materialization(&plan),
"SELECT a WHERE b=1 ORDER BY c scans [a,b,c] but outputs [a]; must \
materialize to trim helper columns b,c"
);
}
#[tokio::test]
async fn no_materialization_when_output_equals_scan() {
let plan = plan_for("SELECT a, b FROM ks.t WHERE a = 1").await;
let executor = create_test_executor().await;
assert!(
!executor.requires_materialization(&plan),
"SELECT a,b WHERE a=1 scans exactly [a,b] and outputs [a,b]; it must \
stream directly without materialization"
);
}
#[tokio::test]
async fn no_materialization_for_reordered_select_no_helpers() {
let plan = plan_for("SELECT b, a FROM ks.t WHERE a = 1").await;
let executor = create_test_executor().await;
assert!(
!executor.requires_materialization(&plan),
"a reordered select with no helper columns (same column set) must \
stream directly"
);
}
}