Skip to main content

sct_ecl/
eval.rs

1//! The evaluator: an expression constraint to the set of concept ordinals it
2//! selects, as set algebra over the materialized closure, the attribute
3//! graph, and the reference set tables of one edition.
4//!
5//! The semantics are the ECL specification's
6//! (<https://docs.snomed.org/snomed-ct-specifications/snomed-ct-expression-constraint-language>,
7//! the quick reference in Appendix D): `<` is the closure's descendants,
8//! `^` the active members, a refinement the sources whose attribute rows
9//! satisfy the attributes with their cardinalities per role group, and the
10//! filters and history supplements restrict or extend the set. The edition
11//! is reached through [`Model`]; nothing here walks a graph edge by edge.
12
13use std::fmt;
14
15use concept_graph::attributes::{Row, ValueRef};
16use concept_graph::ordinal::Ordinal;
17use concept_graph::refsets::{Table, ValueRef as FieldRef};
18use roaring::RoaringBitmap;
19
20use crate::ast::{
21    Acceptability, Attribute, AttributeSet, AttributeValue, Cardinality, Comparison, ConceptFilter,
22    ConceptSet, ConstraintOperator, DefinitionStatus, DescriptionFilter, DialectIdValue, Equality,
23    ExpressionConstraint, FieldValue, FilterConstraint, FocusConcept, HistorySupplement,
24    MemberFilter, Refinement, RefsetFields, Sctid, SubAttributeSet, SubExpressionConstraint,
25    SubRefinement, TimeValue, TypeToken, TypedSearchTerm,
26};
27
28/// The historical association reference set root
29/// (`900000000000522004 |Historical association reference set|`).
30pub const HISTORICAL_ASSOCIATION: u64 = 900_000_000_000_522_004;
31/// `900000000000527005 |SAME AS association reference set|`.
32pub const SAME_AS: u64 = 900_000_000_000_527_005;
33/// `900000000000526001 |REPLACED BY association reference set|`.
34pub const REPLACED_BY: u64 = 900_000_000_000_526_001;
35/// `900000000000528000 |WAS A association reference set|`.
36pub const WAS_A: u64 = 900_000_000_000_528_000;
37/// `1186924009 |PARTIALLY EQUIVALENT TO association reference set|`.
38pub const PARTIALLY_EQUIVALENT_TO: u64 = 1_186_924_009;
39/// The reference set field every member carries.
40const REFERENCED_COMPONENT: &str = "referencedComponentId";
41/// The association reference set field a history supplement follows.
42const TARGET_COMPONENT: &str = "targetComponentId";
43
44/// A failure to evaluate.
45#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
46pub enum EvalError {
47    /// The expression names a concept the edition does not have.
48    #[error("the edition has no concept {0}")]
49    UnknownConcept(Sctid),
50    /// A `^` focus is not a reference set with members in the edition.
51    #[error("{0} is not a reference set with members in the edition")]
52    NotAReferenceSet(Sctid),
53    /// An alternate identifier scheme alias resolves to no identifier scheme.
54    #[error("`{0}` is not an identifier scheme alias of the edition")]
55    UnknownScheme(String),
56    /// An alternate identifier names no concept.
57    #[error("no concept has the alternate identifier {scheme}#{code}")]
58    UnknownIdentifier {
59        /// The scheme alias.
60        scheme: String,
61        /// The code.
62        code: String,
63    },
64    /// A reference set has no field of the name.
65    #[error("reference set {refset} has no field `{field}`")]
66    UnknownField {
67        /// The reference set.
68        refset: Sctid,
69        /// The field.
70        field: String,
71    },
72    /// A construct the edition's data cannot answer.
73    #[error("{0} is not supported")]
74    Unsupported(&'static str),
75    /// A dialect alias the specification does not list.
76    #[error("`{0}` is not a dialect alias")]
77    UnknownDialect(String),
78    /// The edition's storage failed.
79    #[error("the edition could not be read: {0}")]
80    Storage(String),
81}
82
83/// One concept filter with its concept sets resolved, for the model.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum ConceptPredicate {
86    /// The concept is active (or, `false`, inactive).
87    Active(bool),
88    /// The definition status is one of the allowed ones.
89    DefinitionStatus {
90        /// Sufficiently defined concepts pass.
91        defined: bool,
92        /// Primitive concepts pass.
93        primitive: bool,
94    },
95    /// The module is one of `modules` (SCTIDs), or is not when negated.
96    Module {
97        /// The module SCTIDs.
98        modules: Vec<u64>,
99        /// `!=`.
100        negated: bool,
101    },
102    /// The effective time compares to one of `values` (`YYYYMMDD`; `!=` means
103    /// to none of them).
104    EffectiveTime {
105        /// The operator.
106        operator: Comparison,
107        /// The times.
108        values: Vec<u32>,
109    },
110}
111
112/// One description filter with its concept sets and aliases resolved, for
113/// the model; every predicate of one `{{ D ... }}` must hold for the same
114/// description.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum DescriptionPredicate {
117    /// The term matches one of the search terms (or, `!=`, none).
118    Term {
119        /// The operator.
120        operator: Equality,
121        /// The search terms.
122        terms: Vec<TypedSearchTerm>,
123    },
124    /// The language code (the primary subtag) is one of `codes`.
125    Language {
126        /// The operator.
127        operator: Equality,
128        /// Two-letter codes.
129        codes: Vec<String>,
130    },
131    /// The description type is one of `types` (SCTIDs).
132    Type {
133        /// The operator.
134        operator: Equality,
135        /// The description type SCTIDs.
136        types: Vec<u64>,
137    },
138    /// The description is acceptable in one of the language reference sets,
139    /// with one of the listed acceptabilities when any are listed.
140    Dialect {
141        /// The operator.
142        operator: Equality,
143        /// `(language reference set SCTID, allowed acceptabilities)`.
144        dialects: Vec<(u64, Vec<Acceptability>)>,
145    },
146    /// The description is active (or, `false`, inactive).
147    Active(bool),
148    /// The description identifier is one of `ids`.
149    Id {
150        /// The operator.
151        operator: Equality,
152        /// The description identifiers.
153        ids: Vec<u64>,
154    },
155}
156
157/// `900000000000548007 |Preferred|`.
158const PREFERRED: u64 = 900_000_000_000_548_007;
159/// `900000000000549004 |Acceptable|`.
160const ACCEPTABLE: u64 = 900_000_000_000_549_004;
161/// `900000000000073002 |Defined|`.
162const DEFINED: u64 = 900_000_000_000_073_002;
163/// `900000000000074008 |Primitive|`.
164const PRIMITIVE: u64 = 900_000_000_000_074_008;
165/// `900000000000003001 |Fully specified name|`.
166const FULLY_SPECIFIED_NAME: u64 = 900_000_000_000_003_001;
167/// `900000000000013009 |Synonym|`.
168const SYNONYM: u64 = 900_000_000_000_013_009;
169/// `900000000000550004 |Definition|`.
170const DEFINITION: u64 = 900_000_000_000_550_004;
171
172/// The edition an expression is evaluated against.
173pub trait Model {
174    /// The ordinal of a concept, `None` when the edition lacks it.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`EvalError::Storage`] when the edition cannot be read.
179    fn concept(&self, id: Sctid) -> Result<Option<Ordinal>, EvalError>;
180    /// The SCTID of a concept.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`EvalError::Storage`] when the edition cannot be read.
185    fn sctid(&self, concept: Ordinal) -> Result<Option<Sctid>, EvalError>;
186    /// Every concept, active and inactive.
187    fn all(&self) -> RoaringBitmap;
188    /// The concepts with no parent.
189    fn roots(&self) -> RoaringBitmap;
190    /// The concepts with no child.
191    fn leaves(&self) -> RoaringBitmap;
192    /// The transitive descendants, self excluded.
193    fn descendants(&self, concept: Ordinal) -> &RoaringBitmap;
194    /// The transitive ancestors, self excluded.
195    fn ancestors(&self, concept: Ordinal) -> &RoaringBitmap;
196    /// The direct children.
197    fn children(&self, concept: Ordinal) -> RoaringBitmap;
198    /// The direct parents.
199    fn parents(&self, concept: Ordinal) -> RoaringBitmap;
200    /// The attribute relationships.
201    fn attributes(&self) -> &concept_graph::attributes::Attributes;
202    /// The reference set member tables.
203    fn members(&self) -> &concept_graph::refsets::RefsetMembers;
204    /// The alternate identifiers.
205    fn identifiers(&self) -> &concept_graph::identifiers::Identifiers;
206    /// The identifier scheme an alias names (case-insensitively).
207    ///
208    /// # Errors
209    ///
210    /// Returns [`EvalError::Storage`] when the edition cannot be read.
211    fn scheme(&self, alias: &str) -> Result<Option<u64>, EvalError>;
212    /// The concepts of `within` whose concept rows satisfy every predicate.
213    ///
214    /// # Errors
215    ///
216    /// Returns [`EvalError`] for a predicate the edition cannot answer.
217    fn filter_concepts(
218        &self,
219        within: &RoaringBitmap,
220        predicates: &[ConceptPredicate],
221    ) -> Result<RoaringBitmap, EvalError>;
222    /// The concepts of `within` that have one description satisfying every
223    /// predicate.
224    ///
225    /// # Errors
226    ///
227    /// Returns [`EvalError`] for a predicate the edition cannot answer.
228    fn filter_descriptions(
229        &self,
230        within: &RoaringBitmap,
231        predicates: &[DescriptionPredicate],
232    ) -> Result<RoaringBitmap, EvalError>;
233}
234
235/// Evaluates `constraint` against `model`.
236///
237/// # Errors
238///
239/// Returns [`EvalError`] when the expression names something the edition
240/// does not have, or asks for a construct its data cannot answer.
241pub fn evaluate<M: Model>(
242    model: &M,
243    constraint: &ExpressionConstraint,
244) -> Result<RoaringBitmap, EvalError> {
245    Evaluator { model }.expression(constraint)
246}
247
248struct Evaluator<'m, M: Model> {
249    model: &'m M,
250}
251
252impl<M: Model> fmt::Debug for Evaluator<'_, M> {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        f.write_str("Evaluator")
255    }
256}
257
258/// The kinds of the attribute types a set of type concepts names, or every
259/// kind for `*`.
260#[derive(Debug, Clone, PartialEq, Eq)]
261enum Kinds {
262    Any,
263    These(Vec<u32>),
264}
265
266impl Kinds {
267    fn contains(&self, kind: u32) -> bool {
268        match self {
269            Self::Any => true,
270            Self::These(kinds) => kinds.contains(&kind),
271        }
272    }
273
274    fn iter(&self, total: usize) -> Box<dyn Iterator<Item = u32> + '_> {
275        match self {
276            Self::Any => Box::new(0..u32::try_from(total).unwrap_or(u32::MAX)),
277            Self::These(kinds) => Box::new(kinds.iter().copied()),
278        }
279    }
280}
281
282/// What an attribute's value must satisfy.
283#[derive(Debug)]
284enum ValueTest {
285    /// The value is a concept in (or, negated, not in) the set; `None` is `*`.
286    Concept {
287        set: Option<RoaringBitmap>,
288        negated: bool,
289    },
290    Number(Comparison, f64),
291    Text(Equality, Vec<TypedSearchTerm>),
292    Boolean(Equality, bool),
293}
294
295impl ValueTest {
296    fn matches(&self, value: ValueRef<'_>) -> bool {
297        match (self, value) {
298            // NOTE: `= *` matches any value, a concrete one too; the specification
299            // spells the wildcard as any value and the reference servers agree.
300            (Self::Concept { set: None, negated }, _) => !*negated,
301            (
302                Self::Concept {
303                    set: Some(set),
304                    negated,
305                },
306                ValueRef::Concept(target),
307            ) => set.contains(target.index()) != *negated,
308            (Self::Number(operator, expected), ValueRef::Number(text)) => text
309                .parse::<f64>()
310                .is_ok_and(|actual| compare_numbers(*operator, actual, *expected)),
311            (Self::Text(operator, terms), ValueRef::String(text)) => {
312                let hit = terms.iter().any(|term| term_matches(term, text));
313                hit == (*operator == Equality::Equal)
314            }
315            (Self::Boolean(operator, expected), ValueRef::String(text)) => {
316                let actual = text.eq_ignore_ascii_case("true");
317                let boolean = actual || text.eq_ignore_ascii_case("false");
318                boolean && ((actual == *expected) == (*operator == Equality::Equal))
319            }
320            _ => false,
321        }
322    }
323}
324
325fn compare_numbers(operator: Comparison, actual: f64, expected: f64) -> bool {
326    match operator {
327        Comparison::Equal => (actual - expected).abs() < f64::EPSILON,
328        Comparison::NotEqual => (actual - expected).abs() >= f64::EPSILON,
329        Comparison::Less => actual < expected,
330        Comparison::LessOrEqual => actual <= expected,
331        Comparison::Greater => actual > expected,
332        Comparison::GreaterOrEqual => actual >= expected,
333    }
334}
335
336fn compare_times(operator: Comparison, actual: u32, expected: u32) -> bool {
337    match operator {
338        Comparison::Equal => actual == expected,
339        Comparison::NotEqual => actual != expected,
340        Comparison::Less => actual < expected,
341        Comparison::LessOrEqual => actual <= expected,
342        Comparison::Greater => actual > expected,
343        Comparison::GreaterOrEqual => actual >= expected,
344    }
345}
346
347/// Whether `pattern`, with `*` for any run of characters and `\*`, `\"`,
348/// `\\` for the literal characters, matches the whole of `text`
349/// (case-insensitively, as description matching is).
350#[must_use]
351pub fn wild_matches(pattern: &str, text: &str) -> bool {
352    let pattern: Vec<char> = pattern.to_lowercase().chars().collect();
353    let text: Vec<char> = text.to_lowercase().chars().collect();
354    wild_at(&pattern, &text)
355}
356
357fn wild_at(pattern: &[char], text: &[char]) -> bool {
358    match pattern.split_first() {
359        None => text.is_empty(),
360        Some(('*', rest)) => (0..=text.len()).any(|skip| {
361            text.get(skip..)
362                .is_some_and(|remaining| wild_at(rest, remaining))
363        }),
364        Some(('\\', rest)) => {
365            let Some((escaped, after)) = rest.split_first() else {
366                return false;
367            };
368            text.split_first()
369                .is_some_and(|(first, remaining)| first == escaped && wild_at(after, remaining))
370        }
371        Some((expected, rest)) => text
372            .split_first()
373            .is_some_and(|(first, remaining)| first == expected && wild_at(rest, remaining)),
374    }
375}
376
377/// Whether every word of a match term is a prefix of some word of `text`,
378/// case-insensitively, or a wild pattern matches the whole text.
379#[must_use]
380pub fn term_matches(term: &TypedSearchTerm, text: &str) -> bool {
381    match term {
382        TypedSearchTerm::Match(words) => {
383            let lower = text.to_lowercase();
384            let text_words: Vec<&str> = lower
385                .split(|c: char| !c.is_alphanumeric())
386                .filter(|w| !w.is_empty())
387                .collect();
388            words.iter().all(|word| {
389                let word = unescape(word).to_lowercase();
390                text_words.iter().any(|t| t.starts_with(word.as_str()))
391            })
392        }
393        TypedSearchTerm::Wild(pattern) => wild_matches(pattern, text),
394    }
395}
396
397/// `\"` and `\\` to the character they escape.
398fn unescape(word: &str) -> String {
399    let mut out = String::with_capacity(word.len());
400    let mut chars = word.chars();
401    while let Some(c) = chars.next() {
402        if c == '\\' {
403            if let Some(next) = chars.next() {
404                out.push(next);
405            }
406        } else {
407            out.push(c);
408        }
409    }
410    out
411}
412
413/// Whether `count` satisfies a cardinality; `None` is the default `[1..*]`.
414fn within(cardinality: Option<Cardinality>, count: u32) -> bool {
415    let Cardinality { min, max } = cardinality.unwrap_or(Cardinality { min: 1, max: None });
416    count >= min && max.is_none_or(|max| count <= max)
417}
418
419impl<M: Model> Evaluator<'_, M> {
420    fn expression(&self, constraint: &ExpressionConstraint) -> Result<RoaringBitmap, EvalError> {
421        match constraint {
422            ExpressionConstraint::Sub(sub) => self.sub(sub),
423            ExpressionConstraint::Refined { focus, refinement } => {
424                let focus = self.sub(focus)?;
425                self.refinement(&focus, refinement)
426            }
427            ExpressionConstraint::Conjunction(operands) => {
428                let mut result: Option<RoaringBitmap> = None;
429                for operand in operands {
430                    let set = self.sub(operand)?;
431                    result = Some(match result {
432                        None => set,
433                        Some(current) => current & set,
434                    });
435                }
436                Ok(result.unwrap_or_default())
437            }
438            ExpressionConstraint::Disjunction(operands) => {
439                let mut result = RoaringBitmap::new();
440                for operand in operands {
441                    result |= self.sub(operand)?;
442                }
443                Ok(result)
444            }
445            ExpressionConstraint::Exclusion { left, right } => {
446                Ok(self.sub(left)? - self.sub(right)?)
447            }
448            ExpressionConstraint::Dotted { focus, attributes } => {
449                let mut set = self.sub(focus)?;
450                for attribute in attributes {
451                    let kinds = self.kinds(attribute)?;
452                    set = self.values_of(&set, &kinds);
453                }
454                Ok(set)
455            }
456        }
457    }
458
459    /// `subexpressionconstraint`: the focus, the member-of, the operator, then
460    /// the filters and the history supplement.
461    fn sub(&self, sub: &SubExpressionConstraint) -> Result<RoaringBitmap, EvalError> {
462        let mut set = self.focus(&sub.focus)?;
463        if let Some(member_of) = &sub.member_of {
464            set = self.member_of(&set, member_of.fields.as_ref(), &sub.member_filters)?;
465        }
466        if let Some(operator) = sub.operator {
467            set = self.operate(operator, &set, matches!(sub.focus, FocusConcept::Wildcard));
468        }
469        for filter in &sub.filters {
470            set = match filter {
471                FilterConstraint::Concept(filters) => {
472                    let predicates = self.concept_predicates(filters)?;
473                    self.model.filter_concepts(&set, &predicates)?
474                }
475                FilterConstraint::Description(filters) => {
476                    let predicates = self.description_predicates(filters)?;
477                    self.model.filter_descriptions(&set, &predicates)?
478                }
479            };
480        }
481        if let Some(history) = &sub.history {
482            set = self.history(set, history)?;
483        }
484        Ok(set)
485    }
486
487    fn focus(&self, focus: &FocusConcept) -> Result<RoaringBitmap, EvalError> {
488        match focus {
489            FocusConcept::Wildcard => Ok(self.model.all()),
490            FocusConcept::Reference(reference) => {
491                let ordinal = self
492                    .model
493                    .concept(reference.id)?
494                    .ok_or(EvalError::UnknownConcept(reference.id))?;
495                Ok(RoaringBitmap::from_iter([ordinal.index()]))
496            }
497            FocusConcept::AltIdentifier(alt) => {
498                let scheme = self
499                    .model
500                    .scheme(&alt.scheme)?
501                    .ok_or_else(|| EvalError::UnknownScheme(alt.scheme.clone()))?;
502                let ordinal = self
503                    .model
504                    .identifiers()
505                    .lookup(scheme, &alt.code)
506                    .ok_or_else(|| EvalError::UnknownIdentifier {
507                        scheme: alt.scheme.clone(),
508                        code: alt.code.clone(),
509                    })?;
510                Ok(RoaringBitmap::from_iter([ordinal.index()]))
511            }
512            FocusConcept::Nested(inner) => self.expression(inner),
513        }
514    }
515
516    /// The constraint operator over a set; the whole edition has the roots
517    /// and the leaves as its answers.
518    fn operate(
519        &self,
520        operator: ConstraintOperator,
521        set: &RoaringBitmap,
522        whole: bool,
523    ) -> RoaringBitmap {
524        if whole {
525            let all = self.model.all();
526            return match operator {
527                ConstraintOperator::DescendantOrSelfOf
528                | ConstraintOperator::AncestorOrSelfOf
529                | ConstraintOperator::ChildOrSelfOf
530                | ConstraintOperator::ParentOrSelfOf => all,
531                ConstraintOperator::DescendantOf | ConstraintOperator::ChildOf => {
532                    all - self.model.roots()
533                }
534                ConstraintOperator::AncestorOf | ConstraintOperator::ParentOf => {
535                    all - self.model.leaves()
536                }
537                ConstraintOperator::Top => self.model.roots(),
538                ConstraintOperator::Bottom => self.model.leaves(),
539            };
540        }
541        let mut out = RoaringBitmap::new();
542        match operator {
543            ConstraintOperator::Top => {
544                for concept in set {
545                    if self.model.ancestors(Ordinal::new(concept)).is_disjoint(set) {
546                        out.insert(concept);
547                    }
548                }
549                return out;
550            }
551            ConstraintOperator::Bottom => {
552                for concept in set {
553                    if self
554                        .model
555                        .descendants(Ordinal::new(concept))
556                        .is_disjoint(set)
557                    {
558                        out.insert(concept);
559                    }
560                }
561                return out;
562            }
563            _ => {}
564        }
565        for concept in set {
566            let ordinal = Ordinal::new(concept);
567            match operator {
568                ConstraintOperator::DescendantOf | ConstraintOperator::DescendantOrSelfOf => {
569                    out |= self.model.descendants(ordinal);
570                }
571                ConstraintOperator::AncestorOf | ConstraintOperator::AncestorOrSelfOf => {
572                    out |= self.model.ancestors(ordinal);
573                }
574                ConstraintOperator::ChildOf | ConstraintOperator::ChildOrSelfOf => {
575                    out |= self.model.children(ordinal);
576                }
577                ConstraintOperator::ParentOf | ConstraintOperator::ParentOrSelfOf => {
578                    out |= self.model.parents(ordinal);
579                }
580                ConstraintOperator::Top | ConstraintOperator::Bottom => {}
581            }
582        }
583        if matches!(
584            operator,
585            ConstraintOperator::DescendantOrSelfOf
586                | ConstraintOperator::AncestorOrSelfOf
587                | ConstraintOperator::ChildOrSelfOf
588                | ConstraintOperator::ParentOrSelfOf
589        ) {
590            out |= set;
591        }
592        out
593    }
594
595    /// `^`: the referenced components (or the selected fields) of the active
596    /// members of the reference sets in `set`, those rows passing the member
597    /// filters.
598    fn member_of(
599        &self,
600        set: &RoaringBitmap,
601        fields: Option<&RefsetFields>,
602        filter_groups: &[Vec<MemberFilter>],
603    ) -> Result<RoaringBitmap, EvalError> {
604        let mut out = RoaringBitmap::new();
605        for concept in set {
606            let Some(id) = self.model.sctid(Ordinal::new(concept))? else {
607                continue;
608            };
609            let Some(table) = self.model.members().table(id.0) else {
610                return Err(EvalError::NotAReferenceSet(id));
611            };
612            let columns = Self::selected_columns(id, table, fields)?;
613            for row in 0..table.len() {
614                if !self.row_passes(table, row, filter_groups)? {
615                    continue;
616                }
617                for column in &columns {
618                    match column {
619                        None => {
620                            if let Some(member) = table.concept(row) {
621                                out.insert(member.index());
622                            }
623                        }
624                        Some(field) => {
625                            if let Some(FieldRef::Concept(value)) = table.value(row, *field) {
626                                out.insert(value.index());
627                            }
628                        }
629                    }
630                }
631            }
632        }
633        Ok(out)
634    }
635
636    /// The columns a field selection names: `None` is the referenced
637    /// component, `Some(i)` a field; `[*]` is every concept-valued column.
638    fn selected_columns(
639        refset: Sctid,
640        table: &Table,
641        fields: Option<&RefsetFields>,
642    ) -> Result<Vec<Option<usize>>, EvalError> {
643        Ok(match fields {
644            None => vec![None],
645            Some(RefsetFields::Any) => {
646                let mut columns = vec![None];
647                columns.extend(
648                    table
649                        .kinds()
650                        .iter()
651                        .enumerate()
652                        .filter(|(_, kind)| **kind == concept_graph::refsets::FieldKind::Component)
653                        .map(|(i, _)| Some(i)),
654                );
655                columns
656            }
657            Some(RefsetFields::Names(names)) => {
658                let mut columns = Vec::new();
659                for name in names {
660                    if name.eq_ignore_ascii_case(REFERENCED_COMPONENT) {
661                        columns.push(None);
662                    } else {
663                        columns.push(Some(table.field(name).ok_or_else(|| {
664                            EvalError::UnknownField {
665                                refset,
666                                field: name.clone(),
667                            }
668                        })?));
669                    }
670                }
671                columns
672            }
673        })
674    }
675
676    /// Whether row `row` of `table` satisfies every member filter.
677    fn row_passes(
678        &self,
679        table: &Table,
680        row: usize,
681        filter_groups: &[Vec<MemberFilter>],
682    ) -> Result<bool, EvalError> {
683        for filter in filter_groups.iter().flatten() {
684            let passes = match filter {
685                MemberFilter::Active { operator, value } => {
686                    if *value != (*operator == Equality::Equal) {
687                        return Err(EvalError::Unsupported(
688                            "a member filter on inactive members: the tables hold active members",
689                        ));
690                    }
691                    true
692                }
693                MemberFilter::EffectiveTime { operator, values } => {
694                    let actual = table.effective_time(row).unwrap_or_default();
695                    times_match(*operator, values, actual)
696                }
697                MemberFilter::Module { operator, value } => {
698                    let modules = self.sctids_of(value)?;
699                    let inside = table.module(row).is_some_and(|m| modules.contains(&m));
700                    inside == (*operator == Equality::Equal)
701                }
702                MemberFilter::Field { name, value } => {
703                    let Some(column) = table.field(name) else {
704                        return Ok(false);
705                    };
706                    self.field_passes(table.value(row, column), value)?
707                }
708            };
709            if !passes {
710                return Ok(false);
711            }
712        }
713        Ok(true)
714    }
715
716    fn field_passes(
717        &self,
718        actual: Option<FieldRef<'_>>,
719        expected: &FieldValue,
720    ) -> Result<bool, EvalError> {
721        Ok(match (expected, actual) {
722            (FieldValue::Expression { operator, value }, Some(FieldRef::Concept(concept))) => {
723                let set = self.sub(value)?;
724                set.contains(concept.index()) == (*operator == Equality::Equal)
725            }
726            (FieldValue::Expression { operator, .. }, _) => *operator == Equality::NotEqual,
727            (FieldValue::Numeric { operator, value }, Some(FieldRef::Integer(actual))) => {
728                let expected: f64 = value.0.parse().unwrap_or(f64::NAN);
729                #[expect(
730                    clippy::as_conversions,
731                    clippy::cast_precision_loss,
732                    reason = "reference set integers are small"
733                )]
734                let actual = actual as f64;
735                compare_numbers(*operator, actual, expected)
736            }
737            (FieldValue::String { operator, terms }, Some(FieldRef::String(text))) => {
738                terms.iter().any(|t| term_matches(t, text)) == (*operator == Equality::Equal)
739            }
740            (FieldValue::Boolean { operator, value }, Some(FieldRef::String(text))) => {
741                let actual = text.eq_ignore_ascii_case("true");
742                (actual == *value) == (*operator == Equality::Equal)
743            }
744            (FieldValue::Time { operator, values }, Some(FieldRef::String(text))) => {
745                let actual: u32 = text.parse().unwrap_or_default();
746                times_match(*operator, values, actual)
747            }
748            (FieldValue::Time { operator, values }, Some(FieldRef::Integer(actual))) => {
749                let actual = u32::try_from(actual).unwrap_or_default();
750                times_match(*operator, values, actual)
751            }
752            _ => false,
753        })
754    }
755
756    /// The SCTIDs a concept set names: a plain reference or a reference set
757    /// as written (the metadata concepts need not be in the edition), any
758    /// other constraint by evaluation.
759    fn sctids_of(&self, set: &ConceptSet) -> Result<Vec<u64>, EvalError> {
760        match set {
761            ConceptSet::Set(references) => Ok(references.iter().map(|r| r.id.0).collect()),
762            ConceptSet::Expression(sub)
763                if sub.operator.is_none()
764                    && sub.member_of.is_none()
765                    && sub.filters.is_empty()
766                    && sub.member_filters.is_empty()
767                    && sub.history.is_none() =>
768            {
769                match &sub.focus {
770                    FocusConcept::Reference(reference) => Ok(vec![reference.id.0]),
771                    _ => self.sctids_of_set(&self.sub(sub)?),
772                }
773            }
774            ConceptSet::Expression(sub) => self.sctids_of_set(&self.sub(sub)?),
775        }
776    }
777
778    fn sctids_of_set(&self, set: &RoaringBitmap) -> Result<Vec<u64>, EvalError> {
779        let mut ids = Vec::new();
780        for concept in set {
781            if let Some(id) = self.model.sctid(Ordinal::new(concept))? {
782                ids.push(id.0);
783            }
784        }
785        Ok(ids)
786    }
787
788    fn acceptabilities(
789        set: Option<&crate::ast::AcceptabilitySet>,
790    ) -> Result<Vec<Acceptability>, EvalError> {
791        match set {
792            None => Ok(Vec::new()),
793            Some(crate::ast::AcceptabilitySet::Tokens(tokens)) => Ok(tokens.clone()),
794            Some(crate::ast::AcceptabilitySet::Concepts(references)) => references
795                .iter()
796                .map(|reference| match reference.id.0 {
797                    PREFERRED => Ok(Acceptability::Preferred),
798                    ACCEPTABLE => Ok(Acceptability::Acceptable),
799                    _ => Err(EvalError::Unsupported(
800                        "an acceptability concept other than preferred or acceptable",
801                    )),
802                })
803                .collect(),
804        }
805    }
806
807    /// The `{{ C ... }}` filters with their concept sets resolved.
808    fn concept_predicates(
809        &self,
810        filters: &[ConceptFilter],
811    ) -> Result<Vec<ConceptPredicate>, EvalError> {
812        let mut predicates = Vec::new();
813        for filter in filters {
814            predicates.push(match filter {
815                ConceptFilter::Active { operator, value } => {
816                    ConceptPredicate::Active(*value == (*operator == Equality::Equal))
817                }
818                ConceptFilter::DefinitionStatus { operator, tokens } => {
819                    let defined = tokens.contains(&DefinitionStatus::Defined);
820                    let primitive = tokens.contains(&DefinitionStatus::Primitive);
821                    let equal = *operator == Equality::Equal;
822                    ConceptPredicate::DefinitionStatus {
823                        defined: defined == equal,
824                        primitive: primitive == equal,
825                    }
826                }
827                ConceptFilter::DefinitionStatusId { operator, value } => {
828                    let ids = self.sctids_of(value)?;
829                    let equal = *operator == Equality::Equal;
830                    ConceptPredicate::DefinitionStatus {
831                        defined: ids.contains(&DEFINED) == equal,
832                        primitive: ids.contains(&PRIMITIVE) == equal,
833                    }
834                }
835                ConceptFilter::Module { operator, value } => ConceptPredicate::Module {
836                    modules: self.sctids_of(value)?,
837                    negated: *operator == Equality::NotEqual,
838                },
839                ConceptFilter::EffectiveTime { operator, values } => {
840                    ConceptPredicate::EffectiveTime {
841                        operator: *operator,
842                        values: values
843                            .iter()
844                            .map(|v| v.0.parse().unwrap_or_default())
845                            .collect(),
846                    }
847                }
848            });
849        }
850        Ok(predicates)
851    }
852
853    /// The `{{ D ... }}` filters with their concept sets and aliases resolved.
854    fn description_predicates(
855        &self,
856        filters: &[DescriptionFilter],
857    ) -> Result<Vec<DescriptionPredicate>, EvalError> {
858        let mut predicates = Vec::new();
859        for filter in filters {
860            predicates.push(match filter {
861                DescriptionFilter::Term { operator, terms } => DescriptionPredicate::Term {
862                    operator: *operator,
863                    terms: terms.clone(),
864                },
865                DescriptionFilter::Language { operator, codes } => DescriptionPredicate::Language {
866                    operator: *operator,
867                    codes: codes.clone(),
868                },
869                DescriptionFilter::TypeId { operator, value } => DescriptionPredicate::Type {
870                    operator: *operator,
871                    types: self.sctids_of(value)?,
872                },
873                DescriptionFilter::Type { operator, tokens } => DescriptionPredicate::Type {
874                    operator: *operator,
875                    types: tokens
876                        .iter()
877                        .map(|t| match t {
878                            TypeToken::Synonym => SYNONYM,
879                            TypeToken::FullySpecifiedName => FULLY_SPECIFIED_NAME,
880                            TypeToken::Definition => DEFINITION,
881                        })
882                        .collect(),
883                },
884                DescriptionFilter::DialectId {
885                    operator,
886                    value,
887                    acceptability,
888                } => {
889                    let shared = Self::acceptabilities(acceptability.as_ref())?;
890                    let dialects = match value {
891                        DialectIdValue::Expression(sub) => self
892                            .sctids_of(&ConceptSet::Expression(sub.clone()))?
893                            .into_iter()
894                            .map(|id| (id, shared.clone()))
895                            .collect(),
896                        DialectIdValue::Set(items) => {
897                            let mut out = Vec::new();
898                            for (reference, own) in items {
899                                let allowed = if own.is_some() {
900                                    Self::acceptabilities(own.as_ref())?
901                                } else {
902                                    shared.clone()
903                                };
904                                out.push((reference.id.0, allowed));
905                            }
906                            out
907                        }
908                    };
909                    DescriptionPredicate::Dialect {
910                        operator: *operator,
911                        dialects,
912                    }
913                }
914                DescriptionFilter::Dialect {
915                    operator,
916                    aliases,
917                    acceptability,
918                } => {
919                    let shared = Self::acceptabilities(acceptability.as_ref())?;
920                    let mut dialects = Vec::new();
921                    for alias in aliases {
922                        let refset = crate::dialects::refset(&alias.alias)
923                            .ok_or_else(|| EvalError::UnknownDialect(alias.alias.clone()))?;
924                        let allowed = if alias.acceptability.is_some() {
925                            Self::acceptabilities(alias.acceptability.as_ref())?
926                        } else {
927                            shared.clone()
928                        };
929                        dialects.push((refset, allowed));
930                    }
931                    DescriptionPredicate::Dialect {
932                        operator: *operator,
933                        dialects,
934                    }
935                }
936                DescriptionFilter::Module { .. } => {
937                    return Err(EvalError::Unsupported(
938                        "a description module filter: the store keeps no description module",
939                    ));
940                }
941                DescriptionFilter::EffectiveTime { .. } => {
942                    return Err(EvalError::Unsupported(
943                        "a description effective time filter: the store keeps no description time",
944                    ));
945                }
946                DescriptionFilter::Active { operator, value } => {
947                    DescriptionPredicate::Active(*value == (*operator == Equality::Equal))
948                }
949                DescriptionFilter::Id { operator, ids } => DescriptionPredicate::Id {
950                    operator: *operator,
951                    ids: ids.iter().map(|id| id.0).collect(),
952                },
953            });
954        }
955        Ok(predicates)
956    }
957
958    /// The history supplement: the inactive concepts whose association in the
959    /// chosen reference sets targets a member of `set`.
960    fn history(
961        &self,
962        mut set: RoaringBitmap,
963        history: &HistorySupplement,
964    ) -> Result<RoaringBitmap, EvalError> {
965        let refsets: Vec<u64> = match history {
966            HistorySupplement::Minimum => vec![SAME_AS],
967            HistorySupplement::Moderate => {
968                vec![SAME_AS, REPLACED_BY, WAS_A, PARTIALLY_EQUIVALENT_TO]
969            }
970            HistorySupplement::Default | HistorySupplement::Maximum => {
971                match self.model.concept(Sctid(HISTORICAL_ASSOCIATION))? {
972                    Some(root) => {
973                        let mut ids = Vec::new();
974                        for concept in self.model.descendants(root) {
975                            if let Some(id) = self.model.sctid(Ordinal::new(concept))? {
976                                ids.push(id.0);
977                            }
978                        }
979                        ids
980                    }
981                    None => Vec::new(),
982                }
983            }
984            HistorySupplement::Subset(constraint) => {
985                let mut ids = Vec::new();
986                for concept in self.expression(constraint)? {
987                    if let Some(id) = self.model.sctid(Ordinal::new(concept))? {
988                        ids.push(id.0);
989                    }
990                }
991                ids
992            }
993        };
994        let mut added = RoaringBitmap::new();
995        for refset in refsets {
996            let Some(table) = self.model.members().table(refset) else {
997                continue;
998            };
999            let Some(target) = table.field(TARGET_COMPONENT) else {
1000                continue;
1001            };
1002            for row in 0..table.len() {
1003                if let (Some(FieldRef::Concept(value)), Some(member)) =
1004                    (table.value(row, target), table.concept(row))
1005                    && set.contains(value.index())
1006                {
1007                    added.insert(member.index());
1008                }
1009            }
1010        }
1011        set |= added;
1012        Ok(set)
1013    }
1014
1015    /// The attribute type kinds an attribute name names.
1016    fn kinds(&self, name: &SubExpressionConstraint) -> Result<Kinds, EvalError> {
1017        if matches!(name.focus, FocusConcept::Wildcard)
1018            && name.operator.is_none()
1019            && name.filters.is_empty()
1020        {
1021            return Ok(Kinds::Any);
1022        }
1023        let types = self.sub(name)?;
1024        let attributes = self.model.attributes();
1025        let mut kinds = Vec::new();
1026        for concept in types {
1027            if let Some(id) = self.model.sctid(Ordinal::new(concept))?
1028                && let Some(kind) = attributes.kind(id.0)
1029            {
1030                kinds.push(kind);
1031            }
1032        }
1033        Ok(Kinds::These(kinds))
1034    }
1035
1036    /// The concept values of the attributes of `kinds` on the sources in `set`.
1037    fn values_of(&self, set: &RoaringBitmap, kinds: &Kinds) -> RoaringBitmap {
1038        let attributes = self.model.attributes();
1039        let mut out = RoaringBitmap::new();
1040        for source in set {
1041            for row in attributes.rows(Ordinal::new(source)) {
1042                if let ValueRef::Concept(target) = row.value
1043                    && kinds.contains(row.kind)
1044                {
1045                    out.insert(target.index());
1046                }
1047            }
1048        }
1049        out
1050    }
1051
1052    /// `eclrefinement` over the focus set.
1053    fn refinement(
1054        &self,
1055        focus: &RoaringBitmap,
1056        refinement: &Refinement,
1057    ) -> Result<RoaringBitmap, EvalError> {
1058        match refinement {
1059            Refinement::Single(one) => self.sub_refinement(focus, one),
1060            Refinement::Conjunction(items) => {
1061                let mut result = focus.clone();
1062                for item in items {
1063                    result &= self.sub_refinement(&result, item)?;
1064                }
1065                Ok(result)
1066            }
1067            Refinement::Disjunction(items) => {
1068                let mut result = RoaringBitmap::new();
1069                for item in items {
1070                    result |= self.sub_refinement(focus, item)?;
1071                }
1072                Ok(result)
1073            }
1074        }
1075    }
1076
1077    fn sub_refinement(
1078        &self,
1079        focus: &RoaringBitmap,
1080        item: &SubRefinement,
1081    ) -> Result<RoaringBitmap, EvalError> {
1082        match item {
1083            SubRefinement::AttributeSet(set) => self.attribute_set(focus, set),
1084            SubRefinement::Nested(inner) => self.refinement(focus, inner),
1085            SubRefinement::Group {
1086                cardinality,
1087                attributes,
1088            } => {
1089                let tests = self.compile_set(attributes)?;
1090                let graph = self.model.attributes();
1091                // A concept whose rows satisfy the set within one group satisfies
1092                // it over all its rows, so the ungrouped answer (the inverted
1093                // index) narrows the concepts whose groups are counted, unless
1094                // zero groups may match.
1095                let candidates = if cardinality.is_none_or(|c| c.min >= 1) {
1096                    self.attribute_set(focus, attributes)?
1097                } else {
1098                    focus.clone()
1099                };
1100                let mut out = RoaringBitmap::new();
1101                for concept in &candidates {
1102                    let rows: Vec<Row<'_>> = graph.rows(Ordinal::new(concept)).collect();
1103                    let mut groups: Vec<Vec<&Row<'_>>> = Vec::new();
1104                    let mut current: Option<u32> = None;
1105                    for row in &rows {
1106                        // NOTE: ECL treats each ungrouped relationship (group 0) as
1107                        // its own group; the rows arrive sorted by group.
1108                        if row.group == 0 || current != Some(row.group) {
1109                            groups.push(Vec::new());
1110                            current = Some(row.group);
1111                        }
1112                        if let Some(last) = groups.last_mut() {
1113                            last.push(row);
1114                        }
1115                    }
1116                    let matching = groups
1117                        .iter()
1118                        .filter(|group| set_holds(&tests, group))
1119                        .count();
1120                    if within(*cardinality, u32::try_from(matching).unwrap_or(u32::MAX)) {
1121                        out.insert(concept);
1122                    }
1123                }
1124                Ok(out)
1125            }
1126        }
1127    }
1128
1129    /// An ungrouped attribute set over the focus: each attribute is a set of
1130    /// sources (a fast path when the default cardinality and a concept value
1131    /// allow the inverted index, else a row scan), joined by the junction.
1132    fn attribute_set(
1133        &self,
1134        focus: &RoaringBitmap,
1135        set: &AttributeSet,
1136    ) -> Result<RoaringBitmap, EvalError> {
1137        match set {
1138            AttributeSet::Single(one) => self.sub_attribute_set(focus, one),
1139            AttributeSet::Conjunction(items) => {
1140                let mut result = focus.clone();
1141                for item in items {
1142                    result &= self.sub_attribute_set(&result, item)?;
1143                }
1144                Ok(result)
1145            }
1146            AttributeSet::Disjunction(items) => {
1147                let mut result = RoaringBitmap::new();
1148                for item in items {
1149                    result |= self.sub_attribute_set(focus, item)?;
1150                }
1151                Ok(result)
1152            }
1153        }
1154    }
1155
1156    fn sub_attribute_set(
1157        &self,
1158        focus: &RoaringBitmap,
1159        item: &SubAttributeSet,
1160    ) -> Result<RoaringBitmap, EvalError> {
1161        match item {
1162            SubAttributeSet::Attribute(attribute) => self.attribute(focus, attribute),
1163            SubAttributeSet::Nested(inner) => self.attribute_set(focus, inner),
1164        }
1165    }
1166
1167    /// One attribute over the focus, ungrouped.
1168    fn attribute(
1169        &self,
1170        focus: &RoaringBitmap,
1171        attribute: &Attribute,
1172    ) -> Result<RoaringBitmap, EvalError> {
1173        let test = self.compile(attribute)?;
1174        let graph = self.model.attributes();
1175        if attribute.reverse {
1176            return Ok(self.reverse(focus, &test));
1177        }
1178        let default = attribute.cardinality.is_none();
1179        if default
1180            && let ValueTest::Concept {
1181                set,
1182                negated: false,
1183            } = &test.value
1184        {
1185            let mut out = RoaringBitmap::new();
1186            for kind in test.kinds.iter(graph.types().len()) {
1187                match set {
1188                    None => {
1189                        if let Some(sources) = graph.sources_of_kind(kind) {
1190                            out |= sources;
1191                        }
1192                    }
1193                    Some(values) => {
1194                        for target in graph.targets_of_kind(kind) {
1195                            if values.contains(*target) {
1196                                out.extend(
1197                                    graph.sources(kind, Ordinal::new(*target)).iter().copied(),
1198                                );
1199                            }
1200                        }
1201                    }
1202                }
1203            }
1204            return Ok(out & focus);
1205        }
1206        let mut out = RoaringBitmap::new();
1207        for concept in focus {
1208            let count = graph
1209                .rows(Ordinal::new(concept))
1210                .filter(|row| test.matches(row))
1211                .count();
1212            if within(
1213                attribute.cardinality,
1214                u32::try_from(count).unwrap_or(u32::MAX),
1215            ) {
1216                out.insert(concept);
1217            }
1218        }
1219        Ok(out)
1220    }
1221
1222    /// `R attribute = X`: the values of the attribute whose sources are in the
1223    /// value set, counted per value for the cardinality, within the focus.
1224    fn reverse(&self, focus: &RoaringBitmap, test: &AttributeTest) -> RoaringBitmap {
1225        let graph = self.model.attributes();
1226        let sources = match &test.value {
1227            ValueTest::Concept {
1228                set: Some(set),
1229                negated: false,
1230            } => Some(set),
1231            _ => None,
1232        };
1233        let mut out = RoaringBitmap::new();
1234        for target in focus {
1235            let mut count = 0_u32;
1236            for kind in test.kinds.iter(graph.types().len()) {
1237                for source in graph.sources(kind, Ordinal::new(target)) {
1238                    if sources.is_none_or(|set| set.contains(*source)) {
1239                        count = count.saturating_add(1);
1240                    }
1241                }
1242            }
1243            if within(test.cardinality, count) {
1244                out.insert(target);
1245            }
1246        }
1247        out
1248    }
1249
1250    /// The compiled tests of an attribute set, for per-group evaluation.
1251    fn compile_set(&self, set: &AttributeSet) -> Result<CompiledSet, EvalError> {
1252        Ok(match set {
1253            AttributeSet::Single(one) => CompiledSet::Single(Box::new(self.compile_item(one)?)),
1254            AttributeSet::Conjunction(items) => CompiledSet::Conjunction(
1255                items
1256                    .iter()
1257                    .map(|i| self.compile_item(i))
1258                    .collect::<Result<_, _>>()?,
1259            ),
1260            AttributeSet::Disjunction(items) => CompiledSet::Disjunction(
1261                items
1262                    .iter()
1263                    .map(|i| self.compile_item(i))
1264                    .collect::<Result<_, _>>()?,
1265            ),
1266        })
1267    }
1268
1269    fn compile_item(&self, item: &SubAttributeSet) -> Result<CompiledItem, EvalError> {
1270        Ok(match item {
1271            SubAttributeSet::Attribute(attribute) => {
1272                CompiledItem::Attribute(self.compile(attribute)?)
1273            }
1274            SubAttributeSet::Nested(inner) => CompiledItem::Nested(self.compile_set(inner)?),
1275        })
1276    }
1277
1278    fn compile(&self, attribute: &Attribute) -> Result<AttributeTest, EvalError> {
1279        let kinds = self.kinds(&attribute.name)?;
1280        let value = match &attribute.value {
1281            AttributeValue::Expression { operator, value } => {
1282                let set = if matches!(value.focus, FocusConcept::Wildcard)
1283                    && value.operator.is_none()
1284                    && value.filters.is_empty()
1285                    && value.member_of.is_none()
1286                {
1287                    None
1288                } else {
1289                    Some(self.sub(value)?)
1290                };
1291                ValueTest::Concept {
1292                    set,
1293                    negated: *operator == Equality::NotEqual,
1294                }
1295            }
1296            AttributeValue::Numeric { operator, value } => {
1297                ValueTest::Number(*operator, value.0.parse().unwrap_or(f64::NAN))
1298            }
1299            AttributeValue::String { operator, terms } => ValueTest::Text(*operator, terms.clone()),
1300            AttributeValue::Boolean { operator, value } => ValueTest::Boolean(*operator, *value),
1301        };
1302        Ok(AttributeTest {
1303            kinds,
1304            value,
1305            cardinality: attribute.cardinality,
1306        })
1307    }
1308}
1309
1310/// Whether `values` (one time or a set) match `actual` under `operator`.
1311fn times_match(operator: Comparison, values: &[TimeValue], actual: u32) -> bool {
1312    let expected = values
1313        .iter()
1314        .map(|v| v.0.parse::<u32>().unwrap_or_default());
1315    match operator {
1316        Comparison::NotEqual => expected.clone().all(|e| actual != e),
1317        _ => expected
1318            .into_iter()
1319            .any(|e| compare_times(operator, actual, e)),
1320    }
1321}
1322
1323/// A compiled attribute: the type kinds, the value test, the cardinality.
1324#[derive(Debug)]
1325struct AttributeTest {
1326    kinds: Kinds,
1327    value: ValueTest,
1328    cardinality: Option<Cardinality>,
1329}
1330
1331impl AttributeTest {
1332    fn matches(&self, row: &Row<'_>) -> bool {
1333        self.kinds.contains(row.kind) && self.value.matches(row.value)
1334    }
1335}
1336
1337#[derive(Debug)]
1338enum CompiledSet {
1339    Single(Box<CompiledItem>),
1340    Conjunction(Vec<CompiledItem>),
1341    Disjunction(Vec<CompiledItem>),
1342}
1343
1344#[derive(Debug)]
1345enum CompiledItem {
1346    Attribute(AttributeTest),
1347    Nested(CompiledSet),
1348}
1349
1350/// Whether the rows of one group satisfy the set.
1351fn set_holds(set: &CompiledSet, rows: &[&Row<'_>]) -> bool {
1352    match set {
1353        CompiledSet::Single(item) => item_holds(item, rows),
1354        CompiledSet::Conjunction(items) => items.iter().all(|i| item_holds(i, rows)),
1355        CompiledSet::Disjunction(items) => items.iter().any(|i| item_holds(i, rows)),
1356    }
1357}
1358
1359fn item_holds(item: &CompiledItem, rows: &[&Row<'_>]) -> bool {
1360    match item {
1361        CompiledItem::Nested(set) => set_holds(set, rows),
1362        CompiledItem::Attribute(test) => {
1363            let count = rows.iter().filter(|row| test.matches(row)).count();
1364            within(test.cardinality, u32::try_from(count).unwrap_or(u32::MAX))
1365        }
1366    }
1367}
1368
1369#[cfg(test)]
1370mod tests {
1371    use super::{Cardinality, compare_numbers, term_matches, wild_matches, within};
1372    use crate::ast::{Comparison, TypedSearchTerm};
1373
1374    #[test]
1375    fn cardinalities_patterns_and_words_match_as_the_specification_says() {
1376        assert!(within(None, 1) && within(None, 5) && !within(None, 0));
1377        assert!(within(
1378            Some(Cardinality {
1379                min: 0,
1380                max: Some(0)
1381            }),
1382            0
1383        ));
1384        assert!(!within(
1385            Some(Cardinality {
1386                min: 0,
1387                max: Some(0)
1388            }),
1389            1
1390        ));
1391        assert!(within(Some(Cardinality { min: 2, max: None }), 2));
1392        assert!(wild_matches("cardi*opathy", "Cardiomyopathy"));
1393        assert!(!wild_matches("cardi*opathy", "Cardiomyopathy X"));
1394        assert!(wild_matches("*itis", "Bronchitis"));
1395        assert!(wild_matches("a\\*b", "A*B"));
1396        assert!(!wild_matches("a\\*b", "AXB"));
1397        let term = TypedSearchTerm::Match(vec![String::from("hea"), String::from("att")]);
1398        assert!(term_matches(&term, "Heart attack"));
1399        assert!(!term_matches(&term, "Heart"));
1400        assert!(compare_numbers(Comparison::GreaterOrEqual, 500.0, 500.0));
1401        assert!(!compare_numbers(Comparison::Less, 500.0, 500.0));
1402    }
1403}