Skip to main content

teaql_core/
dynamic_properties.rs

1//! Readonly result-schema descriptors. Definitions do not imply property presence.
2use crate::{DataType, Value};
3use std::{collections::BTreeMap, sync::Arc};
4
5#[derive(Debug, PartialEq, Eq)]
6pub struct DynamicPropertyDefinitions {
7    types: BTreeMap<String, DataType>,
8    hash: u64,
9}
10
11impl DynamicPropertyDefinitions {
12    pub fn new(fields: impl IntoIterator<Item = (String, DataType)>) -> Result<Arc<Self>, String> {
13        let mut types = BTreeMap::new();
14        for (name, data_type) in fields {
15            if !name.starts_with('_') || name[1..].trim().is_empty() {
16                return Err("dynamic property metadata requires '_' names".into());
17            }
18            if types.insert(name.clone(), data_type).is_some() {
19                return Err(format!("duplicate dynamic property definition: {name}"));
20            }
21        }
22        Ok(Self::from_types(types))
23    }
24    fn from_types(types: BTreeMap<String, DataType>) -> Arc<Self> {
25        use std::hash::{Hash, Hasher};
26        let mut hash = std::collections::hash_map::DefaultHasher::new();
27        for (name, data_type) in &types {
28            name.hash(&mut hash);
29            std::mem::discriminant(data_type).hash(&mut hash);
30        }
31        Arc::new(Self {
32            types,
33            hash: hash.finish(),
34        })
35    }
36    pub fn types(&self) -> &BTreeMap<String, DataType> {
37        &self.types
38    }
39    pub fn data_type(&self, name: &str) -> Option<DataType> {
40        self.types.get(name).copied()
41    }
42
43    pub fn validate_value(&self, name: &str, value: &Value) -> Result<(), String> {
44        let Some(declared) = self.data_type(name) else {
45            return Ok(());
46        };
47        let matches = match value {
48            Value::Null => true,
49            Value::TypedNull(actual) => *actual == declared,
50            Value::Bool(_) => declared == DataType::Bool,
51            Value::I64(_) => declared == DataType::I64,
52            Value::U64(_) => declared == DataType::U64,
53            Value::F64(_) => declared == DataType::F64,
54            Value::Decimal(_) => declared == DataType::Decimal,
55            Value::Text(_) => matches!(declared, DataType::Text | DataType::LargeText),
56            Value::Date(_) => declared == DataType::Date,
57            Value::Timestamp(_) => declared == DataType::Timestamp,
58            Value::Json(json) => {
59                json.is_null()
60                    || match declared {
61                        DataType::Json => true,
62                        DataType::Bool => json.is_boolean(),
63                        DataType::I64 => json.as_i64().is_some(),
64                        DataType::U64 => json.as_u64().is_some(),
65                        DataType::F64 => json.is_number(),
66                        DataType::Text | DataType::LargeText => json.is_string(),
67                        _ => false,
68                    }
69            }
70            Value::Object(_) | Value::List(_) => declared == DataType::Json,
71        };
72        if matches {
73            Ok(())
74        } else {
75            Err(format!("invalid dynamic property type: {name}"))
76        }
77    }
78
79    /// Merge schema information only; conflicting declarations never win by row order.
80    pub fn merge(left: &Arc<Self>, right: &Arc<Self>) -> Result<Arc<Self>, String> {
81        for (name, data_type) in &right.types {
82            if left
83                .data_type(name)
84                .is_some_and(|previous| previous != *data_type)
85            {
86                return Err(name.clone());
87            }
88        }
89        if right.types.keys().all(|name| left.types.contains_key(name)) {
90            return Ok(left.clone());
91        }
92        if left.types.keys().all(|name| right.types.contains_key(name)) {
93            return Ok(right.clone());
94        }
95        let mut types = left.types.clone();
96        types.extend(right.types.clone());
97        Ok(Self::from_types(types))
98    }
99}
100
101impl std::hash::Hash for DynamicPropertyDefinitions {
102    fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
103        hasher.write_u64(self.hash);
104    }
105}