use super::select_ast::*;
use super::select_naming::{
aggregate_arg_source_columns, aggregate_column_and_alias, aggregate_output_name,
projection_source_and_output, unwrap_aggregate,
};
use crate::{schema::SchemaManager, storage::StorageEngine, Error, Result, TableId, Value};
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(test)]
thread_local! {
pub(crate) static OPTIMIZE_INVOCATIONS: std::cell::Cell<usize> =
const { std::cell::Cell::new(0) };
}
#[derive(Debug)]
pub struct SelectOptimizer {
#[allow(dead_code)]
schema: Arc<SchemaManager>,
#[allow(dead_code)]
storage: Arc<StorageEngine>,
}
#[derive(Debug, Clone)]
pub struct OptimizedQueryPlan {
pub statement: SelectStatement,
pub execution_steps: Vec<ExecutionStep>,
pub sstable_predicates: Vec<SSTablePredicate>,
pub aggregation_plan: Option<AggregationPlan>,
}
#[derive(Debug, Clone)]
pub enum ExecutionStep {
SSTableScan {
table: TableId,
predicates: Vec<SSTablePredicate>,
projection: Vec<String>,
},
Filter {
expression: WhereExpression,
},
Sort {
order_by: OrderByClause,
},
Aggregate {
plan: AggregationPlan,
},
Limit {
count: u64,
offset: Option<u64>,
},
PerPartitionLimit {
count: u64,
},
Project {
columns: Vec<SelectExpression>,
},
}
impl ExecutionStep {
pub fn variant_name(&self) -> &'static str {
match self {
ExecutionStep::SSTableScan { .. } => "SSTableScan",
ExecutionStep::Filter { .. } => "Filter",
ExecutionStep::Sort { .. } => "Sort",
ExecutionStep::Aggregate { .. } => "Aggregate",
ExecutionStep::Limit { .. } => "Limit",
ExecutionStep::PerPartitionLimit { .. } => "PerPartitionLimit",
ExecutionStep::Project { .. } => "Project",
}
}
}
#[derive(Debug, Clone)]
pub struct SSTablePredicate {
pub column: String,
pub operation: SSTableFilterOp,
pub values: Vec<Value>,
pub token_columns: Option<Vec<String>>,
}
impl SSTablePredicate {
pub fn column(
column: impl Into<String>,
operation: SSTableFilterOp,
values: Vec<Value>,
) -> Self {
Self {
column: column.into(),
operation,
values,
token_columns: None,
}
}
pub fn token(
token_columns: Vec<String>,
operation: SSTableFilterOp,
values: Vec<Value>,
) -> Self {
let label = format!("token({})", token_columns.join(", "));
Self {
column: label,
operation,
values,
token_columns: Some(token_columns),
}
}
pub fn is_token(&self) -> bool {
self.token_columns.is_some()
}
}
#[derive(Debug, Clone)]
pub enum SSTableFilterOp {
Equal,
Range,
Gt,
Gte,
Lt,
Lte,
In,
Prefix,
BloomFilter,
}
#[derive(Debug, Clone)]
pub struct AggregationPlan {
pub group_by_columns: Vec<String>,
pub group_by_output_names: Vec<String>,
pub aggregates: Vec<AggregateComputation>,
}
#[derive(Debug, Clone)]
pub struct AggregateComputation {
pub function: AggregateType,
pub column: String,
pub alias: String,
pub distinct: bool,
}
impl SelectOptimizer {
pub fn new(schema: Arc<SchemaManager>, storage: Arc<StorageEngine>) -> Self {
Self { schema, storage }
}
pub async fn optimize(&self, statement: SelectStatement) -> Result<OptimizedQueryPlan> {
#[cfg(test)]
OPTIMIZE_INVOCATIONS.with(|c| c.set(c.get() + 1));
let mut plan = OptimizedQueryPlan {
statement: statement.clone(),
execution_steps: Vec::new(),
sstable_predicates: Vec::new(),
aggregation_plan: None,
};
let Some(from_clause) = statement.from_clause.as_ref() else {
return Ok(plan);
};
let table_id = match from_clause {
FromClause::Table(t) | FromClause::TableAlias(t, _) => t.clone(),
};
if let SelectClause::Distinct(exprs) = &statement.select_clause {
if exprs.iter().any(|e| e.is_aggregate()) {
return Err(Error::query_execution(
"SELECT DISTINCT with an aggregate function is not supported; \
DISTINCT over an aggregate is redundant because the aggregate \
already collapses rows to a single value"
.to_string(),
));
}
}
if let Some(where_clause) = &statement.where_clause {
validate_token_forms_whole_tree(where_clause, true)?;
plan.sstable_predicates = collect_sstable_predicates(where_clause)?;
}
let scan_projection = extract_projection_columns(&statement);
plan.execution_steps.push(ExecutionStep::SSTableScan {
table: table_id,
predicates: plan.sstable_predicates.clone(),
projection: scan_projection.clone(),
});
if let Some(where_clause) = &statement.where_clause {
if plan.sstable_predicates.is_empty() {
plan.execution_steps.push(ExecutionStep::Filter {
expression: where_clause.clone(),
});
}
}
let needs_aggregation = statement.requires_aggregation();
if needs_aggregation {
let agg_plan = plan_aggregation(&statement);
plan.execution_steps.push(ExecutionStep::Aggregate {
plan: agg_plan.clone(),
});
plan.aggregation_plan = Some(agg_plan);
}
if let Some(order_by) = &statement.order_by {
plan.execution_steps.push(ExecutionStep::Sort {
order_by: order_by.clone(),
});
}
if let Some(count) = statement.per_partition_limit {
plan.execution_steps
.push(ExecutionStep::PerPartitionLimit { count });
}
if let Some(limit) = &statement.limit {
plan.execution_steps.push(ExecutionStep::Limit {
count: limit.count,
offset: statement.offset,
});
}
if !needs_aggregation {
if let SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) =
&statement.select_clause
{
let is_bare_columns = matches!(&statement.select_clause, SelectClause::Columns(_))
&& exprs
.iter()
.all(|e| matches!(e, SelectExpression::Column(_)));
let projection_is_exactly_selected = is_bare_columns && {
let selected: Vec<&str> = exprs
.iter()
.filter_map(|e| match e {
SelectExpression::Column(c) => Some(c.column.as_str()),
_ => None,
})
.collect();
scan_projection.len() == selected.len()
&& scan_projection
.iter()
.zip(&selected)
.all(|(scanned, wanted)| scanned.as_str() == *wanted)
};
if !projection_is_exactly_selected {
plan.execution_steps.push(ExecutionStep::Project {
columns: exprs.clone(),
});
}
}
}
Ok(plan)
}
}
fn collect_sstable_predicates(expr: &WhereExpression) -> Result<Vec<SSTablePredicate>> {
let mut out = Vec::new();
fn walk(expr: &WhereExpression, out: &mut Vec<SSTablePredicate>) -> Result<()> {
match expr {
WhereExpression::Comparison(comp) => {
if let Some(predicate) = comparison_to_sstable_predicate(comp)? {
out.push(predicate);
}
}
WhereExpression::And(exprs) => {
for e in exprs {
walk(e, out)?;
}
}
WhereExpression::Parentheses(inner) => walk(inner, out)?,
WhereExpression::Or(_) | WhereExpression::Not(_) => {}
}
Ok(())
}
walk(expr, &mut out)?;
Ok(out)
}
fn validate_token_forms_whole_tree(expr: &WhereExpression, pushable: bool) -> Result<()> {
match expr {
WhereExpression::Comparison(comp) => {
if is_token_comparison(comp) {
let _supported = comparison_to_sstable_predicate(comp)?;
if !pushable {
return Err(Error::query_execution(
"token() restriction is only supported at the top level or within an \
AND conjunction; token() under OR/NOT cannot be pushed down and the \
row-level evaluator cannot compute a token, so it is rejected rather \
than silently ignored"
.to_string(),
));
}
}
}
WhereExpression::And(exprs) => {
for e in exprs {
validate_token_forms_whole_tree(e, pushable)?;
}
}
WhereExpression::Or(exprs) => {
for e in exprs {
validate_token_forms_whole_tree(e, false)?;
}
}
WhereExpression::Not(inner) => validate_token_forms_whole_tree(inner, false)?,
WhereExpression::Parentheses(inner) => validate_token_forms_whole_tree(inner, pushable)?,
}
Ok(())
}
fn is_token_comparison(comp: &ComparisonExpression) -> bool {
matches!(
&comp.left,
SelectExpression::Function(func) if func.name.eq_ignore_ascii_case("token")
)
}
fn comparison_to_sstable_predicate(
comp: &ComparisonExpression,
) -> Result<Option<SSTablePredicate>> {
match &comp.left {
SelectExpression::Column(col_ref) => {
Ok(column_comparison_to_predicate(col_ref.column.clone(), comp))
}
SelectExpression::Function(func) if func.name.eq_ignore_ascii_case("token") => {
token_comparison_to_predicate(func, comp).map(Some)
}
_ => Ok(None),
}
}
fn column_comparison_to_predicate(
column: String,
comp: &ComparisonExpression,
) -> Option<SSTablePredicate> {
use SSTableFilterOp as Op;
match (&comp.operator, &comp.right) {
(ComparisonOperator::Equal, ComparisonRightSide::Value(value_expr)) => Some(
SSTablePredicate::column(column, Op::Equal, vec![literal_value(value_expr)?]),
),
(ComparisonOperator::In, ComparisonRightSide::ValueList(value_exprs)) => {
let values: Vec<Value> = value_exprs.iter().filter_map(literal_value).collect();
(!values.is_empty()).then(|| SSTablePredicate::column(column, Op::In, values))
}
(ComparisonOperator::Between, ComparisonRightSide::Range(start_expr, end_expr)) => {
let start = literal_value(start_expr)?;
let end = literal_value(end_expr)?;
Some(SSTablePredicate::column(
column,
Op::Range,
vec![start, end],
))
}
(ComparisonOperator::GreaterThan, ComparisonRightSide::Value(v)) => Some(
SSTablePredicate::column(column, Op::Gt, vec![literal_value(v)?]),
),
(ComparisonOperator::GreaterThanOrEqual, ComparisonRightSide::Value(v)) => Some(
SSTablePredicate::column(column, Op::Gte, vec![literal_value(v)?]),
),
(ComparisonOperator::LessThan, ComparisonRightSide::Value(v)) => Some(
SSTablePredicate::column(column, Op::Lt, vec![literal_value(v)?]),
),
(ComparisonOperator::LessThanOrEqual, ComparisonRightSide::Value(v)) => Some(
SSTablePredicate::column(column, Op::Lte, vec![literal_value(v)?]),
),
_ => None,
}
}
fn token_comparison_to_predicate(
func: &FunctionCall,
comp: &ComparisonExpression,
) -> Result<SSTablePredicate> {
use SSTableFilterOp as Op;
let mut token_columns = Vec::with_capacity(func.args.len());
for arg in &func.args {
match arg {
SelectExpression::Column(col_ref) => token_columns.push(col_ref.column.clone()),
other => {
return Err(Error::query_execution(format!(
"token() argument must be a partition-key column; got {other:?}"
)));
}
}
}
if token_columns.is_empty() {
return Err(Error::query_execution(
"token() restriction requires at least one partition-key column argument".to_string(),
));
}
let op = match &comp.operator {
ComparisonOperator::GreaterThan => Op::Gt,
ComparisonOperator::GreaterThanOrEqual => Op::Gte,
ComparisonOperator::LessThan => Op::Lt,
ComparisonOperator::LessThanOrEqual => Op::Lte,
ComparisonOperator::Equal => Op::Equal,
other => {
return Err(Error::query_execution(format!(
"unsupported token() restriction operator {other:?}; \
token() supports only range bounds (<, <=, >, >=) and equality (=)"
)));
}
};
let ComparisonRightSide::Value(value_expr) = &comp.right else {
return Err(Error::query_execution(
"token() restriction requires a single integer token bound on the right-hand side"
.to_string(),
));
};
let bound = match literal_value(value_expr) {
Some(Value::BigInt(n)) => Value::BigInt(n),
Some(Value::Integer(n)) => Value::BigInt(n as i64),
_ => {
return Err(Error::query_execution(
"token() restriction bound must be an integer token value".to_string(),
));
}
};
Ok(SSTablePredicate::token(token_columns, op, vec![bound]))
}
fn literal_value(expr: &SelectExpression) -> Option<Value> {
match expr {
SelectExpression::Literal(value) => Some(value.clone()),
_ => None,
}
}
fn extract_projection_columns(statement: &SelectStatement) -> Vec<String> {
match &statement.select_clause {
SelectClause::All => Vec::new(),
SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) => {
let mut columns: Vec<String> = Vec::new();
let push_unique = |columns: &mut Vec<String>, col: String| {
if !columns.contains(&col) {
columns.push(col);
}
};
for expr in exprs {
if let Some((source, _)) = projection_source_and_output(expr) {
push_unique(&mut columns, source);
}
for source in aggregate_arg_source_columns(expr) {
push_unique(&mut columns, source);
}
}
if let Some(group_by) = &statement.group_by {
for col in &group_by.columns {
push_unique(&mut columns, col.column.clone());
}
}
if columns.is_empty() {
return columns;
}
if let Some(where_clause) = &statement.where_clause {
for col_ref in where_clause.get_column_refs() {
push_unique(&mut columns, col_ref.column);
}
}
if let Some(order_by) = &statement.order_by {
for item in &order_by.items {
for col_ref in item.expression.get_column_refs() {
push_unique(&mut columns, col_ref.column);
}
}
}
columns
}
}
}
fn plan_aggregation(statement: &SelectStatement) -> AggregationPlan {
let group_by_columns: Vec<String> = statement
.group_by
.as_ref()
.map(|g| g.columns.iter().map(|col| col.column.clone()).collect())
.unwrap_or_default();
let mut output_for_source: HashMap<String, String> = HashMap::new();
if let SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) = &statement.select_clause {
for expr in exprs {
if let Some((source, output)) = projection_source_and_output(expr) {
output_for_source.entry(source).or_insert(output);
}
}
}
let group_by_output_names: Vec<String> = group_by_columns
.iter()
.map(|col| {
output_for_source
.get(col.as_str())
.cloned()
.unwrap_or_else(|| col.clone())
})
.collect();
let mut aggregates = Vec::new();
if let SelectClause::Columns(exprs) = &statement.select_clause {
for expr in exprs {
let Some((agg, alias)) = unwrap_aggregate(expr).zip(aggregate_output_name(expr)) else {
continue;
};
let (column, _) = aggregate_column_and_alias(agg);
aggregates.push(AggregateComputation {
function: agg.function.clone(),
column,
alias,
distinct: agg.distinct,
});
}
}
AggregationPlan {
group_by_columns,
group_by_output_names,
aggregates,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{platform::Platform, schema::SchemaManager, storage::StorageEngine, Config};
use tempfile::TempDir;
#[tokio::test]
async fn test_optimizer_creation() {
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());
let optimizer = SelectOptimizer { schema, storage };
assert!(std::mem::size_of_val(&optimizer) > 0);
}
async fn make_optimizer() -> SelectOptimizer {
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());
SelectOptimizer { schema, storage }
}
fn project_step_count(plan: &OptimizedQueryPlan) -> usize {
plan.execution_steps
.iter()
.filter(|s| matches!(s, ExecutionStep::Project { .. }))
.count()
}
#[tokio::test]
async fn bare_column_select_projects_rows_once() {
let optimizer = make_optimizer().await;
let bare = crate::query::select_parser::parse_select("SELECT a, b, c FROM t").unwrap();
let plan = optimizer.optimize(bare).await.unwrap();
let projection_passes = 1 + project_step_count(&plan);
assert_eq!(
projection_passes, 1,
"issue #1587: a bare-column SELECT must project each row once (scan only), not twice"
);
let scan_projection = plan
.execution_steps
.iter()
.find_map(|s| match s {
ExecutionStep::SSTableScan { projection, .. } => Some(projection.clone()),
_ => None,
})
.expect("plan has an SSTable scan");
assert_eq!(scan_projection, vec!["a", "b", "c"]);
let mut aliased = crate::query::select_parser::parse_select("SELECT a FROM t").unwrap();
aliased.select_clause = SelectClause::Columns(vec![SelectExpression::Aliased(
Box::new(SelectExpression::Column(ColumnRef {
table: None,
column: "a".to_string(),
})),
"x".to_string(),
)]);
let aplan = optimizer.optimize(aliased).await.unwrap();
assert_eq!(
1 + project_step_count(&aplan),
2,
"an aliased projection must keep its Project pass"
);
}
fn scan_projection_of(query: &str) -> Vec<String> {
let statement = crate::query::select_parser::parse_select(query)
.unwrap_or_else(|e| panic!("{query} must parse: {e}"));
extract_projection_columns(&statement)
}
#[test]
fn where_predicate_columns_are_scanned() {
let projection = scan_projection_of(
"SELECT SUM(value) FROM test_basic.multi_partition_table WHERE category = 'A'",
);
assert!(
projection.iter().any(|c| c == "value"),
"aggregate argument `value` must be scanned; got {projection:?}"
);
assert!(
projection.iter().any(|c| c == "category"),
"WHERE column `category` must be scanned so the predicate backstop can \
evaluate it; got {projection:?}"
);
}
#[test]
fn where_columns_from_compound_predicate_are_scanned() {
let projection = scan_projection_of(
"SELECT SUM(value) FROM t WHERE category = 'A' AND (name = 'x' OR metadata = 'y')",
);
for col in ["value", "category", "name", "metadata"] {
assert!(
projection.iter().any(|c| c == col),
"referenced column `{col}` must be scanned; got {projection:?}"
);
}
}
#[test]
fn order_by_columns_are_scanned() {
let projection = scan_projection_of("SELECT name FROM t ORDER BY value");
assert!(
projection.iter().any(|c| c == "name"),
"selected column `name` must be scanned; got {projection:?}"
);
assert!(
projection.iter().any(|c| c == "value"),
"ORDER BY column `value` must be scanned for the Sort step; got {projection:?}"
);
}
#[test]
fn empty_projection_stays_empty_for_scan_all() {
assert!(
scan_projection_of("SELECT * FROM t WHERE category = 'A'").is_empty(),
"SELECT * must keep an empty (scan-all) projection"
);
assert!(
scan_projection_of("SELECT COUNT(*) FROM t WHERE category = 'A'").is_empty(),
"bare COUNT(*) with no dimension must keep an empty (scan-all) projection"
);
}
#[tokio::test]
async fn bare_column_with_where_helper_keeps_project() {
let optimizer = make_optimizer().await;
let with_helper =
crate::query::select_parser::parse_select("SELECT a FROM t WHERE b = 1").unwrap();
let plan = optimizer.optimize(with_helper).await.unwrap();
assert_eq!(
project_step_count(&plan),
1,
"a bare-column SELECT with a non-selected WHERE column must keep its \
Project step to trim the helper column from the output"
);
let no_helper =
crate::query::select_parser::parse_select("SELECT a, b FROM t WHERE a = 1").unwrap();
let plan = optimizer.optimize(no_helper).await.unwrap();
assert_eq!(
project_step_count(&plan),
0,
"a bare-column SELECT whose WHERE only references selected columns must \
still skip the redundant Project step (#1587)"
);
}
fn cmp(op: ComparisonOperator, column: &str, value: Value) -> ComparisonExpression {
ComparisonExpression {
left: SelectExpression::Column(ColumnRef {
table: None,
column: column.to_string(),
}),
operator: op,
right: ComparisonRightSide::Value(SelectExpression::Literal(value)),
}
}
#[test]
fn inequality_operators_convert_to_single_bound_predicates() {
let cases = [
(ComparisonOperator::GreaterThan, SSTableFilterOp::Gt),
(ComparisonOperator::GreaterThanOrEqual, SSTableFilterOp::Gte),
(ComparisonOperator::LessThan, SSTableFilterOp::Lt),
(ComparisonOperator::LessThanOrEqual, SSTableFilterOp::Lte),
];
for (op, expected_op) in cases {
let comp = cmp(op.clone(), "ck", Value::Integer(200));
let predicate = comparison_to_sstable_predicate(&comp)
.expect("conversion must not error")
.unwrap_or_else(|| panic!("operator {op:?} must convert to a predicate"));
assert_eq!(predicate.column, "ck");
assert!(
std::mem::discriminant(&predicate.operation)
== std::mem::discriminant(&expected_op),
"operator {op:?} produced {:?}, expected {expected_op:?}",
predicate.operation
);
assert_eq!(predicate.values, vec![Value::Integer(200)]);
}
}
#[test]
fn query_plan_carries_clustering_inequality_bounds() {
use crate::query::select_parser::parse_select;
let statement = parse_select(
"SELECT * FROM perf.wide_rows WHERE pk = 'p0000' AND ck >= 0 AND ck < 200",
)
.expect("issue #788 query must parse");
let where_clause = statement
.where_clause
.expect("WHERE clause must be present");
let predicates = collect_sstable_predicates(&where_clause).expect("planning must succeed");
let has = |col: &str, want: &SSTableFilterOp| {
predicates.iter().any(|p| {
p.column == col
&& std::mem::discriminant(&p.operation) == std::mem::discriminant(want)
})
};
assert!(
has("pk", &SSTableFilterOp::Equal),
"partition equality must be pushed; got {predicates:?}"
);
assert!(
has("ck", &SSTableFilterOp::Gte),
"Issue #788: `ck >= 0` must be pushed as Gte (was dropped); got {predicates:?}"
);
assert!(
has("ck", &SSTableFilterOp::Lt),
"Issue #788: `ck < 200` must be pushed as Lt (was dropped); got {predicates:?}"
);
assert_eq!(
predicates.len(),
3,
"all three restrictions must be captured; got {predicates:?}"
);
}
#[test]
fn query_plan_carries_in_predicate() {
use crate::query::select_parser::parse_select;
let statement =
parse_select("SELECT * FROM ks.t WHERE pk IN (1, 2, 3)").expect("IN query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
let predicates = collect_sstable_predicates(&where_clause).expect("planning must succeed");
assert_eq!(predicates.len(), 1, "one IN predicate; got {predicates:?}");
let p = &predicates[0];
assert_eq!(p.column, "pk");
assert!(matches!(p.operation, SSTableFilterOp::In));
assert!(!p.is_token());
assert_eq!(
p.values,
vec![Value::BigInt(1), Value::BigInt(2), Value::BigInt(3)]
);
}
#[test]
fn query_plan_carries_token_range_predicate() {
use crate::query::select_parser::parse_select;
let statement =
parse_select("SELECT * FROM ks.t WHERE token(pk) >= -100 AND token(pk) < 5000")
.expect("token-range query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
let predicates = collect_sstable_predicates(&where_clause).expect("planning must succeed");
assert_eq!(predicates.len(), 2, "two token bounds; got {predicates:?}");
assert!(
predicates.iter().all(|p| p.is_token()),
"both predicates must be token predicates; got {predicates:?}"
);
let lower = predicates
.iter()
.find(|p| matches!(p.operation, SSTableFilterOp::Gte))
.expect("a Gte token bound");
assert_eq!(
lower.token_columns.as_deref(),
Some(["pk".to_string()].as_slice())
);
assert_eq!(lower.values, vec![Value::BigInt(-100)]);
let upper = predicates
.iter()
.find(|p| matches!(p.operation, SSTableFilterOp::Lt))
.expect("a Lt token bound");
assert_eq!(upper.values, vec![Value::BigInt(5000)]);
}
#[test]
fn token_equal_lowers_to_token_equal_predicate() {
use crate::query::select_parser::parse_select;
let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) = 4242")
.expect("token-equal query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
let predicates = collect_sstable_predicates(&where_clause).expect("planning must succeed");
assert_eq!(
predicates.len(),
1,
"token(pk) = ? must produce exactly one predicate (not be dropped); got {predicates:?}"
);
let p = &predicates[0];
assert!(p.is_token(), "must be a token predicate; got {p:?}");
assert!(
matches!(p.operation, SSTableFilterOp::Equal),
"token(pk) = ? must lower to a token Equal op; got {:?}",
p.operation
);
assert_eq!(
p.token_columns.as_deref(),
Some(["pk".to_string()].as_slice())
);
assert_eq!(p.values, vec![Value::BigInt(4242)]);
}
#[test]
fn token_equal_combined_with_other_predicate_keeps_both() {
use crate::query::select_parser::parse_select;
let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) = 7 AND ck > 0")
.expect("combined token-equal query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
let predicates = collect_sstable_predicates(&where_clause).expect("planning must succeed");
assert!(
predicates
.iter()
.any(|p| p.is_token() && matches!(p.operation, SSTableFilterOp::Equal)),
"the token(pk) = 7 restriction must be pushed as a token Equal; got {predicates:?}"
);
assert!(
predicates.iter().any(|p| !p.is_token() && p.column == "ck"),
"the ck > 0 restriction must also be pushed; got {predicates:?}"
);
}
#[test]
fn token_in_is_a_planning_error() {
use crate::query::select_parser::parse_select;
let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) IN (1, 2, 3)")
.expect("token-IN query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
let err = collect_sstable_predicates(&where_clause)
.expect_err("token(pk) IN (...) must be rejected, not silently dropped");
let msg = err.to_string();
assert!(
msg.contains("token()"),
"error must explain the token() restriction; got: {msg}"
);
}
#[test]
fn token_in_combined_with_other_predicate_is_a_planning_error() {
use crate::query::select_parser::parse_select;
let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) IN (1, 2) AND ck > 0")
.expect("combined token-IN query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
assert!(
collect_sstable_predicates(&where_clause).is_err(),
"token(pk) IN (...) AND ck > 0 must error, not silently ignore the token restriction"
);
}
#[test]
fn token_between_is_a_planning_error() {
use crate::query::select_parser::parse_select;
let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) BETWEEN 1 AND 9")
.expect("token-BETWEEN query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
let err = collect_sstable_predicates(&where_clause)
.expect_err("token(pk) BETWEEN must be rejected, not silently dropped");
assert!(
err.to_string().contains("token()"),
"error must explain the token() restriction; got: {err}"
);
}
#[test]
fn token_in_under_not_is_a_planning_error() {
use crate::query::select_parser::parse_select;
let statement = parse_select("SELECT * FROM ks.t WHERE ck > 0 AND NOT token(pk) IN (1, 2)")
.expect("token-IN-under-NOT query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
let err = validate_token_forms_whole_tree(&where_clause, true)
.expect_err("token(pk) IN under NOT must be rejected, not silently dropped");
assert!(
err.to_string().contains("token()"),
"error must explain the token() restriction; got: {err}"
);
}
#[test]
fn token_in_under_or_is_a_planning_error() {
use crate::query::select_parser::parse_select;
let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) IN (1, 2) OR ck = 3")
.expect("token-IN-under-OR query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
assert!(
validate_token_forms_whole_tree(&where_clause, true).is_err(),
"token(pk) IN (...) under OR must error, not silently ignore the token restriction"
);
}
#[test]
fn token_between_under_or_is_a_planning_error() {
use crate::query::select_parser::parse_select;
let statement =
parse_select("SELECT * FROM ks.t WHERE token(pk) BETWEEN 1 AND 9 OR ck = 3")
.expect("token-BETWEEN-under-OR query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
assert!(
validate_token_forms_whole_tree(&where_clause, true).is_err(),
"token() BETWEEN under OR must error"
);
}
#[test]
fn supported_token_range_under_or_is_a_planning_error() {
use crate::query::select_parser::parse_select;
let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) > 5 OR ck = 3")
.expect("token-range-under-OR query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
let err = validate_token_forms_whole_tree(&where_clause, true).expect_err(
"a supported token range under OR must error (cannot be pushed nor row-evaluated)",
);
let msg = err.to_string();
assert!(
msg.contains("token()") && msg.contains("OR/NOT"),
"error must explain the OR/NOT limitation; got: {msg}"
);
}
#[test]
fn supported_token_range_under_not_is_a_planning_error() {
use crate::query::select_parser::parse_select;
let statement = parse_select("SELECT * FROM ks.t WHERE ck = 3 AND NOT token(pk) > 5")
.expect("token-range-under-NOT query must parse");
let where_clause = statement.where_clause.expect("WHERE present");
assert!(
validate_token_forms_whole_tree(&where_clause, true).is_err(),
"a supported token range under NOT must error"
);
}
#[test]
fn supported_token_forms_pass_whole_tree_validation() {
use crate::query::select_parser::parse_select;
for q in [
"SELECT * FROM ks.t WHERE token(pk) > 0",
"SELECT * FROM ks.t WHERE token(pk) >= -100 AND token(pk) < 5000",
"SELECT * FROM ks.t WHERE token(pk) = 4242",
"SELECT * FROM ks.t WHERE token(pk) = 7 AND ck > 0",
"SELECT * FROM ks.t WHERE (token(pk) > 0 AND ck > 1)",
] {
let statement = parse_select(q).unwrap_or_else(|e| panic!("{q} must parse: {e}"));
let where_clause = statement.where_clause.expect("WHERE present");
validate_token_forms_whole_tree(&where_clause, true)
.unwrap_or_else(|e| panic!("{q} must pass whole-tree validation: {e}"));
}
}
#[test]
fn token_range_and_equality_still_plan() {
use crate::query::select_parser::parse_select;
for q in [
"SELECT * FROM ks.t WHERE token(pk) > 0",
"SELECT * FROM ks.t WHERE token(pk) >= -100 AND token(pk) < 5000",
"SELECT * FROM ks.t WHERE token(pk) = 4242",
"SELECT * FROM ks.t WHERE token(pk) = 7 AND ck > 0",
] {
let statement = parse_select(q).unwrap_or_else(|e| panic!("{q} must parse: {e}"));
let where_clause = statement.where_clause.expect("WHERE present");
let predicates = collect_sstable_predicates(&where_clause)
.unwrap_or_else(|e| panic!("{q} must plan without error: {e}"));
assert!(
predicates.iter().any(|p| p.is_token()),
"{q} must still push a token predicate; got {predicates:?}"
);
}
}
}