1use crate::prelude::*;
2use toasty_core::driver::Operation;
3
4#[driver_test]
7pub async fn composite_index_basic(t: &mut Test) -> Result<()> {
8 #[derive(Debug, toasty::Model)]
9 #[key(user_id, game_title)]
10 #[index(game_title, top_score)]
11 struct GameScore {
12 user_id: String,
13 game_title: String,
14 top_score: i64,
15 }
16
17 let mut db = t.setup_db(models!(GameScore)).await;
18
19 toasty::create!(GameScore::[
20 { user_id: "u1", game_title: "chess", top_score: 100_i64 },
21 { user_id: "u2", game_title: "chess", top_score: 200_i64 },
22 { user_id: "u1", game_title: "go", top_score: 50_i64 },
23 ])
24 .exec(&mut db)
25 .await?;
26
27 let mut scores: Vec<GameScore> = GameScore::filter_by_game_title("chess")
28 .exec(&mut db)
29 .await?;
30 scores.sort_by_key(|s| s.top_score);
31
32 assert_eq!(scores.len(), 2);
33 assert_eq!(scores[0].top_score, 100);
34 assert_eq!(scores[1].top_score, 200);
35
36 Ok(())
37}
38
39#[driver_test]
44pub async fn composite_index_struct_level(t: &mut Test) -> Result<()> {
45 #[derive(Debug, toasty::Model)]
46 #[key(id, name)]
47 #[index(user_id)]
48 struct Post {
49 id: String,
50 name: String,
51 user_id: String,
52 title: String,
53 }
54
55 let mut db = t.setup_db(models!(Post)).await;
56
57 toasty::create!(Post::[
58 { id: "p1", name: "first", user_id: "alice", title: "Hello World" },
59 { id: "p2", name: "second", user_id: "alice", title: "Another Post" },
60 { id: "p3", name: "third", user_id: "bob", title: "Bob's Post" },
61 ])
62 .exec(&mut db)
63 .await?;
64
65 t.log().clear();
66
67 let mut posts: Vec<Post> = Post::filter_by_user_id("alice").exec(&mut db).await?;
68 posts.sort_by(|a, b| a.id.cmp(&b.id));
69
70 assert_eq!(posts.len(), 2);
71 assert_eq!(posts[0].title, "Hello World");
72 assert_eq!(posts[1].title, "Another Post");
73
74 let op = t.log().pop_op();
76 if t.capability().sql {
77 assert_struct!(op, Operation::QuerySql(_));
78 } else {
79 assert_struct!(op, Operation::QueryPk(_));
80 }
81
82 Ok(())
83}
84
85#[driver_test]
93pub async fn composite_index_prefix_queries(t: &mut Test) -> Result<()> {
94 #[derive(Debug, toasty::Model)]
95 #[key(user_id, game_title)]
96 #[index(game_title, top_score)]
97 struct GameScore {
98 user_id: String,
99 game_title: String,
100 top_score: i64,
101 }
102
103 let mut db = t.setup_db(models!(GameScore)).await;
104
105 toasty::create!(GameScore::[
106 { user_id: "u1", game_title: "chess", top_score: 100_i64 },
107 { user_id: "u2", game_title: "chess", top_score: 200_i64 },
108 { user_id: "u3", game_title: "chess", top_score: 200_i64 },
109 { user_id: "u1", game_title: "go", top_score: 50_i64 },
110 ])
111 .exec(&mut db)
112 .await?;
113
114 t.log().clear();
115
116 let scores: Vec<GameScore> = GameScore::filter_by_game_title("chess")
118 .exec(&mut db)
119 .await?;
120 assert_eq!(scores.len(), 3);
121
122 let op = t.log().pop_op();
123 if t.capability().sql {
124 assert_struct!(op, Operation::QuerySql(_));
125 } else {
126 assert_struct!(op, Operation::QueryPk(_));
127 }
128
129 t.log().clear();
130
131 let scores: Vec<GameScore> = GameScore::filter_by_game_title_and_top_score("chess", 100)
133 .exec(&mut db)
134 .await?;
135 assert_eq!(scores.len(), 1);
136 assert_eq!(scores[0].user_id, "u1");
137
138 let op = t.log().pop_op();
139 if t.capability().sql {
140 assert_struct!(op, Operation::QuerySql(_));
141 } else {
142 assert_struct!(op, Operation::QueryPk(_));
143 }
144
145 Ok(())
146}
147
148#[driver_test(requires(not(sql)))]
153pub async fn composite_index_multi_hash(t: &mut Test) -> Result<()> {
154 #[derive(Debug, toasty::Model)]
155 #[key(id)]
156 #[index(partition = [tournament_id, region], local = [round])]
157 struct Match {
158 id: String,
159 tournament_id: String,
160 region: String,
161 round: String,
162 player1_id: String,
163 player2_id: String,
164 }
165
166 let mut db = t.setup_db(models!(Match)).await;
167
168 toasty::create!(Match::[
169 { id: "m1", tournament_id: "WINTER2024", region: "NA-EAST", round: "SEMIFINALS", player1_id: "alice", player2_id: "bob" },
170 { id: "m2", tournament_id: "WINTER2024", region: "NA-EAST", round: "FINALS", player1_id: "charlie", player2_id: "dave" },
171 { id: "m3", tournament_id: "WINTER2024", region: "EU-WEST", round: "SEMIFINALS", player1_id: "eve", player2_id: "frank" },
172 ])
173 .exec(&mut db)
174 .await?;
175
176 t.log().clear();
177
178 let mut matches: Vec<Match> =
180 Match::filter_by_tournament_id_and_region("WINTER2024", "NA-EAST")
181 .exec(&mut db)
182 .await?;
183 matches.sort_by(|a, b| a.id.cmp(&b.id));
184
185 assert_eq!(matches.len(), 2);
186 assert_eq!(matches[0].round, "SEMIFINALS");
187 assert_eq!(matches[1].round, "FINALS");
188
189 let op = t.log().pop_op();
190 assert_struct!(op, Operation::QueryPk(_));
191
192 t.log().clear();
193
194 let matches: Vec<Match> =
196 Match::filter_by_tournament_id_and_region_and_round("WINTER2024", "NA-EAST", "SEMIFINALS")
197 .exec(&mut db)
198 .await?;
199
200 assert_eq!(matches.len(), 1);
201 assert_eq!(matches[0].player1_id, "alice");
202
203 let op = t.log().pop_op();
204 assert_struct!(op, Operation::QueryPk(_));
205
206 Ok(())
207}
208
209#[driver_test(requires(not(sql)))]
214pub async fn composite_index_multi_range(t: &mut Test) -> Result<()> {
215 #[derive(Debug, toasty::Model)]
216 #[key(id)]
217 #[index(partition = [player_id], local = [match_date, round])]
218 struct PlayerMatch {
219 id: String,
220 player_id: String,
221 match_date: String,
222 round: String,
223 opponent_id: String,
224 score: String,
225 }
226
227 let mut db = t.setup_db(models!(PlayerMatch)).await;
228
229 toasty::create!(PlayerMatch::[
230 { id: "pm1", player_id: "101", match_date: "2024-01-18", round: "SEMIFINALS", opponent_id: "102", score: "3-1" },
231 { id: "pm2", player_id: "101", match_date: "2024-01-18", round: "FINALS", opponent_id: "103", score: "2-1" },
232 { id: "pm3", player_id: "101", match_date: "2024-01-25", round: "SEMIFINALS", opponent_id: "104", score: "3-0" },
233 { id: "pm4", player_id: "999", match_date: "2024-01-18", round: "QUARTERFINALS", opponent_id: "101", score: "1-3" },
234 ])
235 .exec(&mut db)
236 .await?;
237
238 t.log().clear();
239
240 let matches: Vec<PlayerMatch> = PlayerMatch::filter_by_player_id("101")
242 .exec(&mut db)
243 .await?;
244 assert_eq!(matches.len(), 3);
245
246 let op = t.log().pop_op();
247 assert_struct!(op, Operation::QueryPk(_));
248
249 t.log().clear();
250
251 let matches: Vec<PlayerMatch> =
253 PlayerMatch::filter_by_player_id_and_match_date("101", "2024-01-18")
254 .exec(&mut db)
255 .await?;
256 assert_eq!(matches.len(), 2);
257
258 let op = t.log().pop_op();
259 assert_struct!(op, Operation::QueryPk(_));
260
261 t.log().clear();
262
263 let matches: Vec<PlayerMatch> = PlayerMatch::filter_by_player_id_and_match_date_and_round(
265 "101",
266 "2024-01-18",
267 "SEMIFINALS",
268 )
269 .exec(&mut db)
270 .await?;
271 assert_eq!(matches.len(), 1);
272 assert_eq!(matches[0].opponent_id, "102");
273
274 let op = t.log().pop_op();
275 assert_struct!(op, Operation::QueryPk(_));
276
277 Ok(())
278}
279
280#[driver_test(requires(sql))]
284pub async fn composite_index_three_columns(t: &mut Test) -> Result<()> {
285 #[derive(Debug, toasty::Model)]
286 #[key(id)]
287 #[index(country, city, zip_code)]
288 struct Address {
289 #[auto]
290 id: u64,
291 country: String,
292 city: String,
293 zip_code: String,
294 street: String,
295 }
296
297 let mut db = t.setup_db(models!(Address)).await;
298
299 toasty::create!(Address::[
300 { country: "US", city: "Seattle", zip_code: "98101", street: "1st Ave" },
301 { country: "US", city: "Seattle", zip_code: "98102", street: "2nd Ave" },
302 { country: "US", city: "Portland", zip_code: "97201", street: "Oak St" },
303 { country: "CA", city: "Toronto", zip_code: "M5V", street: "King St" },
304 ])
305 .exec(&mut db)
306 .await?;
307
308 t.log().clear();
309
310 let addrs: Vec<Address> = Address::filter_by_country("US").exec(&mut db).await?;
312 assert_eq!(addrs.len(), 3);
313
314 let op = t.log().pop_op();
315 assert_struct!(op, Operation::QuerySql(_));
316
317 t.log().clear();
318
319 let addrs: Vec<Address> = Address::filter_by_country_and_city("US", "Seattle")
321 .exec(&mut db)
322 .await?;
323 assert_eq!(addrs.len(), 2);
324
325 let op = t.log().pop_op();
326 assert_struct!(op, Operation::QuerySql(_));
327
328 t.log().clear();
329
330 let addrs: Vec<Address> =
332 Address::filter_by_country_and_city_and_zip_code("US", "Seattle", "98101")
333 .exec(&mut db)
334 .await?;
335 assert_eq!(addrs.len(), 1);
336 assert_eq!(addrs[0].street, "1st Ave");
337
338 let op = t.log().pop_op();
339 assert_struct!(op, Operation::QuerySql(_));
340
341 Ok(())
342}
343
344#[driver_test(requires(not(sql)))]
349pub async fn composite_index_simple_three_column_ddb(t: &mut Test) -> Result<()> {
350 #[derive(Debug, toasty::Model)]
351 #[key(id)]
352 #[index(country, city, zip_code)]
353 struct Address {
354 id: String,
355 country: String,
356 city: String,
357 zip_code: String,
358 street: String,
359 }
360
361 let mut db = t.setup_db(models!(Address)).await;
362
363 toasty::create!(Address::[
364 { id: "a1", country: "US", city: "Seattle", zip_code: "98101", street: "1st Ave" },
365 { id: "a2", country: "US", city: "Seattle", zip_code: "98102", street: "2nd Ave" },
366 { id: "a3", country: "US", city: "Portland", zip_code: "97201", street: "Oak St" },
367 { id: "a4", country: "CA", city: "Toronto", zip_code: "M5V", street: "King St" },
368 ])
369 .exec(&mut db)
370 .await?;
371
372 t.log().clear();
373
374 let addrs: Vec<Address> = Address::filter_by_country("US").exec(&mut db).await?;
376 assert_eq!(addrs.len(), 3);
377
378 let op = t.log().pop_op();
379 assert_struct!(op, Operation::QueryPk(_));
380
381 t.log().clear();
382
383 let addrs: Vec<Address> = Address::filter_by_country_and_city("US", "Seattle")
385 .exec(&mut db)
386 .await?;
387 assert_eq!(addrs.len(), 2);
388
389 let op = t.log().pop_op();
390 assert_struct!(op, Operation::QueryPk(_));
391
392 t.log().clear();
393
394 let addrs: Vec<Address> =
396 Address::filter_by_country_and_city_and_zip_code("US", "Seattle", "98101")
397 .exec(&mut db)
398 .await?;
399 assert_eq!(addrs.len(), 1);
400 assert_eq!(addrs[0].street, "1st Ave");
401
402 let op = t.log().pop_op();
403 assert_struct!(op, Operation::QueryPk(_));
404
405 Ok(())
406}
407
408#[driver_test]
414pub async fn composite_index_multiple_indexes(t: &mut Test) -> Result<()> {
415 #[derive(Debug, toasty::Model)]
416 #[key(id)]
417 #[index(category)]
418 #[index(brand)]
419 struct Product {
420 id: String,
421 category: String,
422 brand: String,
423 name: String,
424 }
425
426 let mut db = t.setup_db(models!(Product)).await;
427
428 toasty::create!(Product::[
429 { id: "p1", category: "electronics", brand: "acme", name: "Widget A" },
430 { id: "p2", category: "electronics", brand: "globex", name: "Widget B" },
431 { id: "p3", category: "clothing", brand: "acme", name: "Shirt C" },
432 { id: "p4", category: "clothing", brand: "initech", name: "Pants D" },
433 ])
434 .exec(&mut db)
435 .await?;
436
437 t.log().clear();
438
439 let mut products: Vec<Product> = Product::filter_by_category("electronics")
441 .exec(&mut db)
442 .await?;
443 products.sort_by(|a, b| a.id.cmp(&b.id));
444
445 assert_eq!(products.len(), 2);
446 assert_eq!(products[0].name, "Widget A");
447 assert_eq!(products[1].name, "Widget B");
448
449 let op = t.log().pop_op();
450 if t.capability().sql {
451 assert_struct!(op, Operation::QuerySql(_));
452 } else {
453 assert_struct!(op, Operation::QueryPk(_));
454 }
455
456 t.log().clear();
457
458 let mut products: Vec<Product> = Product::filter_by_brand("acme").exec(&mut db).await?;
460 products.sort_by(|a, b| a.id.cmp(&b.id));
461
462 assert_eq!(products.len(), 2);
463 assert_eq!(products[0].name, "Widget A");
464 assert_eq!(products[1].name, "Shirt C");
465
466 let op = t.log().pop_op();
467 if t.capability().sql {
468 assert_struct!(op, Operation::QuerySql(_));
469 } else {
470 assert_struct!(op, Operation::QueryPk(_));
471 }
472
473 Ok(())
474}
475
476#[driver_test(requires(not(sql)))]
482#[allow(clippy::too_many_arguments)]
483pub async fn composite_index_max_attributes(t: &mut Test) -> Result<()> {
484 #[derive(Debug, toasty::Model)]
485 #[key(id)]
486 #[index(partition = [f1, f2, f3, f4], local = [f5, f6, f7, f8])]
487 struct MaxIndex {
488 id: String,
489 f1: String,
490 f2: String,
491 f3: String,
492 f4: String,
493 f5: String,
494 f6: String,
495 f7: String,
496 f8: String,
497 value: String,
498 }
499
500 let mut db = t.setup_db(models!(MaxIndex)).await;
502
503 toasty::create!(MaxIndex::[
504 { id: "r1", f1: "a1", f2: "b1", f3: "c1", f4: "d1", f5: "e1", f6: "g1", f7: "h1", f8: "i1", value: "found" },
505 { id: "r2", f1: "a1", f2: "b1", f3: "c1", f4: "d2", f5: "e1", f6: "g1", f7: "h1", f8: "i1", value: "other" },
506 ])
507 .exec(&mut db)
508 .await?;
509
510 t.log().clear();
511
512 let records: Vec<MaxIndex> =
514 MaxIndex::filter_by_f1_and_f2_and_f3_and_f4("a1", "b1", "c1", "d1")
515 .exec(&mut db)
516 .await?;
517
518 assert_eq!(records.len(), 1);
519 assert_eq!(records[0].value, "found");
520
521 let op = t.log().pop_op();
522 assert_struct!(op, Operation::QueryPk(_));
523
524 Ok(())
525}
526
527#[driver_test(requires(not(sql)))]
533#[allow(clippy::too_many_arguments)]
534pub async fn composite_index_too_many_range_columns(t: &mut Test) -> Result<()> {
535 #[derive(Debug, toasty::Model)]
536 #[key(id)]
537 #[index(a, b, c, d, e, f)]
538 struct TooManyRange {
539 id: String,
540 a: String,
541 b: String,
542 c: String,
543 d: String,
544 e: String,
545 f: String,
546 }
547
548 let result = t.try_setup_db(models!(TooManyRange)).await;
550
551 assert!(
552 result.is_err(),
553 "expected setup_db to fail for 1 HASH + 5 RANGE index"
554 );
555 let err = result.unwrap_err();
556 assert!(
557 err.is_invalid_schema(),
558 "expected invalid_schema error, got: {err}"
559 );
560
561 Ok(())
562}
563
564#[driver_test]
570pub async fn composite_index_sort_key_range_filter(t: &mut Test) -> Result<()> {
571 #[derive(Debug, toasty::Model)]
572 #[key(user_id, game_title)]
573 #[index(game_title, top_score)]
574 struct GameScore {
575 user_id: String,
576 game_title: String,
577 top_score: i64,
578 }
579
580 let mut db = t.setup_db(models!(GameScore)).await;
581
582 toasty::create!(GameScore::[
583 { user_id: "u1", game_title: "chess", top_score: 100_i64 },
584 { user_id: "u2", game_title: "chess", top_score: 200_i64 },
585 { user_id: "u3", game_title: "chess", top_score: 1500_i64 },
586 { user_id: "u4", game_title: "chess", top_score: 50_i64 },
587 { user_id: "u1", game_title: "go", top_score: 9999_i64 },
588 ])
589 .exec(&mut db)
590 .await?;
591
592 let mut scores: Vec<GameScore> = GameScore::filter_by_game_title("chess")
593 .filter(GameScore::fields().top_score().gt(150))
594 .exec(&mut db)
595 .await?;
596 scores.sort_by_key(|s| s.top_score);
597
598 assert_eq!(scores.len(), 2);
599 assert_eq!(scores[0].top_score, 200);
600 assert_eq!(scores[1].top_score, 1500);
601
602 assert!(scores.iter().all(|s| s.game_title == "chess"));
604
605 Ok(())
606}