join_query_builder/
join_query_builder.rs

1// Demonstrates JOIN operations between collections using keypaths
2// This example shows how to:
3// 1. Perform inner joins between collections
4// 2. Perform left joins with optional results
5// 3. Join on matching field values
6// 4. Create multi-table queries
7// 5. Use keypaths for type-safe join conditions
8// cargo run --example join_query_builder
9
10use rust_queries_builder::JoinQuery;
11use key_paths_derive::Keypaths;
12use std::collections::HashMap;
13
14// Database schema: Users, Orders, Products
15#[derive(Debug, Clone, Keypaths)]
16struct User {
17    id: u32,
18    name: String,
19    email: String,
20    city: String,
21}
22
23#[derive(Debug, Clone, Keypaths)]
24struct Order {
25    id: u32,
26    user_id: u32,
27    product_id: u32,
28    quantity: u32,
29    total: f64,
30}
31
32#[derive(Debug, Clone, Keypaths)]
33struct Product {
34    id: u32,
35    name: String,
36    price: f64,
37    category: String,
38}
39
40// Join result types
41#[derive(Debug, Clone)]
42struct UserOrder {
43    user_name: String,
44    user_email: String,
45    order_id: u32,
46    quantity: u32,
47    total: f64,
48}
49
50#[derive(Debug, Clone)]
51struct OrderDetail {
52    order_id: u32,
53    user_name: String,
54    product_name: String,
55    quantity: u32,
56    price: f64,
57    total: f64,
58}
59
60#[derive(Debug, Clone)]
61struct UserOrderCount {
62    user_name: String,
63    user_city: String,
64    order_count: usize,
65    total_spent: f64,
66}
67
68#[derive(Debug, Clone)]
69struct CategorySales {
70    category: String,
71    total_orders: usize,
72    total_revenue: f64,
73    unique_customers: usize,
74}
75
76// Helper function for creating sample data
77fn create_sample_data() -> (Vec<User>, Vec<Order>, Vec<Product>) {
78    let users = vec![
79        User {
80            id: 1,
81            name: "Alice".to_string(),
82            email: "alice@example.com".to_string(),
83            city: "New York".to_string(),
84        },
85        User {
86            id: 2,
87            name: "Bob".to_string(),
88            email: "bob@example.com".to_string(),
89            city: "San Francisco".to_string(),
90        },
91        User {
92            id: 3,
93            name: "Charlie".to_string(),
94            email: "charlie@example.com".to_string(),
95            city: "New York".to_string(),
96        },
97        User {
98            id: 4,
99            name: "Diana".to_string(),
100            email: "diana@example.com".to_string(),
101            city: "Boston".to_string(),
102        },
103    ];
104
105    let products = vec![
106        Product {
107            id: 101,
108            name: "Laptop".to_string(),
109            price: 999.99,
110            category: "Electronics".to_string(),
111        },
112        Product {
113            id: 102,
114            name: "Mouse".to_string(),
115            price: 29.99,
116            category: "Electronics".to_string(),
117        },
118        Product {
119            id: 103,
120            name: "Desk Chair".to_string(),
121            price: 199.99,
122            category: "Furniture".to_string(),
123        },
124        Product {
125            id: 104,
126            name: "Monitor".to_string(),
127            price: 299.99,
128            category: "Electronics".to_string(),
129        },
130        Product {
131            id: 105,
132            name: "Keyboard".to_string(),
133            price: 79.99,
134            category: "Electronics".to_string(),
135        },
136    ];
137
138    let orders = vec![
139        Order {
140            id: 1001,
141            user_id: 1,
142            product_id: 101,
143            quantity: 1,
144            total: 999.99,
145        },
146        Order {
147            id: 1002,
148            user_id: 1,
149            product_id: 102,
150            quantity: 2,
151            total: 59.98,
152        },
153        Order {
154            id: 1003,
155            user_id: 2,
156            product_id: 103,
157            quantity: 1,
158            total: 199.99,
159        },
160        Order {
161            id: 1004,
162            user_id: 2,
163            product_id: 104,
164            quantity: 1,
165            total: 299.99,
166        },
167        Order {
168            id: 1005,
169            user_id: 3,
170            product_id: 102,
171            quantity: 3,
172            total: 89.97,
173        },
174        Order {
175            id: 1006,
176            user_id: 1,
177            product_id: 105,
178            quantity: 1,
179            total: 79.99,
180        },
181        Order {
182            id: 1007,
183            user_id: 3,
184            product_id: 101,
185            quantity: 1,
186            total: 999.99,
187        },
188    ];
189
190    (users, orders, products)
191}
192
193fn main() {
194    println!("=== Join Query Builder Demo ===\n");
195
196    let (users, orders, products) = create_sample_data();
197
198    println!("Database:");
199    println!("  Users: {}", users.len());
200    println!("  Orders: {}", orders.len());
201    println!("  Products: {}\n", products.len());
202
203    // Join 1: Inner join Users and Orders
204    println!("--- Join 1: Users with Their Orders ---");
205    let user_orders = JoinQuery::new(&users, &orders).inner_join(
206        User::id_r(),
207        Order::user_id_r(),
208        |user, order| UserOrder {
209            user_name: user.name.clone(),
210            user_email: user.email.clone(),
211            order_id: order.id,
212            quantity: order.quantity,
213            total: order.total,
214        },
215    );
216
217    for uo in &user_orders {
218        println!(
219            "  • {} - Order #{} - {} items - ${:.2}",
220            uo.user_name, uo.order_id, uo.quantity, uo.total
221        );
222    }
223    println!("Total: {} user-order pairs", user_orders.len());
224
225    // Join 2: Three-way join (Orders -> Users, Orders -> Products)
226    println!("\n--- Join 2: Complete Order Details (3-Way Join) ---");
227    
228    // First join: Orders with Users
229    let orders_with_users = JoinQuery::new(&orders, &users).inner_join(
230        Order::user_id_r(),
231        User::id_r(),
232        |order, user| (order.clone(), user.clone()),
233    );
234
235    // Second join: (Orders+Users) with Products
236    let mut order_details = Vec::new();
237    for (order, user) in &orders_with_users {
238        for product in &products {
239            if order.product_id == product.id {
240                order_details.push(OrderDetail {
241                    order_id: order.id,
242                    user_name: user.name.clone(),
243                    product_name: product.name.clone(),
244                    quantity: order.quantity,
245                    price: product.price,
246                    total: order.total,
247                });
248            }
249        }
250    }
251
252    for od in &order_details {
253        println!(
254            "  • Order #{}: {} bought {} x {} @ ${:.2} = ${:.2}",
255            od.order_id, od.user_name, od.quantity, od.product_name, od.price, od.total
256        );
257    }
258
259    // Join 3: Left join to show all users (including those without orders)
260    println!("\n--- Join 3: All Users with Order Count (Left Join) ---");
261    
262    // Use left_join to get all users with their orders (or None)
263    let user_order_pairs = JoinQuery::new(&users, &orders).left_join(
264        User::id_r(),
265        Order::user_id_r(),
266        |user, order| (user.clone(), order.map(|o| o.clone())),
267    );
268
269    // Group by user to count orders
270    let mut user_stats: HashMap<u32, (String, String, usize, f64)> = HashMap::new();
271    for (user, order) in &user_order_pairs {
272        let entry = user_stats
273            .entry(user.id)
274            .or_insert_with(|| (user.name.clone(), user.city.clone(), 0, 0.0));
275        if let Some(order) = order {
276            entry.2 += 1; // order count
277            entry.3 += order.total; // total spent
278        }
279    }
280
281    let mut user_order_stats: Vec<_> = user_stats
282        .into_iter()
283        .map(|(_, (name, city, count, total))| UserOrderCount {
284            user_name: name,
285            user_city: city,
286            order_count: count,
287            total_spent: total,
288        })
289        .collect();
290
291    user_order_stats.sort_by(|a, b| a.user_name.cmp(&b.user_name));
292
293    for stat in &user_order_stats {
294        if stat.order_count > 0 {
295            println!(
296                "  • {} ({}) - {} orders - ${:.2} total",
297                stat.user_name, stat.user_city, stat.order_count, stat.total_spent
298            );
299        } else {
300            println!("  • {} ({}) - No orders yet", stat.user_name, stat.user_city);
301        }
302    }
303
304    // Join 4: Aggregated join - Category sales analysis
305    println!("\n--- Join 4: Sales by Product Category ---");
306
307    // Join orders with products to get category information
308    let order_products = JoinQuery::new(&orders, &products).inner_join(
309        Order::product_id_r(),
310        Product::id_r(),
311        |order, product| (order.clone(), product.clone()),
312    );
313
314    // Aggregate by category
315    let mut category_stats: HashMap<String, (Vec<u32>, f64, std::collections::HashSet<u32>)> =
316        HashMap::new();
317
318    for (order, product) in &order_products {
319        let entry = category_stats
320            .entry(product.category.clone())
321            .or_insert_with(|| (Vec::new(), 0.0, std::collections::HashSet::new()));
322        entry.0.push(order.id);
323        entry.1 += order.total;
324        entry.2.insert(order.user_id);
325    }
326
327    let mut category_sales: Vec<CategorySales> = category_stats
328        .into_iter()
329        .map(|(category, (orders, revenue, customers))| CategorySales {
330            category,
331            total_orders: orders.len(),
332            total_revenue: revenue,
333            unique_customers: customers.len(),
334        })
335        .collect();
336
337    category_sales.sort_by(|a, b| {
338        b.total_revenue
339            .partial_cmp(&a.total_revenue)
340            .unwrap_or(std::cmp::Ordering::Equal)
341    });
342
343    for cs in &category_sales {
344        println!(
345            "  • {}: {} orders - ${:.2} revenue - {} customers",
346            cs.category, cs.total_orders, cs.total_revenue, cs.unique_customers
347        );
348    }
349
350    // Join 5: Filtered join - High value orders
351    println!("\n--- Join 5: High Value Orders (>$100) with User Details ---");
352    let high_value_orders = JoinQuery::new(&orders, &users).inner_join_where(
353        Order::user_id_r(),
354        User::id_r(),
355        |order, _user| order.total > 100.0,
356        |order, user| (user.name.clone(), order.id, order.total),
357    );
358
359    for (name, order_id, total) in &high_value_orders {
360        println!("  • {} - Order #{} - ${:.2}", name, order_id, total);
361    }
362
363    // Join 6: Users in same city analysis
364    println!("\n--- Join 6: Users from Same City ---");
365    let user_pairs = JoinQuery::new(&users, &users).inner_join_where(
366        User::city_r(),
367        User::city_r(),
368        |u1, u2| u1.id < u2.id, // Avoid duplicates and self-pairs
369        |u1, u2| (u1.name.clone(), u2.name.clone(), u1.city.clone()),
370    );
371
372    for (name1, name2, city) in &user_pairs {
373        println!("  • {} and {} both live in {}", name1, name2, city);
374    }
375
376    // Join 7: Product popularity
377    println!("\n--- Join 7: Product Popularity Ranking ---");
378    
379    // Join orders with products
380    let product_order_pairs = JoinQuery::new(&products, &orders).inner_join(
381        Product::id_r(),
382        Order::product_id_r(),
383        |product, order| (product.clone(), order.clone()),
384    );
385
386    // Aggregate by product
387    let mut product_sales: HashMap<u32, (String, usize, u32, f64)> = HashMap::new();
388    for (product, order) in &product_order_pairs {
389        let entry = product_sales
390            .entry(product.id)
391            .or_insert_with(|| (product.name.clone(), 0, 0, 0.0));
392        entry.1 += 1; // order count
393        entry.2 += order.quantity; // total quantity
394        entry.3 += order.total; // total revenue
395    }
396
397    let mut popularity: Vec<_> = product_sales.into_iter().collect();
398    popularity.sort_by(|a, b| b.1 .1.cmp(&a.1 .1)); // sort by order count
399
400    for (_, (name, order_count, total_qty, revenue)) in &popularity {
401        println!(
402            "  • {} - {} orders - {} units - ${:.2}",
403            name, order_count, total_qty, revenue
404        );
405    }
406
407    // Join 8: User spending by city
408    println!("\n--- Join 8: Total Spending by City ---");
409    
410    // Join users with orders to get city and spending info
411    let user_city_orders = JoinQuery::new(&users, &orders).inner_join(
412        User::id_r(),
413        Order::user_id_r(),
414        |user, order| (user.city.clone(), order.total, user.id),
415    );
416
417    // Aggregate by city
418    let mut city_spending: HashMap<String, (f64, std::collections::HashSet<u32>)> = HashMap::new();
419    for (city, total, user_id) in &user_city_orders {
420        let entry = city_spending
421            .entry(city.clone())
422            .or_insert_with(|| (0.0, std::collections::HashSet::new()));
423        entry.0 += total;
424        entry.1.insert(*user_id);
425    }
426
427    let mut city_stats: Vec<_> = city_spending
428        .into_iter()
429        .map(|(city, (total, customers))| (city, total, customers.len()))
430        .collect();
431    
432    city_stats.sort_by(|a, b| {
433        b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
434    });
435
436    for (city, total, customer_count) in &city_stats {
437        println!(
438            "  • {} - ${:.2} total - {} customers - ${:.2} avg",
439            city,
440            total,
441            customer_count,
442            total / *customer_count as f64
443        );
444    }
445
446    // Statistics summary
447    println!("\n=== Summary Statistics ===");
448    println!("Total orders: {}", orders.len());
449    
450    let total_revenue: f64 = orders.iter().map(|o| o.total).sum();
451    println!("Total revenue: ${:.2}", total_revenue);
452    println!("Average order value: ${:.2}", total_revenue / orders.len() as f64);
453    
454    // Count unique customers using a join
455    let unique_customers: std::collections::HashSet<u32> = 
456        orders.iter().map(|o| o.user_id).collect();
457    println!("Active customers: {}", unique_customers.len());
458    println!(
459        "Average orders per customer: {:.1}",
460        orders.len() as f64 / unique_customers.len() as f64
461    );
462
463    println!("\n✓ Join query builder demo complete!");
464}
465