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;