Skip to main content

tank_tests/
operations.rs

1use std::{pin::pin, sync::LazyLock};
2use tank::{
3    Driver, DynQuery, Entity, Executor, QueryBuilder, QueryResult, Result, RowsAffected, SqlWriter,
4    cols, expr, join,
5    stream::{StreamExt, TryStreamExt},
6};
7use time::{Date, Month, OffsetDateTime, Time, UtcOffset, macros::date};
8use tokio::sync::Mutex;
9use uuid::Uuid;
10
11static MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
12
13#[derive(Entity)]
14#[tank(schema = "operations", name = "radio_operator")]
15pub struct Operator {
16    #[tank(primary_key)]
17    pub id: Uuid,
18    pub callsign: String,
19    #[tank(name = "rank")]
20    pub service_rank: String,
21    #[tank(name = "enlistment_date")]
22    pub enlisted: Date,
23    pub is_certified: bool,
24}
25
26#[derive(Entity)]
27#[tank(schema = "operations")]
28pub struct RadioLog {
29    #[tank(primary_key)]
30    pub id: Uuid,
31    #[tank(references = Operator::id)]
32    pub operator: Uuid,
33    pub message: String,
34    pub unit_callsign: String,
35    #[tank(name = "tx_time")]
36    pub transmission_time: OffsetDateTime,
37    #[tank(name = "rssi")]
38    pub signal_strength: i8,
39}
40
41pub async fn operations<E: Executor>(executor: &mut E) -> Result<()> {
42    let _lock = MUTEX.lock().await;
43
44    // Setup
45    RadioLog::drop_table(executor, true, false).await?;
46    Operator::drop_table(executor, true, false).await?;
47
48    Operator::create_table(executor, false, true).await?;
49    RadioLog::create_table(executor, false, false).await?;
50
51    // Insert
52    let operator = Operator {
53        id: Uuid::parse_str("21c90df5-00db-4062-9f5a-bcfa2e759e78").unwrap(),
54        callsign: "SteelHammer".into(),
55        service_rank: "Major".into(),
56        enlisted: date!(2015 - 06 - 20),
57        is_certified: true,
58    };
59    Operator::insert_one(executor, &operator).await?;
60
61    let op_id = operator.id;
62    let logs: Vec<RadioLog> = (0..5)
63        .map(|i| RadioLog {
64            id: Uuid::new_v4(),
65            operator: op_id,
66            message: format!("Ping #{i}"),
67            unit_callsign: "Alpha-1".into(),
68            transmission_time: OffsetDateTime::now_utc(),
69            signal_strength: 42,
70        })
71        .collect();
72    RadioLog::insert_many(executor, &logs).await?;
73
74    // Find
75    if let Some(radio_log) =
76        RadioLog::find_one(executor, expr!(RadioLog::unit_callsign == "Alpha-1")).await?
77    {
78        log::debug!("Found radio log: {:?}", radio_log.id);
79    }
80
81    {
82        let mut stream = pin!(RadioLog::find_many(
83            executor,
84            expr!(RadioLog::signal_strength >= 40),
85            Some(100)
86        ));
87        while let Some(radio_log) = stream.try_next().await? {
88            log::debug!("Found radio log: {:?}", radio_log.id);
89        }
90        // Executor is released from the stream at the end of the scope
91    }
92
93    // Save
94    let mut operator = operator;
95    operator.callsign = "SteelHammerX".into();
96    operator.save(executor).await?;
97
98    let mut log = RadioLog::find_one(executor, expr!(RadioLog::message == "Ping #2"))
99        .await?
100        .expect("Missing log");
101    log.message = "Ping #2 ACK".into();
102    log.save(executor).await?;
103
104    // Delete
105    RadioLog::delete_many(executor, log.primary_key_expr()).await?;
106
107    let operator_id = operator.id;
108    RadioLog::delete_many(executor, expr!(RadioLog::operator == #operator_id)).await?;
109
110    operator.delete(executor).await?;
111
112    // Prepare
113    let mut query =
114        RadioLog::prepare_find(executor, expr!(RadioLog::signal_strength > ?), None).await?;
115    query.bind(40)?;
116    let _messages: Vec<_> = executor
117        .fetch(query)
118        .map_ok(|row| row.values[0].clone())
119        .try_collect()
120        .await?;
121
122    // Multiple statements
123    #[cfg(not(feature = "disable-multiple-statements"))]
124    {
125        let writer = executor.driver().sql_writer();
126        let mut query = DynQuery::default();
127        writer.write_delete::<RadioLog>(&mut query, expr!(RadioLog::signal_strength < 10));
128        writer.write_insert(&mut query, [&operator], false);
129        writer.write_insert(
130            &mut query,
131            [&RadioLog {
132                id: Uuid::new_v4(),
133                operator: operator.id,
134                message: "Status report".into(),
135                unit_callsign: "Alpha-1".into(),
136                transmission_time: OffsetDateTime::now_utc(),
137                signal_strength: 55,
138            }],
139            false,
140        );
141        writer.write_select(
142            &mut query,
143            &QueryBuilder::new()
144                .select(RadioLog::columns())
145                .from(RadioLog::table())
146                .where_expr(true)
147                .limit(Some(50)),
148        );
149        {
150            let mut stream = pin!(executor.run(query));
151            while let Some(result) = stream.try_next().await? {
152                match result {
153                    QueryResult::Row(row) => log::debug!("Row: {row:?}"),
154                    QueryResult::Affected(RowsAffected { rows_affected, .. }) => {
155                        log::debug!("Affected rows: {rows_affected:?}")
156                    }
157                }
158            }
159        }
160    }
161
162    Ok(())
163}
164
165pub async fn advanced_operations<E: Executor>(executor: &mut E) -> Result<()> {
166    let _lock = MUTEX.lock().await;
167
168    RadioLog::drop_table(executor, true, false).await?;
169    Operator::drop_table(executor, true, false).await?;
170
171    Operator::create_table(executor, false, true).await?;
172    RadioLog::create_table(executor, false, false).await?;
173
174    let operators = vec![
175        Operator {
176            id: Uuid::new_v4(),
177            callsign: "SteelHammer".into(),
178            service_rank: "Major".into(),
179            enlisted: date!(2015 - 06 - 20),
180            is_certified: true,
181        },
182        Operator {
183            id: Uuid::new_v4(),
184            callsign: "Viper".into(),
185            service_rank: "Sgt".into(),
186            enlisted: date!(2019 - 11 - 01),
187            is_certified: true,
188        },
189        Operator {
190            id: Uuid::new_v4(),
191            callsign: "Rook".into(),
192            service_rank: "Pvt".into(),
193            enlisted: date!(2023 - 01 - 15),
194            is_certified: false,
195        },
196    ];
197    let radio_logs = vec![
198        RadioLog {
199            id: Uuid::new_v4(),
200            operator: operators[0].id,
201            message: "Radio check, channel 3. How copy?".into(),
202            unit_callsign: "Alpha-1".into(),
203            transmission_time: OffsetDateTime::new_in_offset(
204                Date::from_calendar_date(2025, Month::November, 4).unwrap(),
205                Time::from_hms(19, 45, 21).unwrap(),
206                UtcOffset::from_hms(1, 0, 0).unwrap(),
207            ),
208            signal_strength: -42,
209        },
210        RadioLog {
211            id: Uuid::new_v4(),
212            operator: operators[0].id,
213            message: "Target acquired. Requesting coordinates.".into(),
214            unit_callsign: "Alpha-1".into(),
215            transmission_time: OffsetDateTime::new_in_offset(
216                Date::from_calendar_date(2025, Month::November, 4).unwrap(),
217                Time::from_hms(19, 54, 12).unwrap(),
218                UtcOffset::from_hms(1, 0, 0).unwrap(),
219            ),
220            signal_strength: -55,
221        },
222        RadioLog {
223            id: Uuid::new_v4(),
224            operator: operators[0].id,
225            message: "Heavy armor spotted, grid 4C.".into(),
226            unit_callsign: "Alpha-1".into(),
227            transmission_time: OffsetDateTime::new_in_offset(
228                Date::from_calendar_date(2025, Month::November, 4).unwrap(),
229                Time::from_hms(19, 51, 9).unwrap(),
230                UtcOffset::from_hms(1, 0, 0).unwrap(),
231            ),
232            signal_strength: -52,
233        },
234        RadioLog {
235            id: Uuid::new_v4(),
236            operator: operators[1].id,
237            message: "Perimeter secure. All clear.".into(),
238            unit_callsign: "Bravo-2".into(),
239            transmission_time: OffsetDateTime::new_in_offset(
240                Date::from_calendar_date(2025, Month::November, 4).unwrap(),
241                Time::from_hms(19, 51, 9).unwrap(),
242                UtcOffset::from_hms(1, 0, 0).unwrap(),
243            ),
244            signal_strength: -68,
245        },
246        RadioLog {
247            id: Uuid::new_v4(),
248            operator: operators[2].id,
249            message: "Radio check, grid 1A. Over.".into(),
250            unit_callsign: "Charlie-3".into(),
251            transmission_time: OffsetDateTime::new_in_offset(
252                Date::from_calendar_date(2025, Month::November, 4).unwrap(),
253                Time::from_hms(18, 59, 11).unwrap(),
254                UtcOffset::from_hms(2, 0, 0).unwrap(),
255            ),
256            signal_strength: -41,
257        },
258        RadioLog {
259            id: Uuid::new_v4(),
260            operator: operators[0].id,
261            message: "Affirmative, engaging.".into(),
262            unit_callsign: "Alpha-1".into(),
263            transmission_time: OffsetDateTime::new_in_offset(
264                Date::from_calendar_date(2025, Month::November, 3).unwrap(),
265                Time::from_hms(23, 11, 54).unwrap(),
266                UtcOffset::from_hms(0, 0, 0).unwrap(),
267            ),
268            signal_strength: -54,
269        },
270    ];
271    Operator::insert_many(executor, &operators)
272        .await
273        .expect("Could not insert operators");
274    RadioLog::insert_many(executor, &radio_logs)
275        .await
276        .expect("Could not insert radio logs");
277
278    #[cfg(not(feature = "disable-joins"))]
279    {
280        let messages = executor
281            .fetch(
282                QueryBuilder::new()
283                    .select(cols!(
284                        RadioLog::signal_strength as strength,
285                        Operator::callsign,
286                        RadioLog::message,
287                    ))
288                    .from(join!(Operator JOIN RadioLog ON Operator::id == RadioLog::operator))
289                    .where_expr(expr!(
290                        // X != Y as LIKE => X NOT LIKE Y
291                        Operator::is_certified && RadioLog::message != "Radio check%" as LIKE
292                    ))
293                    .order_by(cols!(RadioLog::signal_strength DESC, Operator::callsign ASC))
294                    .limit(Some(100))
295                    .build(&executor.driver()),
296            )
297            .map(|row| {
298                row.and_then(|row| {
299                    #[derive(Entity)]
300                    struct Row {
301                        message: String,
302                        callsign: String,
303                    }
304                    Row::from_row(row).and_then(|row| Ok((row.message, row.callsign)))
305                })
306            })
307            .try_collect::<Vec<_>>()
308            .await?;
309        assert!(
310            messages.iter().map(|(a, b)| (a.as_str(), b.as_str())).eq([
311                ("Heavy armor spotted, grid 4C.", "SteelHammer"),
312                ("Affirmative, engaging.", "SteelHammer"),
313                ("Target acquired. Requesting coordinates.", "SteelHammer"),
314                ("Perimeter secure. All clear.", "Viper"),
315            ]
316            .into_iter())
317        );
318    }
319    Ok(())
320}