tank_tests/
operations.rs

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