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