Skip to main content

teaql_core/
load_state.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4// Type-local optimization hints, not ownership of every live result shape.
5const MAX_SHARED_STATE_HINTS: usize = 512;
6
7/// Immutable type metadata. Positions originate in the generated library, never access order.
8#[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    /// Framework snapshot construction: entity identity belongs to the type,
97    /// not to each row's value payload or mutation ledger.
98    #[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/// Value-free availability shared by compatible rows. Overflow and named selections are lazy.
117#[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    /// Intern immutable geometry only. Bounded weak hints never own row values.
128    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        // Ordinary queries keep the existing hash work when no readonly schema
142        // was declared. A schema's fingerprint is precomputed, never per-row.
143        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        // A live hit must not scan every historical projection. On a miss,
163        // reclaim dead hints and count references (including hash collisions).
164        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        // Saturation affects cache admission only. Operation-local result
175        // layouts still share this state; held views remain fully valid.
176        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    /// Result-schema metadata only. Fixed slots and actual availability never change.
238    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    /// Replace only persistent dynamic selection. Caller batches by actual shape;
251    /// fixed bits, overflow and relation materialization are left untouched.
252    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        // Value-only edits do not change availability. Compare borrowed codes before
260        // building an owned set or formatting prefixed names; relation markers remain
261        // outside this comparison and must survive genuine dynamic-selection changes.
262        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, // Derived properties and unknown aliases never acquire fixed slots.
308        }
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        // One provider/result layout owns the actual list shape even when
421        // the optional type-level interner has exhausted its admission budget.
422        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        // A live hit leaves unrelated weak hints alone; the final dead hint
484        // owns no snapshot fields and will be swept by the next miss.
485        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}