Skip to main content

tank_tests/
operations.rs

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