reinhardt_db/orm/
loading.rs1use crate::orm::Model;
19use std::marker::PhantomData;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum LoadingStrategy {
25 Joined,
29
30 Selectin,
34
35 Subquery,
38
39 Lazy,
42
43 Raise,
47
48 NoLoad,
51
52 WriteOnly,
56
57 Dynamic,
61}
62
63impl LoadingStrategy {
64 pub fn is_eager(&self) -> bool {
67 matches!(
68 self,
69 LoadingStrategy::Joined | LoadingStrategy::Selectin | LoadingStrategy::Subquery
70 )
71 }
72 pub fn is_lazy(&self) -> bool {
75 matches!(self, LoadingStrategy::Lazy)
76 }
77 pub fn prevents_load(&self) -> bool {
80 matches!(
81 self,
82 LoadingStrategy::Raise | LoadingStrategy::NoLoad | LoadingStrategy::WriteOnly
83 )
84 }
85 pub fn sql_hint(&self) -> Option<&'static str> {
88 match self {
89 LoadingStrategy::Joined => Some("/* +JOINEDLOAD */"),
90 LoadingStrategy::Selectin => Some("/* +SELECTINLOAD */"),
91 LoadingStrategy::Subquery => Some("/* +SUBQUERYLOAD */"),
92 _ => None,
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
100pub struct LoadOption {
101 path: String,
103 strategy: LoadingStrategy,
105}
106
107impl LoadOption {
108 pub fn new(path: impl Into<String>, strategy: LoadingStrategy) -> Self {
119 Self {
120 path: path.into(),
121 strategy,
122 }
123 }
124 pub fn path(&self) -> &str {
127 &self.path
128 }
129 pub fn strategy(&self) -> LoadingStrategy {
132 self.strategy
133 }
134 pub fn path_components(&self) -> Vec<&str> {
137 self.path.split('.').collect()
138 }
139}
140
141pub struct LoadOptionBuilder<T: Model> {
144 options: Vec<LoadOption>,
145 _phantom: PhantomData<T>,
146}
147
148impl<T: Model> LoadOptionBuilder<T> {
149 pub fn new() -> Self {
189 Self {
190 options: Vec::new(),
191 _phantom: PhantomData,
192 }
193 }
194 pub fn joinedload(mut self, path: impl Into<String>) -> Self {
198 self.options
199 .push(LoadOption::new(path, LoadingStrategy::Joined));
200 self
201 }
202 pub fn selectinload(mut self, path: impl Into<String>) -> Self {
206 self.options
207 .push(LoadOption::new(path, LoadingStrategy::Selectin));
208 self
209 }
210 pub fn subqueryload(mut self, path: impl Into<String>) -> Self {
214 self.options
215 .push(LoadOption::new(path, LoadingStrategy::Subquery));
216 self
217 }
218 pub fn lazyload(mut self, path: impl Into<String>) -> Self {
222 self.options
223 .push(LoadOption::new(path, LoadingStrategy::Lazy));
224 self
225 }
226 pub fn raiseload(mut self, path: impl Into<String>) -> Self {
230 self.options
231 .push(LoadOption::new(path, LoadingStrategy::Raise));
232 self
233 }
234 pub fn noload(mut self, path: impl Into<String>) -> Self {
238 self.options
239 .push(LoadOption::new(path, LoadingStrategy::NoLoad));
240 self
241 }
242 pub fn build(self) -> Vec<LoadOption> {
245 self.options
246 }
247}
248
249impl<T: Model> Default for LoadOptionBuilder<T> {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255pub fn joinedload(path: impl Into<String>) -> LoadOption {
262 LoadOption::new(path, LoadingStrategy::Joined)
263}
264pub fn selectinload(path: impl Into<String>) -> LoadOption {
267 LoadOption::new(path, LoadingStrategy::Selectin)
268}
269pub fn subqueryload(path: impl Into<String>) -> LoadOption {
272 LoadOption::new(path, LoadingStrategy::Subquery)
273}
274pub fn lazyload(path: impl Into<String>) -> LoadOption {
277 LoadOption::new(path, LoadingStrategy::Lazy)
278}
279pub fn raiseload(path: impl Into<String>) -> LoadOption {
282 LoadOption::new(path, LoadingStrategy::Raise)
283}
284pub fn noload(path: impl Into<String>) -> LoadOption {
287 LoadOption::new(path, LoadingStrategy::NoLoad)
288}
289
290#[derive(Debug, Default)]
293pub struct LoadContext {
294 loaded_paths: Vec<String>,
296 strategies: Vec<(String, LoadingStrategy)>,
298}
299
300impl LoadContext {
301 pub fn new() -> Self {
311 Self::default()
312 }
313 pub fn mark_loaded(&mut self, path: String, strategy: LoadingStrategy) {
316 self.loaded_paths.push(path.clone());
317 self.strategies.push((path, strategy));
318 }
319 pub fn is_loaded(&self, path: &str) -> bool {
322 self.loaded_paths.contains(&path.to_string())
323 }
324 pub fn strategy_for(&self, path: &str) -> Option<LoadingStrategy> {
327 self.strategies
328 .iter()
329 .find(|(p, _)| p == path)
330 .map(|(_, s)| *s)
331 }
332 pub fn loaded_paths(&self) -> &[String] {
335 &self.loaded_paths
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::orm::Manager;
343 use reinhardt_core::validators::TableName;
344 use serde::{Deserialize, Serialize};
345
346 #[derive(Debug, Clone, Serialize, Deserialize)]
347 struct User {
348 id: Option<i64>,
349 }
350
351 #[derive(Clone)]
352 struct UserFields;
353 impl crate::orm::model::FieldSelector for UserFields {
354 fn with_alias(self, _alias: &str) -> Self {
355 self
356 }
357 }
358
359 const USER_TABLE: TableName = TableName::new_const("users");
360
361 impl Model for User {
362 type PrimaryKey = i64;
363 type Fields = UserFields;
364 type Objects = Manager<Self>;
365
366 fn table_name() -> &'static str {
367 USER_TABLE.as_str()
368 }
369
370 fn new_fields() -> Self::Fields {
371 UserFields
372 }
373
374 fn primary_key(&self) -> Option<Self::PrimaryKey> {
375 self.id
376 }
377
378 fn set_primary_key(&mut self, value: Self::PrimaryKey) {
379 self.id = Some(value);
380 }
381 }
382
383 #[test]
384 fn test_loading_strategy_properties_unit() {
385 assert!(LoadingStrategy::Joined.is_eager());
386 assert!(LoadingStrategy::Selectin.is_eager());
387 assert!(LoadingStrategy::Subquery.is_eager());
388 assert!(LoadingStrategy::Lazy.is_lazy());
389 assert!(LoadingStrategy::Raise.prevents_load());
390 assert!(LoadingStrategy::NoLoad.prevents_load());
391 }
392
393 #[test]
394 fn test_load_option_creation() {
395 let opt = LoadOption::new("author.posts", LoadingStrategy::Joined);
396 assert_eq!(opt.path(), "author.posts");
397 assert_eq!(opt.strategy(), LoadingStrategy::Joined);
398 assert_eq!(opt.path_components(), vec!["author", "posts"]);
399 }
400
401 #[test]
402 fn test_load_option_builder() {
403 let options = LoadOptionBuilder::<User>::new()
404 .joinedload("posts")
405 .selectinload("comments")
406 .raiseload("profile")
407 .build();
408
409 assert_eq!(options.len(), 3);
410 assert_eq!(options[0].path(), "posts");
411 assert_eq!(options[0].strategy(), LoadingStrategy::Joined);
412 assert_eq!(options[1].path(), "comments");
413 assert_eq!(options[1].strategy(), LoadingStrategy::Selectin);
414 assert_eq!(options[2].path(), "profile");
415 assert_eq!(options[2].strategy(), LoadingStrategy::Raise);
416 }
417
418 #[test]
419 fn test_load_context() {
420 let mut ctx = LoadContext::new();
421 ctx.mark_loaded("posts".to_string(), LoadingStrategy::Joined);
422 ctx.mark_loaded("comments".to_string(), LoadingStrategy::Selectin);
423
424 assert!(ctx.is_loaded("posts"));
425 assert!(ctx.is_loaded("comments"));
426 assert!(!ctx.is_loaded("profile"));
427 assert_eq!(ctx.strategy_for("posts"), Some(LoadingStrategy::Joined));
428 assert_eq!(
429 ctx.strategy_for("comments"),
430 Some(LoadingStrategy::Selectin)
431 );
432 }
433
434 #[test]
435 fn test_helper_functions() {
436 let joined = joinedload("posts");
437 assert_eq!(joined.strategy(), LoadingStrategy::Joined);
438
439 let selectin = selectinload("comments");
440 assert_eq!(selectin.strategy(), LoadingStrategy::Selectin);
441
442 let raise = raiseload("profile");
443 assert_eq!(raise.strategy(), LoadingStrategy::Raise);
444 }
445
446 #[test]
447 fn test_loading_sql_hints() {
448 assert!(LoadingStrategy::Joined.sql_hint().is_some());
449 assert!(LoadingStrategy::Selectin.sql_hint().is_some());
450 assert!(LoadingStrategy::Subquery.sql_hint().is_some());
451 assert!(LoadingStrategy::Lazy.sql_hint().is_none());
452 }
453}