tank_tests/
operations.rs

1use std::{pin::pin, sync::LazyLock};
2use tank::{
3    DataSet, Driver, Entity, Executor, QueryResult, Result, RowsAffected, SqlWriter, cols, expr,
4    join,
5    stream::{StreamExt, TryStreamExt},
6};
7use time::{Date, Month, OffsetDateTime, Time, UtcOffset, macros::date};
8use tokio::sync::Mutex;
9use uuid::Uuid;
10
11#[derive(Entity)]
12#[tank(schema = "operations", name = "radio_operator")]
13pub struct Operator {
14    #[tank(primary_key)]
15    pub id: Uuid,
16    pub callsign: String,
17    #[tank(name = "rank")]
18    pub service_rank: String,
19    #[tank(name = "enlistment_date")]
20    pub enlisted: Date,
21    pub is_certified: bool,
22}
23
24#[derive(Entity)]
25#[tank(schema = "operations")]
26pub struct RadioLog {
27    #[tank(primary_key)]
28    pub id: Uuid,
29    #[tank(references = Operator::id)]
30    pub operator: Uuid,
31    pub message: String,
32    pub unit_callsign: String,
33    #[tank(name = "tx_time")]
34    pub transmission_time: OffsetDateTime,
35    #[tank(name = "rssi")]
36    pub signal_strength: i8,
37}
38static MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
39
40pub async fn operations<E: Executor>(executor: &mut E) -> Result<()> {
41    let _lock = MUTEX.lock().await;
42
43    // Setup
44    RadioLog::drop_table(executor, true, false).await?;
45    Operator::drop_table(executor, true, false).await?;
46
47    Operator::create_table(executor, false, true).await?;
48    RadioLog::create_table(executor, false, false).await?;
49
50    // Insert
51    let operator = Operator {
52        id: Uuid::new_v4(),
53        callsign: "SteelHammer".into(),
54        service_rank: "Major".into(),
55        enlisted: date!(2015 - 06 - 20),
56        is_certified: true,
57    };
58    Operator::insert_one(executor, &operator).await?;
59
60    let op_id = operator.id;
61    let logs: Vec<RadioLog> = (0..5)
62        .map(|i| RadioLog {
63            id: Uuid::new_v4(),
64            operator: op_id,
65            message: format!("Ping #{i}"),
66            unit_callsign: "Alpha-1".into(),
67            transmission_time: OffsetDateTime::now_utc(),
68            signal_strength: 42,
69        })
70        .collect();
71    RadioLog::insert_many(executor, &logs).await?;
72
73    // Find
74    let found = Operator::find_pk(executor, &operator.primary_key()).await?;
75    if let Some(op) = found {
76        log::debug!("Found operator: {:?}", op.callsign);
77    }
78
79    if let Some(radio_log) =
80        RadioLog::find_one(executor, &expr!(RadioLog::unit_callsign == "Alpha-1")).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_one(executor, log.primary_key()).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    // Multi-Statement
127    let writer = executor.driver().sql_writer();
128    let mut sql = String::new();
129    writer.write_delete::<RadioLog>(&mut sql, &expr!(RadioLog::signal_strength < 10));
130    writer.write_insert(&mut sql, [&operator], false);
131    writer.write_insert(
132        &mut sql,
133        [&RadioLog {
134            id: Uuid::new_v4(),
135            operator: operator.id,
136            message: "Status report".into(),
137            unit_callsign: "Alpha-1".into(),
138            transmission_time: OffsetDateTime::now_utc(),
139            signal_strength: 55,
140        }],
141        false,
142    );
143    writer.write_select(
144        &mut sql,
145        RadioLog::columns(),
146        RadioLog::table(),
147        &expr!(true),
148        Some(50),
149    );
150    {
151        let mut stream = pin!(executor.run(sql));
152        while let Some(result) = stream.try_next().await? {
153            match result {
154                QueryResult::Row(row) => log::debug!("Row: {row:?}"),
155                QueryResult::Affected(RowsAffected { rows_affected, .. }) => {
156                    log::debug!("Affected rows: {rows_affected:?}")
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    let messages = join!(Operator JOIN RadioLog ON Operator::id == RadioLog::operator)
279        .select(
280            executor,
281            cols!(
282                RadioLog::signal_strength as strength DESC,
283                Operator::callsign ASC,
284                RadioLog::message,
285            ),
286            // X != Y as LIKE => X NOT LIKE Y
287            &expr!(Operator::is_certified && RadioLog::message != "Radio check%" as LIKE),
288            Some(100),
289        )
290        .map(|row| {
291            row.and_then(|row| {
292                #[derive(Entity)]
293                struct Row {
294                    message: String,
295                    callsign: String,
296                }
297                Row::from_row(row).and_then(|row| Ok((row.message, row.callsign)))
298            })
299        })
300        .try_collect::<Vec<_>>()
301        .await?;
302    assert!(
303        messages.iter().map(|(a, b)| (a.as_str(), b.as_str())).eq([
304            ("Heavy armor spotted, grid 4C.", "SteelHammer"),
305            ("Affirmative, engaging.", "SteelHammer"),
306            ("Target acquired. Requesting coordinates.", "SteelHammer"),
307            ("Perimeter secure. All clear.", "Viper"),
308        ]
309        .into_iter())
310    );
311    Ok(())
312}