Skip to main content

teaql_core/
eval.rs

1use serde::{Deserialize, Serialize};
2
3/// The load state metadata hidden inside an entity.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
5pub enum LoadState {
6    #[default]
7    NotLoaded,
8    Partial(std::collections::HashSet<String>),
9    /// Compact generated-entity representation. Known fields borrow their generated static
10    /// names; only genuinely dynamic projection aliases need to own a string.
11    PartialCompact(smallvec::SmallVec<[std::borrow::Cow<'static, str>; 8]>),
12    /// Column layout shared by every row in one database result set.
13    SharedColumns(std::sync::Arc<[String]>),
14    /// Runtime-only validated fixed layout. Never deserialize internal masks from a client.
15    #[serde(skip)]
16    Indexed(std::sync::Arc<crate::LoadedSnapshot>),
17    FullyLoaded,
18}
19
20impl LoadState {
21    pub fn into_indexed(self, layout: std::sync::Arc<crate::FieldLayout>) -> Result<Self, String> {
22        let state = match self {
23            Self::Indexed(state) => {
24                if !std::sync::Arc::ptr_eq(state.layout(), &layout) {
25                    return Err("incompatible loaded-state type or layout revision".to_owned());
26                }
27                return Ok(Self::Indexed(state));
28            }
29            Self::FullyLoaded => crate::LoadedSnapshot::fully_loaded(layout),
30            Self::NotLoaded => crate::LoadedSnapshot::projection(layout, std::iter::empty()),
31            Self::Partial(fields) => {
32                crate::LoadedSnapshot::projection(layout, fields.iter().map(String::as_str))
33            }
34            Self::PartialCompact(fields) => {
35                crate::LoadedSnapshot::projection(layout, fields.iter().map(|field| field.as_ref()))
36            }
37            Self::SharedColumns(fields) => {
38                crate::LoadedSnapshot::projection(layout, fields.iter().map(String::as_str))
39            }
40        };
41        Ok(Self::Indexed(state.into_shared()))
42    }
43
44    pub fn is_loaded(&self, field_or_relation: &str) -> bool {
45        match self {
46            LoadState::NotLoaded => false,
47            LoadState::FullyLoaded => true,
48            LoadState::Partial(set) => set.contains(field_or_relation),
49            LoadState::PartialCompact(fields) => fields
50                .iter()
51                .any(|field| field.as_ref() == field_or_relation),
52            LoadState::SharedColumns(columns) => {
53                columns.iter().any(|column| column == field_or_relation)
54            }
55            LoadState::Indexed(state) => state.is_loaded(field_or_relation),
56        }
57    }
58
59    pub fn mark_loaded(&mut self, field: &str) -> Result<(), String> {
60        match self {
61            Self::Indexed(state) => {
62                *state = crate::LoadedSnapshot::with_loaded(state, field, true)?
63            }
64            Self::NotLoaded => {
65                *self = Self::PartialCompact(smallvec::smallvec![field.to_owned().into()])
66            }
67            Self::Partial(fields) => {
68                fields.insert(field.to_owned());
69            }
70            Self::PartialCompact(fields) => {
71                if !fields.iter().any(|loaded| loaded.as_ref() == field) {
72                    fields.push(field.to_owned().into());
73                }
74            }
75            Self::SharedColumns(columns) => {
76                if !columns.iter().any(|loaded| loaded == field) {
77                    let mut next = columns.to_vec();
78                    next.push(field.to_owned());
79                    *columns = next.into();
80                }
81            }
82            Self::FullyLoaded => {}
83        }
84        Ok(())
85    }
86
87    pub fn mark_unloaded(&mut self, field: &str) -> Result<(), String> {
88        match self {
89            Self::Indexed(state) => {
90                *state = crate::LoadedSnapshot::with_loaded(state, field, false)?
91            }
92            Self::Partial(fields) => {
93                fields.remove(field);
94            }
95            Self::PartialCompact(fields) => fields.retain(|loaded| loaded.as_ref() != field),
96            Self::SharedColumns(columns) => {
97                *columns = columns
98                    .iter()
99                    .filter(|name| name.as_str() != field)
100                    .cloned()
101                    .collect::<Vec<_>>()
102                    .into()
103            }
104            Self::NotLoaded => {}
105            Self::FullyLoaded => {
106                return Err("clearing availability requires a generated layout".to_owned());
107            }
108        }
109        Ok(())
110    }
111}
112
113/// A wrapper type for Expression API evaluation results.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub enum EvalResult<T> {
116    /// Value is successfully loaded and present.
117    Value(T),
118    /// Value is loaded but it is legitimately Null.
119    Null,
120    /// Value is not loaded, trapping the evaluation path.
121    NotLoaded {
122        failed_node: String,
123        attempted_path: String,
124    },
125}
126
127impl<T> EvalResult<T> {
128    pub fn and_then<U, F: FnOnce(T) -> EvalResult<U>>(
129        self,
130        field_name: &str,
131        f: F,
132    ) -> EvalResult<U> {
133        match self {
134            EvalResult::Value(val) => match f(val) {
135                EvalResult::NotLoaded {
136                    failed_node,
137                    attempted_path,
138                } => {
139                    let new_path = match (attempted_path == field_name, attempted_path.is_empty()) {
140                        (true, _) => attempted_path,
141                        (_, true) => field_name.to_string(),
142                        _ => format!("{}.{}", field_name, attempted_path),
143                    };
144                    EvalResult::NotLoaded {
145                        failed_node,
146                        attempted_path: new_path,
147                    }
148                }
149                other => other,
150            },
151            EvalResult::Null => EvalResult::Null,
152            EvalResult::NotLoaded {
153                failed_node,
154                attempted_path,
155            } => EvalResult::NotLoaded {
156                failed_node,
157                attempted_path,
158            },
159        }
160    }
161
162    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> EvalResult<U> {
163        match self {
164            EvalResult::Value(val) => EvalResult::Value(f(val)),
165            EvalResult::Null => EvalResult::Null,
166            EvalResult::NotLoaded {
167                failed_node,
168                attempted_path,
169            } => EvalResult::NotLoaded {
170                failed_node,
171                attempted_path,
172            },
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    struct Company {
182        pub name: Option<String>,
183        pub __load_state: LoadState,
184    }
185
186    impl Company {
187        fn eval_name(&self) -> EvalResult<&str> {
188            if !self.__load_state.is_loaded("name") {
189                return EvalResult::NotLoaded {
190                    failed_node: "name".to_string(),
191                    attempted_path: "name".to_string(),
192                };
193            }
194            match &self.name {
195                Some(n) => EvalResult::Value(n.as_str()),
196                None => EvalResult::Null,
197            }
198        }
199    }
200
201    struct Platform {
202        pub company: Option<Box<Company>>,
203        pub __load_state: LoadState,
204    }
205
206    impl Platform {
207        fn eval_company(&self) -> EvalResult<&Company> {
208            if !self.__load_state.is_loaded("company") {
209                return EvalResult::NotLoaded {
210                    failed_node: "company".to_string(),
211                    attempted_path: "company".to_string(),
212                };
213            }
214            match &self.company {
215                Some(c) => EvalResult::Value(c.as_ref()),
216                None => EvalResult::Null,
217            }
218        }
219    }
220
221    struct User {
222        pub platform: Option<Box<Platform>>,
223        pub __load_state: LoadState,
224    }
225
226    impl User {
227        fn eval_platform(&self) -> EvalResult<&Platform> {
228            if !self.__load_state.is_loaded("platform") {
229                return EvalResult::NotLoaded {
230                    failed_node: "platform".to_string(),
231                    attempted_path: "platform".to_string(),
232                };
233            }
234            match &self.platform {
235                Some(p) => EvalResult::Value(p.as_ref()),
236                None => EvalResult::Null,
237            }
238        }
239    }
240
241    #[test]
242    fn test_eval_tracking_chain_perfect_path() {
243        // Build the mocked entity graph:
244        // User -> Platform -> Company
245        // But we simulate a logic bug: Company is NOT fully loaded, its "name" is missing!
246
247        let company = Company {
248            name: None,
249            // Company only partially loaded (doesn't include "name")
250            __load_state: LoadState::NotLoaded,
251        };
252
253        let platform = Platform {
254            company: Some(Box::new(company)),
255            // Platform is fully loaded
256            __load_state: LoadState::FullyLoaded,
257        };
258
259        let user = User {
260            platform: Some(Box::new(platform)),
261            // User is fully loaded
262            __load_state: LoadState::FullyLoaded,
263        };
264
265        // Let's evaluate the expression: user.platform.company.name
266        let result = user.eval_platform().and_then("platform", |p| {
267            p.eval_company().and_then("company", |c| c.eval_name())
268        });
269
270        // We expect it to fail exactly at "name" and bubble up the path!
271        match &result {
272            EvalResult::NotLoaded { attempted_path, .. } => {
273                assert_eq!(attempted_path, "platform.company.name");
274                println!("\n\n>>> 【系统捕获到未加载异常】 <<<\n{:#?}\n\n", result);
275            }
276            _ => panic!("Expected NotLoaded but got {:?}", result),
277        }
278    }
279
280    #[test]
281    fn test_eval_tracking_chain_middle_break() {
282        // If the platform exists, but company itself wasn't loaded
283        let platform = Platform {
284            company: None,                      // No data
285            __load_state: LoadState::NotLoaded, // Missing loaded state for company
286        };
287
288        let user = User {
289            platform: Some(Box::new(platform)),
290            __load_state: LoadState::FullyLoaded,
291        };
292
293        let result = user.eval_platform().and_then("platform", |p| {
294            p.eval_company().and_then("company", |c| c.eval_name())
295        });
296
297        match result {
298            EvalResult::NotLoaded { attempted_path, .. } => {
299                assert_eq!(attempted_path, "platform.company");
300                println!(
301                    "Success! Intercepted middle missing path: {}",
302                    attempted_path
303                );
304            }
305            _ => panic!("Expected NotLoaded"),
306        }
307    }
308
309    #[test]
310    fn test_eval_tracking_chain_normal_null() {
311        // If the platform exists, company is fully loaded, but its name is truly empty (NULL in DB)
312        let company = Company {
313            name: None, // Real database null
314            __load_state: LoadState::FullyLoaded,
315        };
316
317        let platform = Platform {
318            company: Some(Box::new(company)),
319            __load_state: LoadState::FullyLoaded,
320        };
321
322        let user = User {
323            platform: Some(Box::new(platform)),
324            __load_state: LoadState::FullyLoaded,
325        };
326
327        let result = user.eval_platform().and_then("platform", |p| {
328            p.eval_company().and_then("company", |c| c.eval_name())
329        });
330
331        match result {
332            EvalResult::Null => {
333                println!("Success! Legitimately empty (Null), not an error.");
334            }
335            _ => panic!("Expected Null"),
336        }
337    }
338}