1use std::collections::HashMap;
4
5use crate::filter::FilterValue;
6use crate::traits::QueryEngine;
7
8use super::include::IncludeSpec;
9use super::spec::RelationSpec;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum RelationLoadStrategy {
14 #[default]
16 Separate,
17 Join,
19 Lazy,
21}
22
23impl RelationLoadStrategy {
24 pub fn is_separate(&self) -> bool {
26 matches!(self, Self::Separate)
27 }
28
29 pub fn is_join(&self) -> bool {
31 matches!(self, Self::Join)
32 }
33
34 pub fn is_lazy(&self) -> bool {
36 matches!(self, Self::Lazy)
37 }
38}
39
40pub struct RelationLoader<E: QueryEngine> {
42 engine: E,
43 strategy: RelationLoadStrategy,
44 batch_size: usize,
45}
46
47impl<E: QueryEngine> RelationLoader<E> {
48 pub fn new(engine: E) -> Self {
50 Self {
51 engine,
52 strategy: RelationLoadStrategy::Separate,
53 batch_size: 100,
54 }
55 }
56
57 pub fn with_strategy(mut self, strategy: RelationLoadStrategy) -> Self {
59 self.strategy = strategy;
60 self
61 }
62
63 pub fn with_batch_size(mut self, size: usize) -> Self {
68 self.batch_size = size;
69 self
70 }
71
72 pub fn engine(&self) -> &E {
74 &self.engine
75 }
76
77 pub fn build_one_to_many_query(
83 &self,
84 spec: &RelationSpec,
85 include: &IncludeSpec,
86 parent_ids: &[FilterValue],
87 ) -> (String, Vec<FilterValue>) {
88 let dialect = self.engine.dialect();
89
90 let mut sql = format!(
91 "SELECT * FROM {} WHERE {} IN (",
92 spec.related_table,
93 spec.references.first().unwrap_or(&"id".to_string())
94 );
95
96 let placeholders: Vec<_> = (1..=parent_ids.len())
97 .map(|i| dialect.placeholder(i))
98 .collect();
99 sql.push_str(&placeholders.join(", "));
100 sql.push(')');
101
102 let mut params = parent_ids.to_vec();
103
104 if let Some(ref filter) = include.filter {
106 let (filter_sql, filter_params) = filter.to_sql(parent_ids.len(), dialect);
107 sql.push_str(" AND ");
108 sql.push_str(&filter_sql);
109 params.extend(filter_params);
110 }
111
112 if let Some(ref order) = include.order_by {
114 sql.push_str(" ORDER BY ");
115 sql.push_str(&order.to_sql());
116 }
117
118 if let Some(ref pagination) = include.pagination {
120 let pagination_sql = pagination.to_sql();
121 if !pagination_sql.is_empty() {
122 sql.push(' ');
123 sql.push_str(&pagination_sql);
124 }
125 }
126
127 (sql, params)
128 }
129
130 pub fn build_one_to_many_queries(
137 &self,
138 spec: &RelationSpec,
139 include: &IncludeSpec,
140 parent_ids: &[FilterValue],
141 ) -> Vec<(String, Vec<FilterValue>)> {
142 let batch_size = self.batch_size.max(1);
144 parent_ids
145 .chunks(batch_size)
146 .map(|chunk| self.build_one_to_many_query(spec, include, chunk))
147 .collect()
148 }
149
150 pub fn build_many_to_one_query(
152 &self,
153 spec: &RelationSpec,
154 child_foreign_keys: &[FilterValue],
155 ) -> (String, Vec<FilterValue>) {
156 let default_pk = "id".to_string();
157 let pk = spec.references.first().unwrap_or(&default_pk);
158
159 let mut sql = format!("SELECT * FROM {} WHERE {} IN (", spec.related_table, pk);
160
161 let placeholders: Vec<_> = (1..=child_foreign_keys.len())
162 .map(|i| format!("${}", i))
163 .collect();
164 sql.push_str(&placeholders.join(", "));
165 sql.push(')');
166
167 (sql, child_foreign_keys.to_vec())
168 }
169
170 pub fn build_many_to_many_query(
172 &self,
173 spec: &RelationSpec,
174 include: &IncludeSpec,
175 parent_ids: &[FilterValue],
176 ) -> (String, Vec<FilterValue>) {
177 let jt = spec.join_table.as_ref().unwrap_or_else(|| {
178 panic!(
179 "build_many_to_many_query: relation `{}` (table `{}`) has no join table; \
180 a many-to-many RelationSpec must be constructed via \
181 RelationSpec::many_to_many(...), which requires a JoinTableSpec",
182 spec.name, spec.related_table
183 )
184 });
185
186 let mut sql = format!(
187 "SELECT t.*, jt.{} as _parent_id FROM {} t \
188 INNER JOIN {} jt ON t.{} = jt.{} \
189 WHERE jt.{} IN (",
190 jt.source_column,
191 spec.related_table,
192 jt.table_name,
193 spec.references.first().unwrap_or(&"id".to_string()),
194 jt.target_column,
195 jt.source_column
196 );
197
198 let placeholders: Vec<_> = (1..=parent_ids.len()).map(|i| format!("${}", i)).collect();
199 sql.push_str(&placeholders.join(", "));
200 sql.push(')');
201
202 if let Some(ref order) = include.order_by {
204 sql.push_str(" ORDER BY ");
205 sql.push_str(&order.to_sql());
206 }
207
208 (sql, parent_ids.to_vec())
209 }
210}
211
212impl<E: QueryEngine + Clone> Clone for RelationLoader<E> {
213 fn clone(&self) -> Self {
214 Self {
215 engine: self.engine.clone(),
216 strategy: self.strategy,
217 batch_size: self.batch_size,
218 }
219 }
220}
221
222pub type RelationLoadResult<T> = HashMap<String, Vec<T>>;
224
225#[derive(Debug)]
227pub struct BatchLoadContext {
228 pub parent_ids: Vec<FilterValue>,
230 pub group_by_field: String,
232}
233
234impl BatchLoadContext {
235 pub fn new(parent_ids: Vec<FilterValue>, group_by_field: impl Into<String>) -> Self {
237 Self {
238 parent_ids,
239 group_by_field: group_by_field.into(),
240 }
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::error::{QueryError, QueryResult};
248 use crate::filter::Filter;
249 use crate::traits::{BoxFuture, Model};
250 use crate::types::OrderByField;
251
252 struct TestModel;
253
254 impl Model for TestModel {
255 const MODEL_NAME: &'static str = "TestModel";
256 const TABLE_NAME: &'static str = "test_models";
257 const PRIMARY_KEY: &'static [&'static str] = &["id"];
258 const COLUMNS: &'static [&'static str] = &["id", "name"];
259 }
260
261 #[derive(Clone)]
262 struct MockEngine;
263
264 impl QueryEngine for MockEngine {
265 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
266 &crate::dialect::Postgres
267 }
268
269 fn query_many<T: Model + Send + 'static>(
270 &self,
271 _sql: &str,
272 _params: Vec<FilterValue>,
273 ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
274 Box::pin(async { Ok(Vec::new()) })
275 }
276
277 fn query_one<T: Model + Send + 'static>(
278 &self,
279 _sql: &str,
280 _params: Vec<FilterValue>,
281 ) -> BoxFuture<'_, QueryResult<T>> {
282 Box::pin(async { Err(QueryError::not_found("test")) })
283 }
284
285 fn query_optional<T: Model + Send + 'static>(
286 &self,
287 _sql: &str,
288 _params: Vec<FilterValue>,
289 ) -> BoxFuture<'_, QueryResult<Option<T>>> {
290 Box::pin(async { Ok(None) })
291 }
292
293 fn execute_insert<T: Model + Send + 'static>(
294 &self,
295 _sql: &str,
296 _params: Vec<FilterValue>,
297 ) -> BoxFuture<'_, QueryResult<T>> {
298 Box::pin(async { Err(QueryError::not_found("test")) })
299 }
300
301 fn execute_update<T: Model + Send + 'static>(
302 &self,
303 _sql: &str,
304 _params: Vec<FilterValue>,
305 ) -> BoxFuture<'_, QueryResult<Vec<T>>> {
306 Box::pin(async { Ok(Vec::new()) })
307 }
308
309 fn execute_delete(
310 &self,
311 _sql: &str,
312 _params: Vec<FilterValue>,
313 ) -> BoxFuture<'_, QueryResult<u64>> {
314 Box::pin(async { Ok(0) })
315 }
316
317 fn execute_raw(
318 &self,
319 _sql: &str,
320 _params: Vec<FilterValue>,
321 ) -> BoxFuture<'_, QueryResult<u64>> {
322 Box::pin(async { Ok(0) })
323 }
324
325 fn count(&self, _sql: &str, _params: Vec<FilterValue>) -> BoxFuture<'_, QueryResult<u64>> {
326 Box::pin(async { Ok(0) })
327 }
328 }
329
330 #[test]
331 fn test_relation_load_strategy() {
332 assert!(RelationLoadStrategy::Separate.is_separate());
333 assert!(RelationLoadStrategy::Join.is_join());
334 assert!(RelationLoadStrategy::Lazy.is_lazy());
335 }
336
337 #[test]
338 fn test_one_to_many_query() {
339 let loader = RelationLoader::new(MockEngine);
340 let spec = RelationSpec::one_to_many("posts", "Post", "posts").references(["author_id"]);
341 let include = IncludeSpec::new("posts");
342 let parent_ids = vec![FilterValue::Int(1), FilterValue::Int(2)];
343
344 let (sql, params) = loader.build_one_to_many_query(&spec, &include, &parent_ids);
345
346 assert!(sql.contains("SELECT * FROM posts"));
347 assert!(sql.contains("WHERE author_id IN"));
348 assert_eq!(params.len(), 2);
349 }
350
351 #[test]
352 fn test_one_to_many_query_with_filter_order_and_pagination() {
353 let loader = RelationLoader::new(MockEngine);
354 let spec = RelationSpec::one_to_many("posts", "Post", "posts").references(["author_id"]);
355 let include = IncludeSpec::new("posts")
356 .r#where(Filter::Equals("published".into(), FilterValue::Bool(true)))
357 .order_by(OrderByField::desc("created_at"))
358 .take(5);
359 let parent_ids = vec![FilterValue::Int(1), FilterValue::Int(2)];
360
361 let (sql, params) = loader.build_one_to_many_query(&spec, &include, &parent_ids);
362
363 assert!(sql.contains("WHERE author_id IN"));
364 assert!(sql.contains("AND"));
365 assert!(sql.contains("ORDER BY created_at DESC"));
366 assert!(sql.contains("LIMIT 5"));
367 assert!(sql.find("AND").unwrap() < sql.find("ORDER BY").unwrap());
369 assert!(sql.find("ORDER BY").unwrap() < sql.find("LIMIT").unwrap());
370 assert_eq!(params.len(), 3);
372 }
373
374 #[test]
375 fn test_one_to_many_batched_queries() {
376 let loader = RelationLoader::new(MockEngine).with_batch_size(2);
377 let spec = RelationSpec::one_to_many("posts", "Post", "posts").references(["author_id"]);
378 let include = IncludeSpec::new("posts");
379 let parent_ids: Vec<_> = (1..=5).map(FilterValue::Int).collect();
380
381 let queries = loader.build_one_to_many_queries(&spec, &include, &parent_ids);
382
383 assert_eq!(queries.len(), 3);
385 assert_eq!(queries[0].1.len(), 2);
386 assert_eq!(queries[1].1.len(), 2);
387 assert_eq!(queries[2].1.len(), 1);
388 assert!(queries[0].0.contains("$1, $2"));
390 assert!(queries[2].0.contains("$1)"));
391 assert!(!queries[2].0.contains("$2"));
392 }
393
394 #[test]
395 fn test_many_to_one_query() {
396 let loader = RelationLoader::new(MockEngine);
397 let spec = RelationSpec::many_to_one("author", "User", "users").references(["id"]);
398 let foreign_keys = vec![FilterValue::Int(1), FilterValue::Int(2)];
399
400 let (sql, params) = loader.build_many_to_one_query(&spec, &foreign_keys);
401
402 assert!(sql.contains("SELECT * FROM users"));
403 assert!(sql.contains("WHERE id IN"));
404 assert_eq!(params.len(), 2);
405 }
406}