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
176fn val_to_key_string(val: &AttributeValue) -> String {
177 val.to_key_string().unwrap_or_default()
178}
179
180fn resolve_sk_condition(
181 cond: &SortKeyCondition,
182 tracker: &TrackedExpressionAttributes,
183) -> Result<ResolvedSortKeyCondition, String> {
184 match cond {
185 SortKeyCondition::Eq(sk, vr) => {
186 let v = tracker.resolve_value(vr)?.clone();
187 Ok(ResolvedSortKeyCondition::Eq(sk.clone(), v))
188 }
189 SortKeyCondition::Lt(sk, vr) => {
190 let v = tracker.resolve_value(vr)?.clone();
191 Ok(ResolvedSortKeyCondition::Lt(sk.clone(), v))
192 }
193 SortKeyCondition::Le(sk, vr) => {
194 let v = tracker.resolve_value(vr)?.clone();
195 Ok(ResolvedSortKeyCondition::Le(sk.clone(), v))
196 }
197 SortKeyCondition::Gt(sk, vr) => {
198 let v = tracker.resolve_value(vr)?.clone();
199 Ok(ResolvedSortKeyCondition::Gt(sk.clone(), v))
200 }
201 SortKeyCondition::Ge(sk, vr) => {
202 let v = tracker.resolve_value(vr)?.clone();
203 Ok(ResolvedSortKeyCondition::Ge(sk.clone(), v))
204 }
205 SortKeyCondition::Between(sk, lo_ref, hi_ref) => {
206 let lo = tracker.resolve_value(lo_ref)?.clone();
207 let hi = tracker.resolve_value(hi_ref)?.clone();
208 if std::mem::discriminant(&lo) != std::mem::discriminant(&hi) {
210 return Err(format!(
211 "Invalid KeyConditionExpression: The BETWEEN operator requires same data type \
212 for lower and upper bounds; lower bound operand: AttributeValue: {{{}}}, \
213 upper bound operand: AttributeValue: {{{}}}",
214 format_attr_value_short(&lo),
215 format_attr_value_short(&hi)
216 ));
217 }
218 if !between_order_valid(&lo, &hi) {
220 return Err(format!(
221 "Invalid KeyConditionExpression: The BETWEEN operator requires upper bound \
222 to be greater than or equal to lower bound; lower bound operand: \
223 AttributeValue: {{{}}}, upper bound operand: AttributeValue: {{{}}}",
224 format_attr_value_short(&lo),
225 format_attr_value_short(&hi)
226 ));
227 }
228 Ok(ResolvedSortKeyCondition::Between(sk.clone(), lo, hi))
229 }
230 SortKeyCondition::BeginsWith(sk, vr) => {
231 let v = tracker.resolve_value(vr)?.clone();
232 Ok(ResolvedSortKeyCondition::BeginsWith(sk.clone(), v))
233 }
234 }
235}
236
237#[derive(Debug)]
242enum ParsedCond {
243 Eq(String, String), Lt(String, String),
245 Le(String, String),
246 Gt(String, String),
247 Ge(String, String),
248 Between(String, String, String), BeginsWith(String, String), }
251
252impl ParsedCond {
253 fn into_sk_condition(self) -> Result<SortKeyCondition, String> {
254 match self {
255 ParsedCond::Eq(n, v) => Ok(SortKeyCondition::Eq(n, v)),
256 ParsedCond::Lt(n, v) => Ok(SortKeyCondition::Lt(n, v)),
257 ParsedCond::Le(n, v) => Ok(SortKeyCondition::Le(n, v)),
258 ParsedCond::Gt(n, v) => Ok(SortKeyCondition::Gt(n, v)),
259 ParsedCond::Ge(n, v) => Ok(SortKeyCondition::Ge(n, v)),
260 ParsedCond::Between(n, lo, hi) => Ok(SortKeyCondition::Between(n, lo, hi)),
261 ParsedCond::BeginsWith(n, v) => Ok(SortKeyCondition::BeginsWith(n, v)),
262 }
263 }
264}
265
266fn strip_outer_parens(mut tokens: Vec<(Token, TokenSpan)>) -> Vec<(Token, TokenSpan)> {
271 loop {
272 if tokens.len() < 2 {
273 break;
274 }
275 if !matches!(tokens.first().map(|(t, _)| t), Some(Token::LParen)) {
276 break;
277 }
278 let mut depth = 0;
281 let mut close_pos = None;
282 for (i, (tok, _)) in tokens.iter().enumerate() {
283 match tok {
284 Token::LParen => depth += 1,
285 Token::RParen => {
286 depth -= 1;
287 if depth == 0 {
288 close_pos = Some(i);
289 break;
290 }
291 }
292 _ => {}
293 }
294 }
295 if close_pos == Some(tokens.len() - 1) {
296 tokens.remove(tokens.len() - 1);
298 tokens.remove(0);
299 } else {
300 break;
301 }
302 }
303 tokens
304}
305
306fn parse_single_condition(
307 stream: &mut TokenStream,
308 tracker: &TrackedExpressionAttributes,
309) -> Result<ParsedCond, String> {
310 let mut parens = 0;
312 while matches!(stream.peek(), Some(Token::LParen)) {
313 stream.next();
314 parens += 1;
315 }
316
317 if let Some(Token::Identifier(name)) = stream.peek() {
319 if name.to_lowercase() == "begins_with" {
320 stream.next();
321 stream.expect(&Token::LParen)?;
322 let path = parse_raw_path(stream)?;
323 let attr_name = resolve_path_to_name(&path, tracker)?;
324 stream.expect(&Token::Comma)?;
325 let val_ref = expect_value_ref(stream)?;
326 stream.expect(&Token::RParen)?;
327 consume_close_parens(stream, parens)?;
328 return Ok(ParsedCond::BeginsWith(attr_name, val_ref));
329 }
330 }
331
332 if matches!(stream.peek(), Some(Token::ValueRef(_))) {
336 let val_ref = expect_value_ref(stream)?;
337 let build: Option<fn(String, String) -> ParsedCond> = match stream.next() {
339 Some(Token::Eq) => Some(ParsedCond::Eq),
340 Some(Token::Lt) => Some(ParsedCond::Gt),
341 Some(Token::Le) => Some(ParsedCond::Ge),
342 Some(Token::Gt) => Some(ParsedCond::Lt),
343 Some(Token::Ge) => Some(ParsedCond::Le),
344 _ => None,
345 };
346 let build = build.ok_or_else(|| {
347 "Invalid KeyConditionExpression: unsupported operator with a value on the left"
348 .to_string()
349 })?;
350 let path = parse_raw_path(stream)?;
351 let attr_name = resolve_path_to_name(&path, tracker)?;
352 consume_close_parens(stream, parens)?;
353 return Ok(build(attr_name, val_ref));
354 }
355
356 let path = parse_raw_path(stream)?;
358 let attr_name = resolve_path_to_name(&path, tracker)?;
359
360 let result = match stream.next() {
361 Some(Token::Eq) => {
362 let val_ref = expect_value_ref(stream)?;
363 Ok(ParsedCond::Eq(attr_name, val_ref))
364 }
365 Some(Token::Lt) => {
366 let val_ref = expect_value_ref(stream)?;
367 Ok(ParsedCond::Lt(attr_name, val_ref))
368 }
369 Some(Token::Le) => {
370 let val_ref = expect_value_ref(stream)?;
371 Ok(ParsedCond::Le(attr_name, val_ref))
372 }
373 Some(Token::Gt) => {
374 let val_ref = expect_value_ref(stream)?;
375 Ok(ParsedCond::Gt(attr_name, val_ref))
376 }
377 Some(Token::Ge) => {
378 let val_ref = expect_value_ref(stream)?;
379 Ok(ParsedCond::Ge(attr_name, val_ref))
380 }
381 Some(Token::Between) => {
382 let lo_ref = expect_value_ref(stream)?;
383 stream.expect(&Token::And)?;
384 let hi_ref = expect_value_ref(stream)?;
385 Ok(ParsedCond::Between(attr_name, lo_ref, hi_ref))
386 }
387 Some(t) => Err(format!(
388 "Unexpected operator in KeyConditionExpression: {t}"
389 )),
390 None => Err("Unexpected end of KeyConditionExpression".to_string()),
391 };
392
393 consume_close_parens(stream, parens)?;
394 result
395}
396
397fn consume_close_parens(stream: &mut TokenStream, count: usize) -> Result<(), String> {
399 for _ in 0..count {
400 match stream.next() {
401 Some(Token::RParen) => {}
402 Some(t) => {
403 return Err(format!(
404 "Expected closing parenthesis in KeyConditionExpression, got {t}"
405 ));
406 }
407 None => {
408 return Err(
409 "Unexpected end of KeyConditionExpression, expected closing parenthesis"
410 .to_string(),
411 );
412 }
413 }
414 }
415 Ok(())
416}
417
418fn resolve_path_to_name(
419 path: &[PathElement],
420 tracker: &TrackedExpressionAttributes,
421) -> Result<String, String> {
422 if path.len() != 1 {
423 return Err(
425 "Invalid KeyConditionExpression: KeyConditionExpressions cannot have conditions on nested attributes"
426 .to_string(),
427 );
428 }
429 match &path[0] {
430 PathElement::Attribute(name) => {
431 if name.starts_with('#') {
432 tracker.resolve_name(name)
433 } else {
434 Ok(name.clone())
435 }
436 }
437 PathElement::Index(_) => Err("KeyConditionExpression cannot use index paths".to_string()),
438 }
439}
440
441fn format_attr_value_short(val: &AttributeValue) -> String {
443 match val {
444 AttributeValue::S(s) => format!("S:{s}"),
445 AttributeValue::N(n) => format!("N:{n}"),
446 AttributeValue::B(b) => {
447 use base64::Engine;
448 let encoded = base64::engine::general_purpose::STANDARD.encode(b);
449 format!("B:{encoded}")
450 }
451 AttributeValue::BOOL(b) => format!("BOOL:{b}"),
452 AttributeValue::NULL(_) => "NULL:true".to_string(),
453 AttributeValue::SS(set) => format!("SS:{:?}", set),
454 AttributeValue::NS(set) => format!("NS:{:?}", set),
455 AttributeValue::BS(_) => "BS:[...]".to_string(),
456 AttributeValue::L(_) => "L:[...]".to_string(),
457 AttributeValue::M(_) => "M:{...}".to_string(),
458 }
459}
460
461fn between_order_valid(lo: &AttributeValue, hi: &AttributeValue) -> bool {
463 match (lo, hi) {
464 (AttributeValue::S(a), AttributeValue::S(b)) => a <= b,
465 (AttributeValue::N(a), AttributeValue::N(b)) => {
466 let a_f = a.parse::<f64>().unwrap_or(0.0);
467 let b_f = b.parse::<f64>().unwrap_or(0.0);
468 a_f <= b_f
469 }
470 (AttributeValue::B(a), AttributeValue::B(b)) => a <= b,
471 _ => true,
472 }
473}
474
475fn expect_value_ref(stream: &mut TokenStream) -> Result<String, String> {
476 match stream.next() {
477 Some(Token::ValueRef(name)) => Ok(name.clone()),
478 Some(t) => Err(format!("Expected value reference (:name), got {t}")),
479 None => Err("Expected value reference, got end of expression".to_string()),
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 use std::collections::HashMap;
487
488 fn make_tracker<'a>(
489 names: &'a Option<HashMap<String, String>>,
490 values: &'a Option<HashMap<String, AttributeValue>>,
491 ) -> TrackedExpressionAttributes<'a> {
492 TrackedExpressionAttributes::new(names, values)
493 }
494
495 #[test]
496 fn accepts_reversed_operand_sort_key() {
497 let names = Some(HashMap::from([
498 ("#pk".to_string(), "pk".to_string()),
499 ("#sk".to_string(), "sk".to_string()),
500 ]));
501 let values = Some(HashMap::from([
502 (":pk".to_string(), AttributeValue::S("x".into())),
503 (":lo".to_string(), AttributeValue::S("a".into())),
504 ]));
505 let tracker = make_tracker(&names, &values);
506 for (op, expected) in [("<", "Gt"), ("<=", "Ge"), (">", "Lt"), (">=", "Le")] {
508 let expr = format!("#pk = :pk AND :lo {op} #sk");
509 let kc = parse(&expr, &tracker).unwrap();
510 let tag = match &kc.sk_condition {
511 Some(SortKeyCondition::Gt(n, v)) if n == "sk" && v == ":lo" => "Gt",
512 Some(SortKeyCondition::Ge(n, v)) if n == "sk" && v == ":lo" => "Ge",
513 Some(SortKeyCondition::Lt(n, v)) if n == "sk" && v == ":lo" => "Lt",
514 Some(SortKeyCondition::Le(n, v)) if n == "sk" && v == ":lo" => "Le",
515 _ => "other",
516 };
517 assert_eq!(tag, expected, "reversed {op} should flip to {expected}");
518 }
519 }
520
521 #[test]
522 fn accepts_reversed_equality_on_sort_key() {
523 let names = Some(HashMap::from([
524 ("#pk".to_string(), "pk".to_string()),
525 ("#sk".to_string(), "sk".to_string()),
526 ]));
527 let values = Some(HashMap::from([
528 (":pk".to_string(), AttributeValue::S("x".into())),
529 (":sk".to_string(), AttributeValue::S("m".into())),
530 ]));
531 let tracker = make_tracker(&names, &values);
532 let kc = parse("#pk = :pk AND :sk = #sk", &tracker).unwrap();
533 assert!(
534 matches!(&kc.sk_condition, Some(SortKeyCondition::Eq(n, v)) if n == "sk" && v == ":sk")
535 );
536 }
537
538 #[test]
539 fn rejects_reversed_operand_with_nested_key_path() {
540 let names = Some(HashMap::from([
541 ("#pk".to_string(), "pk".to_string()),
542 ("#sk".to_string(), "sk".to_string()),
543 ]));
544 let values = Some(HashMap::from([
545 (":pk".to_string(), AttributeValue::S("x".into())),
546 (":lo".to_string(), AttributeValue::S("a".into())),
547 ]));
548 let tracker = make_tracker(&names, &values);
549 let err = parse("#pk = :pk AND :lo <= #sk.foo", &tracker).unwrap_err();
550 assert_eq!(
551 err,
552 "Invalid KeyConditionExpression: KeyConditionExpressions cannot have conditions on nested attributes"
553 );
554 }
555
556 #[test]
557 fn rejects_nested_path_on_key() {
558 let names = Some(HashMap::from([
559 ("#pk".to_string(), "pk".to_string()),
560 ("#sk".to_string(), "sk".to_string()),
561 ]));
562 let values = Some(HashMap::from([
563 (":pk".to_string(), AttributeValue::S("x".into())),
564 (":v".to_string(), AttributeValue::S("y".into())),
565 ]));
566 let tracker = make_tracker(&names, &values);
567 let err = parse("#pk = :pk AND #sk.foo = :v", &tracker).unwrap_err();
568 assert_eq!(
569 err,
570 "Invalid KeyConditionExpression: KeyConditionExpressions cannot have conditions on nested attributes"
571 );
572 }
573
574 #[test]
575 fn test_pk_only() {
576 let no_names = None;
577 let no_values = None;
578 let tracker = make_tracker(&no_names, &no_values);
579 let kc = parse("pk = :pk", &tracker).unwrap();
580 assert_eq!(kc.pk_name, "pk");
581 assert_eq!(kc.pk_value_ref, ":pk");
582 assert!(kc.sk_condition.is_none());
583 }
584
585 #[test]
586 fn test_pk_and_sk_eq() {
587 let no_names = None;
588 let no_values = None;
589 let tracker = make_tracker(&no_names, &no_values);
590 let kc = parse("pk = :pk AND sk = :sk", &tracker).unwrap();
591 assert_eq!(kc.pk_name, "pk");
592 assert!(matches!(kc.sk_condition, Some(SortKeyCondition::Eq(_, _))));
593 }
594
595 #[test]
596 fn test_pk_and_sk_between() {
597 let no_names = None;
598 let no_values = None;
599 let tracker = make_tracker(&no_names, &no_values);
600 let kc = parse("pk = :pk AND sk BETWEEN :lo AND :hi", &tracker).unwrap();
601 assert!(matches!(
602 kc.sk_condition,
603 Some(SortKeyCondition::Between(_, _, _))
604 ));
605 }
606
607 #[test]
608 fn test_pk_and_begins_with() {
609 let no_names = None;
610 let no_values = None;
611 let tracker = make_tracker(&no_names, &no_values);
612 let kc = parse("pk = :pk AND begins_with(sk, :prefix)", &tracker).unwrap();
613 assert!(matches!(
614 kc.sk_condition,
615 Some(SortKeyCondition::BeginsWith(_, _))
616 ));
617 }
618
619 #[test]
620 fn test_with_attribute_names() {
621 let an = Some(HashMap::from([
622 ("#pk".to_string(), "partitionKey".to_string()),
623 ("#sk".to_string(), "sortKey".to_string()),
624 ]));
625 let no_values = None;
626 let tracker = make_tracker(&an, &no_values);
627 let kc = parse("#pk = :pk AND #sk > :sk", &tracker).unwrap();
628 assert_eq!(kc.pk_name, "partitionKey");
629 assert!(matches!(kc.sk_condition, Some(SortKeyCondition::Gt(ref n, _)) if n == "sortKey"));
630 }
631
632 #[test]
633 fn test_resolve_values() {
634 let no_names = None;
635 let no_values = None;
636 let parse_tracker = make_tracker(&no_names, &no_values);
637 let kc = parse("pk = :pk AND sk >= :sk", &parse_tracker).unwrap();
638 let av = Some(HashMap::from([
639 (":pk".to_string(), AttributeValue::S("user#1".into())),
640 (":sk".to_string(), AttributeValue::S("2024-01-01".into())),
641 ]));
642 let resolve_tracker = make_tracker(&no_names, &av);
643 let resolved = resolve_values(&kc, &resolve_tracker).unwrap();
644 assert_eq!(resolved.pk_value, AttributeValue::S("user#1".into()));
645 assert!(matches!(
646 resolved.sk_condition,
647 Some(ResolvedSortKeyCondition::Ge(_, _))
648 ));
649 }
650
651 #[test]
652 fn test_parenthesized_conditions() {
653 let no_names = None;
654 let no_values = None;
655
656 let tracker = make_tracker(&no_names, &no_values);
658 let kc = parse("(pk = :pk) AND (sk = :sk)", &tracker).unwrap();
659 assert_eq!(kc.pk_name, "pk");
660 assert!(matches!(kc.sk_condition, Some(SortKeyCondition::Eq(_, _))));
661
662 let tracker = make_tracker(&no_names, &no_values);
664 let kc = parse("(pk = :pk AND sk = :sk)", &tracker).unwrap();
665 assert_eq!(kc.pk_name, "pk");
666 assert!(matches!(kc.sk_condition, Some(SortKeyCondition::Eq(_, _))));
667
668 let tracker = make_tracker(&no_names, &no_values);
670 let kc = parse("(pk = :pk AND (sk > :sk))", &tracker).unwrap();
671 assert_eq!(kc.pk_name, "pk");
672 assert!(matches!(kc.sk_condition, Some(SortKeyCondition::Gt(_, _))));
673
674 let tracker = make_tracker(&no_names, &no_values);
677 let err = parse("((pk = :pk)) AND ((sk > :sk))", &tracker).unwrap_err();
678 assert!(
679 err.contains("redundant parentheses"),
680 "expected redundant-parentheses rejection, got: {err}"
681 );
682
683 let tracker = make_tracker(&no_names, &no_values);
685 let kc = parse("(pk = :pk) AND (begins_with(sk, :prefix))", &tracker).unwrap();
686 assert!(matches!(
687 kc.sk_condition,
688 Some(SortKeyCondition::BeginsWith(_, _))
689 ));
690
691 let an = Some(HashMap::from([
693 ("#pk".to_string(), "PK".to_string()),
694 ("#sk".to_string(), "SK".to_string()),
695 ]));
696 let tracker = make_tracker(&an, &no_values);
697 let kc = parse("(#pk = :pk) AND (#sk = :sk)", &tracker).unwrap();
698 assert_eq!(kc.pk_name, "PK");
699 }
700
701 #[test]
702 fn test_sk_comparisons() {
703 let no_names = None;
704 let no_values = None;
705 for (op, variant) in [("<", "Lt"), ("<=", "Le"), (">", "Gt"), (">=", "Ge")] {
706 let tracker = make_tracker(&no_names, &no_values);
707 let kc = parse(&format!("pk = :pk AND sk {op} :sk"), &tracker).unwrap();
708 let sk = kc.sk_condition.unwrap();
709 let name = match &sk {
710 SortKeyCondition::Lt(n, _) => format!("Lt:{n}"),
711 SortKeyCondition::Le(n, _) => format!("Le:{n}"),
712 SortKeyCondition::Gt(n, _) => format!("Gt:{n}"),
713 SortKeyCondition::Ge(n, _) => format!("Ge:{n}"),
714 _ => "other".to_string(),
715 };
716 assert!(name.starts_with(variant), "Expected {variant}, got {name}");
717 }
718 }
719}