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    FullyLoaded,
15}
16
17impl LoadState {
18    pub fn is_loaded(&self, field_or_relation: &str) -> bool {
19        match self {
20            LoadState::NotLoaded => false,
21            LoadState::FullyLoaded => true,
22            LoadState::Partial(set) => set.contains(field_or_relation),
23            LoadState::PartialCompact(fields) => fields
24                .iter()
25                .any(|field| field.as_ref() == field_or_relation),
26            LoadState::SharedColumns(columns) => {
27                columns.iter().any(|column| column == field_or_relation)
28            }
29        }
30    }
31}
32
33/// A wrapper type for Expression API evaluation results.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub enum EvalResult<T> {
36    /// Value is successfully loaded and present.
37    Value(T),
38    /// Value is loaded but it is legitimately Null.
39    Null,
40    /// Value is not loaded, trapping the evaluation path.
41    NotLoaded {
42        failed_node: String,
43        attempted_path: String,
44    },
45}
46
47impl<T> EvalResult<T> {
48    pub fn and_then<U, F: FnOnce(T) -> EvalResult<U>>(
49        self,
50        field_name: &str,
51        f: F,
52    ) -> EvalResult<U> {
53        match self {
54            EvalResult::Value(val) => match f(val) {
55                EvalResult::NotLoaded {
56                    failed_node,
57                    attempted_path,
58                } => {
59                    let new_path = match (attempted_path == field_name, attempted_path.is_empty()) {
60                        (true, _) => attempted_path,
61                        (_, true) => field_name.to_string(),
62                        _ => format!("{}.{}", field_name, attempted_path),
63                    };
64                    EvalResult::NotLoaded {
65                        failed_node,
66                        attempted_path: new_path,
67                    }
68                }
69                other => other,
70            },
71            EvalResult::Null => EvalResult::Null,
72            EvalResult::NotLoaded {
73                failed_node,
74                attempted_path,
75            } => EvalResult::NotLoaded {
76                failed_node,
77                attempted_path,
78            },
79        }
80    }
81
82    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> EvalResult<U> {
83        match self {
84            EvalResult::Value(val) => EvalResult::Value(f(val)),
85            EvalResult::Null => EvalResult::Null,
86            EvalResult::NotLoaded {
87                failed_node,
88                attempted_path,
89            } => EvalResult::NotLoaded {
90                failed_node,
91                attempted_path,
92            },
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    struct Company {
102        pub name: Option<String>,
103        pub __load_state: LoadState,
104    }
105
106    impl Company {
107        fn eval_name(&self) -> EvalResult<&str> {
108            if !self.__load_state.is_loaded("name") {
109                return EvalResult::NotLoaded {
110                    failed_node: "name".to_string(),
111                    attempted_path: "name".to_string(),
112                };
113            }
114            match &self.name {
115                Some(n) => EvalResult::Value(n.as_str()),
116                None => EvalResult::Null,
117            }
118        }
119    }
120
121    struct Platform {
122        pub company: Option<Box<Company>>,
123        pub __load_state: LoadState,
124    }
125
126    impl Platform {
127        fn eval_company(&self) -> EvalResult<&Company> {
128            if !self.__load_state.is_loaded("company") {
129                return EvalResult::NotLoaded {
130                    failed_node: "company".to_string(),
131                    attempted_path: "company".to_string(),
132                };
133            }
134            match &self.company {
135                Some(c) => EvalResult::Value(c.as_ref()),
136                None => EvalResult::Null,
137            }
138        }
139    }
140
141    struct User {
142        pub platform: Option<Box<Platform>>,
143        pub __load_state: LoadState,
144    }
145
146    impl User {
147        fn eval_platform(&self) -> EvalResult<&Platform> {
148            if !self.__load_state.is_loaded("platform") {
149                return EvalResult::NotLoaded {
150                    failed_node: "platform".to_string(),
151                    attempted_path: "platform".to_string(),
152                };
153            }
154            match &self.platform {
155                Some(p) => EvalResult::Value(p.as_ref()),
156                None => EvalResult::Null,
157            }
158        }
159    }
160
161    #[test]
162    fn test_eval_tracking_chain_perfect_path() {
163        // Build the mocked entity graph:
164        // User -> Platform -> Company
165        // But we simulate a logic bug: Company is NOT fully loaded, its "name" is missing!
166
167        let company = Company {
168            name: None,
169            // Company only partially loaded (doesn't include "name")
170            __load_state: LoadState::NotLoaded,
171        };
172
173        let platform = Platform {
174            company: Some(Box::new(company)),
175            // Platform is fully loaded
176            __load_state: LoadState::FullyLoaded,
177        };
178
179        let user = User {
180            platform: Some(Box::new(platform)),
181            // User is fully loaded
182            __load_state: LoadState::FullyLoaded,
183        };
184
185        // Let's evaluate the expression: user.platform.company.name
186        let result = user.eval_platform().and_then("platform", |p| {
187            p.eval_company().and_then("company", |c| c.eval_name())
188        });
189
190        // We expect it to fail exactly at "name" and bubble up the path!
191        match &result {
192            EvalResult::NotLoaded { attempted_path, .. } => {
193                assert_eq!(attempted_path, "platform.company.name");
194                println!("\n\n>>> 【系统捕获到未加载异常】 <<<\n{:#?}\n\n", result);
195            }
196            _ => panic!("Expected NotLoaded but got {:?}", result),
197        }
198    }
199
200    #[test]
201    fn test_eval_tracking_chain_middle_break() {
202        // If the platform exists, but company itself wasn't loaded
203        let platform = Platform {
204            company: None,                      // No data
205            __load_state: LoadState::NotLoaded, // Missing loaded state for company
206        };
207
208        let user = User {
209            platform: Some(Box::new(platform)),
210            __load_state: LoadState::FullyLoaded,
211        };
212
213        let result = user.eval_platform().and_then("platform", |p| {
214            p.eval_company().and_then("company", |c| c.eval_name())
215        });
216
217        match result {
218            EvalResult::NotLoaded { attempted_path, .. } => {
219                assert_eq!(attempted_path, "platform.company");
220                println!(
221                    "Success! Intercepted middle missing path: {}",
222                    attempted_path
223                );
224            }
225            _ => panic!("Expected NotLoaded"),
226        }
227    }
228
229    #[test]
230    fn test_eval_tracking_chain_normal_null() {
231        // If the platform exists, company is fully loaded, but its name is truly empty (NULL in DB)
232        let company = Company {
233            name: None, // Real database null
234            __load_state: LoadState::FullyLoaded,
235        };
236
237        let platform = Platform {
238            company: Some(Box::new(company)),
239            __load_state: LoadState::FullyLoaded,
240        };
241
242        let user = User {
243            platform: Some(Box::new(platform)),
244            __load_state: LoadState::FullyLoaded,
245        };
246
247        let result = user.eval_platform().and_then("platform", |p| {
248            p.eval_company().and_then("company", |c| c.eval_name())
249        });
250
251        match result {
252            EvalResult::Null => {
253                println!("Success! Legitimately empty (Null), not an error.");
254            }
255            _ => panic!("Expected Null"),
256        }
257    }
258}