Skip to main content

teaql_core/
dynamic_fields.rs

1//! Persistable object extensions. Names are runtime-defined and never receive fixed bit indexes.
2//! Readonly computed properties (`_name`) are a different namespace and are not accepted here.
3
4use std::collections::{BTreeMap, HashMap, HashSet};
5use std::sync::Arc;
6
7use crate::{DataType, Value};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct DynamicFieldError {
11    pub code: &'static str,
12    pub field: String,
13}
14
15impl std::fmt::Display for DynamicFieldError {
16    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        write!(formatter, "{}: {}", self.code, self.field)
18    }
19}
20
21impl std::error::Error for DynamicFieldError {}
22
23fn error(code: &'static str, field: impl Into<String>) -> DynamicFieldError {
24    DynamicFieldError {
25        code,
26        field: field.into(),
27    }
28}
29
30/// Runtime names/types, not SQL columns or generated field slots.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum DynamicFieldSelection {
33    All,
34    Fields(BTreeMap<String, DataType>),
35}
36
37impl DynamicFieldSelection {
38    pub fn fields(
39        fields: impl IntoIterator<Item = (String, DataType)>,
40    ) -> Result<Self, DynamicFieldError> {
41        let mut selected = BTreeMap::new();
42        for (code, data_type) in fields {
43            if code.trim().is_empty() || code.starts_with(['#', '_']) {
44                return Err(error("DYNAMIC_FIELD_INVALID_CODE", code));
45            }
46            if selected.insert(code.clone(), data_type).is_some() {
47                return Err(error("DYNAMIC_FIELD_DUPLICATE_CODE", code));
48            }
49        }
50        Ok(Self::Fields(selected))
51    }
52
53    pub fn validate(&self, definitions: &DynamicFieldDefinitions) -> Result<(), DynamicFieldError> {
54        if let Self::Fields(fields) = self {
55            for (code, selected_type) in fields {
56                if definitions.data_type(code)? != *selected_type {
57                    return Err(error("DYNAMIC_FIELD_TYPE_MISMATCH", code));
58                }
59            }
60        }
61        Ok(())
62    }
63
64    pub fn contains(&self, code: &str) -> bool {
65        match self {
66            Self::All => true,
67            Self::Fields(fields) => fields.contains_key(code),
68        }
69    }
70}
71
72/// Immutable owner-specific definitions, shared by all compatible row views.
73#[derive(Debug, PartialEq, Eq)]
74pub struct DynamicFieldDefinitions {
75    owner_type: String,
76    revision: String,
77    fields: Arc<HashMap<String, DataType>>,
78    storage_binding: Option<DynamicFieldStorageBinding>,
79}
80
81#[derive(PartialEq, Eq)]
82struct DynamicFieldStorageBinding {
83    profile: String,
84    executor: u64,
85}
86
87impl std::fmt::Debug for DynamicFieldStorageBinding {
88    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        formatter.write_str("DynamicFieldStorageBinding(<opaque>)")
90    }
91}
92
93impl DynamicFieldDefinitions {
94    pub fn new(
95        owner_type: impl Into<String>,
96        revision: impl Into<String>,
97        fields: impl IntoIterator<Item = (String, DataType)>,
98    ) -> Result<Arc<Self>, DynamicFieldError> {
99        let owner_type = owner_type.into();
100        let revision = revision.into();
101        if owner_type.trim().is_empty() || revision.trim().is_empty() {
102            return Err(error("DYNAMIC_FIELD_INVALID_DEFINITIONS", owner_type));
103        }
104        let mut definitions = HashMap::new();
105        for (code, data_type) in fields {
106            if code.trim().is_empty() || code.starts_with(['#', '_']) {
107                return Err(error("DYNAMIC_FIELD_INVALID_CODE", code));
108            }
109            if definitions.insert(code.clone(), data_type).is_some() {
110                return Err(error("DYNAMIC_FIELD_DUPLICATE_CODE", code));
111            }
112        }
113        Ok(Arc::new(Self {
114            owner_type,
115            revision,
116            fields: Arc::new(definitions),
117            storage_binding: None,
118        }))
119    }
120
121    pub fn owner_type(&self) -> &str {
122        &self.owner_type
123    }
124    pub fn revision(&self) -> &str {
125        &self.revision
126    }
127    pub fn fields(&self) -> &HashMap<String, DataType> {
128        &self.fields
129    }
130
131    /// Trusted provider metadata only; not tenant/permission semantics or wire input.
132    #[doc(hidden)]
133    pub fn with_storage_binding(self: &Arc<Self>, profile: &str, executor: u64) -> Arc<Self> {
134        if self.storage_binding_matches(profile, executor) {
135            return self.clone();
136        }
137        Arc::new(Self {
138            owner_type: self.owner_type.clone(),
139            revision: self.revision.clone(),
140            fields: self.fields.clone(),
141            storage_binding: Some(DynamicFieldStorageBinding {
142                profile: profile.into(),
143                executor,
144            }),
145        })
146    }
147
148    #[doc(hidden)]
149    pub fn storage_binding_matches(&self, profile: &str, executor: u64) -> bool {
150        self.storage_binding
151            .as_ref()
152            .is_some_and(|binding| binding.profile == profile && binding.executor == executor)
153    }
154
155    #[doc(hidden)]
156    pub fn same_schema(&self, other: &Self) -> bool {
157        self.owner_type == other.owner_type
158            && self.revision == other.revision
159            && self.fields == other.fields
160    }
161
162    pub fn data_type(&self, code: &str) -> Result<DataType, DynamicFieldError> {
163        self.fields
164            .get(code)
165            .copied()
166            .ok_or_else(|| error("DYNAMIC_FIELD_NOT_FOUND", code))
167    }
168
169    /// Validate a borrowed operand without cloning its strings/JSON or building
170    /// an intermediate row map. Providers use the same type contract as carriers.
171    pub fn validate_value(&self, code: &str, value: &Value) -> Result<(), DynamicFieldError> {
172        validate_field(self, code, value)
173    }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum DynamicFieldState {
178    Value,
179    Null,
180    NotLoaded,
181}
182
183/// Explicit persistence intent. A stored NULL is Set(Null), never Delete or NotLoaded.
184#[derive(Debug, Clone, PartialEq)]
185pub enum DynamicFieldMutation {
186    Set(Value),
187    Delete,
188}
189
190/// A borrowed wrapper: reading a missing known field allocates no value or string and performs no I/O.
191#[derive(Debug, Clone, Copy)]
192pub struct DynamicFieldValue<'a> {
193    data_type: DataType,
194    stored: Option<&'a Value>,
195}
196
197impl<'a> DynamicFieldValue<'a> {
198    pub fn data_type(&self) -> DataType {
199        self.data_type
200    }
201    pub fn state(&self) -> DynamicFieldState {
202        match self.stored {
203            None => DynamicFieldState::NotLoaded,
204            Some(Value::Null | Value::TypedNull(_)) => DynamicFieldState::Null,
205            Some(_) => DynamicFieldState::Value,
206        }
207    }
208    /// Nullable payload does not erase the separate Null / NotLoaded state.
209    pub fn value(&self) -> Option<&'a Value> {
210        (self.state() == DynamicFieldState::Value)
211            .then_some(self.stored)
212            .flatten()
213    }
214    pub fn is_loaded(&self) -> bool {
215        self.state() != DynamicFieldState::NotLoaded
216    }
217}
218
219/// Values belong to the row. Only immutable definitions and selected-name geometry are shared.
220#[derive(Debug, Clone, PartialEq)]
221pub struct DynamicFieldValues {
222    definitions: Arc<DynamicFieldDefinitions>,
223    selected_codes: Arc<HashSet<String>>,
224    values: HashMap<String, Value>,
225}
226
227/// Operation-local, value-free selection unions. Retaining both source Arcs
228/// prevents allocator address reuse from mixing unrelated selection shapes.
229#[doc(hidden)]
230#[derive(Default)]
231pub struct DynamicFieldMergeShapes {
232    #[allow(clippy::type_complexity)]
233    unions: HashMap<
234        (usize, usize),
235        (
236            Arc<HashSet<String>>,
237            Arc<HashSet<String>>,
238            Arc<HashSet<String>>,
239        ),
240    >,
241}
242
243impl DynamicFieldMergeShapes {
244    fn union(
245        &mut self,
246        left: &Arc<HashSet<String>>,
247        right: &Arc<HashSet<String>>,
248    ) -> Arc<HashSet<String>> {
249        if right.is_subset(left) {
250            return left.clone();
251        }
252        if left.is_subset(right) {
253            return right.clone();
254        }
255        let key = (Arc::as_ptr(left) as usize, Arc::as_ptr(right) as usize);
256        self.unions
257            .entry(key)
258            .or_insert_with(|| {
259                (
260                    left.clone(),
261                    right.clone(),
262                    Arc::new(left.union(right).cloned().collect()),
263                )
264            })
265            .2
266            .clone()
267    }
268}
269
270impl DynamicFieldValues {
271    #[doc(hidden)]
272    pub fn validate_merge(&self, other: &Self) -> Result<(), DynamicFieldError> {
273        if !Arc::ptr_eq(&self.definitions, &other.definitions)
274            && self.definitions != other.definitions
275        {
276            return Err(error(
277                "DYNAMIC_FIELD_INCOMPATIBLE_VIEW",
278                self.definitions.owner_type(),
279            ));
280        }
281        Ok(())
282    }
283
284    /// Merge actual loaded values, not requested-but-absent names. Incoming
285    /// values win; missing operands never clear an already loaded value.
286    #[doc(hidden)]
287    pub fn merge_loaded(
288        &mut self,
289        other: Self,
290        shapes: &mut DynamicFieldMergeShapes,
291    ) -> Result<(), DynamicFieldError> {
292        self.validate_merge(&other)?;
293        self.selected_codes = shapes.union(&self.selected_codes, &other.selected_codes);
294        self.values.extend(other.values);
295        Ok(())
296    }
297    pub fn from_values(
298        definitions: Arc<DynamicFieldDefinitions>,
299        values: HashMap<String, Value>,
300    ) -> Result<Self, DynamicFieldError> {
301        validate_values(&definitions, &values)?;
302        let selected_codes = Arc::new(values.keys().cloned().collect());
303        Ok(Self {
304            definitions,
305            selected_codes,
306            values,
307        })
308    }
309
310    /// Build each actual selected-name set once, not one copied HashSet per row.
311    /// Missing and loaded-NULL rows may have different shapes; never union their fields.
312    pub fn from_batch(
313        definitions: Arc<DynamicFieldDefinitions>,
314        rows: impl IntoIterator<Item = HashMap<String, Value>>,
315    ) -> Result<Vec<Self>, DynamicFieldError> {
316        use std::hash::{Hash, Hasher};
317        let mut shapes: HashMap<u64, Vec<Arc<HashSet<String>>>> = HashMap::new();
318        let mut result = Vec::new();
319        for values in rows {
320            validate_values(&definitions, &values)?;
321            let hash = values.keys().fold(0_u64, |total, code| {
322                let mut hasher = std::collections::hash_map::DefaultHasher::new();
323                code.hash(&mut hasher);
324                total.wrapping_add(hasher.finish())
325            });
326            let bucket = shapes.entry(hash).or_default();
327            let selected_codes = match bucket.iter().find(|shape| {
328                shape.len() == values.len() && values.keys().all(|code| shape.contains(code))
329            }) {
330                Some(shape) => shape.clone(),
331                None => {
332                    let shape: Arc<HashSet<String>> = Arc::new(values.keys().cloned().collect());
333                    bucket.push(shape.clone());
334                    shape
335                }
336            };
337            result.push(Self {
338                definitions: definitions.clone(),
339                selected_codes,
340                values,
341            });
342        }
343        Ok(result)
344    }
345
346    pub fn definitions(&self) -> &Arc<DynamicFieldDefinitions> {
347        &self.definitions
348    }
349    pub fn selected_codes(&self) -> &Arc<HashSet<String>> {
350        &self.selected_codes
351    }
352    pub fn values(&self) -> &HashMap<String, Value> {
353        &self.values
354    }
355
356    pub fn into_values(self) -> HashMap<String, Value> {
357        self.values
358    }
359
360    /// Row-local update. Existing selected geometry retains its exact Arc reference.
361    pub fn assign(&mut self, code: &str, value: Value) -> Result<(), DynamicFieldError> {
362        validate_field(&self.definitions, code, &value)?;
363        self.values.insert(code.to_owned(), value);
364        if !self.selected_codes.contains(code) {
365            Arc::make_mut(&mut self.selected_codes).insert(code.to_owned());
366        }
367        Ok(())
368    }
369
370    /// Explicit delete changes this view to NotLoaded; it is distinct from assigning NULL.
371    pub fn delete(&mut self, code: &str) -> Result<(), DynamicFieldError> {
372        self.definitions.data_type(code)?;
373        self.values.remove(code);
374        if self.selected_codes.contains(code) {
375            Arc::make_mut(&mut self.selected_codes).remove(code);
376        }
377        Ok(())
378    }
379
380    pub fn field(&self, code: &str) -> Result<DynamicFieldValue<'_>, DynamicFieldError> {
381        Ok(DynamicFieldValue {
382            data_type: self.definitions.data_type(code)?,
383            stored: self.values.get(code),
384        })
385    }
386}
387
388fn validate_values(
389    definitions: &DynamicFieldDefinitions,
390    values: &HashMap<String, Value>,
391) -> Result<(), DynamicFieldError> {
392    for (code, value) in values {
393        validate_field(definitions, code, value)?;
394    }
395    Ok(())
396}
397
398fn validate_field(
399    definitions: &DynamicFieldDefinitions,
400    code: &str,
401    value: &Value,
402) -> Result<(), DynamicFieldError> {
403    let declared = definitions.data_type(code)?;
404    let matches = match value {
405        Value::Null => true,
406        Value::TypedNull(data_type) => *data_type == declared,
407        Value::Bool(_) => declared == DataType::Bool,
408        Value::I64(_) => declared == DataType::I64,
409        Value::U64(_) => declared == DataType::U64,
410        Value::F64(_) => declared == DataType::F64,
411        Value::Decimal(_) => declared == DataType::Decimal,
412        Value::Text(_) => matches!(declared, DataType::Text | DataType::LargeText),
413        Value::Json(_) => declared == DataType::Json,
414        Value::Date(_) => declared == DataType::Date,
415        Value::Timestamp(_) => declared == DataType::Timestamp,
416        Value::Object(_) | Value::List(_) => false,
417    };
418    if !matches {
419        return Err(error("DYNAMIC_FIELD_TYPE_MISMATCH", code));
420    }
421    Ok(())
422}