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            return self.operate_over_edition(operator);
526        }
527        match operator {
528            ConstraintOperator::Top => self.top_of_set(set),
529            ConstraintOperator::Bottom => self.bottom_of_set(set),
530            _ => {
531                let mut out = self.related(operator, set);
532                if matches!(
533                    operator,
534                    ConstraintOperator::DescendantOrSelfOf
535                        | ConstraintOperator::AncestorOrSelfOf
536                        | ConstraintOperator::ChildOrSelfOf
537                        | ConstraintOperator::ParentOrSelfOf
538                ) {
539                    out |= set;
540                }
541                out
542            }
543        }
544    }
545
546    /// The constraint operator over `*`: the whole edition, less the roots or
547    /// the leaves where the operator excludes them.
548    fn operate_over_edition(&self, operator: ConstraintOperator) -> RoaringBitmap {
549        let all = self.model.all();
550        match operator {
551            ConstraintOperator::DescendantOrSelfOf
552            | ConstraintOperator::AncestorOrSelfOf
553            | ConstraintOperator::ChildOrSelfOf
554            | ConstraintOperator::ParentOrSelfOf => all,
555            ConstraintOperator::DescendantOf | ConstraintOperator::ChildOf => {
556                all - self.model.roots()
557            }
558            ConstraintOperator::AncestorOf | ConstraintOperator::ParentOf => {
559                all - self.model.leaves()
560            }
561            ConstraintOperator::Top => self.model.roots(),
562            ConstraintOperator::Bottom => self.model.leaves(),
563        }
564    }
565
566    /// `!!>`: the members of the set with no ancestor in the set.
567    fn top_of_set(&self, set: &RoaringBitmap) -> RoaringBitmap {
568        let mut out = RoaringBitmap::new();
569        for concept in set {
570            if self.model.ancestors(Ordinal::new(concept)).is_disjoint(set) {
571                out.insert(concept);
572            }
573        }
574        out
575    }
576
577    /// `!!<`: the members of the set with no descendant in the set.
578    fn bottom_of_set(&self, set: &RoaringBitmap) -> RoaringBitmap {
579        let mut out = RoaringBitmap::new();
580        for concept in set {
581            if self
582                .model
583                .descendants(Ordinal::new(concept))
584                .is_disjoint(set)
585            {
586                out.insert(concept);
587            }
588        }
589        out
590    }
591
592    /// The concepts the hierarchy operator reaches from the set, without the
593    /// set itself.
594    fn related(&self, operator: ConstraintOperator, set: &RoaringBitmap) -> RoaringBitmap {
595        let mut out = RoaringBitmap::new();
596        for concept in set {
597            let ordinal = Ordinal::new(concept);
598            match operator {
599                ConstraintOperator::DescendantOf | ConstraintOperator::DescendantOrSelfOf => {
600                    out |= self.model.descendants(ordinal);
601                }
602                ConstraintOperator::AncestorOf | ConstraintOperator::AncestorOrSelfOf => {
603                    out |= self.model.ancestors(ordinal);
604                }
605                ConstraintOperator::ChildOf | ConstraintOperator::ChildOrSelfOf => {
606                    out |= self.model.children(ordinal);
607                }
608                ConstraintOperator::ParentOf | ConstraintOperator::ParentOrSelfOf => {
609                    out |= self.model.parents(ordinal);
610                }
611                ConstraintOperator::Top | ConstraintOperator::Bottom => {}
612            }
613        }
614        out
615    }
616
617    /// `^`: the referenced components (or the selected fields) of the active
618    /// members of the reference sets in `set`, those rows passing the member
619    /// filters.
620    fn member_of(
621        &self,
622        set: &RoaringBitmap,
623        fields: Option<&RefsetFields>,
624        filter_groups: &[Vec<MemberFilter>],
625    ) -> Result<RoaringBitmap, EvalError> {
626        let mut out = RoaringBitmap::new();
627        for concept in set {
628            let Some(id) = self.model.sctid(Ordinal::new(concept))? else {
629                continue;
630            };
631            let Some(table) = self.model.members().table(id.0) else {
632                return Err(EvalError::NotAReferenceSet(id));
633            };
634            out |= self.table_members(id, table, fields, filter_groups)?;
635        }
636        Ok(out)
637    }
638
639    /// The concepts one reference set contributes: the selected columns of
640    /// every row that passes the member filters.
641    fn table_members(
642        &self,
643        refset: Sctid,
644        table: &Table,
645        fields: Option<&RefsetFields>,
646        filter_groups: &[Vec<MemberFilter>],
647    ) -> Result<RoaringBitmap, EvalError> {
648        let columns = Self::selected_columns(refset, table, fields)?;
649        let mut out = RoaringBitmap::new();
650        for row in 0..table.len() {
651            if !self.row_passes(table, row, filter_groups)? {
652                continue;
653            }
654            for column in &columns {
655                if let Some(value) = Self::column_concept(table, row, *column) {
656                    out.insert(value.index());
657                }
658            }
659        }
660        Ok(out)
661    }
662
663    /// The concept one selected column of a row holds: the referenced
664    /// component for `None`, else the field's concept value.
665    fn column_concept(table: &Table, row: usize, column: Option<usize>) -> Option<Ordinal> {
666        match column {
667            None => table.concept(row),
668            Some(field) => match table.value(row, field) {
669                Some(FieldRef::Concept(value)) => Some(value),
670                _ => None,
671            },
672        }
673    }
674
675    /// The columns a field selection names: `None` is the referenced
676    /// component, `Some(i)` a field; `[*]` is every concept-valued column.
677    fn selected_columns(
678        refset: Sctid,
679        table: &Table,
680        fields: Option<&RefsetFields>,
681    ) -> Result<Vec<Option<usize>>, EvalError> {
682        Ok(match fields {
683            None => vec![None],
684            Some(RefsetFields::Any) => {
685                let mut columns = vec![None];
686                columns.extend(
687                    table
688                        .kinds()
689                        .iter()
690                        .enumerate()
691                        .filter(|(_, kind)| **kind == concept_graph::refsets::FieldKind::Component)
692                        .map(|(i, _)| Some(i)),
693                );
694                columns
695            }
696            Some(RefsetFields::Names(names)) => {
697                let mut columns = Vec::new();
698                for name in names {
699                    if name.eq_ignore_ascii_case(REFERENCED_COMPONENT) {
700                        columns.push(None);
701                    } else {
702                        columns.push(Some(table.field(name).ok_or_else(|| {
703                            EvalError::UnknownField {
704                                refset,
705                                field: name.clone(),
706                            }
707                        })?));
708                    }
709                }
710                columns
711            }
712        })
713    }
714
715    /// Whether row `row` of `table` satisfies every member filter.
716    fn row_passes(
717        &self,
718        table: &Table,
719        row: usize,
720        filter_groups: &[Vec<MemberFilter>],
721    ) -> Result<bool, EvalError> {
722        for filter in filter_groups.iter().flatten() {
723            let passes = match filter {
724                MemberFilter::Active { operator, value } => {
725                    if *value != (*operator == Equality::Equal) {
726                        return Err(EvalError::Unsupported(
727                            "a member filter on inactive members: the tables hold active members",
728                        ));
729                    }
730                    true
731                }
732                MemberFilter::EffectiveTime { operator, values } => {
733                    let actual = table.effective_time(row).unwrap_or_default();
734                    times_match(*operator, values, actual)
735                }
736                MemberFilter::Module { operator, value } => {
737                    let modules = self.sctids_of(value)?;
738                    let inside = table.module(row).is_some_and(|m| modules.contains(&m));
739                    inside == (*operator == Equality::Equal)
740                }
741                MemberFilter::Field { name, value } => {
742                    let Some(column) = table.field(name) else {
743                        return Ok(false);
744                    };
745                    self.field_passes(table.value(row, column), value)?
746                }
747            };
748            if !passes {
749                return Ok(false);
750            }
751        }
752        Ok(true)
753    }
754
755    fn field_passes(
756        &self,
757        actual: Option<FieldRef<'_>>,
758        expected: &FieldValue,
759    ) -> Result<bool, EvalError> {
760        Ok(match (expected, actual) {
761            (FieldValue::Expression { operator, value }, Some(FieldRef::Concept(concept))) => {
762                let set = self.sub(value)?;
763                set.contains(concept.index()) == (*operator == Equality::Equal)
764            }
765            (FieldValue::Expression { operator, .. }, _) => *operator == Equality::NotEqual,
766            (FieldValue::Numeric { operator, value }, Some(FieldRef::Integer(actual))) => {
767                let expected: f64 = value.0.parse().unwrap_or(f64::NAN);
768                #[expect(
769                    clippy::as_conversions,
770                    clippy::cast_precision_loss,
771                    reason = "reference set integers are small"
772                )]
773                let actual = actual as f64;
774                compare_numbers(*operator, actual, expected)
775            }
776            (FieldValue::String { operator, terms }, Some(FieldRef::String(text))) => {
777                terms.iter().any(|t| term_matches(t, text)) == (*operator == Equality::Equal)
778            }
779            (FieldValue::Boolean { operator, value }, Some(FieldRef::String(text))) => {
780                let actual = text.eq_ignore_ascii_case("true");
781                (actual == *value) == (*operator == Equality::Equal)
782            }
783            (FieldValue::Time { operator, values }, Some(FieldRef::String(text))) => {
784                let actual: u32 = text.parse().unwrap_or_default();
785                times_match(*operator, values, actual)
786            }
787            (FieldValue::Time { operator, values }, Some(FieldRef::Integer(actual))) => {
788                let actual = u32::try_from(actual).unwrap_or_default();
789                times_match(*operator, values, actual)
790            }
791            _ => false,
792        })
793    }
794
795    /// The SCTIDs a concept set names: a plain reference or a reference set
796    /// as written (the metadata concepts need not be in the edition), any
797    /// other constraint by evaluation.
798    fn sctids_of(&self, set: &ConceptSet) -> Result<Vec<u64>, EvalError> {
799        match set {
800            ConceptSet::Set(references) => Ok(references.iter().map(|r| r.id.0).collect()),
801            ConceptSet::Expression(sub)
802                if sub.operator.is_none()
803                    && sub.member_of.is_none()
804                    && sub.filters.is_empty()
805                    && sub.member_filters.is_empty()
806                    && sub.history.is_none() =>
807            {
808                match &sub.focus {
809                    FocusConcept::Reference(reference) => Ok(vec![reference.id.0]),
810                    _ => self.sctids_of_set(&self.sub(sub)?),
811                }
812            }
813            ConceptSet::Expression(sub) => self.sctids_of_set(&self.sub(sub)?),
814        }
815    }
816
817    fn sctids_of_set(&self, set: &RoaringBitmap) -> Result<Vec<u64>, EvalError> {
818        let mut ids = Vec::new();
819        for concept in set {
820            if let Some(id) = self.model.sctid(Ordinal::new(concept))? {
821                ids.push(id.0);
822            }
823        }
824        Ok(ids)
825    }
826
827    fn acceptabilities(
828        set: Option<&crate::ast::AcceptabilitySet>,
829    ) -> Result<Vec<Acceptability>, EvalError> {
830        match set {
831            None => Ok(Vec::new()),
832            Some(crate::ast::AcceptabilitySet::Tokens(tokens)) => Ok(tokens.clone()),
833            Some(crate::ast::AcceptabilitySet::Concepts(references)) => references
834                .iter()
835                .map(|reference| match reference.id.0 {
836                    PREFERRED => Ok(Acceptability::Preferred),
837                    ACCEPTABLE => Ok(Acceptability::Acceptable),
838                    _ => Err(EvalError::Unsupported(
839                        "an acceptability concept other than preferred or acceptable",
840                    )),
841                })
842                .collect(),
843        }
844    }
845
846    /// The `{{ C ... }}` filters with their concept sets resolved.
847    fn concept_predicates(
848        &self,
849        filters: &[ConceptFilter],
850    ) -> Result<Vec<ConceptPredicate>, EvalError> {
851        let mut predicates = Vec::new();
852        for filter in filters {
853            predicates.push(match filter {
854                ConceptFilter::Active { operator, value } => {
855                    ConceptPredicate::Active(*value == (*operator == Equality::Equal))
856                }
857                ConceptFilter::DefinitionStatus { operator, tokens } => {
858                    let defined = tokens.contains(&DefinitionStatus::Defined);
859                    let primitive = tokens.contains(&DefinitionStatus::Primitive);
860                    let equal = *operator == Equality::Equal;
861                    ConceptPredicate::DefinitionStatus {
862                        defined: defined == equal,
863                        primitive: primitive == equal,
864                    }
865                }
866                ConceptFilter::DefinitionStatusId { operator, value } => {
867                    let ids = self.sctids_of(value)?;
868                    let equal = *operator == Equality::Equal;
869                    ConceptPredicate::DefinitionStatus {
870                        defined: ids.contains(&DEFINED) == equal,
871                        primitive: ids.contains(&PRIMITIVE) == equal,
872                    }
873                }
874                ConceptFilter::Module { operator, value } => ConceptPredicate::Module {
875                    modules: self.sctids_of(value)?,
876                    negated: *operator == Equality::NotEqual,
877                },
878                ConceptFilter::EffectiveTime { operator, values } => {
879                    ConceptPredicate::EffectiveTime {
880                        operator: *operator,
881                        values: values
882                            .iter()
883                            .map(|v| v.0.parse().unwrap_or_default())
884                            .collect(),
885                    }
886                }
887            });
888        }
889        Ok(predicates)
890    }
891
892    /// The `{{ D ... }}` filters with their concept sets and aliases resolved.
893    fn description_predicates(
894        &self,
895        filters: &[DescriptionFilter],
896    ) -> Result<Vec<DescriptionPredicate>, EvalError> {
897        let mut predicates = Vec::new();
898        for filter in filters {
899            predicates.push(self.description_predicate(filter)?);
900        }
901        Ok(predicates)
902    }
903
904    /// One `{{ D ... }}` filter with its concept sets and aliases resolved.
905    fn description_predicate(
906        &self,
907        filter: &DescriptionFilter,
908    ) -> Result<DescriptionPredicate, EvalError> {
909        Ok(match filter {
910            DescriptionFilter::Term { operator, terms } => DescriptionPredicate::Term {
911                operator: *operator,
912                terms: terms.clone(),
913            },
914            DescriptionFilter::Language { operator, codes } => DescriptionPredicate::Language {
915                operator: *operator,
916                codes: codes.clone(),
917            },
918            DescriptionFilter::TypeId { operator, value } => DescriptionPredicate::Type {
919                operator: *operator,
920                types: self.sctids_of(value)?,
921            },
922            DescriptionFilter::Type { operator, tokens } => DescriptionPredicate::Type {
923                operator: *operator,
924                types: tokens
925                    .iter()
926                    .map(|t| match t {
927                        TypeToken::Synonym => SYNONYM,
928                        TypeToken::FullySpecifiedName => FULLY_SPECIFIED_NAME,
929                        TypeToken::Definition => DEFINITION,
930                    })
931                    .collect(),
932            },
933            DescriptionFilter::DialectId {
934                operator,
935                value,
936                acceptability,
937            } => DescriptionPredicate::Dialect {
938                operator: *operator,
939                dialects: self.dialects_by_id(value, acceptability.as_ref())?,
940            },
941            DescriptionFilter::Dialect {
942                operator,
943                aliases,
944                acceptability,
945            } => DescriptionPredicate::Dialect {
946                operator: *operator,
947                dialects: Self::dialects_by_alias(aliases, acceptability.as_ref())?,
948            },
949            DescriptionFilter::Module { .. } => {
950                return Err(EvalError::Unsupported(
951                    "a description module filter: the store keeps no description module",
952                ));
953            }
954            DescriptionFilter::EffectiveTime { .. } => {
955                return Err(EvalError::Unsupported(
956                    "a description effective time filter: the store keeps no description time",
957                ));
958            }
959            DescriptionFilter::Active { operator, value } => {
960                DescriptionPredicate::Active(*value == (*operator == Equality::Equal))
961            }
962            DescriptionFilter::Id { operator, ids } => DescriptionPredicate::Id {
963                operator: *operator,
964                ids: ids.iter().map(|id| id.0).collect(),
965            },
966        })
967    }
968
969    /// The language reference sets a `dialectId` filter names, each with the
970    /// acceptabilities it admits; an entry's own set wins over the shared one.
971    fn dialects_by_id(
972        &self,
973        value: &DialectIdValue,
974        acceptability: Option<&crate::ast::AcceptabilitySet>,
975    ) -> Result<Vec<(u64, Vec<Acceptability>)>, EvalError> {
976        let shared = Self::acceptabilities(acceptability)?;
977        match value {
978            DialectIdValue::Expression(sub) => Ok(self
979                .sctids_of(&ConceptSet::Expression(sub.clone()))?
980                .into_iter()
981                .map(|id| (id, shared.clone()))
982                .collect()),
983            DialectIdValue::Set(items) => {
984                let mut out = Vec::new();
985                for (reference, own) in items {
986                    let allowed = if own.is_some() {
987                        Self::acceptabilities(own.as_ref())?
988                    } else {
989                        shared.clone()
990                    };
991                    out.push((reference.id.0, allowed));
992                }
993                Ok(out)
994            }
995        }
996    }
997
998    /// The language reference sets a `dialect` filter's aliases name, each
999    /// with the acceptabilities it admits.
1000    fn dialects_by_alias(
1001        aliases: &[crate::ast::DialectAlias],
1002        acceptability: Option<&crate::ast::AcceptabilitySet>,
1003    ) -> Result<Vec<(u64, Vec<Acceptability>)>, EvalError> {
1004        let shared = Self::acceptabilities(acceptability)?;
1005        let mut dialects = Vec::new();
1006        for alias in aliases {
1007            let refset = crate::dialects::refset(&alias.alias)
1008                .ok_or_else(|| EvalError::UnknownDialect(alias.alias.clone()))?;
1009            let allowed = if alias.acceptability.is_some() {
1010                Self::acceptabilities(alias.acceptability.as_ref())?
1011            } else {
1012                shared.clone()
1013            };
1014            dialects.push((refset, allowed));
1015        }
1016        Ok(dialects)
1017    }
1018
1019    /// The history supplement: the inactive concepts whose association in the
1020    /// chosen reference sets targets a member of `set`.
1021    fn history(
1022        &self,
1023        mut set: RoaringBitmap,
1024        history: &HistorySupplement,
1025    ) -> Result<RoaringBitmap, EvalError> {
1026        let refsets = self.history_refsets(history)?;
1027        let added = self.associated_members(&set, &refsets);
1028        set |= added;
1029        Ok(set)
1030    }
1031
1032    /// The association reference sets a history supplement follows.
1033    fn history_refsets(&self, history: &HistorySupplement) -> Result<Vec<u64>, EvalError> {
1034        match history {
1035            HistorySupplement::Minimum => Ok(vec![SAME_AS]),
1036            HistorySupplement::Moderate => {
1037                Ok(vec![SAME_AS, REPLACED_BY, WAS_A, PARTIALLY_EQUIVALENT_TO])
1038            }
1039            HistorySupplement::Default | HistorySupplement::Maximum => {
1040                match self.model.concept(Sctid(HISTORICAL_ASSOCIATION))? {
1041                    Some(root) => self.sctids_of_set(self.model.descendants(root)),
1042                    None => Ok(Vec::new()),
1043                }
1044            }
1045            HistorySupplement::Subset(constraint) => {
1046                self.sctids_of_set(&self.expression(constraint)?)
1047            }
1048        }
1049    }
1050
1051    /// The members of the association reference sets whose target lies in
1052    /// `set`.
1053    fn associated_members(&self, set: &RoaringBitmap, refsets: &[u64]) -> RoaringBitmap {
1054        let mut added = RoaringBitmap::new();
1055        for refset in refsets {
1056            let Some(table) = self.model.members().table(*refset) else {
1057                continue;
1058            };
1059            let Some(target) = table.field(TARGET_COMPONENT) else {
1060                continue;
1061            };
1062            added |= Self::members_targeting(table, target, set);
1063        }
1064        added
1065    }
1066
1067    /// The members of one association table whose target field lies in `set`.
1068    fn members_targeting(table: &Table, target: usize, set: &RoaringBitmap) -> RoaringBitmap {
1069        let mut out = RoaringBitmap::new();
1070        for row in 0..table.len() {
1071            if let (Some(FieldRef::Concept(value)), Some(member)) =
1072                (table.value(row, target), table.concept(row))
1073                && set.contains(value.index())
1074            {
1075                out.insert(member.index());
1076            }
1077        }
1078        out
1079    }
1080
1081    /// The attribute type kinds an attribute name names.
1082    fn kinds(&self, name: &SubExpressionConstraint) -> Result<Kinds, EvalError> {
1083        if matches!(name.focus, FocusConcept::Wildcard)
1084            && name.operator.is_none()
1085            && name.filters.is_empty()
1086        {
1087            return Ok(Kinds::Any);
1088        }
1089        let types = self.sub(name)?;
1090        let attributes = self.model.attributes();
1091        let mut kinds = Vec::new();
1092        for concept in types {
1093            if let Some(id) = self.model.sctid(Ordinal::new(concept))?
1094                && let Some(kind) = attributes.kind(id.0)
1095            {
1096                kinds.push(kind);
1097            }
1098        }
1099        Ok(Kinds::These(kinds))
1100    }
1101
1102    /// The concept values of the attributes of `kinds` on the sources in `set`.
1103    fn values_of(&self, set: &RoaringBitmap, kinds: &Kinds) -> RoaringBitmap {
1104        let attributes = self.model.attributes();
1105        let mut out = RoaringBitmap::new();
1106        for source in set {
1107            for row in attributes.rows(Ordinal::new(source)) {
1108                if let ValueRef::Concept(target) = row.value
1109                    && kinds.contains(row.kind)
1110                {
1111                    out.insert(target.index());
1112                }
1113            }
1114        }
1115        out
1116    }
1117
1118    /// `eclrefinement` over the focus set.
1119    fn refinement(
1120        &self,
1121        focus: &RoaringBitmap,
1122        refinement: &Refinement,
1123    ) -> Result<RoaringBitmap, EvalError> {
1124        match refinement {
1125            Refinement::Single(one) => self.sub_refinement(focus, one),
1126            Refinement::Conjunction(items) => {
1127                let mut result = focus.clone();
1128                for item in items {
1129                    result &= self.sub_refinement(&result, item)?;
1130                }
1131                Ok(result)
1132            }
1133            Refinement::Disjunction(items) => {
1134                let mut result = RoaringBitmap::new();
1135                for item in items {
1136                    result |= self.sub_refinement(focus, item)?;
1137                }
1138                Ok(result)
1139            }
1140        }
1141    }
1142
1143    fn sub_refinement(
1144        &self,
1145        focus: &RoaringBitmap,
1146        item: &SubRefinement,
1147    ) -> Result<RoaringBitmap, EvalError> {
1148        match item {
1149            SubRefinement::AttributeSet(set) => self.attribute_set(focus, set),
1150            SubRefinement::Nested(inner) => self.refinement(focus, inner),
1151            SubRefinement::Group {
1152                cardinality,
1153                attributes,
1154            } => self.attribute_group(focus, *cardinality, attributes),
1155        }
1156    }
1157
1158    /// `eclattributegroup`: the concepts whose relationship groups satisfy the
1159    /// attribute set as often as the cardinality demands.
1160    fn attribute_group(
1161        &self,
1162        focus: &RoaringBitmap,
1163        cardinality: Option<Cardinality>,
1164        attributes: &AttributeSet,
1165    ) -> Result<RoaringBitmap, EvalError> {
1166        let tests = self.compile_set(attributes)?;
1167        let graph = self.model.attributes();
1168        // A concept whose rows satisfy the set within one group satisfies
1169        // it over all its rows, so the ungrouped answer (the inverted
1170        // index) narrows the concepts whose groups are counted, unless
1171        // zero groups may match.
1172        let candidates = if cardinality.is_none_or(|c| c.min >= 1) {
1173            self.attribute_set(focus, attributes)?
1174        } else {
1175            focus.clone()
1176        };
1177        let mut out = RoaringBitmap::new();
1178        for concept in &candidates {
1179            let rows: Vec<Row<'_>> = graph.rows(Ordinal::new(concept)).collect();
1180            let matching = relationship_groups(&rows)
1181                .iter()
1182                .filter(|group| set_holds(&tests, group))
1183                .count();
1184            if within(cardinality, u32::try_from(matching).unwrap_or(u32::MAX)) {
1185                out.insert(concept);
1186            }
1187        }
1188        Ok(out)
1189    }
1190
1191    /// An ungrouped attribute set over the focus: each attribute is a set of
1192    /// sources (a fast path when the default cardinality and a concept value
1193    /// allow the inverted index, else a row scan), joined by the junction.
1194    fn attribute_set(
1195        &self,
1196        focus: &RoaringBitmap,
1197        set: &AttributeSet,
1198    ) -> Result<RoaringBitmap, EvalError> {
1199        match set {
1200            AttributeSet::Single(one) => self.sub_attribute_set(focus, one),
1201            AttributeSet::Conjunction(items) => {
1202                let mut result = focus.clone();
1203                for item in items {
1204                    result &= self.sub_attribute_set(&result, item)?;
1205                }
1206                Ok(result)
1207            }
1208            AttributeSet::Disjunction(items) => {
1209                let mut result = RoaringBitmap::new();
1210                for item in items {
1211                    result |= self.sub_attribute_set(focus, item)?;
1212                }
1213                Ok(result)
1214            }
1215        }
1216    }
1217
1218    fn sub_attribute_set(
1219        &self,
1220        focus: &RoaringBitmap,
1221        item: &SubAttributeSet,
1222    ) -> Result<RoaringBitmap, EvalError> {
1223        match item {
1224            SubAttributeSet::Attribute(attribute) => self.attribute(focus, attribute),
1225            SubAttributeSet::Nested(inner) => self.attribute_set(focus, inner),
1226        }
1227    }
1228
1229    /// One attribute over the focus, ungrouped.
1230    fn attribute(
1231        &self,
1232        focus: &RoaringBitmap,
1233        attribute: &Attribute,
1234    ) -> Result<RoaringBitmap, EvalError> {
1235        let test = self.compile(attribute)?;
1236        if attribute.reverse {
1237            return Ok(self.reverse(focus, &test));
1238        }
1239        let default = attribute.cardinality.is_none();
1240        if default
1241            && let ValueTest::Concept {
1242                set,
1243                negated: false,
1244            } = &test.value
1245        {
1246            let sources = match set {
1247                None => self.sources_of_kinds(&test.kinds),
1248                Some(values) => self.sources_of_values(&test.kinds, values),
1249            };
1250            return Ok(sources & focus);
1251        }
1252        let graph = self.model.attributes();
1253        let mut out = RoaringBitmap::new();
1254        for concept in focus {
1255            let count = graph
1256                .rows(Ordinal::new(concept))
1257                .filter(|row| test.matches(row))
1258                .count();
1259            if within(
1260                attribute.cardinality,
1261                u32::try_from(count).unwrap_or(u32::MAX),
1262            ) {
1263                out.insert(concept);
1264            }
1265        }
1266        Ok(out)
1267    }
1268
1269    /// The concepts carrying an attribute of one of `kinds` with any value,
1270    /// from the inverted index.
1271    fn sources_of_kinds(&self, kinds: &Kinds) -> RoaringBitmap {
1272        let graph = self.model.attributes();
1273        let mut out = RoaringBitmap::new();
1274        for kind in kinds.iter(graph.types().len()) {
1275            if let Some(sources) = graph.sources_of_kind(kind) {
1276                out |= sources;
1277            }
1278        }
1279        out
1280    }
1281
1282    /// The concepts carrying an attribute of one of `kinds` whose value is in
1283    /// `values`, from the inverted index.
1284    fn sources_of_values(&self, kinds: &Kinds, values: &RoaringBitmap) -> RoaringBitmap {
1285        let graph = self.model.attributes();
1286        let mut out = RoaringBitmap::new();
1287        for kind in kinds.iter(graph.types().len()) {
1288            for target in graph.targets_of_kind(kind) {
1289                if values.contains(*target) {
1290                    out.extend(graph.sources(kind, Ordinal::new(*target)).iter().copied());
1291                }
1292            }
1293        }
1294        out
1295    }
1296
1297    /// `R attribute = X`: the values of the attribute whose sources are in the
1298    /// value set, counted per value for the cardinality, within the focus.
1299    fn reverse(&self, focus: &RoaringBitmap, test: &AttributeTest) -> RoaringBitmap {
1300        let graph = self.model.attributes();
1301        let sources = match &test.value {
1302            ValueTest::Concept {
1303                set: Some(set),
1304                negated: false,
1305            } => Some(set),
1306            _ => None,
1307        };
1308        let mut out = RoaringBitmap::new();
1309        for target in focus {
1310            let mut count = 0_u32;
1311            for kind in test.kinds.iter(graph.types().len()) {
1312                for source in graph.sources(kind, Ordinal::new(target)) {
1313                    if sources.is_none_or(|set| set.contains(*source)) {
1314                        count = count.saturating_add(1);
1315                    }
1316                }
1317            }
1318            if within(test.cardinality, count) {
1319                out.insert(target);
1320            }
1321        }
1322        out
1323    }
1324
1325    /// The compiled tests of an attribute set, for per-group evaluation.
1326    fn compile_set(&self, set: &AttributeSet) -> Result<CompiledSet, EvalError> {
1327        Ok(match set {
1328            AttributeSet::Single(one) => CompiledSet::Single(Box::new(self.compile_item(one)?)),
1329            AttributeSet::Conjunction(items) => CompiledSet::Conjunction(
1330                items
1331                    .iter()
1332                    .map(|i| self.compile_item(i))
1333                    .collect::<Result<_, _>>()?,
1334            ),
1335            AttributeSet::Disjunction(items) => CompiledSet::Disjunction(
1336                items
1337                    .iter()
1338                    .map(|i| self.compile_item(i))
1339                    .collect::<Result<_, _>>()?,
1340            ),
1341        })
1342    }
1343
1344    fn compile_item(&self, item: &SubAttributeSet) -> Result<CompiledItem, EvalError> {
1345        Ok(match item {
1346            SubAttributeSet::Attribute(attribute) => {
1347                CompiledItem::Attribute(self.compile(attribute)?)
1348            }
1349            SubAttributeSet::Nested(inner) => CompiledItem::Nested(self.compile_set(inner)?),
1350        })
1351    }
1352
1353    fn compile(&self, attribute: &Attribute) -> Result<AttributeTest, EvalError> {
1354        let kinds = self.kinds(&attribute.name)?;
1355        let value = match &attribute.value {
1356            AttributeValue::Expression { operator, value } => {
1357                let set = if matches!(value.focus, FocusConcept::Wildcard)
1358                    && value.operator.is_none()
1359                    && value.filters.is_empty()
1360                    && value.member_of.is_none()
1361                {
1362                    None
1363                } else {
1364                    Some(self.sub(value)?)
1365                };
1366                ValueTest::Concept {
1367                    set,
1368                    negated: *operator == Equality::NotEqual,
1369                }
1370            }
1371            AttributeValue::Numeric { operator, value } => {
1372                ValueTest::Number(*operator, value.0.parse().unwrap_or(f64::NAN))
1373            }
1374            AttributeValue::String { operator, terms } => ValueTest::Text(*operator, terms.clone()),
1375            AttributeValue::Boolean { operator, value } => ValueTest::Boolean(*operator, *value),
1376        };
1377        Ok(AttributeTest {
1378            kinds,
1379            value,
1380            cardinality: attribute.cardinality,
1381        })
1382    }
1383}
1384
1385/// Whether `values` (one time or a set) match `actual` under `operator`.
1386fn times_match(operator: Comparison, values: &[TimeValue], actual: u32) -> bool {
1387    let expected = values
1388        .iter()
1389        .map(|v| v.0.parse::<u32>().unwrap_or_default());
1390    match operator {
1391        Comparison::NotEqual => expected.clone().all(|e| actual != e),
1392        _ => expected
1393            .into_iter()
1394            .any(|e| compare_times(operator, actual, e)),
1395    }
1396}
1397
1398/// A compiled attribute: the type kinds, the value test, the cardinality.
1399#[derive(Debug)]
1400struct AttributeTest {
1401    kinds: Kinds,
1402    value: ValueTest,
1403    cardinality: Option<Cardinality>,
1404}
1405
1406impl AttributeTest {
1407    fn matches(&self, row: &Row<'_>) -> bool {
1408        self.kinds.contains(row.kind) && self.value.matches(row.value)
1409    }
1410}
1411
1412#[derive(Debug)]
1413enum CompiledSet {
1414    Single(Box<CompiledItem>),
1415    Conjunction(Vec<CompiledItem>),
1416    Disjunction(Vec<CompiledItem>),
1417}
1418
1419#[derive(Debug)]
1420enum CompiledItem {
1421    Attribute(AttributeTest),
1422    Nested(CompiledSet),
1423}
1424
1425/// The relationship groups of one concept, in row order.
1426fn relationship_groups<'row, 'graph>(rows: &'row [Row<'graph>]) -> Vec<Vec<&'row Row<'graph>>> {
1427    let mut groups: Vec<Vec<&Row<'_>>> = Vec::new();
1428    let mut current: Option<u32> = None;
1429    for row in rows {
1430        // NOTE: ECL treats each ungrouped relationship (group 0) as
1431        // its own group; the rows arrive sorted by group.
1432        if row.group == 0 || current != Some(row.group) {
1433            groups.push(Vec::new());
1434            current = Some(row.group);
1435        }
1436        if let Some(last) = groups.last_mut() {
1437            last.push(row);
1438        }
1439    }
1440    groups
1441}
1442
1443/// Whether the rows of one group satisfy the set.
1444fn set_holds(set: &CompiledSet, rows: &[&Row<'_>]) -> bool {
1445    match set {
1446        CompiledSet::Single(item) => item_holds(item, rows),
1447        CompiledSet::Conjunction(items) => items.iter().all(|i| item_holds(i, rows)),
1448        CompiledSet::Disjunction(items) => items.iter().any(|i| item_holds(i, rows)),
1449    }
1450}
1451
1452fn item_holds(item: &CompiledItem, rows: &[&Row<'_>]) -> bool {
1453    match item {
1454        CompiledItem::Nested(set) => set_holds(set, rows),
1455        CompiledItem::Attribute(test) => {
1456            let count = rows.iter().filter(|row| test.matches(row)).count();
1457            within(test.cardinality, u32::try_from(count).unwrap_or(u32::MAX))
1458        }
1459    }
1460}
1461
1462#[cfg(test)]
1463mod tests {
1464    use super::{Cardinality, compare_numbers, term_matches, wild_matches, within};
1465    use crate::ast::{Comparison, TypedSearchTerm};
1466
1467    #[test]
1468    fn cardinalities_patterns_and_words_match_as_the_specification_says() {
1469        assert!(within(None, 1) && within(None, 5) && !within(None, 0));
1470        assert!(within(
1471            Some(Cardinality {
1472                min: 0,
1473                max: Some(0)
1474            }),
1475            0
1476        ));
1477        assert!(!within(
1478            Some(Cardinality {
1479                min: 0,
1480                max: Some(0)
1481            }),
1482            1
1483        ));
1484        assert!(within(Some(Cardinality { min: 2, max: None }), 2));
1485        assert!(wild_matches("cardi*opathy", "Cardiomyopathy"));
1486        assert!(!wild_matches("cardi*opathy", "Cardiomyopathy X"));
1487        assert!(wild_matches("*itis", "Bronchitis"));
1488        assert!(wild_matches("a\\*b", "A*B"));
1489        assert!(!wild_matches("a\\*b", "AXB"));
1490        let term = TypedSearchTerm::Match(vec![String::from("hea"), String::from("att")]);
1491        assert!(term_matches(&term, "Heart attack"));
1492        assert!(!term_matches(&term, "Heart"));
1493        assert!(compare_numbers(Comparison::GreaterOrEqual, 500.0, 500.0));
1494        assert!(!compare_numbers(Comparison::Less, 500.0, 500.0));
1495    }
1496}