1use crate::expressions::condition::parse_raw_path;
7use crate::expressions::tokenizer::{
8 Token, TokenSpan, TokenStream, check_redundant_parens, tokenize,
9};
10use crate::expressions::{PathElement, TrackedExpressionAttributes};
11use crate::types::AttributeValue;
12
13#[derive(Debug)]
15pub struct KeyCondition {
16 pub pk_name: String,
18 pub pk_value_ref: String,
20 pub sk_condition: Option<SortKeyCondition>,
22}
23
24#[derive(Debug)]
26pub enum SortKeyCondition {
27 Eq(String, String), Lt(String, String),
29 Le(String, String),
30 Gt(String, String),
31 Ge(String, String),
32 Between(String, String, String), BeginsWith(String, String), }
35
36pub fn parse(expr: &str, tracker: &TrackedExpressionAttributes) -> Result<KeyCondition, String> {
41 super::check_expression_size(expr)
47 .map_err(|e| format!("Invalid KeyConditionExpression: {e}"))?;
48 let tokens = tokenize(expr).map_err(|e| format!("Invalid KeyConditionExpression: {e}"))?;
49 check_redundant_parens(&tokens).map_err(|e| format!("Invalid KeyConditionExpression: {e}"))?;
52 let tokens = strip_outer_parens(tokens);
53 let mut stream = TokenStream::new(tokens);
54
55 let cond1 = parse_single_condition(&mut stream, tracker)?;
56
57 let (pk_cond, sk_cond) = if matches!(stream.peek(), Some(Token::And)) {
58 stream.next();
59 let cond2 = parse_single_condition(&mut stream, tracker)?;
60 match (cond1, cond2) {
61 (ParsedCond::Eq(n1, v1), c2) => ((n1, v1), Some(c2)),
62 (c1, ParsedCond::Eq(n2, v2)) => ((n2, v2), Some(c1)),
63 _ => {
64 return Err(
65 "Invalid KeyConditionExpression: partition key must use equality".to_string(),
66 );
67 }
68 }
69 } else {
70 match cond1 {
71 ParsedCond::Eq(name, val_ref) => ((name, val_ref), None),
72 _ => {
73 return Err(
74 "Invalid KeyConditionExpression: partition key must use equality".to_string(),
75 );
76 }
77 }
78 };
79
80 if !stream.at_end() {
81 return Err(format!(
82 "Unexpected token in KeyConditionExpression: {}",
83 stream.peek().unwrap()
84 ));
85 }
86
87 let (pk_name, pk_value_ref) = pk_cond;
88 let sk_condition = sk_cond.map(|c| c.into_sk_condition()).transpose()?;
89
90 Ok(KeyCondition {
91 pk_name,
92 pk_value_ref,
93 sk_condition,
94 })
95}
96
97pub fn resolve_values(
99 condition: &KeyCondition,
100 tracker: &TrackedExpressionAttributes,
101) -> Result<ResolvedKeyCondition, String> {
102 let pk_val = tracker.resolve_value(&condition.pk_value_ref)?.clone();
103
104 let sk = if let Some(ref sk_cond) = condition.sk_condition {
105 Some(resolve_sk_condition(sk_cond, tracker)?)
106 } else {
107 None
108 };
109
110 Ok(ResolvedKeyCondition {
111 pk_name: condition.pk_name.clone(),
112 pk_value: pk_val,
113 sk_condition: sk,
114 })
115}
116
117#[derive(Debug)]
119pub struct ResolvedKeyCondition {
120 pub pk_name: String,
121 pub pk_value: AttributeValue,
122 pub sk_condition: Option<ResolvedSortKeyCondition>,
123}
124
125#[derive(Debug)]
126pub enum ResolvedSortKeyCondition {
127 Eq(String, AttributeValue),
128 Lt(String, AttributeValue),
129 Le(String, AttributeValue),
130 Gt(String, AttributeValue),
131 Ge(String, AttributeValue),
132 Between(String, AttributeValue, AttributeValue),
133 BeginsWith(String, AttributeValue),
134}
135
136impl ResolvedSortKeyCondition {
137 pub fn sk_name(&self) -> &str {
138 match self {
139 Self::Eq(n, _)
140 | Self::Lt(n, _)
141 | Self::Le(n, _)
142 | Self::Gt(n, _)
143 | Self::Ge(n, _)
144 | Self::Between(n, _, _)
145 | Self::BeginsWith(n, _) => n,
146 }
147 }
148
149 pub fn to_sql_conditions(&self) -> Vec<(String, String)> {
153 match self {
154 Self::Eq(_, v) => vec![("=".into(), val_to_key_string(v))],
155 Self::Lt(_, v) => vec![("<".into(), val_to_key_string(v))],
156 Self::Le(_, v) => vec![("<=".into(), val_to_key_string(v))],
157 Self::Gt(_, v) => vec![(">".into(), val_to_key_string(v))],
158 Self::Ge(_, v) => vec![(">=".into(), val_to_key_string(v))],
159 Self::Between(_, lo, hi) => vec![
160 (">=".into(), val_to_key_string(lo)),
161 ("<=".into(), val_to_key_string(hi)),
162 ],
163 Self::BeginsWith(_, prefix) => {
164 let prefix_str = val_to_key_string(prefix);
165 let escaped = prefix_str
167 .replace('\\', "\\\\")
168 .replace('%', "\\%")
169 .replace('_', "\\_");
170 vec![("LIKE".into(), format!("{escaped}%"))]
171 }
172 }
173 }
174}
175
176pub fn sk_conditions_to_sql(
181 conditions: &[ResolvedSortKeyCondition],
182) -> (Option<String>, Vec<String>) {
183 let mut sql_parts = Vec::new();
184 let mut param_values = Vec::new();
185 for cond in conditions {
186 for (op, val) in cond.to_sql_conditions() {
187 let param_idx = param_values.len() + 2;
188 if op == "LIKE" {
189 sql_parts.push(format!("AND sk LIKE ?{param_idx} ESCAPE '\\'"));
190 } else {
191 sql_parts.push(format!("AND sk {op} ?{param_idx}"));
192 }
193 param_values.push(val);
194 }
195 }
196 let sql = if sql_parts.is_empty() {
197 None
198 } else {
199 Some(sql_parts.join(" "))
200 };
201 (sql, param_values)
202}
203
204fn val_to_key_string(val: &AttributeValue) -> String {
205 val.to_key_string().unwrap_or_default()
206}
207
208fn resolve_sk_condition(
209 cond: &SortKeyCondition,
210 tracker: &TrackedExpressionAttributes,
211) -> Result<ResolvedSortKeyCondition, String> {
212 match cond {
213 SortKeyCondition::Eq(sk, vr) => {
214 let v = tracker.resolve_value(vr)?.clone();
215 Ok(ResolvedSortKeyCondition::Eq(sk.clone(), v))
216 }
217 SortKeyCondition::Lt(sk, vr) => {
218 let v = tracker.resolve_value(vr)?.clone();
219 Ok(ResolvedSortKeyCondition::Lt(sk.clone(), v))
220 }
221 SortKeyCondition::Le(sk, vr) => {
222 let v = tracker.resolve_value(vr)?.clone();
223 Ok(ResolvedSortKeyCondition::Le(sk.clone(), v))
224 }
225 SortKeyCondition::Gt(sk, vr) => {
226 let v = tracker.resolve_value(vr)?.clone();
227 Ok(ResolvedSortKeyCondition::Gt(sk.clone(), v))
228 }
229 SortKeyCondition::Ge(sk, vr) => {
230 let v = tracker.resolve_value(vr)?.clone();
231 Ok(ResolvedSortKeyCondition::Ge(sk.clone(), v))
232 }
233 SortKeyCondition::Between(sk, lo_ref, hi_ref) => {
234 let lo = tracker.resolve_value(lo_ref)?.clone();
235 let hi = tracker.resolve_value(hi_ref)?.clone();
236 if std::mem::discriminant(&lo) != std::mem::discriminant(&hi) {
238 return Err(format!(
239 "Invalid KeyConditionExpression: The BETWEEN operator requires same data type \
240 for lower and upper bounds; lower bound operand: AttributeValue: {{{}}}, \
241 upper bound operand: AttributeValue: {{{}}}",
242 format_attr_value_short(&lo),
243 format_attr_value_short(&hi)
244 ));
245 }
246 if !between_order_valid(&lo, &hi) {
248 return Err(format!(
249 "Invalid KeyConditionExpression: The BETWEEN operator requires upper bound \
250 to be greater than or equal to lower bound; lower bound operand: \
251 AttributeValue: {{{}}}, upper bound operand: AttributeValue: {{{}}}",
252 format_attr_value_short(&lo),
253 format_attr_value_short(&hi)
254 ));
255 }
256 Ok(ResolvedSortKeyCondition::Between(sk.clone(), lo, hi))
257 }
258 SortKeyCondition::BeginsWith(sk, vr) => {
259 let v = tracker.resolve_value(vr)?.clone();
260 Ok(ResolvedSortKeyCondition::BeginsWith(sk.clone(), v))
261 }
262 }
263}
264
265#[derive(Debug)]
270enum ParsedCond {
271 Eq(String, String), Lt(String, String),
273 Le(String, String),
274 Gt(String, String),
275 Ge(String, String),
276 Between(String, String, String), BeginsWith(String, String), }
279
280impl ParsedCond {
281 fn into_sk_condition(self) -> Result<SortKeyCondition, String> {
282 match self {
283 ParsedCond::Eq(n, v) => Ok(SortKeyCondition::Eq(n, v)),
284 ParsedCond::Lt(n, v) => Ok(SortKeyCondition::Lt(n, v)),
285 ParsedCond::Le(n, v) => Ok(SortKeyCondition::Le(n, v)),
286 ParsedCond::Gt(n, v) => Ok(SortKeyCondition::Gt(n, v)),
287 ParsedCond::Ge(n, v) => Ok(SortKeyCondition::Ge(n, v)),
288 ParsedCond::Between(n, lo, hi) => Ok(SortKeyCondition::Between(n, lo, hi)),
289 ParsedCond::BeginsWith(n, v) => Ok(SortKeyCondition::BeginsWith(n, v)),
290 }
291 }
292}
293
294fn strip_outer_parens(mut tokens: Vec<(Token, TokenSpan)>) -> Vec<(Token, TokenSpan)> {
299 loop {
300 if tokens.len() < 2 {
301 break;
302 }
303 if !matches!(tokens.first().map(|(t, _)| t), Some(Token::LParen)) {
304 break;
305 }
306 let mut depth = 0;
309 let mut close_pos = None;
310 for (i, (tok, _)) in tokens.iter().enumerate() {
311 match tok {
312 Token::LParen => depth += 1,
313 Token::RParen => {
314 depth -= 1;
315 if depth == 0 {
316 close_pos = Some(i);
317 break;
318 }
319 }
320 _ => {}
321 }
322 }
323 if close_pos == Some(tokens.len() - 1) {
324 tokens.remove(tokens.len() - 1);
326 tokens.remove(0);
327 } else {
328 break;
329 }
330 }
331 tokens
332}
333
334fn parse_single_condition(
335 stream: &mut TokenStream,
336 tracker: &TrackedExpressionAttributes,
337) -> Result<ParsedCond, String> {
338 let mut parens = 0;
340 while matches!(stream.peek(), Some(Token::LParen)) {
341 stream.next();
342 parens += 1;
343 }
344
345 if let Some(Token::Identifier(name)) = stream.peek() {
347 if name.to_lowercase() == "begins_with" {
348 stream.next();
349 stream.expect(&Token::LParen)?;
350 let path = parse_raw_path(stream)?;
351 let attr_name = resolve_path_to_name(&path, tracker)?;
352 stream.expect(&Token::Comma)?;
353 let val_ref = expect_value_ref(stream)?;
354 stream.expect(&Token::RParen)?;
355 consume_close_parens(stream, parens)?;
356 return Ok(ParsedCond::BeginsWith(attr_name, val_ref));
357 }
358 }
359
360 if matches!(stream.peek(), Some(Token::ValueRef(_))) {
364 let val_ref = expect_value_ref(stream)?;
365 let build: Option<fn(String, String) -> ParsedCond> = match stream.next() {
367 Some(Token::Eq) => Some(ParsedCond::Eq),
368 Some(Token::Lt) => Some(ParsedCond::Gt),
369 Some(Token::Le) => Some(ParsedCond::Ge),
370 Some(Token::Gt) => Some(ParsedCond::Lt),
371 Some(Token::Ge) => Some(ParsedCond::Le),
372 _ => None,
373 };
374 let build = build.ok_or_else(|| {
375 "Invalid KeyConditionExpression: unsupported operator with a value on the left"
376 .to_string()
377 })?;
378 let path = parse_raw_path(stream)?;
379 let attr_name = resolve_path_to_name(&path, tracker)?;
380 consume_close_parens(stream, parens)?;
381 return Ok(build(attr_name, val_ref));
382 }
383
384 let path = parse_raw_path(stream)?;
386 let attr_name = resolve_path_to_name(&path, tracker)?;
387
388 let result = match stream.next() {
389 Some(Token::Eq) => {
390 let val_ref = expect_value_ref(stream)?;
391 Ok(ParsedCond::Eq(attr_name, val_ref))
392 }
393 Some(Token::Lt) => {
394 let val_ref = expect_value_ref(stream)?;
395 Ok(ParsedCond::Lt(attr_name, val_ref))
396 }
397 Some(Token::Le) => {
398 let val_ref = expect_value_ref(stream)?;
399 Ok(ParsedCond::Le(attr_name, val_ref))
400 }
401 Some(Token::Gt) => {
402 let val_ref = expect_value_ref(stream)?;
403 Ok(ParsedCond::Gt(attr_name, val_ref))
404 }
405 Some(Token::Ge) => {
406 let val_ref = expect_value_ref(stream)?;
407 Ok(ParsedCond::Ge(attr_name, val_ref))
408 }
409 Some(Token::Between) => {
410 let lo_ref = expect_value_ref(stream)?;
411 stream.expect(&Token::And)?;
412 let hi_ref = expect_value_ref(stream)?;
413 Ok(ParsedCond::Between(attr_name, lo_ref, hi_ref))
414 }
415 Some(t) => Err(format!(
416 "Unexpected operator in KeyConditionExpression: {t}"
417 )),
418 None => Err("Unexpected end of KeyConditionExpression".to_string()),
419 };
420
421 consume_close_parens(stream, parens)?;
422 result
423}
424
425fn consume_close_parens(stream: &mut TokenStream, count: usize) -> Result<(), String> {
427 for _ in 0..count {
428 match stream.next() {
429 Some(Token::RParen) => {}
430 Some(t) => {
431 return Err(format!(
432 "Expected closing parenthesis in KeyConditionExpression, got {t}"
433 ));
434 }
435 None => {
436 return Err(
437 "Unexpected end of KeyConditionExpression, expected closing parenthesis"
438 .to_string(),
439 );
440 }
441 }
442 }
443 Ok(())
444}
445
446fn resolve_path_to_name(
447 path: &[PathElement],
448 tracker: &TrackedExpressionAttributes,
449) -> Result<String, String> {
450 if path.len() != 1 {
451 return Err(
453 "Invalid KeyConditionExpression: KeyConditionExpressions cannot have conditions on nested attributes"
454 .to_string(),
455 );
456 }
457 match &path[0] {
458 PathElement::Attribute(name) => {
459 if name.starts_with('#') {
460 tracker.resolve_name(name)
461 } else {
462 Ok(name.clone())
463 }
464 }
465 PathElement::Index(_) => Err("KeyConditionExpression cannot use index paths".to_string()),
466 }
467}
468
469fn format_attr_value_short(val: &AttributeValue) -> String {
471 match val {
472 AttributeValue::S(s) => format!("S:{s}"),
473 AttributeValue::N(n) => format!("N:{n}"),
474 AttributeValue::B(b) => {
475 use base64::Engine;
476 let encoded = base64::engine::general_purpose::STANDARD.encode(b);
477 format!("B:{encoded}")
478 }
479 AttributeValue::BOOL(b) => format!("BOOL:{b}"),
480 AttributeValue::NULL(_) => "NULL:true".to_string(),
481 AttributeValue::SS(set) => format!("SS:{:?}", set),
482 AttributeValue::NS(set) => format!("NS:{:?}", set),
483 AttributeValue::BS(_) => "BS:[...]".to_string(),
484 AttributeValue::L(_) => "L:[...]".to_string(),
485 AttributeValue::M(_) => "M:{...}".to_string(),
486 }
487}
488
489fn between_order_valid(lo: &AttributeValue, hi: &AttributeValue) -> bool {
491 match (lo, hi) {
492 (AttributeValue::S(a), AttributeValue::S(b)) => a <= b,
493 (AttributeValue::N(a), AttributeValue::N(b)) => {
494 let a_f = a.parse::<f64>().unwrap_or(0.0);
495 let b_f = b.parse::<f64>().unwrap_or(0.0);
496 a_f <= b_f
497 }
498 (AttributeValue::B(a), AttributeValue::B(b)) => a <= b,
499 _ => true,
500 }
501}
502
503fn expect_value_ref(stream: &mut TokenStream) -> Result<String, String> {
504 match stream.next() {
505 Some(Token::ValueRef(name)) => Ok(name.clone()),
506 Some(t) => Err(format!("Expected value reference (:name), got {t}")),
507 None => Err("Expected value reference, got end of expression".to_string()),
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use std::collections::HashMap;
515
516 fn make_tracker<'a>(
517 names: &'a Option<HashMap<String, String>>,
518 values: &'a Option<HashMap<String, AttributeValue>>,
519 ) -> TrackedExpressionAttributes<'a> {
520 TrackedExpressionAttributes::new(names, values)
521 }
522
523 #[test]
524 fn accepts_reversed_operand_sort_key() {
525 let names = Some(HashMap::from([
526 ("#pk".to_string(), "pk".to_string()),
527 ("#sk".to_string(), "sk".to_string()),
528 ]));
529 let values = Some(HashMap::from([
530 (":pk".to_string(), AttributeValue::S("x".into())),
531 (":lo".to_string(), AttributeValue::S("a".into())),
532 ]));
533 let tracker = make_tracker(&names, &values);
534 for (op, expected) in [("<", "Gt"), ("<=", "Ge"), (">", "Lt"), (">=", "Le")] {
536 let expr = format!("#pk = :pk AND :lo {op} #sk");
537 let kc = parse(&expr, &tracker).unwrap();
538 let tag = match &kc.sk_condition {
539 Some(SortKeyCondition::Gt(n, v)) if n == "sk" && v == ":lo" => "Gt",
540 Some(SortKeyCondition::Ge(n, v)) if n == "sk" && v == ":lo" => "Ge",
541 Some(SortKeyCondition::Lt(n, v)) if n == "sk" && v == ":lo" => "Lt",
542 Some(SortKeyCondition::Le(n, v)) if n == "sk" && v == ":lo" => "Le",
543 _ => "other",
544 };
545 assert_eq!(tag, expected, "reversed {op} should flip to {expected}");
546 }
547 }
548
549 #[test]
550 fn accepts_reversed_equality_on_sort_key() {
551 let names = Some(HashMap::from([
552 ("#pk".to_string(), "pk".to_string()),
553 ("#sk".to_string(), "sk".to_string()),
554 ]));
555 let values = Some(HashMap::from([
556 (":pk".to_string(), AttributeValue::S("x".into())),
557 (":sk".to_string(), AttributeValue::S("m".into())),
558 ]));
559 let tracker = make_tracker(&names, &values);
560 let kc = parse("#pk = :pk AND :sk = #sk", &tracker).unwrap();
561 assert!(
562 matches!(&kc.sk_condition, Some(SortKeyCondition::Eq(n, v)) if n == "sk" && v == ":sk")
563 );
564 }
565
566 #[test]
567 fn rejects_reversed_operand_with_nested_key_path() {
568 let names = Some(HashMap::from([
569 ("#pk".to_string(), "pk".to_string()),
570 ("#sk".to_string(), "sk".to_string()),
571 ]));
572 let values = Some(HashMap::from([
573 (":pk".to_string(), AttributeValue::S("x".into())),
574 (":lo".to_string(), AttributeValue::S("a".into())),
575 ]));
576 let tracker = make_tracker(&names, &values);
577 let err = parse("#pk = :pk AND :lo <= #sk.foo", &tracker).unwrap_err();
578 assert_eq!(
579 err,
580 "Invalid KeyConditionExpression: KeyConditionExpressions cannot have conditions on nested attributes"
581 );
582 }
583
584 #[test]
585 fn rejects_nested_path_on_key() {
586 let names = Some(HashMap::from([
587 ("#pk".to_string(), "pk".to_string()),
588 ("#sk".to_string(), "sk".to_string()),
589 ]));
590 let values = Some(HashMap::from([
591 (":pk".to_string(), AttributeValue::S("x".into())),
592 (":v".to_string(), AttributeValue::S("y".into())),
593 ]));
594 let tracker = make_tracker(&names, &values);
595 let err = parse("#pk = :pk AND #sk.foo = :v", &tracker).unwrap_err();
596 assert_eq!(
597 err,
598 "Invalid KeyConditionExpression: KeyConditionExpressions cannot have conditions on nested attributes"
599 );
600 }
601
602 #[test]
603 fn test_pk_only() {
604 let no_names = None;
605 let no_values = None;
606 let tracker = make_tracker(&no_names, &no_values);
607 let kc = parse("pk = :pk", &tracker).unwrap();
608 assert_eq!(kc.pk_name, "pk");
609 assert_eq!(kc.pk_value_ref, ":pk");
610 assert!(kc.sk_condition.is_none());
611 }
612
613 #[test]
614 fn test_pk_and_sk_eq() {
615 let no_names = None;
616 let no_values = None;
617 let tracker = make_tracker(&no_names, &no_values);
618 let kc = parse("pk = :pk AND sk = :sk", &tracker).unwrap();
619 assert_eq!(kc.pk_name, "pk");
620 assert!(matches!(kc.sk_condition, Some(SortKeyCondition::Eq(_, _))));
621 }
622
623 #[test]
624 fn test_pk_and_sk_between() {
625 let no_names = None;
626 let no_values = None;
627 let tracker = make_tracker(&no_names, &no_values);
628 let kc = parse("pk = :pk AND sk BETWEEN :lo AND :hi", &tracker).unwrap();
629 assert!(matches!(
630 kc.sk_condition,
631 Some(SortKeyCondition::Between(_, _, _))
632 ));
633 }
634
635 #[test]
636 fn test_pk_and_begins_with() {
637 let no_names = None;
638 let no_values = None;
639 let tracker = make_tracker(&no_names, &no_values);
640 let kc = parse("pk = :pk AND begins_with(sk, :prefix)", &tracker).unwrap();
641 assert!(matches!(
642 kc.sk_condition,
643 Some(SortKeyCondition::BeginsWith(_, _))
644 ));
645 }
646
647 #[test]
648 fn test_with_attribute_names() {
649 let an = Some(HashMap::from([
650 ("#pk".to_string(), "partitionKey".to_string()),
651 ("#sk".to_string(), "sortKey".to_string()),
652 ]));
653 let no_values = None;
654 let tracker = make_tracker(&an, &no_values);
655 let kc = parse("#pk = :pk AND #sk > :sk", &tracker).unwrap();
656 assert_eq!(kc.pk_name, "partitionKey");
657 assert!(matches!(kc.sk_condition, Some(SortKeyCondition::Gt(ref n, _)) if n == "sortKey"));
658 }
659
660 #[test]
661 fn test_resolve_values() {
662 let no_names = None;
663 let no_values = None;
664 let parse_tracker = make_tracker(&no_names, &no_values);
665 let kc = parse("pk = :pk AND sk >= :sk", &parse_tracker).unwrap();
666 let av = Some(HashMap::from([
667 (":pk".to_string(), AttributeValue::S("user#1".into())),
668 (":sk".to_string(), AttributeValue::S("2024-01-01".into())),
669 ]));
670 let resolve_tracker = make_tracker(&no_names, &av);
671 let resolved = resolve_values(&kc, &resolve_tracker).unwrap();
672 assert_eq!(resolved.pk_value, AttributeValue::S("user#1".into()));
673 assert!(matches!(
674 resolved.sk_condition,
675 Some(ResolvedSortKeyCondition::Ge(_, _))
676 ));
677 }
678
679 #[test]
680 fn test_parenthesized_conditions() {
681 let no_names = None;
682 let no_values = None;
683
684 let tracker = make_tracker(&no_names, &no_values);
686 let kc = parse("(pk = :pk) AND (sk = :sk)", &tracker).unwrap();
687 assert_eq!(kc.pk_name, "pk");
688 assert!(matches!(kc.sk_condition, Some(SortKeyCondition::Eq(_, _))));
689
690 let tracker = make_tracker(&no_names, &no_values);
692 let kc = parse("(pk = :pk AND sk = :sk)", &tracker).unwrap();
693 assert_eq!(kc.pk_name, "pk");
694 assert!(matches!(kc.sk_condition, Some(SortKeyCondition::Eq(_, _))));
695
696 let tracker = make_tracker(&no_names, &no_values);
698 let kc = parse("(pk = :pk AND (sk > :sk))", &tracker).unwrap();
699 assert_eq!(kc.pk_name, "pk");
700 assert!(matches!(kc.sk_condition, Some(SortKeyCondition::Gt(_, _))));
701
702 let tracker = make_tracker(&no_names, &no_values);
705 let err = parse("((pk = :pk)) AND ((sk > :sk))", &tracker).unwrap_err();
706 assert!(
707 err.contains("redundant parentheses"),
708 "expected redundant-parentheses rejection, got: {err}"
709 );
710
711 let tracker = make_tracker(&no_names, &no_values);
713 let kc = parse("(pk = :pk) AND (begins_with(sk, :prefix))", &tracker).unwrap();
714 assert!(matches!(
715 kc.sk_condition,
716 Some(SortKeyCondition::BeginsWith(_, _))
717 ));
718
719 let an = Some(HashMap::from([
721 ("#pk".to_string(), "PK".to_string()),
722 ("#sk".to_string(), "SK".to_string()),
723 ]));
724 let tracker = make_tracker(&an, &no_values);
725 let kc = parse("(#pk = :pk) AND (#sk = :sk)", &tracker).unwrap();
726 assert_eq!(kc.pk_name, "PK");
727 }
728
729 #[test]
730 fn test_sk_comparisons() {
731 let no_names = None;
732 let no_values = None;
733 for (op, variant) in [("<", "Lt"), ("<=", "Le"), (">", "Gt"), (">=", "Ge")] {
734 let tracker = make_tracker(&no_names, &no_values);
735 let kc = parse(&format!("pk = :pk AND sk {op} :sk"), &tracker).unwrap();
736 let sk = kc.sk_condition.unwrap();
737 let name = match &sk {
738 SortKeyCondition::Lt(n, _) => format!("Lt:{n}"),
739 SortKeyCondition::Le(n, _) => format!("Le:{n}"),
740 SortKeyCondition::Gt(n, _) => format!("Gt:{n}"),
741 SortKeyCondition::Ge(n, _) => format!("Ge:{n}"),
742 _ => "other".to_string(),
743 };
744 assert!(name.starts_with(variant), "Expected {variant}, got {name}");
745 }
746 }
747}