Skip to main content

toasty_driver_integration_suite/tests/
crud_update_arithmetic.rs

1use crate::prelude::*;
2
3use toasty_core::{
4    driver::Operation,
5    stmt::{Assignment, Statement, UpdateTarget},
6};
7
8#[driver_test(scenario(crate::scenarios::counter_value))]
9pub async fn increment_adds_one(t: &mut Test) -> Result<()> {
10    let mut db = setup(t).await;
11    let mut counter = toasty::create!(Counter {
12        id: uuid::Uuid::new_v4(),
13        value: 10,
14    })
15    .exec(&mut db)
16    .await?;
17
18    counter
19        .update()
20        .value(toasty::stmt::increment())
21        .exec(&mut db)
22        .await?;
23
24    let reloaded = Counter::get_by_id(&mut db, &counter.id).await?;
25    assert_eq!(reloaded.value, 11);
26    Ok(())
27}
28
29#[driver_test(scenario(crate::scenarios::counter_value))]
30pub async fn decrement_subtracts_one(t: &mut Test) -> Result<()> {
31    let mut db = setup(t).await;
32    let mut counter = toasty::create!(Counter {
33        id: uuid::Uuid::new_v4(),
34        value: 10,
35    })
36    .exec(&mut db)
37    .await?;
38
39    counter
40        .update()
41        .value(toasty::stmt::decrement())
42        .exec(&mut db)
43        .await?;
44
45    let reloaded = Counter::get_by_id(&mut db, &counter.id).await?;
46    assert_eq!(reloaded.value, 9);
47    Ok(())
48}
49
50#[driver_test(scenario(crate::scenarios::counter_value))]
51pub async fn add_adds_value(t: &mut Test) -> Result<()> {
52    let mut db = setup(t).await;
53    let mut counter = toasty::create!(Counter {
54        id: uuid::Uuid::new_v4(),
55        value: 10,
56    })
57    .exec(&mut db)
58    .await?;
59
60    counter
61        .update()
62        .value(toasty::stmt::add(25))
63        .exec(&mut db)
64        .await?;
65
66    let reloaded = Counter::get_by_id(&mut db, &counter.id).await?;
67    assert_eq!(reloaded.value, 35);
68    Ok(())
69}
70
71#[driver_test(scenario(crate::scenarios::counter_value))]
72pub async fn subtract_subtracts_value(t: &mut Test) -> Result<()> {
73    let mut db = setup(t).await;
74    let mut counter = toasty::create!(Counter {
75        id: uuid::Uuid::new_v4(),
76        value: 10,
77    })
78    .exec(&mut db)
79    .await?;
80
81    counter
82        .update()
83        .value(toasty::stmt::subtract(3))
84        .exec(&mut db)
85        .await?;
86
87    let reloaded = Counter::get_by_id(&mut db, &counter.id).await?;
88    assert_eq!(reloaded.value, 7);
89    Ok(())
90}
91
92#[driver_test(scenario(crate::scenarios::counter_value))]
93pub async fn add_negative_value(t: &mut Test) -> Result<()> {
94    let mut db = setup(t).await;
95    let mut counter = toasty::create!(Counter {
96        id: uuid::Uuid::new_v4(),
97        value: 10,
98    })
99    .exec(&mut db)
100    .await?;
101
102    counter
103        .update()
104        .value(toasty::stmt::add(-4))
105        .exec(&mut db)
106        .await?;
107
108    let reloaded = Counter::get_by_id(&mut db, &counter.id).await?;
109    assert_eq!(reloaded.value, 6);
110    Ok(())
111}
112
113#[driver_test(scenario(crate::scenarios::counter_value))]
114pub async fn increment_emits_add_assignment(t: &mut Test) -> Result<()> {
115    let mut db = setup(t).await;
116    let mut counter = toasty::create!(Counter {
117        id: uuid::Uuid::new_v4(),
118        value: 10,
119    })
120    .exec(&mut db)
121    .await?;
122
123    let counter_table_id = table_id(&db, "counters");
124    let is_sql = t.capability().sql;
125
126    t.log().clear();
127    counter
128        .update()
129        .value(toasty::stmt::increment())
130        .exec(&mut db)
131        .await?;
132
133    let (op, _resp) = t.log().pop();
134    // Column index 1 = value (id=0, value=1). Confirms the engine emits
135    // an `Assignment::Add` for `stmt::increment()`. Driver-specific shape
136    // (RETURNING vs follow-up SELECT) is exercised in the value-check
137    // tests above; here we only care about the assignment variant.
138    if is_sql {
139        assert_struct!(op, Operation::QuerySql({
140            stmt: Statement::Update({
141                target: UpdateTarget::Table(== counter_table_id),
142                assignments: #{ [1]: Assignment::Add(_) },
143            }),
144            ..
145        }));
146    } else {
147        assert_struct!(op, Operation::UpdateByKey({
148            table: == counter_table_id,
149            assignments: #{ [1]: Assignment::Add(_) },
150            ..
151        }));
152    }
153    Ok(())
154}
155
156#[driver_test]
157pub async fn arithmetic_chains_with_other_updates(t: &mut Test) -> Result<()> {
158    #[derive(Debug, toasty::Model)]
159    struct Profile {
160        #[key]
161        id: uuid::Uuid,
162
163        name: String,
164        login_count: i64,
165    }
166
167    let mut db = t.setup_db(models!(Profile)).await;
168    let mut profile = toasty::create!(Profile {
169        id: uuid::Uuid::new_v4(),
170        name: "alice",
171        login_count: 5,
172    })
173    .exec(&mut db)
174    .await?;
175
176    profile
177        .update()
178        .name("alice2")
179        .login_count(toasty::stmt::increment())
180        .exec(&mut db)
181        .await?;
182
183    let reloaded = Profile::get_by_id(&mut db, &profile.id).await?;
184    assert_struct!(reloaded, _ { name: "alice2", login_count: 6, .. });
185    Ok(())
186}
187
188#[driver_test(scenario(crate::scenarios::counter_value))]
189pub async fn multiple_add_on_one_field(t: &mut Test) -> Result<()> {
190    // Regression: chaining two arithmetic ops on the same field used to crash
191    // lowering. `Assignments::add` batches duplicate keys into
192    // `Assignment::Batch([Add, Add])`, and `fold_append_batch` only handles
193    // `Append`. Two `stmt::add` on the same field should compose to a single
194    // net add of (2 + 3).
195    let mut db = setup(t).await;
196    let mut counter = toasty::create!(Counter {
197        id: uuid::Uuid::new_v4(),
198        value: 10,
199    })
200    .exec(&mut db)
201    .await?;
202
203    counter
204        .update()
205        .value(toasty::stmt::add(2))
206        .value(toasty::stmt::add(3))
207        .exec(&mut db)
208        .await?;
209
210    let reloaded = Counter::get_by_id(&mut db, &counter.id).await?;
211    assert_eq!(reloaded.value, 15);
212    Ok(())
213}
214
215#[driver_test(scenario(crate::scenarios::counter_value))]
216pub async fn subtract_then_add_on_one_field(t: &mut Test) -> Result<()> {
217    // Covers the sign-flip path in the arithmetic-batch fold: when
218    // `Subtract` leads, subsequent `Add` operands must flip to subtraction
219    // inside the running operand (`col - a + b = col - (a - b)`).
220    let mut db = setup(t).await;
221    let mut counter = toasty::create!(Counter {
222        id: uuid::Uuid::new_v4(),
223        value: 10,
224    })
225    .exec(&mut db)
226    .await?;
227
228    counter
229        .update()
230        .value(toasty::stmt::subtract(3))
231        .value(toasty::stmt::add(7))
232        .exec(&mut db)
233        .await?;
234
235    let reloaded = Counter::get_by_id(&mut db, &counter.id).await?;
236    assert_eq!(reloaded.value, 14);
237    Ok(())
238}
239
240#[driver_test(scenario(crate::scenarios::counter_value))]
241pub async fn set_then_arithmetic_on_one_field(t: &mut Test) -> Result<()> {
242    // Covers the Set+arithmetic fold: a literal write followed by an
243    // arithmetic op on the same field should reduce to `Set(literal ± op)`
244    // — Set clobbers prior state and absorbs subsequent arithmetic.
245    let mut db = setup(t).await;
246    let mut counter = toasty::create!(Counter {
247        id: uuid::Uuid::new_v4(),
248        value: 10,
249    })
250    .exec(&mut db)
251    .await?;
252
253    counter
254        .update()
255        .value(50)
256        .value(toasty::stmt::add(8))
257        .value(toasty::stmt::subtract(3))
258        .exec(&mut db)
259        .await?;
260
261    let reloaded = Counter::get_by_id(&mut db, &counter.id).await?;
262    assert_eq!(reloaded.value, 55);
263    Ok(())
264}
265
266#[driver_test]
267pub async fn increment_unique_column(t: &mut Test) -> Result<()> {
268    // Regression: DynamoDB's unique-index update path assumes every
269    // assignment on a unique column is `Set` (`let Set(expr) = assignment
270    // else unreachable!()`). Filtering by projection alone lets `Add` /
271    // `Subtract` reach the let-else and panic. Incrementing a unique
272    // numeric column should succeed on every backend.
273    #[derive(Debug, toasty::Model)]
274    struct Slot {
275        #[key]
276        id: uuid::Uuid,
277
278        #[unique]
279        count: i64,
280    }
281
282    let mut db = t.setup_db(models!(Slot)).await;
283    let mut slot = toasty::create!(Slot {
284        id: uuid::Uuid::new_v4(),
285        count: 10,
286    })
287    .exec(&mut db)
288    .await?;
289
290    slot.update()
291        .count(toasty::stmt::increment())
292        .exec(&mut db)
293        .await?;
294
295    let reloaded = Slot::get_by_id(&mut db, &slot.id).await?;
296    assert_eq!(reloaded.count, 11);
297    Ok(())
298}
299
300#[driver_test]
301pub async fn increment_narrow_integer_column(t: &mut Test) -> Result<()> {
302    // Regression: stmt::increment() / stmt::decrement() hardcoded
303    // Value::I64(1), which the PostgreSQL driver had no `(I64, INT2)` arm
304    // for — incrementing an `i8`, `i16`, or `u8` column panicked at value
305    // binding. The literal must encode in a value variant that fits any
306    // integer column on every backend.
307    #[derive(Debug, toasty::Model)]
308    struct Tally {
309        #[key]
310        id: uuid::Uuid,
311
312        count: i16,
313    }
314
315    let mut db = t.setup_db(models!(Tally)).await;
316    let mut tally = toasty::create!(Tally {
317        id: uuid::Uuid::new_v4(),
318        count: 10_i16,
319    })
320    .exec(&mut db)
321    .await?;
322
323    tally
324        .update()
325        .count(toasty::stmt::increment())
326        .exec(&mut db)
327        .await?;
328    assert_eq!(Tally::get_by_id(&mut db, &tally.id).await?.count, 11);
329
330    tally
331        .update()
332        .count(toasty::stmt::decrement())
333        .exec(&mut db)
334        .await?;
335    assert_eq!(Tally::get_by_id(&mut db, &tally.id).await?.count, 10);
336
337    Ok(())
338}
339
340#[driver_test(scenario(crate::scenarios::counter_value))]
341pub async fn filter_update_with_arithmetic(t: &mut Test) -> Result<()> {
342    let mut db = setup(t).await;
343    let counter = toasty::create!(Counter {
344        id: uuid::Uuid::new_v4(),
345        value: 10,
346    })
347    .exec(&mut db)
348    .await?;
349
350    Counter::filter_by_id(counter.id)
351        .update()
352        .value(toasty::stmt::add(100))
353        .exec(&mut db)
354        .await?;
355
356    let reloaded = Counter::get_by_id(&mut db, &counter.id).await?;
357    assert_eq!(reloaded.value, 110);
358    Ok(())
359}