Skip to main content

tank_tests/
metrics.rs

1#![allow(unused_imports)]
2use std::{future, sync::LazyLock};
3use tank::{
4    Entity, Executor, QueryBuilder, cols, expr,
5    stream::{StreamExt, TryStreamExt},
6};
7use time::{Date, macros::date};
8use tokio::sync::Mutex;
9
10static MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
11
12#[derive(Entity)]
13#[tank(primary_key = (name, country, value, date))]
14pub struct Metric {
15    person: String,
16    country: String,
17    #[tank(clustering_key)]
18    date: Date,
19    name: String,
20    #[tank(clustering_key)]
21    value: f64,
22}
23
24#[derive(Entity)]
25struct MetricValue {
26    pub value: f64,
27}
28
29pub async fn metrics(executor: &mut impl Executor) {
30    let _lock = MUTEX.lock().await;
31
32    // Setup
33    Metric::drop_table(executor, true, false)
34        .await
35        .expect("Failed to drop Metric table");
36    Metric::create_table(executor, true, true)
37        .await
38        .expect("Failed to create Metric table");
39
40    // Insert
41    let values = vec![
42        // Alice, IT (female)
43        Metric {
44            person: "alice".into(),
45            country: "IT".into(),
46            date: date!(2024 - 1 - 1),
47            name: "height_cm".into(),
48            value: 165.0,
49        },
50        Metric {
51            person: "alice".into(),
52            country: "IT".into(),
53            date: date!(2024 - 1 - 1),
54            name: "weight_kg".into(),
55            value: 62.5,
56        },
57        Metric {
58            person: "alice".into(),
59            country: "IT".into(),
60            date: date!(2024 - 6 - 1),
61            name: "weight_kg".into(),
62            value: 64.2,
63        },
64        Metric {
65            person: "alice".into(),
66            country: "IT".into(),
67            date: date!(2024 - 6 - 1),
68            name: "income_eur".into(),
69            value: 56000.0,
70        },
71        Metric {
72            person: "alice".into(),
73            country: "IT".into(),
74            date: date!(2025 - 4 - 1),
75            name: "income_eur".into(),
76            value: 68000.0,
77        },
78        // Bob, NL (male)
79        Metric {
80            person: "bob".into(),
81            country: "NL".into(),
82            date: date!(2024 - 1 - 1),
83            name: "height_cm".into(),
84            value: 188.0,
85        },
86        Metric {
87            person: "bob".into(),
88            country: "NL".into(),
89            date: date!(2024 - 1 - 1),
90            name: "weight_kg".into(),
91            value: 81.0,
92        },
93        Metric {
94            person: "bob".into(),
95            country: "NL".into(),
96            date: date!(2025 - 6 - 12),
97            name: "weight_kg".into(),
98            value: 82.5,
99        },
100        Metric {
101            person: "bob".into(),
102            country: "NL".into(),
103            date: date!(2024 - 12 - 1),
104            name: "income_eur".into(),
105            value: 88000.0,
106        },
107        Metric {
108            person: "bob".into(),
109            country: "NL".into(),
110            date: date!(2025 - 3 - 2),
111            name: "income_eur".into(),
112            value: 120000.0,
113        },
114        Metric {
115            person: "bob".into(),
116            country: "NL".into(),
117            date: date!(2026 - 2 - 2),
118            name: "income_eur".into(),
119            value: 130000.0,
120        },
121        // Clara, DE (female)
122        Metric {
123            person: "clara".into(),
124            country: "DE".into(),
125            date: date!(2024 - 2 - 1),
126            name: "height_cm".into(),
127            value: 170.0,
128        },
129        Metric {
130            person: "clara".into(),
131            country: "DE".into(),
132            date: date!(2024 - 2 - 1),
133            name: "weight_kg".into(),
134            value: 60.0,
135        },
136        Metric {
137            person: "clara".into(),
138            country: "DE".into(),
139            date: date!(2025 - 1 - 1),
140            name: "income_eur".into(),
141            value: 72000.0,
142        },
143        // David, UK (male)
144        Metric {
145            person: "david".into(),
146            country: "UK".into(),
147            date: date!(2024 - 3 - 10),
148            name: "height_cm".into(),
149            value: 182.0,
150        },
151        Metric {
152            person: "david".into(),
153            country: "UK".into(),
154            date: date!(2024 - 3 - 10),
155            name: "weight_kg".into(),
156            value: 88.5,
157        },
158        Metric {
159            person: "david".into(),
160            country: "UK".into(),
161            date: date!(2024 - 3 - 10),
162            name: "weight_kg".into(),
163            value: 86.0,
164        },
165        Metric {
166            person: "david".into(),
167            country: "UK".into(),
168            date: date!(2024 - 11 - 1),
169            name: "income_eur".into(),
170            value: 69000.0,
171        },
172        Metric {
173            person: "david".into(),
174            country: "UK".into(),
175            date: date!(2025 - 11 - 1),
176            name: "income_eur".into(),
177            value: 102000.0,
178        },
179        // Eva, ES (female)
180        Metric {
181            person: "eva".into(),
182            country: "ES".into(),
183            date: date!(2024 - 4 - 5),
184            name: "height_cm".into(),
185            value: 162.0,
186        },
187        Metric {
188            person: "eva".into(),
189            country: "ES".into(),
190            date: date!(2024 - 4 - 5),
191            name: "weight_kg".into(),
192            value: 58.0,
193        },
194        Metric {
195            person: "eva".into(),
196            country: "ES".into(),
197            date: date!(2024 - 12 - 1),
198            name: "income_eur".into(),
199            value: 42000.0,
200        },
201        Metric {
202            person: "eva".into(),
203            country: "ES".into(),
204            date: date!(2025 - 12 - 1),
205            name: "income_eur".into(),
206            value: 47000.0,
207        },
208        // Marco, IT (male)
209        Metric {
210            person: "marco".into(),
211            country: "IT".into(),
212            date: date!(2024 - 1 - 15),
213            name: "height_cm".into(),
214            value: 178.0,
215        },
216        Metric {
217            person: "marco".into(),
218            country: "IT".into(),
219            date: date!(2024 - 1 - 15),
220            name: "weight_kg".into(),
221            value: 78.0,
222        },
223        Metric {
224            person: "marco".into(),
225            country: "IT".into(),
226            date: date!(2024 - 10 - 1),
227            name: "weight_kg".into(),
228            value: 80.5,
229        },
230        Metric {
231            person: "marco".into(),
232            country: "IT".into(),
233            date: date!(2024 - 10 - 1),
234            name: "income_eur".into(),
235            value: 61000.0,
236        },
237        Metric {
238            person: "marco".into(),
239            country: "IT".into(),
240            date: date!(2025 - 10 - 1),
241            name: "income_eur".into(),
242            value: 72000.0,
243        },
244        // Sophie, UK (female)
245        Metric {
246            person: "sophie".into(),
247            country: "UK".into(),
248            date: date!(2024 - 2 - 20),
249            name: "height_cm".into(),
250            value: 168.0,
251        },
252        Metric {
253            person: "sophie".into(),
254            country: "UK".into(),
255            date: date!(2024 - 2 - 20),
256            name: "weight_kg".into(),
257            value: 61.0,
258        },
259        Metric {
260            person: "sophie".into(),
261            country: "UK".into(),
262            date: date!(2024 - 12 - 1),
263            name: "income_eur".into(),
264            value: 58000.0,
265        },
266        Metric {
267            person: "sophie".into(),
268            country: "UK".into(),
269            date: date!(2024 - 12 - 2),
270            name: "weight_kg".into(),
271            value: 67.5,
272        },
273        Metric {
274            person: "sophie".into(),
275            country: "UK".into(),
276            date: date!(2025 - 12 - 1),
277            name: "income_eur".into(),
278            value: 66000.0,
279        },
280        // Partition isolation sanity check
281        Metric {
282            person: "alice".into(),
283            country: "CA".into(),
284            date: date!(2024 - 6 - 1),
285            name: "income_eur".into(),
286            value: 77000.0,
287        },
288    ];
289    /*
290    person,country,date,name,value
291    alice,IT,2024-01-01,height_cm,165.0
292    bob,NL,2024-01-01,height_cm,188.0
293    clara,DE,2024-02-01,height_cm,170.0
294    david,UK,2024-03-10,height_cm,182.0
295    eva,ES,2024-04-05,height_cm,162.0
296    marco,IT,2024-01-15,height_cm,178.0
297    sophie,UK,2024-02-20,height_cm,168.0
298    alice,IT,2024-06-01,income_eur,56000.0
299    alice,IT,2025-04-01,income_eur,68000.0
300    bob,NL,2024-12-01,income_eur,88000.0
301    bob,NL,2025-03-02,income_eur,120000.0
302    bob,NL,2026-02-02,income_eur,130000.0
303    clara,DE,2025-01-01,income_eur,72000.0
304    david,UK,2024-11-01,income_eur,69000.0
305    david,UK,2025-11-01,income_eur,102000.0
306    eva,ES,2024-12-01,income_eur,42000.0
307    eva,ES,2025-12-01,income_eur,47000.0
308    marco,IT,2024-10-01,income_eur,61000.0
309    marco,IT,2025-10-01,income_eur,72000.0
310    sophie,UK,2024-12-01,income_eur,58000.0
311    sophie,UK,2025-12-01,income_eur,66000.0
312    alice,CA,2024-06-01,income_eur,77000.0
313    alice,IT,2024-01-01,weight_kg,62.5
314    bob,NL,2024-01-01,weight_kg,81.0
315    bob,NL,2025-06-12,weight_kg,82.5
316    clara,DE,2024-02-01,weight_kg,60.0
317    david,UK,2024-03-10,weight_kg,88.5
318    david,UK,2024-03-10,weight_kg,86.0
319    eva,ES,2024-04-05,weight_kg,58.0
320    marco,IT,2024-01-15,weight_kg,78.0
321    marco,IT,2024-10-01,weight_kg,80.5
322    sophie,UK,2024-02-20,weight_kg,61.0
323    sophie,UK,2024-12-02,weight_kg,67.5
324    */
325
326    Metric::insert_many(executor, &values)
327        .await
328        .expect("Could not insert the entities");
329
330    let height = 170;
331    let heights = executor
332        .fetch(
333            QueryBuilder::new()
334                .select([Metric::value, Metric::date])
335                .from(Metric::table())
336                .where_expr(expr!(
337                    Metric::name == "height_cm"
338                        && Metric::country == "IT"
339                        && Metric::value >= #height
340                ))
341                .order_by(cols!(Metric::value DESC, Metric::date DESC))
342                .build(&executor.driver()),
343        )
344        .and_then(|v| future::ready(MetricValue::from_row(v).map(|v| v.value)))
345        .try_collect::<Vec<_>>()
346        .await
347        .expect("Coult not get the Italy height values");
348    assert_eq!(heights, [178.0]);
349
350    // Incomes in Italy
351    let italy_incomes = executor
352        .fetch(
353            QueryBuilder::new()
354                .select([Metric::value])
355                .from(Metric::table())
356                .where_expr(expr!(
357                    Metric::name == "income_eur" && Metric::country == "IT"
358                ))
359                .order_by(cols!(Metric::value ASC))
360                .build(&executor.driver()),
361        )
362        .and_then(|v| future::ready(MetricValue::from_row(v).map(|v| v.value)))
363        .try_collect::<Vec<_>>()
364        .await
365        .expect("Could not get IT incomes");
366    assert_eq!(italy_incomes, [56000.0, 61000.0, 68000.0, 72000.0]);
367
368    // Highest income in the UK
369    let latest_income = executor
370        .fetch(
371            QueryBuilder::new()
372                .select(cols!(MAX(Metric::value) as value))
373                .from(Metric::table())
374                .where_expr(expr!(
375                    Metric::name == "income_eur" && Metric::country == "UK"
376                ))
377                .build(&executor.driver()),
378        )
379        .and_then(|v| future::ready(MetricValue::from_row(v).map(|v| v.value)))
380        .try_collect::<Vec<_>>()
381        .await
382        .expect("Could not get latest UK income");
383    assert_eq!(latest_income, [102000.0]);
384
385    // Prepared queries
386    let mut prepared = executor
387        .prepare(
388            QueryBuilder::new()
389                .select([Metric::value, Metric::date])
390                .from(Metric::table())
391                .where_expr(expr!(Metric::country == ? && Metric::name == ?))
392                .order_by(cols!(Metric::value DESC, Metric::date DESC))
393                .build(&executor.driver()),
394        )
395        .await
396        .expect("Failed to prepare metric query");
397
398    prepared.bind("ES").unwrap().bind("height_cm").unwrap();
399    let spain_heights = executor
400        .fetch(&mut prepared)
401        .and_then(|v| future::ready(MetricValue::from_row(v).map(|v| v.value)))
402        .try_collect::<Vec<_>>()
403        .await
404        .expect("Could not fetch sophie heights");
405    assert_eq!(spain_heights, [162.0]);
406
407    prepared.bind("NL").unwrap().bind("weight_kg").unwrap();
408    let netherlands_weights = executor
409        .fetch(&mut prepared)
410        .and_then(|v| future::ready(MetricValue::from_row(v).map(|v| v.value)))
411        .try_collect::<Vec<_>>()
412        .await
413        .expect("Could not fetch sophie heights");
414    assert_eq!(netherlands_weights, [82.5, 81.0]);
415
416    prepared.bind("IT").unwrap().bind("weight_kg").unwrap();
417    let italy_weights = executor
418        .fetch(&mut prepared)
419        .and_then(|v| future::ready(MetricValue::from_row(v).map(|v| v.value)))
420        .try_collect::<Vec<_>>()
421        .await
422        .expect("Could not fetch sophie heights");
423    assert_eq!(italy_weights, [80.5, 78.0, 64.2, 62.5]);
424
425    #[cfg(not(feature = "disable-groups"))]
426    {
427        #[derive(Entity, PartialEq, Debug)]
428        struct AverageMetrics {
429            name: String,
430            country: String,
431            avg: f32,
432        }
433        let averages = executor
434            .fetch(
435                QueryBuilder::new()
436                    .select(cols!(
437                        Metric::name,
438                        Metric::country,
439                        AVG(Metric::value) as avg,
440                    ))
441                    .from(Metric::table())
442                    .group_by([Metric::name, Metric::country])
443                    .order_by(cols!(Metric::name ASC, avg DESC))
444                    .build(&executor.driver()),
445            )
446            .map_ok(AverageMetrics::from_row)
447            .map(Result::flatten)
448            .try_collect::<Vec<_>>()
449            .await
450            .expect("Could not get the products ordered by increasing price");
451        assert_eq!(
452            averages,
453            [
454                AverageMetrics {
455                    name: "height_cm".into(),
456                    country: "NL".into(),
457                    avg: 188.0,
458                },
459                AverageMetrics {
460                    name: "height_cm".into(),
461                    country: "UK".into(),
462                    avg: 175.0,
463                },
464                AverageMetrics {
465                    name: "height_cm".into(),
466                    country: "IT".into(),
467                    avg: 171.5,
468                },
469                AverageMetrics {
470                    name: "height_cm".into(),
471                    country: "DE".into(),
472                    avg: 170.0,
473                },
474                AverageMetrics {
475                    name: "height_cm".into(),
476                    country: "ES".into(),
477                    avg: 162.0,
478                },
479                AverageMetrics {
480                    name: "income_eur".into(),
481                    country: "NL".into(),
482                    avg: 112666.664,
483                },
484                AverageMetrics {
485                    name: "income_eur".into(),
486                    country: "CA".into(),
487                    avg: 77000.0,
488                },
489                AverageMetrics {
490                    name: "income_eur".into(),
491                    country: "UK".into(),
492                    avg: 73750.0,
493                },
494                AverageMetrics {
495                    name: "income_eur".into(),
496                    country: "DE".into(),
497                    avg: 72000.0,
498                },
499                AverageMetrics {
500                    name: "income_eur".into(),
501                    country: "IT".into(),
502                    avg: 64250.0,
503                },
504                AverageMetrics {
505                    name: "income_eur".into(),
506                    country: "ES".into(),
507                    avg: 44500.0,
508                },
509                AverageMetrics {
510                    name: "weight_kg".into(),
511                    country: "NL".into(),
512                    avg: 81.75,
513                },
514                AverageMetrics {
515                    name: "weight_kg".into(),
516                    country: "UK".into(),
517                    avg: 75.75,
518                },
519                AverageMetrics {
520                    name: "weight_kg".into(),
521                    country: "IT".into(),
522                    avg: 71.3,
523                },
524                AverageMetrics {
525                    name: "weight_kg".into(),
526                    country: "DE".into(),
527                    avg: 60.0,
528                },
529                AverageMetrics {
530                    name: "weight_kg".into(),
531                    country: "ES".into(),
532                    avg: 58.0,
533                }
534            ]
535        );
536    }
537
538    #[cfg(not(feature = "disable-groups"))]
539    {
540        #[derive(Entity, PartialEq, Debug)]
541        struct CountryMaxIncome {
542            country: String,
543            max_income: f64,
544        }
545
546        let max_incomes = executor
547            .fetch(
548                QueryBuilder::new()
549                    .select(cols!(Metric::country, MAX(value) as max_income,))
550                    .from(Metric::table())
551                    .where_expr(expr!(name == "income_eur"))
552                    .group_by([Metric::country])
553                    .order_by(cols!(max_income DESC, Metric::country ASC))
554                    .build(&executor.driver()),
555            )
556            .map_ok(CountryMaxIncome::from_row)
557            .map(Result::flatten)
558            .try_collect::<Vec<_>>()
559            .await
560            .expect("Could not get max incomes per country");
561
562        assert_eq!(
563            max_incomes,
564            [
565                CountryMaxIncome {
566                    country: "NL".into(),
567                    max_income: 130000.0,
568                },
569                CountryMaxIncome {
570                    country: "UK".into(),
571                    max_income: 102000.0,
572                },
573                CountryMaxIncome {
574                    country: "CA".into(),
575                    max_income: 77000.0,
576                },
577                CountryMaxIncome {
578                    country: "DE".into(),
579                    max_income: 72000.0,
580                },
581                CountryMaxIncome {
582                    country: "IT".into(),
583                    max_income: 72000.0,
584                },
585                CountryMaxIncome {
586                    country: "ES".into(),
587                    max_income: 47000.0,
588                },
589            ]
590        );
591    }
592
593    #[cfg(not(feature = "disable-groups"))]
594    {
595        #[derive(Entity, PartialEq, Debug)]
596        struct MetricGlobalStats {
597            name: String,
598            min_val: f64,
599            max_val: f64,
600            total_val: f64,
601        }
602
603        let global_stats = executor
604            .fetch(
605                QueryBuilder::new()
606                    .select(cols!(
607                        Metric::name,
608                        MIN(Metric::value) as min_val,
609                        MAX(Metric::value) as max_val,
610                        SUM(Metric::value) as total_val,
611                    ))
612                    .from(Metric::table())
613                    .group_by([Metric::name])
614                    .order_by(cols!(name ASC))
615                    .build(&executor.driver()),
616            )
617            .map_ok(MetricGlobalStats::from_row)
618            .map(Result::flatten)
619            .try_collect::<Vec<_>>()
620            .await
621            .expect("Could not get global metric stats");
622
623        assert_eq!(
624            global_stats,
625            [
626                MetricGlobalStats {
627                    name: "height_cm".into(),
628                    min_val: 162.0,
629                    max_val: 188.0,
630                    total_val: 1213.0,
631                },
632                MetricGlobalStats {
633                    name: "income_eur".into(),
634                    min_val: 42000.0,
635                    max_val: 130000.0,
636                    total_val: 1128000.0,
637                },
638                MetricGlobalStats {
639                    name: "weight_kg".into(),
640                    min_val: 58.0,
641                    max_val: 88.5,
642                    total_val: 869.7, // Note: watch out for f64 epsilon precision issues here depending on the DB engine!
643                },
644            ]
645        );
646    }
647}