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::cast_precision_loss,
731                    reason = "reference set integers are small"
732                )]
733                let actual = actual as f64;
734                compare_numbers(*operator, actual, expected)
735            }
736            (FieldValue::String { operator, terms }, Some(FieldRef::String(text))) => {
737                terms.iter().any(|t| term_matches(t, text)) == (*operator == Equality::Equal)
738            }
739            (FieldValue::Boolean { operator, value }, Some(FieldRef::String(text))) => {
740                let actual = text.eq_ignore_ascii_case("true");
741                (actual == *value) == (*operator == Equality::Equal)
742            }
743            (FieldValue::Time { operator, values }, Some(FieldRef::String(text))) => {
744                let actual: u32 = text.parse().unwrap_or_default();
745                times_match(*operator, values, actual)
746            }
747            (FieldValue::Time { operator, values }, Some(FieldRef::Integer(actual))) => {
748                let actual = u32::try_from(actual).unwrap_or_default();
749                times_match(*operator, values, actual)
750            }
751            _ => false,
752        })
753    }
754
755    /// The SCTIDs a concept set names: a plain reference or a reference set
756    /// as written (the metadata concepts need not be in the edition), any
757    /// other constraint by evaluation.
758    fn sctids_of(&self, set: &ConceptSet) -> Result<Vec<u64>, EvalError> {
759        match set {
760            ConceptSet::Set(references) => Ok(references.iter().map(|r| r.id.0).collect()),
761            ConceptSet::Expression(sub)
762                if sub.operator.is_none()
763                    && sub.member_of.is_none()
764                    && sub.filters.is_empty()
765                    && sub.member_filters.is_empty()
766                    && sub.history.is_none() =>
767            {
768                match &sub.focus {
769                    FocusConcept::Reference(reference) => Ok(vec![reference.id.0]),
770                    _ => self.sctids_of_set(&self.sub(sub)?),
771                }
772            }
773            ConceptSet::Expression(sub) => self.sctids_of_set(&self.sub(sub)?),
774        }
775    }
776
777    fn sctids_of_set(&self, set: &RoaringBitmap) -> Result<Vec<u64>, EvalError> {
778        let mut ids = Vec::new();
779        for concept in set {
780            if let Some(id) = self.model.sctid(Ordinal::new(concept))? {
781                ids.push(id.0);
782            }
783        }
784        Ok(ids)
785    }
786
787    fn acceptabilities(
788        set: Option<&crate::ast::AcceptabilitySet>,
789    ) -> Result<Vec<Acceptability>, EvalError> {
790        match set {
791            None => Ok(Vec::new()),
792            Some(crate::ast::AcceptabilitySet::Tokens(tokens)) => Ok(tokens.clone()),
793            Some(crate::ast::AcceptabilitySet::Concepts(references)) => references
794                .iter()
795                .map(|reference| match reference.id.0 {
796                    PREFERRED => Ok(Acceptability::Preferred),
797                    ACCEPTABLE => Ok(Acceptability::Acceptable),
798                    _ => Err(EvalError::Unsupported(
799                        "an acceptability concept other than preferred or acceptable",
800                    )),
801                })
802                .collect(),
803        }
804    }
805
806    /// The `{{ C ... }}` filters with their concept sets resolved.
807    fn concept_predicates(
808        &self,
809        filters: &[ConceptFilter],
810    ) -> Result<Vec<ConceptPredicate>, EvalError> {
811        let mut predicates = Vec::new();
812        for filter in filters {
813            predicates.push(match filter {
814                ConceptFilter::Active { operator, value } => {
815                    ConceptPredicate::Active(*value == (*operator == Equality::Equal))
816                }
817                ConceptFilter::DefinitionStatus { operator, tokens } => {
818                    let defined = tokens.contains(&DefinitionStatus::Defined);
819                    let primitive = tokens.contains(&DefinitionStatus::Primitive);
820                    let equal = *operator == Equality::Equal;
821                    ConceptPredicate::DefinitionStatus {
822                        defined: defined == equal,
823                        primitive: primitive == equal,
824                    }
825                }
826                ConceptFilter::DefinitionStatusId { operator, value } => {
827                    let ids = self.sctids_of(value)?;
828                    let equal = *operator == Equality::Equal;
829                    ConceptPredicate::DefinitionStatus {
830                        defined: ids.contains(&DEFINED) == equal,
831                        primitive: ids.contains(&PRIMITIVE) == equal,
832                    }
833                }
834                ConceptFilter::Module { operator, value } => ConceptPredicate::Module {
835                    modules: self.sctids_of(value)?,
836                    negated: *operator == Equality::NotEqual,
837                },
838                ConceptFilter::EffectiveTime { operator, values } => {
839                    ConceptPredicate::EffectiveTime {
840                        operator: *operator,
841                        values: values
842                            .iter()
843                            .map(|v| v.0.parse().unwrap_or_default())
844                            .collect(),
845                    }
846                }
847            });
848        }
849        Ok(predicates)
850    }
851
852    /// The `{{ D ... }}` filters with their concept sets and aliases resolved.
853    fn description_predicates(
854        &self,
855        filters: &[DescriptionFilter],
856    ) -> Result<Vec<DescriptionPredicate>, EvalError> {
857        let mut predicates = Vec::new();
858        for filter in filters {
859            predicates.push(match filter {
860                DescriptionFilter::Term { operator, terms } => DescriptionPredicate::Term {
861                    operator: *operator,
862                    terms: terms.clone(),
863                },
864                DescriptionFilter::Language { operator, codes } => DescriptionPredicate::Language {
865                    operator: *operator,
866                    codes: codes.clone(),
867                },
868                DescriptionFilter::TypeId { operator, value } => DescriptionPredicate::Type {
869                    operator: *operator,
870                    types: self.sctids_of(value)?,
871                },
872                DescriptionFilter::Type { operator, tokens } => DescriptionPredicate::Type {
873                    operator: *operator,
874                    types: tokens
875                        .iter()
876                        .map(|t| match t {
877                            TypeToken::Synonym => SYNONYM,
878                            TypeToken::FullySpecifiedName => FULLY_SPECIFIED_NAME,
879                            TypeToken::Definition => DEFINITION,
880                        })
881                        .collect(),
882                },
883                DescriptionFilter::DialectId {
884                    operator,
885                    value,
886                    acceptability,
887                } => {
888                    let shared = Self::acceptabilities(acceptability.as_ref())?;
889                    let dialects = match value {
890                        DialectIdValue::Expression(sub) => self
891                            .sctids_of(&ConceptSet::Expression(sub.clone()))?
892                            .into_iter()
893                            .map(|id| (id, shared.clone()))
894                            .collect(),
895                        DialectIdValue::Set(items) => {
896                            let mut out = Vec::new();
897                            for (reference, own) in items {
898                                let allowed = if own.is_some() {
899                                    Self::acceptabilities(own.as_ref())?
900                                } else {
901                                    shared.clone()
902                                };
903                                out.push((reference.id.0, allowed));
904                            }
905                            out
906                        }
907                    };
908                    DescriptionPredicate::Dialect {
909                        operator: *operator,
910                        dialects,
911                    }
912                }
913                DescriptionFilter::Dialect {
914                    operator,
915                    aliases,
916                    acceptability,
917                } => {
918                    let shared = Self::acceptabilities(acceptability.as_ref())?;
919                    let mut dialects = Vec::new();
920                    for alias in aliases {
921                        let refset = crate::dialects::refset(&alias.alias)
922                            .ok_or_else(|| EvalError::UnknownDialect(alias.alias.clone()))?;
923                        let allowed = if alias.acceptability.is_some() {
924                            Self::acceptabilities(alias.acceptability.as_ref())?
925                        } else {
926                            shared.clone()
927                        };
928                        dialects.push((refset, allowed));
929                    }
930                    DescriptionPredicate::Dialect {
931                        operator: *operator,
932                        dialects,
933                    }
934                }
935                DescriptionFilter::Module { .. } => {
936                    return Err(EvalError::Unsupported(
937                        "a description module filter: the store keeps no description module",
938                    ));
939                }
940                DescriptionFilter::EffectiveTime { .. } => {
941                    return Err(EvalError::Unsupported(
942                        "a description effective time filter: the store keeps no description time",
943                    ));
944                }
945                DescriptionFilter::Active { operator, value } => {
946                    DescriptionPredicate::Active(*value == (*operator == Equality::Equal))
947                }
948                DescriptionFilter::Id { operator, ids } => DescriptionPredicate::Id {
949                    operator: *operator,
950                    ids: ids.iter().map(|id| id.0).collect(),
951                },
952            });
953        }
954        Ok(predicates)
955    }
956
957    /// The history supplement: the inactive concepts whose association in the
958    /// chosen reference sets targets a member of `set`.
959    fn history(
960        &self,
961        mut set: RoaringBitmap,
962        history: &HistorySupplement,
963    ) -> Result<RoaringBitmap, EvalError> {
964        let refsets: Vec<u64> = match history {
965            HistorySupplement::Minimum => vec![SAME_AS],
966            HistorySupplement::Moderate => {
967                vec![SAME_AS, REPLACED_BY, WAS_A, PARTIALLY_EQUIVALENT_TO]
968            }
969            HistorySupplement::Default | HistorySupplement::Maximum => {
970                match self.model.concept(Sctid(HISTORICAL_ASSOCIATION))? {
971                    Some(root) => {
972                        let mut ids = Vec::new();
973                        for concept in self.model.descendants(root) {
974                            if let Some(id) = self.model.sctid(Ordinal::new(concept))? {
975                                ids.push(id.0);
976                            }
977                        }
978                        ids
979                    }
980                    None => Vec::new(),
981                }
982            }
983            HistorySupplement::Subset(constraint) => {
984                let mut ids = Vec::new();
985                for concept in self.expression(constraint)? {
986                    if let Some(id) = self.model.sctid(Ordinal::new(concept))? {
987                        ids.push(id.0);
988                    }
989                }
990                ids
991            }
992        };
993        let mut added = RoaringBitmap::new();
994        for refset in refsets {
995            let Some(table) = self.model.members().table(refset) else {
996                continue;
997            };
998            let Some(target) = table.field(TARGET_COMPONENT) else {
999                continue;
1000            };
1001            for row in 0..table.len() {
1002                if let (Some(FieldRef::Concept(value)), Some(member)) =
1003                    (table.value(row, target), table.concept(row))
1004                    && set.contains(value.index())
1005                {
1006                    added.insert(member.index());
1007                }
1008            }
1009        }
1010        set |= added;
1011        Ok(set)
1012    }
1013
1014    /// The attribute type kinds an attribute name names.
1015    fn kinds(&self, name: &SubExpressionConstraint) -> Result<Kinds, EvalError> {
1016        if matches!(name.focus, FocusConcept::Wildcard)
1017            && name.operator.is_none()
1018            && name.filters.is_empty()
1019        {
1020            return Ok(Kinds::Any);
1021        }
1022        let types = self.sub(name)?;
1023        let attributes = self.model.attributes();
1024        let mut kinds = Vec::new();
1025        for concept in types {
1026            if let Some(id) = self.model.sctid(Ordinal::new(concept))?
1027                && let Some(kind) = attributes.kind(id.0)
1028            {
1029                kinds.push(kind);
1030            }
1031        }
1032        Ok(Kinds::These(kinds))
1033    }
1034
1035    /// The concept values of the attributes of `kinds` on the sources in `set`.
1036    fn values_of(&self, set: &RoaringBitmap, kinds: &Kinds) -> RoaringBitmap {
1037        let attributes = self.model.attributes();
1038        let mut out = RoaringBitmap::new();
1039        for source in set {
1040            for row in attributes.rows(Ordinal::new(source)) {
1041                if let ValueRef::Concept(target) = row.value
1042                    && kinds.contains(row.kind)
1043                {
1044                    out.insert(target.index());
1045                }
1046            }
1047        }
1048        out
1049    }
1050
1051    /// `eclrefinement` over the focus set.
1052    fn refinement(
1053        &self,
1054        focus: &RoaringBitmap,
1055        refinement: &Refinement,
1056    ) -> Result<RoaringBitmap, EvalError> {
1057        match refinement {
1058            Refinement::Single(one) => self.sub_refinement(focus, one),
1059            Refinement::Conjunction(items) => {
1060                let mut result = focus.clone();
1061                for item in items {
1062                    result &= self.sub_refinement(&result, item)?;
1063                }
1064                Ok(result)
1065            }
1066            Refinement::Disjunction(items) => {
1067                let mut result = RoaringBitmap::new();
1068                for item in items {
1069                    result |= self.sub_refinement(focus, item)?;
1070                }
1071                Ok(result)
1072            }
1073        }
1074    }
1075
1076    fn sub_refinement(
1077        &self,
1078        focus: &RoaringBitmap,
1079        item: &SubRefinement,
1080    ) -> Result<RoaringBitmap, EvalError> {
1081        match item {
1082            SubRefinement::AttributeSet(set) => self.attribute_set(focus, set),
1083            SubRefinement::Nested(inner) => self.refinement(focus, inner),
1084            SubRefinement::Group {
1085                cardinality,
1086                attributes,
1087            } => {
1088                let tests = self.compile_set(attributes)?;
1089                let graph = self.model.attributes();
1090                // A concept whose rows satisfy the set within one group satisfies
1091                // it over all its rows, so the ungrouped answer (the inverted
1092                // index) narrows the concepts whose groups are counted, unless
1093                // zero groups may match.
1094                let candidates = if cardinality.is_none_or(|c| c.min >= 1) {
1095                    self.attribute_set(focus, attributes)?
1096                } else {
1097                    focus.clone()
1098                };
1099                let mut out = RoaringBitmap::new();
1100                for concept in &candidates {
1101                    let rows: Vec<Row<'_>> = graph.rows(Ordinal::new(concept)).collect();
1102                    let mut groups: Vec<Vec<&Row<'_>>> = Vec::new();
1103                    let mut current: Option<u32> = None;
1104                    for row in &rows {
1105                        // NOTE: ECL treats each ungrouped relationship (group 0) as
1106                        // its own group; the rows arrive sorted by group.
1107                        if row.group == 0 || current != Some(row.group) {
1108                            groups.push(Vec::new());
1109                            current = Some(row.group);
1110                        }
1111                        if let Some(last) = groups.last_mut() {
1112                            last.push(row);
1113                        }
1114                    }
1115                    let matching = groups
1116                        .iter()
1117                        .filter(|group| set_holds(&tests, group))
1118                        .count();
1119                    if within(*cardinality, u32::try_from(matching).unwrap_or(u32::MAX)) {
1120                        out.insert(concept);
1121                    }
1122                }
1123                Ok(out)
1124            }
1125        }
1126    }
1127
1128    /// An ungrouped attribute set over the focus: each attribute is a set of
1129    /// sources (a fast path when the default cardinality and a concept value
1130    /// allow the inverted index, else a row scan), joined by the junction.
1131    fn attribute_set(
1132        &self,
1133        focus: &RoaringBitmap,
1134        set: &AttributeSet,
1135    ) -> Result<RoaringBitmap, EvalError> {
1136        match set {
1137            AttributeSet::Single(one) => self.sub_attribute_set(focus, one),
1138            AttributeSet::Conjunction(items) => {
1139                let mut result = focus.clone();
1140                for item in items {
1141                    result &= self.sub_attribute_set(&result, item)?;
1142                }
1143                Ok(result)
1144            }
1145            AttributeSet::Disjunction(items) => {
1146                let mut result = RoaringBitmap::new();
1147                for item in items {
1148                    result |= self.sub_attribute_set(focus, item)?;
1149                }
1150                Ok(result)
1151            }
1152        }
1153    }
1154
1155    fn sub_attribute_set(
1156        &self,
1157        focus: &RoaringBitmap,
1158        item: &SubAttributeSet,
1159    ) -> Result<RoaringBitmap, EvalError> {
1160        match item {
1161            SubAttributeSet::Attribute(attribute) => self.attribute(focus, attribute),
1162            SubAttributeSet::Nested(inner) => self.attribute_set(focus, inner),
1163        }
1164    }
1165
1166    /// One attribute over the focus, ungrouped.
1167    fn attribute(
1168        &self,
1169        focus: &RoaringBitmap,
1170        attribute: &Attribute,
1171    ) -> Result<RoaringBitmap, EvalError> {
1172        let test = self.compile(attribute)?;
1173        let graph = self.model.attributes();
1174        if attribute.reverse {
1175            return Ok(self.reverse(focus, &test));
1176        }
1177        let default = attribute.cardinality.is_none();
1178        if default
1179            && let ValueTest::Concept {
1180                set,
1181                negated: false,
1182            } = &test.value
1183        {
1184            let mut out = RoaringBitmap::new();
1185            for kind in test.kinds.iter(graph.types().len()) {
1186                match set {
1187                    None => {
1188                        if let Some(sources) = graph.sources_of_kind(kind) {
1189                            out |= sources;
1190                        }
1191                    }
1192                    Some(values) => {
1193                        for target in graph.targets_of_kind(kind) {
1194                            if values.contains(*target) {
1195                                out.extend(
1196                                    graph.sources(kind, Ordinal::new(*target)).iter().copied(),
1197                                );
1198                            }
1199                        }
1200                    }
1201                }
1202            }
1203            return Ok(out & focus);
1204        }
1205        let mut out = RoaringBitmap::new();
1206        for concept in focus {
1207            let count = graph
1208                .rows(Ordinal::new(concept))
1209                .filter(|row| test.matches(row))
1210                .count();
1211            if within(
1212                attribute.cardinality,
1213                u32::try_from(count).unwrap_or(u32::MAX),
1214            ) {
1215                out.insert(concept);
1216            }
1217        }
1218        Ok(out)
1219    }
1220
1221    /// `R attribute = X`: the values of the attribute whose sources are in the
1222    /// value set, counted per value for the cardinality, within the focus.
1223    fn reverse(&self, focus: &RoaringBitmap, test: &AttributeTest) -> RoaringBitmap {
1224        let graph = self.model.attributes();
1225        let sources = match &test.value {
1226            ValueTest::Concept {
1227                set: Some(set),
1228                negated: false,
1229            } => Some(set),
1230            _ => None,
1231        };
1232        let mut out = RoaringBitmap::new();
1233        for target in focus {
1234            let mut count = 0_u32;
1235            for kind in test.kinds.iter(graph.types().len()) {
1236                for source in graph.sources(kind, Ordinal::new(target)) {
1237                    if sources.is_none_or(|set| set.contains(*source)) {
1238                        count = count.saturating_add(1);
1239                    }
1240                }
1241            }
1242            if within(test.cardinality, count) {
1243                out.insert(target);
1244            }
1245        }
1246        out
1247    }
1248
1249    /// The compiled tests of an attribute set, for per-group evaluation.
1250    fn compile_set(&self, set: &AttributeSet) -> Result<CompiledSet, EvalError> {
1251        Ok(match set {
1252            AttributeSet::Single(one) => CompiledSet::Single(Box::new(self.compile_item(one)?)),
1253            AttributeSet::Conjunction(items) => CompiledSet::Conjunction(
1254                items
1255                    .iter()
1256                    .map(|i| self.compile_item(i))
1257                    .collect::<Result<_, _>>()?,
1258            ),
1259            AttributeSet::Disjunction(items) => CompiledSet::Disjunction(
1260                items
1261                    .iter()
1262                    .map(|i| self.compile_item(i))
1263                    .collect::<Result<_, _>>()?,
1264            ),
1265        })
1266    }
1267
1268    fn compile_item(&self, item: &SubAttributeSet) -> Result<CompiledItem, EvalError> {
1269        Ok(match item {
1270            SubAttributeSet::Attribute(attribute) => {
1271                CompiledItem::Attribute(self.compile(attribute)?)
1272            }
1273            SubAttributeSet::Nested(inner) => CompiledItem::Nested(self.compile_set(inner)?),
1274        })
1275    }
1276
1277    fn compile(&self, attribute: &Attribute) -> Result<AttributeTest, EvalError> {
1278        let kinds = self.kinds(&attribute.name)?;
1279        let value = match &attribute.value {
1280            AttributeValue::Expression { operator, value } => {
1281                let set = if matches!(value.focus, FocusConcept::Wildcard)
1282                    && value.operator.is_none()
1283                    && value.filters.is_empty()
1284                    && value.member_of.is_none()
1285                {
1286                    None
1287                } else {
1288                    Some(self.sub(value)?)
1289                };
1290                ValueTest::Concept {
1291                    set,
1292                    negated: *operator == Equality::NotEqual,
1293                }
1294            }
1295            AttributeValue::Numeric { operator, value } => {
1296                ValueTest::Number(*operator, value.0.parse().unwrap_or(f64::NAN))
1297            }
1298            AttributeValue::String { operator, terms } => ValueTest::Text(*operator, terms.clone()),
1299            AttributeValue::Boolean { operator, value } => ValueTest::Boolean(*operator, *value),
1300        };
1301        Ok(AttributeTest {
1302            kinds,
1303            value,
1304            cardinality: attribute.cardinality,
1305        })
1306    }
1307}
1308
1309/// Whether `values` (one time or a set) match `actual` under `operator`.
1310fn times_match(operator: Comparison, values: &[TimeValue], actual: u32) -> bool {
1311    let expected = values
1312        .iter()
1313        .map(|v| v.0.parse::<u32>().unwrap_or_default());
1314    match operator {
1315        Comparison::NotEqual => expected.clone().all(|e| actual != e),
1316        _ => expected
1317            .into_iter()
1318            .any(|e| compare_times(operator, actual, e)),
1319    }
1320}
1321
1322/// A compiled attribute: the type kinds, the value test, the cardinality.
1323#[derive(Debug)]
1324struct AttributeTest {
1325    kinds: Kinds,
1326    value: ValueTest,
1327    cardinality: Option<Cardinality>,
1328}
1329
1330impl AttributeTest {
1331    fn matches(&self, row: &Row<'_>) -> bool {
1332        self.kinds.contains(row.kind) && self.value.matches(row.value)
1333    }
1334}
1335
1336#[derive(Debug)]
1337enum CompiledSet {
1338    Single(Box<CompiledItem>),
1339    Conjunction(Vec<CompiledItem>),
1340    Disjunction(Vec<CompiledItem>),
1341}
1342
1343#[derive(Debug)]
1344enum CompiledItem {
1345    Attribute(AttributeTest),
1346    Nested(CompiledSet),
1347}
1348
1349/// Whether the rows of one group satisfy the set.
1350fn set_holds(set: &CompiledSet, rows: &[&Row<'_>]) -> bool {
1351    match set {
1352        CompiledSet::Single(item) => item_holds(item, rows),
1353        CompiledSet::Conjunction(items) => items.iter().all(|i| item_holds(i, rows)),
1354        CompiledSet::Disjunction(items) => items.iter().any(|i| item_holds(i, rows)),
1355    }
1356}
1357
1358fn item_holds(item: &CompiledItem, rows: &[&Row<'_>]) -> bool {
1359    match item {
1360        CompiledItem::Nested(set) => set_holds(set, rows),
1361        CompiledItem::Attribute(test) => {
1362            let count = rows.iter().filter(|row| test.matches(row)).count();
1363            within(test.cardinality, u32::try_from(count).unwrap_or(u32::MAX))
1364        }
1365    }
1366}
1367
1368#[cfg(test)]
1369mod tests {
1370    use super::{Cardinality, compare_numbers, term_matches, wild_matches, within};
1371    use crate::ast::{Comparison, TypedSearchTerm};
1372
1373    #[test]
1374    fn cardinalities_patterns_and_words_match_as_the_specification_says() {
1375        assert!(within(None, 1) && within(None, 5) && !within(None, 0));
1376        assert!(within(
1377            Some(Cardinality {
1378                min: 0,
1379                max: Some(0)
1380            }),
1381            0
1382        ));
1383        assert!(!within(
1384            Some(Cardinality {
1385                min: 0,
1386                max: Some(0)
1387            }),
1388            1
1389        ));
1390        assert!(within(Some(Cardinality { min: 2, max: None }), 2));
1391        assert!(wild_matches("cardi*opathy", "Cardiomyopathy"));
1392        assert!(!wild_matches("cardi*opathy", "Cardiomyopathy X"));
1393        assert!(wild_matches("*itis", "Bronchitis"));
1394        assert!(wild_matches("a\\*b", "A*B"));
1395        assert!(!wild_matches("a\\*b", "AXB"));
1396        let term = TypedSearchTerm::Match(vec![String::from("hea"), String::from("att")]);
1397        assert!(term_matches(&term, "Heart attack"));
1398        assert!(!term_matches(&term, "Heart"));
1399        assert!(compare_numbers(Comparison::GreaterOrEqual, 500.0, 500.0));
1400        assert!(!compare_numbers(Comparison::Less, 500.0, 500.0));
1401    }
1402}