1use super::super::result::QueryRow;
9use super::super::select_optimizer::SSTablePredicate;
10use super::value_ops::{compare_values_ordering, values_equal};
11use crate::{types::Value, Error, Result};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum LeafOutcome {
24 True,
26 False,
28 Unknown,
30}
31
32pub fn evaluate_leaf(row: &QueryRow, predicate: &SSTablePredicate) -> LeafOutcome {
47 use super::super::select_optimizer::SSTableFilterOp;
48
49 if predicate.is_token() {
55 if row.key.0.is_empty() {
56 return LeafOutcome::Unknown;
57 }
58 let row_token = crate::util::cassandra_murmur3::cassandra_murmur3_token(&row.key.0);
59 let Some(Value::BigInt(bound)) = predicate.values.first() else {
60 return LeafOutcome::Unknown;
61 };
62 let matches = match &predicate.operation {
63 SSTableFilterOp::Gt => row_token > *bound,
64 SSTableFilterOp::Gte => row_token >= *bound,
65 SSTableFilterOp::Lt => row_token < *bound,
66 SSTableFilterOp::Lte => row_token <= *bound,
67 SSTableFilterOp::Equal => row_token == *bound,
68 _ => return LeafOutcome::Unknown,
70 };
71 return if matches {
72 LeafOutcome::True
73 } else {
74 LeafOutcome::False
75 };
76 }
77
78 let column_value = match row.values.get(predicate.column.as_str()) {
79 None | Some(Value::Null) => return LeafOutcome::Unknown,
81 Some(v) => v,
82 };
83 let matches = match &predicate.operation {
84 SSTableFilterOp::Equal => predicate
85 .values
86 .first()
87 .is_some_and(|v| values_equal(column_value, v)),
88 SSTableFilterOp::In => predicate
92 .values
93 .iter()
94 .any(|v| values_equal(column_value, v)),
95 SSTableFilterOp::Range => {
96 if predicate.values.len() < 2 {
97 false
98 } else {
99 let lo = &predicate.values[0];
100 let hi = &predicate.values[1];
101 compare_values_ordering(column_value, lo).is_ge()
102 && compare_values_ordering(column_value, hi).is_le()
103 }
104 }
105 SSTableFilterOp::Gt => predicate
108 .values
109 .first()
110 .is_some_and(|b| compare_values_ordering(column_value, b).is_gt()),
111 SSTableFilterOp::Gte => predicate
112 .values
113 .first()
114 .is_some_and(|b| compare_values_ordering(column_value, b).is_ge()),
115 SSTableFilterOp::Lt => predicate
116 .values
117 .first()
118 .is_some_and(|b| compare_values_ordering(column_value, b).is_lt()),
119 SSTableFilterOp::Lte => predicate
120 .values
121 .first()
122 .is_some_and(|b| compare_values_ordering(column_value, b).is_le()),
123 SSTableFilterOp::Prefix => matches!(
124 (column_value, predicate.values.first()),
125 (Value::Text(s), Some(Value::Text(p))) if s.starts_with(p)
126 ),
127 SSTableFilterOp::BloomFilter => true, };
129 if matches {
130 LeafOutcome::True
131 } else {
132 LeafOutcome::False
133 }
134}
135
136pub fn evaluate_predicates(row: &QueryRow, predicates: &[SSTablePredicate]) -> Result<bool> {
148 for predicate in predicates {
149 if evaluate_leaf(row, predicate) != LeafOutcome::True {
150 return Ok(false);
151 }
152 }
153 Ok(true)
154}
155
156pub(super) fn validate_token_predicates(
172 predicates: &[SSTablePredicate],
173 schema: Option<&crate::schema::TableSchema>,
174) -> Result<()> {
175 let token_predicates: Vec<&SSTablePredicate> =
176 predicates.iter().filter(|p| p.is_token()).collect();
177 if token_predicates.is_empty() {
178 return Ok(());
179 }
180
181 let Some(schema) = schema else {
182 return Err(Error::query_execution(
183 "token() restriction requires a known table schema to validate its argument \
184 against the partition key"
185 .to_string(),
186 ));
187 };
188
189 let mut pk_cols: Vec<&crate::schema::KeyColumn> = schema.partition_keys.iter().collect();
191 pk_cols.sort_by_key(|c| c.position);
192 let expected: Vec<&str> = pk_cols.iter().map(|c| c.name.as_str()).collect();
193
194 for predicate in token_predicates {
195 let cols = predicate.token_columns.as_deref().unwrap_or(&[]);
196 let matches = cols.len() == expected.len()
197 && cols
198 .iter()
199 .zip(expected.iter())
200 .all(|(got, want)| got == want);
201 if !matches {
202 return Err(Error::query_execution(format!(
203 "token() must be applied to the entire partition key in declared order \
204 ({}); got token({})",
205 expected.join(", "),
206 cols.join(", "),
207 )));
208 }
209 }
210 Ok(())
211}
212
213#[cfg(test)]
214mod tests {
215 use super::super::super::select_optimizer::{SSTableFilterOp, SSTablePredicate};
216 use super::super::test_support::{
217 composite_pk_schema, row_with_int, row_with_key, single_pk_schema,
218 };
219 use super::*;
220 use crate::types::RowKey;
221
222 #[test]
227 fn evaluate_leaf_token_predicate_filters_by_token() {
228 let key = b"some-partition-key".to_vec();
229 let token = crate::util::cassandra_murmur3::cassandra_murmur3_token(&key);
230 let row = row_with_key(&key);
231
232 let gte = SSTablePredicate::token(
234 vec!["id".to_string()],
235 SSTableFilterOp::Gte,
236 vec![Value::BigInt(token)],
237 );
238 assert_eq!(evaluate_leaf(&row, >e), LeafOutcome::True);
239
240 let gt = SSTablePredicate::token(
241 vec!["id".to_string()],
242 SSTableFilterOp::Gt,
243 vec![Value::BigInt(token)],
244 );
245 assert_eq!(evaluate_leaf(&row, >), LeafOutcome::False);
246
247 let gte_above = SSTablePredicate::token(
249 vec!["id".to_string()],
250 SSTableFilterOp::Gte,
251 vec![Value::BigInt(token.saturating_add(1))],
252 );
253 assert_eq!(evaluate_leaf(&row, >e_above), LeafOutcome::False);
254
255 let empty = row_with_key(&[]);
257 assert_eq!(evaluate_leaf(&empty, >e), LeafOutcome::Unknown);
258 }
259
260 #[test]
263 fn inequality_predicates_apply_single_bound() {
264 let bound =
265 |op: SSTableFilterOp| SSTablePredicate::column("ck", op, vec![Value::Integer(200)]);
266 let eval = |op: SSTableFilterOp, ck: i64| {
267 evaluate_predicates(&row_with_int("ck", ck), std::slice::from_ref(&bound(op))).unwrap()
268 };
269
270 assert!(!eval(SSTableFilterOp::Gt, 200));
272 assert!(eval(SSTableFilterOp::Gt, 201));
273 assert!(eval(SSTableFilterOp::Gte, 200));
275 assert!(!eval(SSTableFilterOp::Gte, 199));
276 assert!(eval(SSTableFilterOp::Lt, 199));
278 assert!(!eval(SSTableFilterOp::Lt, 200));
279 assert!(eval(SSTableFilterOp::Lte, 200));
281 assert!(!eval(SSTableFilterOp::Lte, 201));
282 }
283
284 #[test]
288 fn evaluate_leaf_is_three_valued() {
289 let gt200 = SSTablePredicate::column("ck", SSTableFilterOp::Gt, vec![Value::Integer(200)]);
290
291 assert_eq!(
293 evaluate_leaf(&row_with_int("ck", 201), >200),
294 LeafOutcome::True
295 );
296 assert_eq!(
297 evaluate_leaf(&row_with_int("ck", 200), >200),
298 LeafOutcome::False
299 );
300
301 assert_eq!(
303 evaluate_leaf(&row_with_int("other", 999), >200),
304 LeafOutcome::Unknown
305 );
306
307 let mut values: std::collections::HashMap<std::sync::Arc<str>, Value> =
309 std::collections::HashMap::new();
310 values.insert("ck".into(), Value::Null);
311 let null_row = QueryRow {
312 values,
313 key: RowKey::new(Vec::new()),
314 metadata: Default::default(),
315 cell_metadata: None,
316 };
317 assert_eq!(evaluate_leaf(&null_row, >200), LeafOutcome::Unknown);
318 }
319
320 #[test]
324 fn evaluate_leaf_in_coerces_narrow_numeric_columns() {
325 let in_pred = SSTablePredicate::column(
327 "v",
328 SSTableFilterOp::In,
329 vec![Value::Integer(7), Value::Integer(9)],
330 );
331
332 let row_of = |val: Value| {
333 let mut values: std::collections::HashMap<std::sync::Arc<str>, Value> =
334 std::collections::HashMap::new();
335 values.insert("v".into(), val);
336 QueryRow {
337 values,
338 key: RowKey::new(Vec::new()),
339 metadata: Default::default(),
340 cell_metadata: None,
341 }
342 };
343
344 assert_eq!(
346 evaluate_leaf(&row_of(Value::TinyInt(7)), &in_pred),
347 LeafOutcome::True
348 );
349 assert_eq!(
350 evaluate_leaf(&row_of(Value::SmallInt(9)), &in_pred),
351 LeafOutcome::True
352 );
353 assert_eq!(
354 evaluate_leaf(&row_of(Value::Float32(7.0)), &in_pred),
355 LeafOutcome::True
356 );
357 assert_eq!(
359 evaluate_leaf(&row_of(Value::TinyInt(8)), &in_pred),
360 LeafOutcome::False
361 );
362 }
363
364 #[test]
368 fn two_bound_inequality_slice_selects_half_open_range() {
369 let predicates = vec![
370 SSTablePredicate::column("ck", SSTableFilterOp::Gte, vec![Value::Integer(0)]),
371 SSTablePredicate::column("ck", SSTableFilterOp::Lt, vec![Value::Integer(200)]),
372 ];
373 let in_slice = |ck: i64| evaluate_predicates(&row_with_int("ck", ck), &predicates).unwrap();
374
375 assert!(in_slice(0), "lower bound is inclusive");
376 assert!(in_slice(199), "last row in [0, 200) is included");
377 assert!(!in_slice(200), "upper bound is exclusive");
378 assert!(!in_slice(1000), "rows past the slice are excluded");
379 assert!(!in_slice(-1), "rows below the slice are excluded");
380 }
381
382 #[test]
386 fn validate_token_predicates_accepts_full_key_in_order() {
387 let schema = composite_pk_schema(("a", "int"), ("b", "int"));
388 let pred = SSTablePredicate::token(
389 vec!["a".to_string(), "b".to_string()],
390 SSTableFilterOp::Gt,
391 vec![Value::BigInt(0)],
392 );
393 assert!(
394 validate_token_predicates(std::slice::from_ref(&pred), Some(&schema)).is_ok(),
395 "token(a, b) over the full partition key in declared order must be accepted",
396 );
397
398 let single = single_pk_schema("id", "int");
400 let pred_single = SSTablePredicate::token(
401 vec!["id".to_string()],
402 SSTableFilterOp::Gte,
403 vec![Value::BigInt(0)],
404 );
405 assert!(
406 validate_token_predicates(std::slice::from_ref(&pred_single), Some(&single)).is_ok(),
407 "token(id) over a single-column partition key must be accepted",
408 );
409 }
410
411 #[test]
415 fn validate_token_predicates_rejects_non_pk_column() {
416 let schema = single_pk_schema("id", "int");
417 let pred = SSTablePredicate::token(
418 vec!["not_the_pk".to_string()],
419 SSTableFilterOp::Gt,
420 vec![Value::BigInt(0)],
421 );
422 assert!(
423 validate_token_predicates(std::slice::from_ref(&pred), Some(&schema)).is_err(),
424 "token(non_pk_col) must be rejected, not evaluated against the real pk token",
425 );
426 }
427
428 #[test]
431 fn validate_token_predicates_rejects_reordered_composite() {
432 let schema = composite_pk_schema(("a", "int"), ("b", "int"));
433 let reordered = SSTablePredicate::token(
434 vec!["b".to_string(), "a".to_string()],
435 SSTableFilterOp::Lt,
436 vec![Value::BigInt(0)],
437 );
438 assert!(
439 validate_token_predicates(std::slice::from_ref(&reordered), Some(&schema)).is_err(),
440 "token(b, a) on a (a, b) key must be rejected (wrong order)",
441 );
442
443 let subset = SSTablePredicate::token(
445 vec!["a".to_string()],
446 SSTableFilterOp::Lt,
447 vec![Value::BigInt(0)],
448 );
449 assert!(
450 validate_token_predicates(std::slice::from_ref(&subset), Some(&schema)).is_err(),
451 "token(a) on a (a, b) key must be rejected (partial key)",
452 );
453 }
454
455 #[test]
458 fn validate_token_predicates_rejects_without_schema() {
459 let pred = SSTablePredicate::token(
460 vec!["id".to_string()],
461 SSTableFilterOp::Gt,
462 vec![Value::BigInt(0)],
463 );
464 assert!(
465 validate_token_predicates(std::slice::from_ref(&pred), None).is_err(),
466 "a token predicate with no schema must be rejected",
467 );
468 let col = SSTablePredicate::column("id", SSTableFilterOp::Equal, vec![Value::Integer(1)]);
470 assert!(
471 validate_token_predicates(std::slice::from_ref(&col), None).is_ok(),
472 "non-token predicates must pass token validation even without a schema",
473 );
474 }
475}