1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4const MAX_SHARED_STATE_HINTS: usize = 512;
6
7#[derive(Debug)]
9pub struct FieldLayout {
10 entity: Arc<str>,
11 revision: String,
12 names: Vec<String>,
13 aliases: HashMap<String, usize>,
14 relations: HashSet<String>,
15 states: std::sync::Mutex<HashMap<u64, Vec<std::sync::Weak<LoadedSnapshot>>>>,
16}
17
18impl FieldLayout {
19 pub fn from_generated(
20 entity: &str,
21 revision: &str,
22 indexes: &[(&str, usize)],
23 mappings: &[(&str, &str, &str)],
24 relations: &[&str],
25 expected_members: &[&str],
26 ) -> Result<Arc<Self>, String> {
27 if entity.is_empty() || revision.is_empty() {
28 return Err("missing entity or field-layout revision".to_owned());
29 }
30 let mut names = vec![String::new(); indexes.len()];
31 let mut aliases = HashMap::with_capacity(indexes.len() * 2);
32 for &(name, index) in indexes {
33 if name.is_empty() || name.starts_with(['#', '_']) || index >= names.len() {
34 return Err(format!("invalid fixed field/index: {name}={index}"));
35 }
36 if !names[index].is_empty() || aliases.insert(name.to_owned(), index).is_some() {
37 return Err(format!("duplicate fixed field/index: {name}={index}"));
38 }
39 names[index] = name.to_owned();
40 }
41 if !aliases.contains_key("id") || !aliases.contains_key("version") {
42 return Err("generated layout must include id and version".to_owned());
43 }
44 let mut mapped = HashSet::new();
45 for &(canonical, member, column) in mappings {
46 let Some(&index) = aliases.get(canonical) else {
47 return Err(format!("mapping has no fixed field: {canonical}"));
48 };
49 if names[index] != canonical {
50 return Err(format!(
51 "mapping names an alias, not a canonical field: {canonical}"
52 ));
53 }
54 if !mapped.insert(canonical) {
55 return Err(format!("duplicate field mapping: {canonical}"));
56 }
57 for alias in [member, column] {
58 if alias.is_empty() {
59 return Err(format!("blank alias for {canonical}"));
60 }
61 if aliases
62 .get(alias)
63 .is_some_and(|previous| *previous != index)
64 {
65 return Err(format!("ambiguous fixed-field alias: {alias}"));
66 }
67 aliases.insert(alias.to_owned(), index);
68 }
69 }
70 if mapped.len() != indexes.len() {
71 return Err("incomplete fixed-field mappings".to_owned());
72 }
73 let expected: HashSet<_> = expected_members
74 .iter()
75 .map(|member| aliases.get(*member).copied())
76 .collect();
77 if expected.contains(&None)
78 || expected.len() != indexes.len()
79 || expected_members.len() != indexes.len()
80 {
81 return Err("generated layout disagrees with typed entity fields".to_owned());
82 }
83 Ok(Arc::new(Self {
84 entity: Arc::from(entity),
85 revision: revision.to_owned(),
86 names,
87 aliases,
88 relations: relations.iter().map(|name| (*name).to_owned()).collect(),
89 states: Default::default(),
90 }))
91 }
92
93 pub fn entity(&self) -> &str {
94 &self.entity
95 }
96 #[doc(hidden)]
99 pub fn shared_entity_name(&self) -> Arc<str> {
100 self.entity.clone()
101 }
102 pub fn revision(&self) -> &str {
103 &self.revision
104 }
105 pub fn index(&self, name: &str) -> Option<usize> {
106 self.aliases.get(name).copied()
107 }
108 pub fn field_count(&self) -> usize {
109 self.names.len()
110 }
111 pub fn is_relation(&self, name: &str) -> bool {
112 self.relations.contains(name)
113 }
114}
115
116#[derive(Debug, Clone)]
118pub struct LoadedSnapshot {
119 layout: Arc<FieldLayout>,
120 bits: u64,
121 overflow: Option<Arc<HashSet<usize>>>,
122 selected_names: Option<Arc<HashSet<String>>>,
123 dynamic_properties: Option<Arc<crate::dynamic_properties::DynamicPropertyDefinitions>>,
124}
125
126impl LoadedSnapshot {
127 pub fn into_shared(self) -> Arc<Self> {
129 use std::hash::{Hash, Hasher};
130 fn unordered_hash<T: Hash>(values: impl Iterator<Item = T>) -> u64 {
131 values.fold(0_u64, |total, value| {
132 let mut hash = std::collections::hash_map::DefaultHasher::new();
133 value.hash(&mut hash);
134 total.wrapping_add(hash.finish())
135 })
136 }
137 let mut hash = std::collections::hash_map::DefaultHasher::new();
138 self.bits.hash(&mut hash);
139 unordered_hash(self.overflow.iter().flat_map(|fields| fields.iter())).hash(&mut hash);
140 unordered_hash(self.selected_names.iter().flat_map(|fields| fields.iter())).hash(&mut hash);
141 if let Some(definitions) = &self.dynamic_properties {
144 1_u8.hash(&mut hash);
145 definitions.hash(&mut hash);
146 }
147 let key = hash.finish();
148 let layout = self.layout.clone();
149 let mut states = layout
150 .states
151 .lock()
152 .unwrap_or_else(std::sync::PoisonError::into_inner);
153 if let Some(existing) = states
154 .get(&key)
155 .into_iter()
156 .flatten()
157 .filter_map(std::sync::Weak::upgrade)
158 .find(|state| **state == self)
159 {
160 return existing;
161 }
162 let mut retained = 0;
165 states.retain(|_, bucket| {
166 bucket.retain(|state| state.strong_count() > 0);
167 retained += bucket.len();
168 !bucket.is_empty()
169 });
170 let state = Arc::new(self);
171 if retained < MAX_SHARED_STATE_HINTS {
172 states.entry(key).or_default().push(Arc::downgrade(&state));
173 }
174 state
177 }
178
179 pub fn fully_loaded(layout: Arc<FieldLayout>) -> Self {
180 let count = layout.field_count();
181 Self {
182 layout,
183 bits: if count >= 64 {
184 u64::MAX
185 } else {
186 (1_u64 << count) - 1
187 },
188 overflow: (count > 64).then(|| Arc::new((64..count).collect())),
189 selected_names: None,
190 dynamic_properties: None,
191 }
192 }
193 pub fn projection<'a>(
194 layout: Arc<FieldLayout>,
195 fields: impl IntoIterator<Item = &'a str>,
196 ) -> Self {
197 let mut state = Self {
198 layout,
199 bits: 0,
200 overflow: None,
201 selected_names: None,
202 dynamic_properties: None,
203 };
204 for field in fields {
205 state.mark(field, true);
206 }
207 state
208 }
209
210 pub fn layout(&self) -> &Arc<FieldLayout> {
211 &self.layout
212 }
213 pub fn bits(&self) -> u64 {
214 self.bits
215 }
216 pub fn overflow(&self) -> Option<&HashSet<usize>> {
217 self.overflow.as_deref()
218 }
219 pub fn selected_names(&self) -> Option<&HashSet<String>> {
220 self.selected_names.as_deref()
221 }
222
223 pub fn dynamic_property_definitions(
224 &self,
225 ) -> Option<&Arc<crate::dynamic_properties::DynamicPropertyDefinitions>> {
226 self.dynamic_properties.as_ref()
227 }
228
229 pub(crate) fn with_property_schema(
230 mut self,
231 definitions: Option<Arc<crate::dynamic_properties::DynamicPropertyDefinitions>>,
232 ) -> Self {
233 self.dynamic_properties = definitions;
234 self
235 }
236
237 pub fn with_dynamic_property_definitions(
239 state: &Arc<Self>,
240 definitions: Arc<crate::dynamic_properties::DynamicPropertyDefinitions>,
241 ) -> Arc<Self> {
242 if state.dynamic_properties.as_ref() == Some(&definitions) {
243 return state.clone();
244 }
245 let mut next = (**state).clone();
246 next.dynamic_properties = Some(definitions);
247 next.into_shared()
248 }
249
250 pub fn with_dynamic_fields(
253 state: &Arc<Self>,
254 values: &crate::dynamic_fields::DynamicFieldValues,
255 ) -> Result<Arc<Self>, String> {
256 if values.definitions().owner_type() != state.layout.entity() {
257 return Err("dynamic field owner disagrees with fixed entity layout".to_owned());
258 }
259 let selected = values.selected_codes();
263 let mut existing_count = 0;
264 let same_selection = state
265 .selected_names
266 .iter()
267 .flat_map(|names| names.iter())
268 .filter_map(|name| name.strip_prefix('#'))
269 .all(|code| {
270 existing_count += 1;
271 selected.contains(code)
272 });
273 if same_selection && existing_count == selected.len() {
274 return Ok(state.clone());
275 }
276 let mut names: HashSet<String> = state
277 .selected_names
278 .iter()
279 .flat_map(|names| names.iter())
280 .filter(|name| !name.starts_with('#'))
281 .cloned()
282 .collect();
283 names.extend(
284 values
285 .selected_codes()
286 .iter()
287 .map(|code| format!("#{code}")),
288 );
289 let mut next = (**state).clone();
290 next.selected_names = (!names.is_empty()).then(|| Arc::new(names));
291 Ok(next.into_shared())
292 }
293
294 pub fn is_loaded(&self, field: &str) -> bool {
295 if self.layout.is_relation(field) || field.starts_with('#') {
296 return self
297 .selected_names
298 .as_ref()
299 .is_some_and(|names| names.contains(field));
300 }
301 match self.layout.index(field) {
302 Some(index) if index < u64::BITS as usize => self.bits & (1_u64 << index) != 0,
303 Some(index) => self
304 .overflow
305 .as_ref()
306 .is_some_and(|fields| fields.contains(&index)),
307 None => false, }
309 }
310
311 pub fn with_loaded(state: &Arc<Self>, field: &str, loaded: bool) -> Result<Arc<Self>, String> {
312 if state.layout.index(field).is_none()
313 && !state.layout.is_relation(field)
314 && !field.starts_with('#')
315 {
316 return Err(format!(
317 "unknown loadable field: {}.{field}",
318 state.layout.entity()
319 ));
320 }
321 if state.is_loaded(field) == loaded {
322 return Ok(state.clone());
323 }
324 let mut next = (**state).clone();
325 next.mark(field, loaded);
326 Ok(next.into_shared())
327 }
328
329 fn mark(&mut self, field: &str, loaded: bool) {
330 if self.layout.is_relation(field) || field.starts_with('#') {
331 if self.is_loaded(field) == loaded {
332 return;
333 }
334 let names = self
335 .selected_names
336 .get_or_insert_with(|| Arc::new(HashSet::new()));
337 if loaded {
338 Arc::make_mut(names).insert(field.to_owned());
339 } else {
340 Arc::make_mut(names).remove(field);
341 }
342 if names.is_empty() {
343 self.selected_names = None;
344 }
345 } else if let Some(index) = self.layout.index(field) {
346 if index < u64::BITS as usize {
347 if loaded {
348 self.bits |= 1_u64 << index;
349 } else {
350 self.bits &= !(1_u64 << index);
351 }
352 } else {
353 if self.is_loaded(field) == loaded {
354 return;
355 }
356 let fields = self
357 .overflow
358 .get_or_insert_with(|| Arc::new(HashSet::new()));
359 if loaded {
360 Arc::make_mut(fields).insert(index);
361 } else {
362 Arc::make_mut(fields).remove(&index);
363 }
364 if fields.is_empty() {
365 self.overflow = None;
366 }
367 }
368 }
369 }
370}
371
372impl PartialEq for LoadedSnapshot {
373 fn eq(&self, other: &Self) -> bool {
374 Arc::ptr_eq(&self.layout, &other.layout)
375 && self.bits == other.bits
376 && self.overflow == other.overflow
377 && self.selected_names == other.selected_names
378 && self.dynamic_properties == other.dynamic_properties
379 }
380}
381impl Eq for LoadedSnapshot {}
382
383#[cfg(test)]
384mod lifetime_tests {
385 use super::*;
386
387 #[test]
388 fn live_projection_cache_is_bounded_and_saturated_lists_still_share_geometry() {
389 let layout = FieldLayout::from_generated(
390 "BoundedProbe",
391 "v1",
392 &[("id", 0), ("version", 1), ("name", 2)],
393 &[
394 ("id", "id", "id"),
395 ("version", "version", "version"),
396 ("name", "name", "name"),
397 ],
398 &[],
399 &["id", "version", "name"],
400 )
401 .unwrap();
402 let retained = LoadedSnapshot::projection(layout.clone(), ["id", "name"]).into_shared();
403 let mut held = Vec::new();
404 for index in 0..10_000 {
405 let code = format!("#held_{index}");
406 let state =
407 LoadedSnapshot::projection(layout.clone(), ["id", code.as_str()]).into_shared();
408 assert!(state.is_loaded(&code));
409 assert!(!retained.is_loaded(&code));
410 held.push(state);
411 }
412 let cache = layout.states.lock().unwrap();
413 assert!(
414 cache.values().map(Vec::len).sum::<usize>() <= 512,
415 "live result shapes must not make the type cache unbounded"
416 );
417 drop(cache);
418 let reused = LoadedSnapshot::projection(layout.clone(), ["id", "name"]).into_shared();
419 assert!(Arc::ptr_eq(&retained, &reused));
420 let columns = crate::CompactRowLayout::new(Arc::from([
423 "id".to_owned(),
424 "version".to_owned(),
425 "name".to_owned(),
426 ]));
427 let mut first = None;
428 for id in 0..10_000 {
429 let row = crate::CompactRow::with_layout(
430 columns.clone(),
431 vec![
432 crate::Value::U64(id),
433 crate::Value::I64(1),
434 crate::Value::Null,
435 ],
436 );
437 let crate::eval::LoadState::Indexed(state) = row.indexed_load_state(layout.clone())
438 else {
439 panic!("indexed layout required");
440 };
441 assert!(state.is_loaded("name"));
442 assert_eq!(row.get("id"), Some(&crate::Value::U64(id)));
443 if let Some(first) = &first {
444 assert!(Arc::ptr_eq(first, &state));
445 } else {
446 first = Some(state);
447 }
448 }
449 drop(columns);
450 drop(held);
451 assert!(first.unwrap().is_loaded("name"));
452 assert!(!retained.is_loaded("version"));
453 }
454
455 #[test]
456 fn dead_projection_entries_are_pruned_without_retaining_states_or_layouts() {
457 let layout = FieldLayout::from_generated(
458 "LifetimeProbe",
459 "v1",
460 &[("id", 0), ("version", 1), ("name", 2)],
461 &[
462 ("id", "id", "id"),
463 ("version", "version", "version"),
464 ("name", "name", "name"),
465 ],
466 &[],
467 &["id", "version", "name"],
468 )
469 .unwrap();
470 let weak_layout = Arc::downgrade(&layout);
471 let retained = LoadedSnapshot::projection(layout.clone(), ["id", "name"]).into_shared();
472 let weak_retained = Arc::downgrade(&retained);
473 for index in 0..1_000 {
474 let code = format!("#temporary_{index}");
475 let temporary =
476 LoadedSnapshot::projection(layout.clone(), ["id", code.as_str()]).into_shared();
477 let weak_temporary = Arc::downgrade(&temporary);
478 assert!(temporary.is_loaded(&code));
479 assert!(!retained.is_loaded(&code));
480 drop(temporary);
481 assert!(weak_temporary.upgrade().is_none());
482 }
483 let reused = LoadedSnapshot::projection(layout.clone(), ["id", "name"]).into_shared();
486 assert!(Arc::ptr_eq(&retained, &reused));
487 let cache = layout.states.lock().unwrap();
488 assert!(cache.len() <= 2);
489 assert!(cache.values().map(Vec::len).sum::<usize>() <= 2);
490 drop(cache);
491 drop(reused);
492 drop(layout);
493 assert!(weak_layout.upgrade().is_some());
494 assert!(retained.is_loaded("name"));
495 assert!(!retained.is_loaded("version"));
496 drop(retained);
497 assert!(weak_retained.upgrade().is_none());
498 assert!(weak_layout.upgrade().is_none());
499 }
500}