ifc_lite_server/services/data_model/
rooted.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Which rows the data model's entities table carries (issue #6034).
6//!
7//! Its own module because it is a WIRE CONTRACT as much as a filter: the parse
8//! routes read it off the query, the data-model cache key is built from it, the
9//! fetch route selects by it, and the client sends it. One definition keeps
10//! those four in step, the same shape as `parquet_layout.rs`.
11
12use super::generated::root_attr_indices;
13use super::types::DataModel;
14use rustc_hash::{FxHashMap, FxHashSet};
15
16/// Which STEP instances the data model's entities table carries.
17///
18/// OPT-IN, defaulting to [`Self::All`], because the default table is a public
19/// payload: a client that looks up a non-rooted instance by id (an
20/// `IfcCartesianPoint`, an `IfcPropertySingleValue`) keeps working, and a
21/// default request writes and reads exactly the bytes it did before #6034.
22///
23/// [`Self::Rooted`] is for the clients that only ever look up objects. On a
24/// large architectural model about five in six entity rows are geometry and
25/// property plumbing with no GlobalId (`IfcPolyLoop`, `IfcFace`,
26/// `IfcCartesianPoint`, `IfcPropertySingleValue`, ...), none of which another
27/// table of the data model points at: property values are already flattened
28/// into the properties table, quantities into the quantities table.
29///
30/// The two variants are cached under separate keys (see
31/// `routes::parse::cache_keys::data_model_cache_key`), so a warm cache can
32/// never answer one with the other.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
34pub enum DataModelEntities {
35    /// Every STEP instance in the file. The only behaviour before #6034.
36    #[default]
37    #[serde(rename = "all")]
38    All,
39    /// Only rooted entities (`IfcRoot` subtypes, the ones carrying a
40    /// GlobalId), plus every non-rooted instance another table of the same
41    /// data model references by id, so no relation dangles. See
42    /// [`retain_rooted_entities`].
43    #[serde(rename = "rooted")]
44    Rooted,
45}
46
47impl DataModelEntities {
48    /// The data-model cache-key infix naming this variant. Empty for
49    /// [`Self::All`], so a default request keeps hitting the entries already
50    /// on disk.
51    pub(crate) fn cache_infix(self) -> &'static str {
52        match self {
53            Self::All => "",
54            Self::Rooted => "-rooted",
55        }
56    }
57
58    /// Narrow `data_model`'s entities table to this variant. A no-op for
59    /// [`Self::All`]; every other table is left untouched either way.
60    pub(crate) fn apply(self, data_model: &mut DataModel) {
61        if self == Self::Rooted {
62            retain_rooted_entities(data_model);
63        }
64    }
65}
66
67/// Keep the entity rows a rooted-only client can need, dropping the rest.
68///
69/// A row is kept when EITHER
70/// - it is rooted: it carries a GlobalId, or its type is one the schema
71///   registry says declares one (an `IfcRoot` subtype whose GlobalId the file
72///   left empty is still an object); or
73/// - another table of this data model references its id: a material, a
74///   material layer set or usage (`materials.definition_id` /
75///   `material_id`), the `RelatingMaterial` / `RelatingClassification` /
76///   `RelatingDocument` of an `IfcRelAssociates*` row, and so on.
77///
78/// The second rule is what keeps every relation resolvable. Without it the
79/// relationships table would name `IfcMaterial` and `IfcClassificationReference`
80/// ids the entities table no longer describes, and a client resolving a
81/// relation's endpoint to its type or name through the entities table would
82/// find nothing. They are few, so keeping them costs nothing measurable; the
83/// rows this drops are the ones nothing points at.
84///
85/// `entity_id`s are untouched, and the kept rows stay in file order.
86pub(crate) fn retain_rooted_entities(data_model: &mut DataModel) {
87    let referenced = referenced_ids(data_model);
88    // Types are few (hundreds) and rows are many (millions), so the registry
89    // lookup, which needs an uppercase copy of the name, runs once per type.
90    let mut rooted_type: FxHashMap<String, bool> = FxHashMap::default();
91    let keep: Vec<bool> = data_model
92        .entities
93        .iter()
94        .map(|entity| {
95            if entity.global_id.is_some() || referenced.contains(&entity.entity_id) {
96                return true;
97            }
98            if let Some(&rooted) = rooted_type.get(entity.type_name.as_str()) {
99                return rooted;
100            }
101            let rooted = declares_global_id(&entity.type_name);
102            rooted_type.insert(entity.type_name.clone(), rooted);
103            rooted
104        })
105        .collect();
106    let mut keep = keep.into_iter();
107    // `retain` visits every element exactly once, in order.
108    data_model
109        .entities
110        .retain(|_| keep.next().unwrap_or(true));
111}
112
113/// Whether the schema registry lists `type_name` as declaring a GlobalId,
114/// i.e. as an `IfcRoot` subtype. A type the registry does not know answers
115/// `false`: for those the extracted `global_id` (read at the `IfcRoot`
116/// position by the metadata fallback) is the only evidence there is.
117fn declares_global_id(type_name: &str) -> bool {
118    root_attr_indices(&type_name.to_ascii_uppercase()).is_some_and(|idx| idx.global_id >= 0)
119}
120
121/// Every entity id another table of `data_model` points at. `0` is the
122/// tables' "none" (a root spatial node's parent, a synthetic relationship's
123/// `rel_id`), never an instance, so it is skipped.
124fn referenced_ids(data_model: &DataModel) -> FxHashSet<u32> {
125    let mut ids = FxHashSet::default();
126    let mut add = |id: u32| {
127        if id != 0 {
128            ids.insert(id);
129        }
130    };
131    for rel in &data_model.relationships {
132        add(rel.rel_id);
133        add(rel.relating_id);
134        add(rel.related_id);
135    }
136    for pset in &data_model.property_sets {
137        add(pset.pset_id);
138    }
139    for qset in &data_model.quantity_sets {
140        add(qset.qset_id);
141    }
142    for row in &data_model.classifications {
143        add(row.element_id);
144    }
145    for row in &data_model.materials {
146        add(row.element_id);
147        add(row.association_id);
148        add(row.definition_id);
149        if let Some(material_id) = row.material_id {
150            add(material_id);
151        }
152    }
153    for row in &data_model.documents {
154        add(row.element_id);
155    }
156    let spatial = &data_model.spatial_hierarchy;
157    add(spatial.project_id);
158    for node in &spatial.nodes {
159        add(node.entity_id);
160        add(node.parent_id);
161        node.children_ids.iter().for_each(|&id| add(id));
162        node.element_ids.iter().for_each(|&id| add(id));
163    }
164    for lookup in [
165        &spatial.element_to_storey,
166        &spatial.element_to_building,
167        &spatial.element_to_site,
168        &spatial.element_to_space,
169    ] {
170        for &(element, container) in lookup {
171            add(element);
172            add(container);
173        }
174    }
175    ids
176}
177
178#[cfg(test)]
179#[path = "rooted_tests.rs"]
180mod tests;