1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
//! Eager Loading System - Prevents N+1 query problems with efficient relationship loading
use sqlx::{Column, Pool, Postgres, Row};
use std::collections::HashMap;
use super::constraints::RelationshipConstraintBuilder;
use crate::error::{ModelError, ModelResult};
use crate::model::Model;
use crate::query::QueryBuilder;
/// Represents a relationship to be eagerly loaded
#[derive(Debug)]
pub struct EagerLoadSpec {
/// Relationship name (e.g., "posts" or "posts.comments")
pub relation: String,
/// Optional constraints for the relationship query
pub constraints: Option<RelationshipConstraintBuilder>,
}
/// Core eager loader that manages relationship loading
pub struct EagerLoader {
/// Relationships to load
specs: Vec<EagerLoadSpec>,
/// Loaded relationship data organized by relationship name and parent key
loaded_data: HashMap<String, HashMap<String, Vec<serde_json::Value>>>,
}
impl EagerLoader {
/// Create a new eager loader
pub fn new() -> Self {
Self {
specs: Vec::new(),
loaded_data: HashMap::new(),
}
}
/// Add a relationship to eagerly load
pub fn with(mut self, relation: &str) -> Self {
self.specs.push(EagerLoadSpec {
relation: relation.to_string(),
constraints: None,
});
self
}
/// Add a relationship with constraints
pub fn with_constraint<F>(mut self, relation: &str, constraint_fn: F) -> Self
where
F: FnOnce(RelationshipConstraintBuilder) -> RelationshipConstraintBuilder + 'static,
{
// Build the constraint and store it
let builder = RelationshipConstraintBuilder::new();
let built_constraints = constraint_fn(builder);
self.specs.push(EagerLoadSpec {
relation: relation.to_string(),
constraints: Some(built_constraints),
});
self
}
/// Load relationships for a collection of models
pub async fn load_for_models<M>(
&mut self,
pool: &Pool<Postgres>,
models: &[M],
) -> ModelResult<()>
where
M: Model + Send + Sync,
{
if models.is_empty() {
return Ok(());
}
// Load each relationship specification
let relations: Vec<String> = self.specs.iter().map(|s| s.relation.clone()).collect();
for relation in relations {
self.load_relationship(pool, models, &relation).await?;
}
Ok(())
}
/// Load a specific relationship for the given models
async fn load_relationship<M>(
&mut self,
pool: &Pool<Postgres>,
models: &[M],
relation: &str,
) -> ModelResult<()>
where
M: Model + Send + Sync,
{
// Parse nested relationships (e.g., "posts.comments.user")
let parts: Vec<&str> = relation.split('.').collect();
if parts.len() == 1 {
// Simple relationship - find constraints for this relation
let spec_index = self.specs.iter().position(|spec| spec.relation == relation);
if let Some(index) = spec_index {
let has_constraints = self.specs[index].constraints.is_some();
if has_constraints {
// We need to work around the borrow checker by taking ownership temporarily
let spec = self.specs.remove(index);
let constraints = spec.constraints.as_ref();
let result = self
.load_simple_relationship(pool, models, relation, constraints)
.await;
self.specs.insert(index, spec);
result?;
} else {
self.load_simple_relationship(pool, models, relation, None)
.await?;
}
} else {
self.load_simple_relationship(pool, models, relation, None)
.await?;
}
} else {
// Nested relationship - load step by step
self.load_nested_relationship(pool, models, &parts).await?;
}
Ok(())
}
/// Load a simple (non-nested) relationship
async fn load_simple_relationship<M>(
&mut self,
pool: &Pool<Postgres>,
models: &[M],
relation: &str,
constraints: Option<&RelationshipConstraintBuilder>,
) -> ModelResult<()>
where
M: Model + Send + Sync,
{
// Collect parent keys
let parent_keys: Vec<String> = models
.iter()
.filter_map(|m| m.primary_key().map(|pk| pk.to_string()))
.collect();
if parent_keys.is_empty() {
return Ok(());
}
// Build the relationship query with constraints
let query = self
.build_relationship_query(relation, &parent_keys, constraints)
.await?;
// Execute the query
let rows = sqlx::query(&query)
.fetch_all(pool)
.await
.map_err(|e| ModelError::Database(e.to_string()))?;
// Group results by parent key
let mut grouped_results: HashMap<String, Vec<serde_json::Value>> = HashMap::new();
for row in rows {
// Extract parent key and convert row to JSON
// This assumes foreign key is always "parent_id" - needs to be dynamic
let parent_key: String = row
.try_get("parent_id")
.map_err(|e| ModelError::Database(e.to_string()))?;
let json_value = self.row_to_json(&row)?;
grouped_results
.entry(parent_key)
.or_default()
.push(json_value);
}
// Store the loaded data
self.loaded_data
.insert(relation.to_string(), grouped_results);
Ok(())
}
/// Load nested relationships (e.g., "posts.comments.user")
async fn load_nested_relationship<M>(
&mut self,
pool: &Pool<Postgres>,
models: &[M],
parts: &[&str],
) -> ModelResult<()>
where
M: Model + Send + Sync,
{
// Start with the root models
let mut current_models: Vec<serde_json::Value> = Vec::new();
// Load the first level relationship
self.load_simple_relationship(pool, models, parts[0], None)
.await?;
// Get the loaded first level data
if let Some(first_level_data) = self.loaded_data.get(parts[0]) {
for values in first_level_data.values() {
current_models.extend(values.iter().cloned());
}
}
// Load subsequent levels
for i in 1..parts.len() {
let relation_path = parts[0..=i].join(".");
let current_relation = parts[i];
// Extract parent keys from current models
let parent_keys: Vec<String> = current_models
.iter()
.filter_map(|v| {
v.get("id")
.and_then(|id| id.as_str())
.map(|s| s.to_string())
})
.collect();
if parent_keys.is_empty() {
continue;
}
// Build and execute query for this level
let query = self
.build_relationship_query(current_relation, &parent_keys, None)
.await?;
let rows = sqlx::query(&query)
.fetch_all(pool)
.await
.map_err(|e| ModelError::Database(e.to_string()))?;
// Group results and store
let mut grouped_results: HashMap<String, Vec<serde_json::Value>> = HashMap::new();
let mut next_level_models = Vec::new();
for row in rows {
let parent_key: String = row
.try_get("parent_id")
.map_err(|e| ModelError::Database(e.to_string()))?;
let json_value = self.row_to_json(&row)?;
next_level_models.push(json_value.clone());
grouped_results
.entry(parent_key)
.or_default()
.push(json_value);
}
self.loaded_data.insert(relation_path, grouped_results);
current_models = next_level_models;
}
Ok(())
}
/// Build SQL query for a relationship with constraints
async fn build_relationship_query(
&self,
relation: &str,
parent_keys: &[String],
constraints: Option<&RelationshipConstraintBuilder>,
) -> ModelResult<String> {
// Build base query using QueryBuilder
let mut query = QueryBuilder::<()>::new();
// Determine table name and foreign key from relation name
// This is a basic implementation - needs proper metadata
let table_name = match relation {
"posts" => "posts",
"comments" => "comments",
"user" => "users",
"profile" => "profiles",
_ => relation, // fallback
};
let foreign_key = match relation {
"posts" => "user_id",
"comments" => "post_id",
"user" => "user_id",
"profile" => "user_id",
_ => "parent_id", // fallback
};
// Build base query
query = query
.select("*")
.from(table_name)
.where_in(foreign_key, parent_keys.to_vec());
// Apply constraints if present
if let Some(constraint_builder) = constraints {
constraint_builder.apply_all(&mut query).await?;
}
// Generate SQL
Ok(query.to_sql())
}
/// Convert a database row to JSON value
fn row_to_json(&self, row: &sqlx::postgres::PgRow) -> ModelResult<serde_json::Value> {
let mut map = serde_json::Map::new();
// This is a simplified conversion
// In practice, we'd need to handle all column types properly
for (i, column) in row.columns().iter().enumerate() {
let column_name = column.name();
// Try to get the value as different types
if let Ok(value) = row.try_get::<Option<String>, _>(i) {
map.insert(
column_name.to_string(),
serde_json::Value::String(value.unwrap_or_default()),
);
} else if let Ok(value) = row.try_get::<Option<i64>, _>(i) {
if let Some(val) = value {
map.insert(
column_name.to_string(),
serde_json::Value::Number(serde_json::Number::from(val)),
);
} else {
map.insert(column_name.to_string(), serde_json::Value::Null);
}
}
// Add more type conversions as needed
}
Ok(serde_json::Value::Object(map))
}
/// Get loaded relationship data for a parent key
pub fn get_loaded_data(
&self,
relation: &str,
parent_key: &str,
) -> Option<&Vec<serde_json::Value>> {
self.loaded_data.get(relation)?.get(parent_key)
}
/// Check if a relationship has been loaded
pub fn is_loaded(&self, relation: &str) -> bool {
self.loaded_data.contains_key(relation)
}
/// Get all loaded relationships
pub fn loaded_relations(&self) -> Vec<&String> {
self.loaded_data.keys().collect()
}
}
impl Default for EagerLoader {
fn default() -> Self {
Self::new()
}
}
/// Extension trait for QueryBuilder to add eager loading support
pub trait QueryBuilderEagerLoading<M> {
/// Add a relationship to eagerly load
fn with(self, relation: &str) -> Self;
/// Add a relationship with constraints
fn with_where<F>(self, relation: &str, constraint: F) -> Self
where
F: FnOnce(RelationshipConstraintBuilder) -> RelationshipConstraintBuilder;
/// Add conditional eager loading
fn with_when(self, condition: bool, relation: &str) -> Self;
/// Load relationship counts without loading the relationships
fn with_count(self, relation: &str) -> Self;
/// Load relationship counts with constraints
fn with_count_where<F>(self, alias: &str, relation: &str, constraint: F) -> Self
where
F: FnOnce(RelationshipConstraintBuilder) -> RelationshipConstraintBuilder;
}
// Implementation will be added in the query builder integration