1use std::collections::HashSet;
18
19use parse_rust_core::{classify, ErrorCode, ParseError, ParseValue};
20use parse_rust_storage::{Comparison, Constraint};
21use serde_json::Value as Json;
22
23#[derive(Debug, Clone, Default)]
25pub struct ParsedWhere {
26 pub clauses: Vec<ParsedClause>,
27}
28
29#[derive(Debug, Clone)]
31pub enum ParsedClause {
32 Field(Constraint),
33 RelatedTo {
37 class_name: String,
38 object_id: String,
39 key: String,
40 },
41 Or(Vec<ParsedWhere>),
42 And(Vec<ParsedWhere>),
43 Nor(Vec<ParsedWhere>),
44}
45
46impl ParsedWhere {
47 pub fn is_empty(&self) -> bool {
48 self.clauses.is_empty()
49 }
50
51 pub fn field_keys(&self) -> Vec<String> {
56 let mut out = Vec::new();
57 self.collect_field_keys(&mut out);
58 out
59 }
60
61 fn collect_field_keys(&self, out: &mut Vec<String>) {
62 for clause in &self.clauses {
63 match clause {
64 ParsedClause::Field(c) => out.push(c.field.clone()),
65 ParsedClause::RelatedTo { .. } => {}
66 ParsedClause::Or(branches)
67 | ParsedClause::And(branches)
68 | ParsedClause::Nor(branches) => {
69 for branch in branches {
70 branch.collect_field_keys(out);
71 }
72 }
73 }
74 }
75 }
76
77 pub fn pinned_object_id(&self) -> Option<&str> {
83 self.clauses.iter().find_map(|c| match c {
84 ParsedClause::Field(Constraint {
85 field,
86 comparison: Comparison::Equal(ParseValue::String(id)),
87 }) if field == "objectId" => Some(id.as_str()),
88 _ => None,
89 })
90 }
91
92 pub fn push(&mut self, clause: ParsedClause) {
93 self.clauses.push(clause);
94 }
95}
96
97pub fn parse_where(where_json: &Json) -> Result<ParsedWhere, ParseError> {
99 parse_where_at(where_json, true)
100}
101
102fn parse_where_at(where_json: &Json, top_level: bool) -> Result<ParsedWhere, ParseError> {
111 let Json::Object(map) = where_json else {
112 return Err(ParseError::invalid_query(
113 "where must be an object".to_string(),
114 ));
115 };
116
117 let mut out = ParsedWhere::default();
118 for (field, value) in map {
119 if field == "ACL" {
123 return Err(ParseError::invalid_query(
124 "Cannot query on ACL.".to_string(),
125 ));
126 }
127
128 if let Some(clause) = parse_query_level_key(field, value)? {
129 out.push(clause);
130 continue;
131 }
132
133 match value {
134 Json::Object(inner) if is_operator_document(inner) => {
136 for constraint in parse_operators(field, inner)? {
137 out.push(ParsedClause::Field(constraint));
138 }
139 }
140 Json::Object(inner) if top_level && is_mixed_document(inner) => {
150 let mut rewritten = serde_json::Map::new();
151 let mut equal_to = serde_json::Map::new();
152 for (key, v) in inner {
153 if key.starts_with('$') {
154 rewritten.insert(key.clone(), v.clone());
155 } else {
156 equal_to.insert(key.clone(), v.clone());
157 }
158 }
159 rewritten.insert("$eq".to_string(), Json::Object(equal_to));
160 for constraint in parse_operators(field, &rewritten)? {
161 out.push(ParsedClause::Field(constraint));
162 }
163 }
164 literal => out.push(ParsedClause::Field(Constraint {
167 field: field.clone(),
168 comparison: Comparison::Equal(parse_rust_core::classify_raw(literal.clone())?),
169 })),
170 }
171 }
172 Ok(out)
173}
174
175fn parse_query_level_key(field: &str, value: &Json) -> Result<Option<ParsedClause>, ParseError> {
181 if !field.starts_with('$') {
182 return Ok(None);
183 }
184 let clause = match field {
185 "$or" | "$and" | "$nor" => {
186 let branches = match value {
187 Json::Array(items) => items
188 .iter()
189 .map(|item| parse_where_at(item, false))
190 .collect::<Result<Vec<_>, _>>()?,
191 _ => {
194 return Err(ParseError::invalid_query(if field == "$nor" {
195 "Bad $nor format - use an array of at least 1 value.".to_string()
196 } else {
197 format!("Bad {field} format - use an array value.")
198 }))
199 }
200 };
201 if field == "$nor" && branches.is_empty() {
202 return Err(ParseError::invalid_query(
203 "Bad $nor format - use an array of at least 1 value.".to_string(),
204 ));
205 }
206 match field {
207 "$or" => ParsedClause::Or(branches),
208 "$and" => ParsedClause::And(branches),
209 _ => ParsedClause::Nor(branches),
210 }
211 }
212 "$relatedTo" => parse_related_to(value)?,
213 other => {
214 return Err(ParseError::invalid_query(format!(
215 "unsupported query operator: {other}"
216 )))
217 }
218 };
219 Ok(Some(clause))
220}
221
222fn parse_related_to(value: &Json) -> Result<ParsedClause, ParseError> {
223 let bad = || ParseError::invalid_query("improper usage of $relatedTo".to_string());
224 let Json::Object(map) = value else {
225 return Err(bad());
226 };
227 let key = match map.get("key") {
228 Some(Json::String(k)) => k.clone(),
229 _ => return Err(bad()),
230 };
231 let object = map.get("object").ok_or_else(bad)?;
232 match classify(object.clone())? {
233 ParseValue::Pointer {
234 class_name,
235 object_id,
236 } => Ok(ParsedClause::RelatedTo {
237 class_name,
238 object_id,
239 key,
240 }),
241 _ => Err(bad()),
242 }
243}
244
245fn parse_operators(
252 field: &str,
253 inner: &serde_json::Map<String, Json>,
254) -> Result<Vec<Constraint>, ParseError> {
255 let mut out = Vec::new();
256
257 let regex = inner.get("$regex");
258 let options = inner.get("$options");
259 if regex.is_none() && options.is_some() {
260 return Err(ParseError::invalid_query(
263 "$options is only valid with $regex".to_string(),
264 ));
265 }
266 if let Some(regex) = regex {
267 let Json::String(pattern) = regex else {
268 return Err(ParseError::invalid_query(
269 "$regex value must be a string".to_string(),
270 ));
271 };
272 let options = match options {
273 None => None,
274 Some(Json::String(o)) => {
275 if !o.chars().all(|c| matches!(c, 'i' | 'm' | 'x' | 's' | 'u')) || o.is_empty() {
276 return Err(ParseError::invalid_query(format!(
277 "Bad $options value for query: {o}"
278 )));
279 }
280 Some(o.clone())
281 }
282 Some(_) => {
283 return Err(ParseError::invalid_query(
284 "$options value must be a string".to_string(),
285 ))
286 }
287 };
288 out.push(Constraint {
289 field: field.to_string(),
290 comparison: Comparison::Regex {
291 pattern: pattern.clone(),
292 options,
293 },
294 });
295 }
296
297 for (op, operand) in inner {
298 if op == "$regex" || op == "$options" {
299 continue;
300 }
301 let operand = parse_rust_core::classify_raw(operand.clone())?;
319 out.push(Constraint {
320 field: field.to_string(),
321 comparison: Comparison::from_operator(op, operand)?,
322 });
323 }
324 Ok(out)
325}
326
327fn is_operator_document(map: &serde_json::Map<String, Json>) -> bool {
332 !map.is_empty() && map.keys().all(|k| k.starts_with('$'))
333}
334
335fn is_mixed_document(map: &serde_json::Map<String, Json>) -> bool {
340 map.keys().any(|k| k.starts_with('$')) && map.keys().any(|k| !k.starts_with('$'))
341}
342
343pub const CLIENT_QUERYABLE_INTERNAL_FIELDS: [&str; 2] = ["_rperm", "_wperm"];
351
352pub const MASTER_QUERYABLE_INTERNAL_FIELDS: [&str; 10] = [
357 "_email_verify_token",
358 "_perishable_token",
359 "_perishable_token_expires_at",
360 "_email_verify_token_expires_at",
361 "_failed_login_count",
362 "_account_lockout_expires_at",
363 "_password_changed_at",
364 "_password_history",
365 "_tombstone",
366 "_session_token",
367];
368
369pub fn validate_query_keys(where_: &ParsedWhere, is_master: bool) -> Result<(), ParseError> {
376 for key in where_.field_keys() {
377 if matches_query_key_regex(&key)
378 || CLIENT_QUERYABLE_INTERNAL_FIELDS.contains(&key.as_str())
379 || (is_master && MASTER_QUERYABLE_INTERNAL_FIELDS.contains(&key.as_str()))
380 {
381 continue;
382 }
383 return Err(ParseError::invalid_key_name(format!(
384 "Invalid key name: {key}"
385 )));
386 }
387 Ok(())
388}
389
390fn matches_query_key_regex(key: &str) -> bool {
392 let mut chars = key.chars();
393 match chars.next() {
394 Some(c) if c.is_ascii_alphabetic() => {}
395 _ => return false,
396 }
397 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
398}
399
400pub const MAX_INCLUDE_DEPTH: usize = 20;
402
403pub const MAX_INCLUDE_PATHS: usize = 500;
406
407pub fn parse_include(include: &str) -> Result<Vec<Vec<String>>, ParseError> {
436 let mut paths: Vec<Vec<String>> = Vec::new();
437 let mut seen: HashSet<&str> = HashSet::new();
438 for raw in include.split(',').map(str::trim).filter(|s| !s.is_empty()) {
439 if raw == "*" {
440 return Err(ParseError::new(
441 ErrorCode::CommandUnavailable,
442 "include=* is not supported yet.",
443 ));
444 }
445 let parts: Vec<&str> = raw.split('.').collect();
446 if parts.len() > MAX_INCLUDE_DEPTH {
447 return Err(ParseError::invalid_query(format!(
448 "include path is too deep: at most {MAX_INCLUDE_DEPTH} components."
449 )));
450 }
451 for depth in 1..=parts.len() {
452 let end = parts[..depth].iter().map(|p| p.len()).sum::<usize>() + depth - 1;
456 if !seen.insert(&raw[..end]) {
457 continue;
458 }
459 if paths.len() == MAX_INCLUDE_PATHS {
460 return Err(ParseError::invalid_query(format!(
461 "too many include paths: at most {MAX_INCLUDE_PATHS}."
462 )));
463 }
464 paths.push(parts[..depth].iter().map(|s| s.to_string()).collect());
465 }
466 }
467 paths.sort_by_key(Vec::len);
469 Ok(paths)
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 fn j(s: &str) -> Json {
477 serde_json::from_str(s).expect("test literal")
478 }
479
480 fn fields(w: &ParsedWhere) -> Vec<&Constraint> {
481 w.clauses
482 .iter()
483 .filter_map(|c| match c {
484 ParsedClause::Field(f) => Some(f),
485 _ => None,
486 })
487 .collect()
488 }
489
490 #[test]
491 fn a_bare_value_is_equality() {
492 let w = parse_where(&j(r#"{"title":"hello"}"#)).expect("parse");
493 let c = fields(&w);
494 assert_eq!(c.len(), 1);
495 assert_eq!(c[0].field, "title");
496 assert!(matches!(
497 &c[0].comparison,
498 Comparison::Equal(ParseValue::String(s)) if s == "hello"
499 ));
500 }
501
502 #[test]
503 fn several_operators_on_one_field_become_several_constraints() {
504 let w = parse_where(&j(r#"{"views":{"$gt":1,"$lt":9}}"#)).expect("parse");
505 assert_eq!(fields(&w).len(), 2);
506 assert!(fields(&w).iter().all(|x| x.field == "views"));
507 }
508
509 #[test]
517 fn a_tagged_value_is_a_literal_and_reaches_the_backend_undecoded() {
518 let w = parse_where(&j(
519 r#"{"author":{"__type":"Pointer","className":"_User","objectId":"u1","extra":7}}"#,
520 ))
521 .expect("parse");
522 let Comparison::Equal(ParseValue::Object(map)) = &fields(&w)[0].comparison else {
523 panic!("expected a raw object operand, got {:?}", fields(&w)[0]);
524 };
525 assert_eq!(map.len(), 4, "{map:?}");
529 assert!(matches!(map.get("objectId"), Some(ParseValue::String(s)) if s == "u1"));
530 assert!(matches!(map.get("extra"), Some(ParseValue::Number(n)) if *n == 7.0));
531 }
532
533 #[test]
537 fn an_unsupported_operator_is_refused() {
538 for op in [
539 "$inQuery",
540 "$notInQuery",
541 "$select",
542 "$dontSelect",
543 "$text",
544 "$nearSphere",
545 "$containedBy",
546 "$geoWithin",
547 ] {
548 let src = format!(r#"{{"title":{{"{op}":1}}}}"#);
549 let e = parse_where(&j(&src)).unwrap_err();
550 assert_eq!(e.code, ErrorCode::InvalidQuery, "{op}");
551 assert!(e.message.contains(op), "{op}: {}", e.message);
552 }
553 }
554
555 #[test]
556 fn an_unknown_query_level_operator_is_refused() {
557 let e = parse_where(&j(r#"{"$nope":[]}"#)).unwrap_err();
558 assert_eq!(e.code, ErrorCode::InvalidQuery);
559 assert!(e.message.contains("$nope"));
560 }
561
562 #[test]
563 fn logical_operators_parse_recursively() {
564 let w = parse_where(&j(
565 r#"{"$or":[{"a":1},{"$and":[{"b":2},{"c":3}]}],"$nor":[{"d":4}]}"#,
566 ))
567 .expect("parse");
568 assert_eq!(w.clauses.len(), 2);
569 match &w.clauses[0] {
570 ParsedClause::Or(branches) => {
571 assert_eq!(branches.len(), 2);
572 assert!(matches!(branches[1].clauses[0], ParsedClause::And(_)));
573 }
574 other => panic!("expected Or, got {other:?}"),
575 }
576 assert!(matches!(w.clauses[1], ParsedClause::Nor(_)));
577 }
578
579 #[test]
580 fn a_non_array_logical_operator_is_invalid() {
581 for src in [r#"{"$or":{"a":1}}"#, r#"{"$and":3}"#, r#"{"$nor":[]}"#] {
582 let e = parse_where(&j(src)).unwrap_err();
583 assert_eq!(e.code, ErrorCode::InvalidQuery, "{src}");
584 }
585 }
586
587 #[test]
588 fn regex_folds_its_options_in() {
589 let w = parse_where(&j(r#"{"title":{"$regex":"^a","$options":"im"}}"#)).expect("parse");
590 let c = fields(&w);
591 assert_eq!(c.len(), 1, "two keys make one comparison");
592 assert!(matches!(
593 &c[0].comparison,
594 Comparison::Regex { pattern, options } if pattern == "^a" && options.as_deref() == Some("im")
595 ));
596 }
597
598 #[test]
599 fn regex_rejects_a_non_string_pattern_and_bad_options() {
600 assert!(parse_where(&j(r#"{"title":{"$regex":3}}"#)).is_err());
601 let e = parse_where(&j(r#"{"title":{"$regex":"a","$options":"z"}}"#)).unwrap_err();
602 assert_eq!(e.code, ErrorCode::InvalidQuery);
603 assert!(e.message.contains("Bad $options value for query: z"));
604 assert!(parse_where(&j(r#"{"title":{"$options":"i"}}"#)).is_err());
605 }
606
607 #[test]
608 fn all_parses() {
609 let w = parse_where(&j(r#"{"tags":{"$all":["a","b"]}}"#)).expect("parse");
610 assert!(matches!(&fields(&w)[0].comparison, Comparison::All(v) if v.len() == 2));
611 }
612
613 #[test]
614 fn related_to_parses_into_its_own_clause() {
615 let w = parse_where(&j(
616 r#"{"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"r1"},"key":"users"}}"#,
617 ))
618 .expect("parse");
619 match &w.clauses[0] {
620 ParsedClause::RelatedTo {
621 class_name,
622 object_id,
623 key,
624 } => {
625 assert_eq!(class_name, "_Role");
626 assert_eq!(object_id, "r1");
627 assert_eq!(key, "users");
628 }
629 other => panic!("expected RelatedTo, got {other:?}"),
630 }
631 assert!(parse_where(&j(r#"{"$relatedTo":{"key":"users"}}"#)).is_err());
632 assert!(parse_where(&j(r#"{"$relatedTo":{"object":3,"key":"u"}}"#)).is_err());
633 }
634
635 #[test]
636 fn querying_on_acl_is_refused() {
637 let e = parse_where(&j(r#"{"ACL":{"*":{"read":true}}}"#)).unwrap_err();
638 assert_eq!(e.code, ErrorCode::InvalidQuery);
639 assert_eq!(e.message, "Cannot query on ACL.");
640 }
641
642 #[test]
643 fn in_and_exists_parse() {
644 let w =
645 parse_where(&j(r#"{"tag":{"$in":["a","b"]},"x":{"$exists":true}}"#)).expect("parse");
646 assert_eq!(fields(&w).len(), 2);
647 }
648
649 #[test]
650 fn an_empty_object_is_an_empty_query_not_an_operator_document() {
651 assert!(parse_where(&j("{}")).expect("parse").is_empty());
652 let w = parse_where(&j(r#"{"meta":{}}"#)).expect("parse");
653 assert!(matches!(
654 &fields(&w)[0].comparison,
655 Comparison::Equal(ParseValue::Object(_))
656 ));
657 }
658
659 #[test]
660 fn where_must_be_an_object() {
661 assert!(parse_where(&j("[]")).is_err());
662 assert!(parse_where(&j("3")).is_err());
663 }
664
665 #[test]
666 fn field_keys_reach_into_logical_clauses() {
667 let w = parse_where(&j(r#"{"a":1,"$or":[{"b":2},{"$and":[{"c":3}]}]}"#)).expect("parse");
668 let mut keys = w.field_keys();
669 keys.sort();
670 assert_eq!(keys, vec!["a", "b", "c"]);
671 }
672
673 #[test]
674 fn pinned_object_id_only_reads_a_top_level_equality() {
675 let w = parse_where(&j(r#"{"objectId":"abc"}"#)).expect("parse");
676 assert_eq!(w.pinned_object_id(), Some("abc"));
677 let w = parse_where(&j(r#"{"$or":[{"objectId":"abc"}]}"#)).expect("parse");
678 assert_eq!(w.pinned_object_id(), None);
679 }
680
681 #[test]
682 fn include_paths_materialize_prefixes_and_sort_by_depth() {
683 assert_eq!(
684 parse_include("a.b.c,d").expect("parse"),
685 vec![
686 vec!["a".to_string()],
687 vec!["d".to_string()],
688 vec!["a".to_string(), "b".to_string()],
689 vec!["a".to_string(), "b".to_string(), "c".to_string()],
690 ]
691 );
692 assert!(parse_include("").expect("parse").is_empty());
693 }
694
695 #[test]
702 fn a_hostile_include_is_refused_rather_than_expanded() {
703 let deep = vec!["a"; MAX_INCLUDE_DEPTH + 1].join(".");
704 let err = parse_include(&deep).expect_err("over the depth limit");
705 assert_eq!(err.code, ErrorCode::InvalidQuery);
706
707 let huge = vec!["a"; 24_000].join(".");
709 assert_eq!(
710 parse_include(&huge).expect_err("over the depth limit").code,
711 ErrorCode::InvalidQuery
712 );
713
714 let wide = (0..MAX_INCLUDE_PATHS + 1)
716 .map(|i| format!("f{i}"))
717 .collect::<Vec<_>>()
718 .join(",");
719 assert_eq!(
720 parse_include(&wide).expect_err("over the path limit").code,
721 ErrorCode::InvalidQuery
722 );
723
724 assert_eq!(
727 parse_include("author.company.owner").expect("parse").len(),
728 3
729 );
730 let at_depth = vec!["a"; MAX_INCLUDE_DEPTH].join(".");
731 assert_eq!(
732 parse_include(&at_depth)
733 .expect("exactly at the limit")
734 .len(),
735 MAX_INCLUDE_DEPTH
736 );
737 }
738
739 #[test]
740 fn a_client_cannot_name_an_internal_column_in_a_query() {
741 let w = parse_where(&j(r#"{"_hashed_password":{"$regex":"^a"}}"#)).expect("parse");
744 for is_master in [false, true] {
745 let e = validate_query_keys(&w, is_master).unwrap_err();
746 assert_eq!(e.code, ErrorCode::InvalidKeyName);
747 assert_eq!(e.message, "Invalid key name: _hashed_password");
748 }
749
750 let w = parse_where(&j(r#"{"_rperm":"u1"}"#)).expect("parse");
752 assert!(validate_query_keys(&w, false).is_ok());
753
754 let w = parse_where(&j(r#"{"_session_token":"r:t"}"#)).expect("parse");
756 assert!(validate_query_keys(&w, false).is_err());
757 assert!(validate_query_keys(&w, true).is_ok());
758
759 let w = parse_where(&j(r#"{"$or":[{"_session_token":"r:t"}]}"#)).expect("parse");
761 assert!(validate_query_keys(&w, false).is_err());
762 }
763
764 #[test]
765 fn ordinary_and_dotted_field_names_pass() {
766 for src in [r#"{"title":"x"}"#, r#"{"meta.a_b":1}"#] {
767 let w = parse_where(&j(src)).expect("parse");
768 assert!(validate_query_keys(&w, false).is_ok(), "{src}");
769 }
770 let w = parse_where(&j(r#"{"1bad":1}"#)).expect("parse");
772 assert!(validate_query_keys(&w, false).is_err());
773 }
774
775 #[test]
776 fn include_all_is_an_explicit_error() {
777 let e = parse_include("*").unwrap_err();
778 assert_eq!(e.code, ErrorCode::CommandUnavailable);
779 assert!(e.message.contains("include=*"));
780 }
781}
782
783#[cfg(test)]
784mod mixed_constraint_tests {
785 use super::*;
786
787 fn parse(json: &str) -> Result<ParsedWhere, ParseError> {
788 parse_where(&serde_json::from_str(json).expect("test literal"))
789 }
790
791 #[test]
793 fn a_mixed_constraint_becomes_an_eq_plus_the_operators() {
794 let parsed = parse(r#"{"meta": {"foo": 1, "$gt": 0}}"#).expect("parses");
795 let mut equals = 0;
796 let mut greater = 0;
797 for clause in &parsed.clauses {
798 let ParsedClause::Field(c) = clause else {
799 panic!("expected field clauses")
800 };
801 assert_eq!(c.field, "meta");
802 match &c.comparison {
803 Comparison::EqualOperator(ParseValue::Object(map)) => {
806 assert_eq!(map.len(), 1, "only the direct keys: {map:?}");
807 assert!(map.contains_key("foo"));
808 equals += 1;
809 }
810 Comparison::GreaterThan(_) => greater += 1,
811 other => panic!("unexpected comparison: {other:?}"),
812 }
813 }
814 assert_eq!((equals, greater), (1, 1));
815 }
816
817 #[test]
820 fn a_mixed_constraint_is_not_one_literal_equality() {
821 let parsed = parse(r#"{"meta": {"foo": 1, "$gt": 0}}"#).expect("parses");
822 assert_eq!(
823 parsed.clauses.len(),
824 2,
825 "one clause means it was read as a literal"
826 );
827 }
828
829 #[test]
831 fn unmixed_documents_are_unchanged() {
832 let ops = parse(r#"{"n": {"$gt": 0, "$lt": 9}}"#).expect("parses");
833 assert_eq!(ops.clauses.len(), 2);
834
835 let literal = parse(r#"{"p": {"__type": "Pointer", "className": "C", "objectId": "x"}}"#)
837 .expect("parses");
838 assert_eq!(literal.clauses.len(), 1);
839 }
840
841 #[test]
847 fn the_rewrite_does_not_reach_inside_or() {
848 let parsed = parse(r#"{"$or": [{"meta": {"foo": 1, "$gt": 0}}]}"#).expect("parses");
849 let [ParsedClause::Or(branches)] = parsed.clauses.as_slice() else {
850 panic!("expected one $or clause")
851 };
852 assert_eq!(branches.len(), 1);
853 assert_eq!(
854 branches[0].clauses.len(),
855 1,
856 "inside $or the mixed document stays one literal equality"
857 );
858 }
859
860 #[test]
862 fn an_explicit_eq_operator_is_accepted() {
863 let parsed = parse(r#"{"n": {"$eq": 5}}"#).expect("parses");
864 assert_eq!(parsed.clauses.len(), 1);
865 let ParsedClause::Field(c) = &parsed.clauses[0] else {
866 panic!("expected a field clause")
867 };
868 assert!(matches!(c.comparison, Comparison::EqualOperator(_)));
869 }
870}