1use super::select_ast::*;
4use super::select_naming::{
5 aggregate_arg_source_columns, aggregate_column_and_alias, aggregate_output_name,
6 projection_source_and_output, unwrap_aggregate,
7};
8use crate::{schema::SchemaManager, storage::StorageEngine, Error, Result, TableId, Value};
9use std::collections::HashMap;
10use std::sync::Arc;
11
12#[cfg(test)]
18thread_local! {
19 pub(crate) static OPTIMIZE_INVOCATIONS: std::cell::Cell<usize> =
20 const { std::cell::Cell::new(0) };
21}
22
23#[derive(Debug)]
25pub struct SelectOptimizer {
26 #[allow(dead_code)]
27 schema: Arc<SchemaManager>,
28 #[allow(dead_code)]
29 storage: Arc<StorageEngine>,
30}
31
32#[derive(Debug, Clone)]
34pub struct OptimizedQueryPlan {
35 pub statement: SelectStatement,
36 pub execution_steps: Vec<ExecutionStep>,
37 pub sstable_predicates: Vec<SSTablePredicate>,
38 pub aggregation_plan: Option<AggregationPlan>,
39}
40
41#[derive(Debug, Clone)]
43pub enum ExecutionStep {
44 SSTableScan {
45 table: TableId,
46 predicates: Vec<SSTablePredicate>,
47 projection: Vec<String>,
48 },
49 Filter {
50 expression: WhereExpression,
51 },
52 Sort {
53 order_by: OrderByClause,
54 },
55 Aggregate {
56 plan: AggregationPlan,
57 },
58 Limit {
59 count: u64,
60 offset: Option<u64>,
61 },
62 PerPartitionLimit {
65 count: u64,
66 },
67 Project {
68 columns: Vec<SelectExpression>,
69 },
70}
71
72impl ExecutionStep {
73 pub fn variant_name(&self) -> &'static str {
78 match self {
79 ExecutionStep::SSTableScan { .. } => "SSTableScan",
80 ExecutionStep::Filter { .. } => "Filter",
81 ExecutionStep::Sort { .. } => "Sort",
82 ExecutionStep::Aggregate { .. } => "Aggregate",
83 ExecutionStep::Limit { .. } => "Limit",
84 ExecutionStep::PerPartitionLimit { .. } => "PerPartitionLimit",
85 ExecutionStep::Project { .. } => "Project",
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct SSTablePredicate {
93 pub column: String,
94 pub operation: SSTableFilterOp,
95 pub values: Vec<Value>,
96 pub token_columns: Option<Vec<String>>,
108}
109
110impl SSTablePredicate {
111 pub fn column(
113 column: impl Into<String>,
114 operation: SSTableFilterOp,
115 values: Vec<Value>,
116 ) -> Self {
117 Self {
118 column: column.into(),
119 operation,
120 values,
121 token_columns: None,
122 }
123 }
124
125 pub fn token(
130 token_columns: Vec<String>,
131 operation: SSTableFilterOp,
132 values: Vec<Value>,
133 ) -> Self {
134 let label = format!("token({})", token_columns.join(", "));
135 Self {
136 column: label,
137 operation,
138 values,
139 token_columns: Some(token_columns),
140 }
141 }
142
143 pub fn is_token(&self) -> bool {
146 self.token_columns.is_some()
147 }
148}
149
150#[derive(Debug, Clone)]
152pub enum SSTableFilterOp {
153 Equal,
154 Range,
155 Gt,
158 Gte,
159 Lt,
160 Lte,
161 In,
162 Prefix,
163 BloomFilter,
164}
165
166#[derive(Debug, Clone)]
168pub struct AggregationPlan {
169 pub group_by_columns: Vec<String>,
171 pub group_by_output_names: Vec<String>,
176 pub aggregates: Vec<AggregateComputation>,
177}
178
179#[derive(Debug, Clone)]
181pub struct AggregateComputation {
182 pub function: AggregateType,
183 pub column: String,
184 pub alias: String,
185 pub distinct: bool,
186}
187
188impl SelectOptimizer {
189 pub fn new(schema: Arc<SchemaManager>, storage: Arc<StorageEngine>) -> Self {
191 Self { schema, storage }
192 }
193
194 pub async fn optimize(&self, statement: SelectStatement) -> Result<OptimizedQueryPlan> {
196 #[cfg(test)]
197 OPTIMIZE_INVOCATIONS.with(|c| c.set(c.get() + 1));
198
199 let mut plan = OptimizedQueryPlan {
200 statement: statement.clone(),
201 execution_steps: Vec::new(),
202 sstable_predicates: Vec::new(),
203 aggregation_plan: None,
204 };
205
206 let Some(from_clause) = statement.from_clause.as_ref() else {
208 return Ok(plan);
209 };
210 let table_id = match from_clause {
211 FromClause::Table(t) | FromClause::TableAlias(t, _) => t.clone(),
212 };
213
214 if let SelectClause::Distinct(exprs) = &statement.select_clause {
225 if exprs.iter().any(|e| e.is_aggregate()) {
226 return Err(Error::query_execution(
227 "SELECT DISTINCT with an aggregate function is not supported; \
228 DISTINCT over an aggregate is redundant because the aggregate \
229 already collapses rows to a single value"
230 .to_string(),
231 ));
232 }
233 }
234
235 if let Some(where_clause) = &statement.where_clause {
236 validate_token_forms_whole_tree(where_clause, true)?;
243 plan.sstable_predicates = collect_sstable_predicates(where_clause)?;
244 }
245
246 let scan_projection = extract_projection_columns(&statement);
252 plan.execution_steps.push(ExecutionStep::SSTableScan {
253 table: table_id,
254 predicates: plan.sstable_predicates.clone(),
255 projection: scan_projection.clone(),
256 });
257
258 if let Some(where_clause) = &statement.where_clause {
261 if plan.sstable_predicates.is_empty() {
262 plan.execution_steps.push(ExecutionStep::Filter {
263 expression: where_clause.clone(),
264 });
265 }
266 }
267
268 let needs_aggregation = statement.requires_aggregation();
269 if needs_aggregation {
270 let agg_plan = plan_aggregation(&statement);
271 plan.execution_steps.push(ExecutionStep::Aggregate {
272 plan: agg_plan.clone(),
273 });
274 plan.aggregation_plan = Some(agg_plan);
275 }
276
277 if let Some(order_by) = &statement.order_by {
278 plan.execution_steps.push(ExecutionStep::Sort {
279 order_by: order_by.clone(),
280 });
281 }
282
283 if let Some(count) = statement.per_partition_limit {
286 plan.execution_steps
287 .push(ExecutionStep::PerPartitionLimit { count });
288 }
289
290 if let Some(limit) = &statement.limit {
291 plan.execution_steps.push(ExecutionStep::Limit {
292 count: limit.count,
293 offset: statement.offset,
294 });
295 }
296
297 if !needs_aggregation {
300 if let SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) =
301 &statement.select_clause
302 {
303 let is_bare_columns = matches!(&statement.select_clause, SelectClause::Columns(_))
317 && exprs
318 .iter()
319 .all(|e| matches!(e, SelectExpression::Column(_)));
320
321 let projection_is_exactly_selected = is_bare_columns && {
330 let selected: Vec<&str> = exprs
331 .iter()
332 .filter_map(|e| match e {
333 SelectExpression::Column(c) => Some(c.column.as_str()),
334 _ => None,
335 })
336 .collect();
337 scan_projection.len() == selected.len()
338 && scan_projection
339 .iter()
340 .zip(&selected)
341 .all(|(scanned, wanted)| scanned.as_str() == *wanted)
342 };
343
344 if !projection_is_exactly_selected {
345 plan.execution_steps.push(ExecutionStep::Project {
346 columns: exprs.clone(),
347 });
348 }
349 }
350 }
351
352 Ok(plan)
353 }
354}
355
356fn collect_sstable_predicates(expr: &WhereExpression) -> Result<Vec<SSTablePredicate>> {
360 let mut out = Vec::new();
361 fn walk(expr: &WhereExpression, out: &mut Vec<SSTablePredicate>) -> Result<()> {
362 match expr {
363 WhereExpression::Comparison(comp) => {
364 if let Some(predicate) = comparison_to_sstable_predicate(comp)? {
367 out.push(predicate);
368 }
369 }
370 WhereExpression::And(exprs) => {
371 for e in exprs {
372 walk(e, out)?;
373 }
374 }
375 WhereExpression::Parentheses(inner) => walk(inner, out)?,
376 WhereExpression::Or(_) | WhereExpression::Not(_) => {}
377 }
378 Ok(())
379 }
380 walk(expr, &mut out)?;
381 Ok(out)
382}
383
384fn validate_token_forms_whole_tree(expr: &WhereExpression, pushable: bool) -> Result<()> {
417 match expr {
418 WhereExpression::Comparison(comp) => {
419 if is_token_comparison(comp) {
420 let _supported = comparison_to_sstable_predicate(comp)?;
424 if !pushable {
425 return Err(Error::query_execution(
426 "token() restriction is only supported at the top level or within an \
427 AND conjunction; token() under OR/NOT cannot be pushed down and the \
428 row-level evaluator cannot compute a token, so it is rejected rather \
429 than silently ignored"
430 .to_string(),
431 ));
432 }
433 }
434 }
435 WhereExpression::And(exprs) => {
438 for e in exprs {
439 validate_token_forms_whole_tree(e, pushable)?;
440 }
441 }
442 WhereExpression::Or(exprs) => {
445 for e in exprs {
446 validate_token_forms_whole_tree(e, false)?;
447 }
448 }
449 WhereExpression::Not(inner) => validate_token_forms_whole_tree(inner, false)?,
450 WhereExpression::Parentheses(inner) => validate_token_forms_whole_tree(inner, pushable)?,
453 }
454 Ok(())
455}
456
457fn is_token_comparison(comp: &ComparisonExpression) -> bool {
460 matches!(
461 &comp.left,
462 SelectExpression::Function(func) if func.name.eq_ignore_ascii_case("token")
463 )
464}
465
466fn comparison_to_sstable_predicate(
475 comp: &ComparisonExpression,
476) -> Result<Option<SSTablePredicate>> {
477 match &comp.left {
482 SelectExpression::Column(col_ref) => {
483 Ok(column_comparison_to_predicate(col_ref.column.clone(), comp))
484 }
485 SelectExpression::Function(func) if func.name.eq_ignore_ascii_case("token") => {
486 token_comparison_to_predicate(func, comp).map(Some)
487 }
488 _ => Ok(None),
489 }
490}
491
492fn column_comparison_to_predicate(
494 column: String,
495 comp: &ComparisonExpression,
496) -> Option<SSTablePredicate> {
497 use SSTableFilterOp as Op;
498 match (&comp.operator, &comp.right) {
499 (ComparisonOperator::Equal, ComparisonRightSide::Value(value_expr)) => Some(
500 SSTablePredicate::column(column, Op::Equal, vec![literal_value(value_expr)?]),
501 ),
502 (ComparisonOperator::In, ComparisonRightSide::ValueList(value_exprs)) => {
503 let values: Vec<Value> = value_exprs.iter().filter_map(literal_value).collect();
504 (!values.is_empty()).then(|| SSTablePredicate::column(column, Op::In, values))
505 }
506 (ComparisonOperator::Between, ComparisonRightSide::Range(start_expr, end_expr)) => {
507 let start = literal_value(start_expr)?;
508 let end = literal_value(end_expr)?;
509 Some(SSTablePredicate::column(
510 column,
511 Op::Range,
512 vec![start, end],
513 ))
514 }
515 (ComparisonOperator::GreaterThan, ComparisonRightSide::Value(v)) => Some(
519 SSTablePredicate::column(column, Op::Gt, vec![literal_value(v)?]),
520 ),
521 (ComparisonOperator::GreaterThanOrEqual, ComparisonRightSide::Value(v)) => Some(
522 SSTablePredicate::column(column, Op::Gte, vec![literal_value(v)?]),
523 ),
524 (ComparisonOperator::LessThan, ComparisonRightSide::Value(v)) => Some(
525 SSTablePredicate::column(column, Op::Lt, vec![literal_value(v)?]),
526 ),
527 (ComparisonOperator::LessThanOrEqual, ComparisonRightSide::Value(v)) => Some(
528 SSTablePredicate::column(column, Op::Lte, vec![literal_value(v)?]),
529 ),
530 _ => None,
531 }
532}
533
534fn token_comparison_to_predicate(
549 func: &FunctionCall,
550 comp: &ComparisonExpression,
551) -> Result<SSTablePredicate> {
552 use SSTableFilterOp as Op;
553 let mut token_columns = Vec::with_capacity(func.args.len());
555 for arg in &func.args {
556 match arg {
557 SelectExpression::Column(col_ref) => token_columns.push(col_ref.column.clone()),
558 other => {
559 return Err(Error::query_execution(format!(
560 "token() argument must be a partition-key column; got {other:?}"
561 )));
562 }
563 }
564 }
565 if token_columns.is_empty() {
566 return Err(Error::query_execution(
567 "token() restriction requires at least one partition-key column argument".to_string(),
568 ));
569 }
570
571 let op = match &comp.operator {
572 ComparisonOperator::GreaterThan => Op::Gt,
573 ComparisonOperator::GreaterThanOrEqual => Op::Gte,
574 ComparisonOperator::LessThan => Op::Lt,
575 ComparisonOperator::LessThanOrEqual => Op::Lte,
576 ComparisonOperator::Equal => Op::Equal,
580 other => {
584 return Err(Error::query_execution(format!(
585 "unsupported token() restriction operator {other:?}; \
586 token() supports only range bounds (<, <=, >, >=) and equality (=)"
587 )));
588 }
589 };
590 let ComparisonRightSide::Value(value_expr) = &comp.right else {
591 return Err(Error::query_execution(
592 "token() restriction requires a single integer token bound on the right-hand side"
593 .to_string(),
594 ));
595 };
596 let bound = match literal_value(value_expr) {
598 Some(Value::BigInt(n)) => Value::BigInt(n),
599 Some(Value::Integer(n)) => Value::BigInt(n as i64),
600 _ => {
601 return Err(Error::query_execution(
602 "token() restriction bound must be an integer token value".to_string(),
603 ));
604 }
605 };
606 Ok(SSTablePredicate::token(token_columns, op, vec![bound]))
607}
608
609fn literal_value(expr: &SelectExpression) -> Option<Value> {
610 match expr {
611 SelectExpression::Literal(value) => Some(value.clone()),
612 _ => None,
613 }
614}
615
616fn extract_projection_columns(statement: &SelectStatement) -> Vec<String> {
669 match &statement.select_clause {
670 SelectClause::All => Vec::new(),
671 SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) => {
672 let mut columns: Vec<String> = Vec::new();
673 let push_unique = |columns: &mut Vec<String>, col: String| {
674 if !columns.contains(&col) {
675 columns.push(col);
676 }
677 };
678 for expr in exprs {
679 if let Some((source, _)) = projection_source_and_output(expr) {
682 push_unique(&mut columns, source);
683 }
684 for source in aggregate_arg_source_columns(expr) {
687 push_unique(&mut columns, source);
688 }
689 }
690 if let Some(group_by) = &statement.group_by {
694 for col in &group_by.columns {
695 push_unique(&mut columns, col.column.clone());
696 }
697 }
698
699 if columns.is_empty() {
704 return columns;
705 }
706
707 if let Some(where_clause) = &statement.where_clause {
710 for col_ref in where_clause.get_column_refs() {
711 push_unique(&mut columns, col_ref.column);
712 }
713 }
714 if let Some(order_by) = &statement.order_by {
717 for item in &order_by.items {
718 for col_ref in item.expression.get_column_refs() {
719 push_unique(&mut columns, col_ref.column);
720 }
721 }
722 }
723 columns
724 }
725 }
726}
727
728fn plan_aggregation(statement: &SelectStatement) -> AggregationPlan {
729 let group_by_columns: Vec<String> = statement
730 .group_by
731 .as_ref()
732 .map(|g| g.columns.iter().map(|col| col.column.clone()).collect())
733 .unwrap_or_default();
734
735 let mut output_for_source: HashMap<String, String> = HashMap::new();
740 if let SelectClause::Columns(exprs) | SelectClause::Distinct(exprs) = &statement.select_clause {
741 for expr in exprs {
742 if let Some((source, output)) = projection_source_and_output(expr) {
743 output_for_source.entry(source).or_insert(output);
745 }
746 }
747 }
748 let group_by_output_names: Vec<String> = group_by_columns
749 .iter()
750 .map(|col| {
751 output_for_source
752 .get(col.as_str())
753 .cloned()
754 .unwrap_or_else(|| col.clone())
755 })
756 .collect();
757
758 let mut aggregates = Vec::new();
759 if let SelectClause::Columns(exprs) = &statement.select_clause {
760 for expr in exprs {
761 let Some((agg, alias)) = unwrap_aggregate(expr).zip(aggregate_output_name(expr)) else {
767 continue;
768 };
769 let (column, _) = aggregate_column_and_alias(agg);
770 aggregates.push(AggregateComputation {
771 function: agg.function.clone(),
772 column,
773 alias,
774 distinct: agg.distinct,
775 });
776 }
777 }
778
779 AggregationPlan {
780 group_by_columns,
781 group_by_output_names,
782 aggregates,
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789 use crate::{platform::Platform, schema::SchemaManager, storage::StorageEngine, Config};
790 use tempfile::TempDir;
791
792 #[tokio::test]
793 async fn test_optimizer_creation() {
794 let temp_dir = TempDir::new().unwrap();
795 let config = Config::default();
796 let platform = Arc::new(Platform::new(&config).await.unwrap());
797 let storage = Arc::new(
798 StorageEngine::open(
799 temp_dir.path(),
800 &config,
801 platform.clone(),
802 #[cfg(feature = "state_machine")]
803 None,
804 )
805 .await
806 .unwrap(),
807 );
808 let schema = Arc::new(SchemaManager::new(temp_dir.path()).await.unwrap());
809 let optimizer = SelectOptimizer { schema, storage };
810 assert!(std::mem::size_of_val(&optimizer) > 0);
811 }
812
813 async fn make_optimizer() -> SelectOptimizer {
814 let temp_dir = TempDir::new().unwrap();
815 let config = Config::default();
816 let platform = Arc::new(Platform::new(&config).await.unwrap());
817 let storage = Arc::new(
818 StorageEngine::open(
819 temp_dir.path(),
820 &config,
821 platform.clone(),
822 #[cfg(feature = "state_machine")]
823 None,
824 )
825 .await
826 .unwrap(),
827 );
828 let schema = Arc::new(SchemaManager::new(temp_dir.path()).await.unwrap());
829 SelectOptimizer { schema, storage }
830 }
831
832 fn project_step_count(plan: &OptimizedQueryPlan) -> usize {
833 plan.execution_steps
834 .iter()
835 .filter(|s| matches!(s, ExecutionStep::Project { .. }))
836 .count()
837 }
838
839 #[tokio::test]
845 async fn bare_column_select_projects_rows_once() {
846 let optimizer = make_optimizer().await;
847
848 let bare = crate::query::select_parser::parse_select("SELECT a, b, c FROM t").unwrap();
849 let plan = optimizer.optimize(bare).await.unwrap();
850 let projection_passes = 1 + project_step_count(&plan);
851 assert_eq!(
852 projection_passes, 1,
853 "issue #1587: a bare-column SELECT must project each row once (scan only), not twice"
854 );
855
856 let scan_projection = plan
858 .execution_steps
859 .iter()
860 .find_map(|s| match s {
861 ExecutionStep::SSTableScan { projection, .. } => Some(projection.clone()),
862 _ => None,
863 })
864 .expect("plan has an SSTable scan");
865 assert_eq!(scan_projection, vec!["a", "b", "c"]);
866
867 let mut aliased = crate::query::select_parser::parse_select("SELECT a FROM t").unwrap();
869 aliased.select_clause = SelectClause::Columns(vec![SelectExpression::Aliased(
870 Box::new(SelectExpression::Column(ColumnRef {
871 table: None,
872 column: "a".to_string(),
873 })),
874 "x".to_string(),
875 )]);
876 let aplan = optimizer.optimize(aliased).await.unwrap();
877 assert_eq!(
878 1 + project_step_count(&aplan),
879 2,
880 "an aliased projection must keep its Project pass"
881 );
882 }
883
884 fn scan_projection_of(query: &str) -> Vec<String> {
886 let statement = crate::query::select_parser::parse_select(query)
887 .unwrap_or_else(|e| panic!("{query} must parse: {e}"));
888 extract_projection_columns(&statement)
889 }
890
891 #[test]
897 fn where_predicate_columns_are_scanned() {
898 let projection = scan_projection_of(
899 "SELECT SUM(value) FROM test_basic.multi_partition_table WHERE category = 'A'",
900 );
901 assert!(
902 projection.iter().any(|c| c == "value"),
903 "aggregate argument `value` must be scanned; got {projection:?}"
904 );
905 assert!(
906 projection.iter().any(|c| c == "category"),
907 "WHERE column `category` must be scanned so the predicate backstop can \
908 evaluate it; got {projection:?}"
909 );
910 }
911
912 #[test]
915 fn where_columns_from_compound_predicate_are_scanned() {
916 let projection = scan_projection_of(
917 "SELECT SUM(value) FROM t WHERE category = 'A' AND (name = 'x' OR metadata = 'y')",
918 );
919 for col in ["value", "category", "name", "metadata"] {
920 assert!(
921 projection.iter().any(|c| c == col),
922 "referenced column `{col}` must be scanned; got {projection:?}"
923 );
924 }
925 }
926
927 #[test]
930 fn order_by_columns_are_scanned() {
931 let projection = scan_projection_of("SELECT name FROM t ORDER BY value");
932 assert!(
933 projection.iter().any(|c| c == "name"),
934 "selected column `name` must be scanned; got {projection:?}"
935 );
936 assert!(
937 projection.iter().any(|c| c == "value"),
938 "ORDER BY column `value` must be scanned for the Sort step; got {projection:?}"
939 );
940 }
941
942 #[test]
946 fn empty_projection_stays_empty_for_scan_all() {
947 assert!(
948 scan_projection_of("SELECT * FROM t WHERE category = 'A'").is_empty(),
949 "SELECT * must keep an empty (scan-all) projection"
950 );
951 assert!(
952 scan_projection_of("SELECT COUNT(*) FROM t WHERE category = 'A'").is_empty(),
953 "bare COUNT(*) with no dimension must keep an empty (scan-all) projection"
954 );
955 }
956
957 #[tokio::test]
961 async fn bare_column_with_where_helper_keeps_project() {
962 let optimizer = make_optimizer().await;
963
964 let with_helper =
968 crate::query::select_parser::parse_select("SELECT a FROM t WHERE b = 1").unwrap();
969 let plan = optimizer.optimize(with_helper).await.unwrap();
970 assert_eq!(
971 project_step_count(&plan),
972 1,
973 "a bare-column SELECT with a non-selected WHERE column must keep its \
974 Project step to trim the helper column from the output"
975 );
976
977 let no_helper =
979 crate::query::select_parser::parse_select("SELECT a, b FROM t WHERE a = 1").unwrap();
980 let plan = optimizer.optimize(no_helper).await.unwrap();
981 assert_eq!(
982 project_step_count(&plan),
983 0,
984 "a bare-column SELECT whose WHERE only references selected columns must \
985 still skip the redundant Project step (#1587)"
986 );
987 }
988
989 fn cmp(op: ComparisonOperator, column: &str, value: Value) -> ComparisonExpression {
991 ComparisonExpression {
992 left: SelectExpression::Column(ColumnRef {
993 table: None,
994 column: column.to_string(),
995 }),
996 operator: op,
997 right: ComparisonRightSide::Value(SelectExpression::Literal(value)),
998 }
999 }
1000
1001 #[test]
1004 fn inequality_operators_convert_to_single_bound_predicates() {
1005 let cases = [
1006 (ComparisonOperator::GreaterThan, SSTableFilterOp::Gt),
1007 (ComparisonOperator::GreaterThanOrEqual, SSTableFilterOp::Gte),
1008 (ComparisonOperator::LessThan, SSTableFilterOp::Lt),
1009 (ComparisonOperator::LessThanOrEqual, SSTableFilterOp::Lte),
1010 ];
1011 for (op, expected_op) in cases {
1012 let comp = cmp(op.clone(), "ck", Value::Integer(200));
1013 let predicate = comparison_to_sstable_predicate(&comp)
1014 .expect("conversion must not error")
1015 .unwrap_or_else(|| panic!("operator {op:?} must convert to a predicate"));
1016 assert_eq!(predicate.column, "ck");
1017 assert!(
1018 std::mem::discriminant(&predicate.operation)
1019 == std::mem::discriminant(&expected_op),
1020 "operator {op:?} produced {:?}, expected {expected_op:?}",
1021 predicate.operation
1022 );
1023 assert_eq!(predicate.values, vec![Value::Integer(200)]);
1024 }
1025 }
1026
1027 #[test]
1032 fn query_plan_carries_clustering_inequality_bounds() {
1033 use crate::query::select_parser::parse_select;
1034
1035 let statement = parse_select(
1036 "SELECT * FROM perf.wide_rows WHERE pk = 'p0000' AND ck >= 0 AND ck < 200",
1037 )
1038 .expect("issue #788 query must parse");
1039 let where_clause = statement
1040 .where_clause
1041 .expect("WHERE clause must be present");
1042
1043 let predicates = collect_sstable_predicates(&where_clause).expect("planning must succeed");
1044
1045 let has = |col: &str, want: &SSTableFilterOp| {
1046 predicates.iter().any(|p| {
1047 p.column == col
1048 && std::mem::discriminant(&p.operation) == std::mem::discriminant(want)
1049 })
1050 };
1051
1052 assert!(
1053 has("pk", &SSTableFilterOp::Equal),
1054 "partition equality must be pushed; got {predicates:?}"
1055 );
1056 assert!(
1057 has("ck", &SSTableFilterOp::Gte),
1058 "Issue #788: `ck >= 0` must be pushed as Gte (was dropped); got {predicates:?}"
1059 );
1060 assert!(
1061 has("ck", &SSTableFilterOp::Lt),
1062 "Issue #788: `ck < 200` must be pushed as Lt (was dropped); got {predicates:?}"
1063 );
1064 assert_eq!(
1065 predicates.len(),
1066 3,
1067 "all three restrictions must be captured; got {predicates:?}"
1068 );
1069 }
1070
1071 #[test]
1075 fn query_plan_carries_in_predicate() {
1076 use crate::query::select_parser::parse_select;
1077
1078 let statement =
1079 parse_select("SELECT * FROM ks.t WHERE pk IN (1, 2, 3)").expect("IN query must parse");
1080 let where_clause = statement.where_clause.expect("WHERE present");
1081 let predicates = collect_sstable_predicates(&where_clause).expect("planning must succeed");
1082
1083 assert_eq!(predicates.len(), 1, "one IN predicate; got {predicates:?}");
1084 let p = &predicates[0];
1085 assert_eq!(p.column, "pk");
1086 assert!(matches!(p.operation, SSTableFilterOp::In));
1087 assert!(!p.is_token());
1088 assert_eq!(
1089 p.values,
1090 vec![Value::BigInt(1), Value::BigInt(2), Value::BigInt(3)]
1091 );
1092 }
1093
1094 #[test]
1098 fn query_plan_carries_token_range_predicate() {
1099 use crate::query::select_parser::parse_select;
1100
1101 let statement =
1102 parse_select("SELECT * FROM ks.t WHERE token(pk) >= -100 AND token(pk) < 5000")
1103 .expect("token-range query must parse");
1104 let where_clause = statement.where_clause.expect("WHERE present");
1105 let predicates = collect_sstable_predicates(&where_clause).expect("planning must succeed");
1106
1107 assert_eq!(predicates.len(), 2, "two token bounds; got {predicates:?}");
1108 assert!(
1109 predicates.iter().all(|p| p.is_token()),
1110 "both predicates must be token predicates; got {predicates:?}"
1111 );
1112 let lower = predicates
1113 .iter()
1114 .find(|p| matches!(p.operation, SSTableFilterOp::Gte))
1115 .expect("a Gte token bound");
1116 assert_eq!(
1117 lower.token_columns.as_deref(),
1118 Some(["pk".to_string()].as_slice())
1119 );
1120 assert_eq!(lower.values, vec![Value::BigInt(-100)]);
1121 let upper = predicates
1122 .iter()
1123 .find(|p| matches!(p.operation, SSTableFilterOp::Lt))
1124 .expect("a Lt token bound");
1125 assert_eq!(upper.values, vec![Value::BigInt(5000)]);
1126 }
1127
1128 #[test]
1133 fn token_equal_lowers_to_token_equal_predicate() {
1134 use crate::query::select_parser::parse_select;
1135
1136 let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) = 4242")
1137 .expect("token-equal query must parse");
1138 let where_clause = statement.where_clause.expect("WHERE present");
1139 let predicates = collect_sstable_predicates(&where_clause).expect("planning must succeed");
1140
1141 assert_eq!(
1142 predicates.len(),
1143 1,
1144 "token(pk) = ? must produce exactly one predicate (not be dropped); got {predicates:?}"
1145 );
1146 let p = &predicates[0];
1147 assert!(p.is_token(), "must be a token predicate; got {p:?}");
1148 assert!(
1149 matches!(p.operation, SSTableFilterOp::Equal),
1150 "token(pk) = ? must lower to a token Equal op; got {:?}",
1151 p.operation
1152 );
1153 assert_eq!(
1154 p.token_columns.as_deref(),
1155 Some(["pk".to_string()].as_slice())
1156 );
1157 assert_eq!(p.values, vec![Value::BigInt(4242)]);
1158 }
1159
1160 #[test]
1165 fn token_equal_combined_with_other_predicate_keeps_both() {
1166 use crate::query::select_parser::parse_select;
1167
1168 let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) = 7 AND ck > 0")
1169 .expect("combined token-equal query must parse");
1170 let where_clause = statement.where_clause.expect("WHERE present");
1171 let predicates = collect_sstable_predicates(&where_clause).expect("planning must succeed");
1172
1173 assert!(
1174 predicates
1175 .iter()
1176 .any(|p| p.is_token() && matches!(p.operation, SSTableFilterOp::Equal)),
1177 "the token(pk) = 7 restriction must be pushed as a token Equal; got {predicates:?}"
1178 );
1179 assert!(
1180 predicates.iter().any(|p| !p.is_token() && p.column == "ck"),
1181 "the ck > 0 restriction must also be pushed; got {predicates:?}"
1182 );
1183 }
1184
1185 #[test]
1189 fn token_in_is_a_planning_error() {
1190 use crate::query::select_parser::parse_select;
1191
1192 let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) IN (1, 2, 3)")
1193 .expect("token-IN query must parse");
1194 let where_clause = statement.where_clause.expect("WHERE present");
1195 let err = collect_sstable_predicates(&where_clause)
1196 .expect_err("token(pk) IN (...) must be rejected, not silently dropped");
1197 let msg = err.to_string();
1198 assert!(
1199 msg.contains("token()"),
1200 "error must explain the token() restriction; got: {msg}"
1201 );
1202 }
1203
1204 #[test]
1209 fn token_in_combined_with_other_predicate_is_a_planning_error() {
1210 use crate::query::select_parser::parse_select;
1211
1212 let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) IN (1, 2) AND ck > 0")
1213 .expect("combined token-IN query must parse");
1214 let where_clause = statement.where_clause.expect("WHERE present");
1215 assert!(
1216 collect_sstable_predicates(&where_clause).is_err(),
1217 "token(pk) IN (...) AND ck > 0 must error, not silently ignore the token restriction"
1218 );
1219 }
1220
1221 #[test]
1224 fn token_between_is_a_planning_error() {
1225 use crate::query::select_parser::parse_select;
1226
1227 let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) BETWEEN 1 AND 9")
1228 .expect("token-BETWEEN query must parse");
1229 let where_clause = statement.where_clause.expect("WHERE present");
1230 let err = collect_sstable_predicates(&where_clause)
1231 .expect_err("token(pk) BETWEEN must be rejected, not silently dropped");
1232 assert!(
1233 err.to_string().contains("token()"),
1234 "error must explain the token() restriction; got: {err}"
1235 );
1236 }
1237
1238 #[test]
1244 fn token_in_under_not_is_a_planning_error() {
1245 use crate::query::select_parser::parse_select;
1246
1247 let statement = parse_select("SELECT * FROM ks.t WHERE ck > 0 AND NOT token(pk) IN (1, 2)")
1248 .expect("token-IN-under-NOT query must parse");
1249 let where_clause = statement.where_clause.expect("WHERE present");
1250 let err = validate_token_forms_whole_tree(&where_clause, true)
1251 .expect_err("token(pk) IN under NOT must be rejected, not silently dropped");
1252 assert!(
1253 err.to_string().contains("token()"),
1254 "error must explain the token() restriction; got: {err}"
1255 );
1256 }
1257
1258 #[test]
1262 fn token_in_under_or_is_a_planning_error() {
1263 use crate::query::select_parser::parse_select;
1264
1265 let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) IN (1, 2) OR ck = 3")
1266 .expect("token-IN-under-OR query must parse");
1267 let where_clause = statement.where_clause.expect("WHERE present");
1268 assert!(
1269 validate_token_forms_whole_tree(&where_clause, true).is_err(),
1270 "token(pk) IN (...) under OR must error, not silently ignore the token restriction"
1271 );
1272 }
1273
1274 #[test]
1277 fn token_between_under_or_is_a_planning_error() {
1278 use crate::query::select_parser::parse_select;
1279
1280 let statement =
1281 parse_select("SELECT * FROM ks.t WHERE token(pk) BETWEEN 1 AND 9 OR ck = 3")
1282 .expect("token-BETWEEN-under-OR query must parse");
1283 let where_clause = statement.where_clause.expect("WHERE present");
1284 assert!(
1285 validate_token_forms_whole_tree(&where_clause, true).is_err(),
1286 "token() BETWEEN under OR must error"
1287 );
1288 }
1289
1290 #[test]
1296 fn supported_token_range_under_or_is_a_planning_error() {
1297 use crate::query::select_parser::parse_select;
1298
1299 let statement = parse_select("SELECT * FROM ks.t WHERE token(pk) > 5 OR ck = 3")
1300 .expect("token-range-under-OR query must parse");
1301 let where_clause = statement.where_clause.expect("WHERE present");
1302 let err = validate_token_forms_whole_tree(&where_clause, true).expect_err(
1303 "a supported token range under OR must error (cannot be pushed nor row-evaluated)",
1304 );
1305 let msg = err.to_string();
1306 assert!(
1307 msg.contains("token()") && msg.contains("OR/NOT"),
1308 "error must explain the OR/NOT limitation; got: {msg}"
1309 );
1310 }
1311
1312 #[test]
1315 fn supported_token_range_under_not_is_a_planning_error() {
1316 use crate::query::select_parser::parse_select;
1317
1318 let statement = parse_select("SELECT * FROM ks.t WHERE ck = 3 AND NOT token(pk) > 5")
1319 .expect("token-range-under-NOT query must parse");
1320 let where_clause = statement.where_clause.expect("WHERE present");
1321 assert!(
1322 validate_token_forms_whole_tree(&where_clause, true).is_err(),
1323 "a supported token range under NOT must error"
1324 );
1325 }
1326
1327 #[test]
1331 fn supported_token_forms_pass_whole_tree_validation() {
1332 use crate::query::select_parser::parse_select;
1333
1334 for q in [
1335 "SELECT * FROM ks.t WHERE token(pk) > 0",
1336 "SELECT * FROM ks.t WHERE token(pk) >= -100 AND token(pk) < 5000",
1337 "SELECT * FROM ks.t WHERE token(pk) = 4242",
1338 "SELECT * FROM ks.t WHERE token(pk) = 7 AND ck > 0",
1339 "SELECT * FROM ks.t WHERE (token(pk) > 0 AND ck > 1)",
1341 ] {
1342 let statement = parse_select(q).unwrap_or_else(|e| panic!("{q} must parse: {e}"));
1343 let where_clause = statement.where_clause.expect("WHERE present");
1344 validate_token_forms_whole_tree(&where_clause, true)
1345 .unwrap_or_else(|e| panic!("{q} must pass whole-tree validation: {e}"));
1346 }
1347 }
1348
1349 #[test]
1352 fn token_range_and_equality_still_plan() {
1353 use crate::query::select_parser::parse_select;
1354
1355 for q in [
1356 "SELECT * FROM ks.t WHERE token(pk) > 0",
1357 "SELECT * FROM ks.t WHERE token(pk) >= -100 AND token(pk) < 5000",
1358 "SELECT * FROM ks.t WHERE token(pk) = 4242",
1359 "SELECT * FROM ks.t WHERE token(pk) = 7 AND ck > 0",
1360 ] {
1361 let statement = parse_select(q).unwrap_or_else(|e| panic!("{q} must parse: {e}"));
1362 let where_clause = statement.where_clause.expect("WHERE present");
1363 let predicates = collect_sstable_predicates(&where_clause)
1364 .unwrap_or_else(|e| panic!("{q} must plan without error: {e}"));
1365 assert!(
1366 predicates.iter().any(|p| p.is_token()),
1367 "{q} must still push a token predicate; got {predicates:?}"
1368 );
1369 }
1370 }
1371}