Skip to main content

toasty_driver_integration_suite/tests/
field_version.rs

1use crate::prelude::*;
2
3/// A newly created record starts with version == 1.
4#[driver_test(scenario(crate::scenarios::versioned_item))]
5pub async fn create_initializes_version(test: &mut Test) -> Result<()> {
6    let mut db = setup(test).await;
7
8    let item = toasty::create!(Item { name: "hello" })
9        .exec(&mut db)
10        .await?;
11    assert_eq!(item.version, 1);
12
13    Ok(())
14}
15
16/// Updating a record increments the version.
17#[driver_test(scenario(crate::scenarios::versioned_item))]
18pub async fn update_increments_version(test: &mut Test) -> Result<()> {
19    let mut db = setup(test).await;
20
21    let mut item = toasty::create!(Item { name: "hello" })
22        .exec(&mut db)
23        .await?;
24    assert_eq!(item.version, 1);
25
26    item.update().name("world").exec(&mut db).await?;
27    assert_eq!(item.version, 2);
28
29    item.update().name("again").exec(&mut db).await?;
30    assert_eq!(item.version, 3);
31
32    Ok(())
33}
34
35/// A relative update (`increment`) on a versioned model. The post-increment
36/// value can't be computed client-side, so the driver returns it; the
37/// client-side `#[version]` bump rides along as a constant in the same
38/// returning projection. The two must not collide: the engine asks the driver
39/// for exactly the relative column, so the version value never lands in the
40/// returned row and shifts `value` out of its slot.
41#[driver_test(scenario(crate::scenarios::versioned_counter))]
42pub async fn relative_update_increments_value_and_version(test: &mut Test) -> Result<()> {
43    let mut db = setup(test).await;
44
45    let mut counter = toasty::create!(Counter { value: 10 }).exec(&mut db).await?;
46    assert_struct!(counter, _ { value: 10, version: 1, .. });
47
48    counter
49        .update()
50        .value(toasty::stmt::increment())
51        .exec(&mut db)
52        .await?;
53
54    // The handle is reloaded from the update's returning row: `value` is read
55    // back from the driver, `version` is bumped to 2.
56    assert_struct!(counter, _ { value: 11, version: 2, .. });
57
58    // And it is durable.
59    let reloaded = Counter::filter_by_id(counter.id).get(&mut db).await?;
60    assert_struct!(reloaded, _ { value: 11, version: 2, .. });
61
62    Ok(())
63}
64
65/// Two updates from the same stale snapshot — the second should fail with a
66/// condition-check error because the DB version has already moved to 2.
67#[driver_test(scenario(crate::scenarios::versioned_item))]
68pub async fn stale_update_fails(test: &mut Test) -> Result<()> {
69    let mut db = setup(test).await;
70
71    let mut item = toasty::create!(Item { name: "hello" })
72        .exec(&mut db)
73        .await?;
74    assert_eq!(item.version, 1);
75
76    // Load a second handle from the DB — same record, version == 1.
77    let mut stale = Item::filter_by_id(item.id).get(&mut db).await?;
78    assert_eq!(stale.version, 1);
79
80    // First update succeeds: DB version goes 1 → 2.
81    item.update().name("updated").exec(&mut db).await?;
82    assert_eq!(item.version, 2);
83
84    // Stale handle still has version == 1; this should fail.
85    let result: Result<()> = stale.update().name("should fail").exec(&mut db).await;
86    assert!(
87        result.is_err(),
88        "expected stale update to fail, but it succeeded"
89    );
90
91    Ok(())
92}
93
94/// Creating the same primary key twice should fail. On DynamoDB the insert
95/// carries an attribute_not_exists condition on the version column; on SQL
96/// backends the primary-key constraint rejects the duplicate.
97#[driver_test(scenario(crate::scenarios::versioned_item))]
98pub async fn duplicate_create_fails(test: &mut Test) -> Result<()> {
99    let mut db = setup(test).await;
100
101    let item = toasty::create!(Item { name: "original" })
102        .exec(&mut db)
103        .await?;
104
105    let result = toasty::create!(Item {
106        id: item.id,
107        name: "duplicate"
108    })
109    .exec(&mut db)
110    .await;
111
112    assert!(
113        result.is_err(),
114        "expected duplicate create to fail, but it succeeded"
115    );
116
117    Ok(())
118}
119
120/// Batch-creating multiple versioned items should initialize all versions to 1,
121/// and a duplicate within the batch should fail.
122#[driver_test(scenario(crate::scenarios::versioned_item))]
123pub async fn batch_insert_checks_version(test: &mut Test) -> Result<()> {
124    let mut db = setup(test).await;
125
126    // Create two items in a single batch — both should succeed with version == 1.
127    let items = toasty::create!(Item::[
128        { name: "first" },
129        { name: "second" },
130    ])
131    .exec(&mut db)
132    .await?;
133
134    assert_eq!(items.len(), 2);
135    assert!(items.iter().all(|i| i.version == 1));
136
137    // Attempt to batch-create a new item alongside a duplicate ID — should fail.
138    let existing_id = items[0].id;
139    let result = toasty::create!(Item::[
140        { id: existing_id, name: "duplicate" },
141        { name: "new" },
142    ])
143    .exec(&mut db)
144    .await;
145
146    assert!(
147        result.is_err(),
148        "expected batch create with duplicate to fail"
149    );
150
151    Ok(())
152}
153
154/// A query-based update increments the version, just like an instance update.
155/// This is the single-key path (filter by primary key).
156///
157/// Unlike an instance update there is no per-row OCC guard — a query-based
158/// update is atomic at the database level — but the version still advances so
159/// concurrent stale writers are detected.
160#[driver_test(scenario(crate::scenarios::versioned_item))]
161pub async fn query_update_increments_version(test: &mut Test) -> Result<()> {
162    let mut db = setup(test).await;
163
164    let item = toasty::create!(Item { name: "hello" })
165        .exec(&mut db)
166        .await?;
167    assert_eq!(item.version, 1);
168
169    Item::filter_by_id(item.id)
170        .update()
171        .name("world")
172        .exec(&mut db)
173        .await?;
174
175    let reloaded = Item::filter_by_id(item.id).get(&mut db).await?;
176    assert_eq!(reloaded.name, "world");
177    assert_eq!(reloaded.version, 2);
178
179    Ok(())
180}
181
182/// A query-based update bumps the version, so a concurrent instance update from
183/// a snapshot taken *before* the query update fails its OCC check rather than
184/// silently clobbering the query update's write.
185#[driver_test(scenario(crate::scenarios::versioned_item))]
186pub async fn query_update_invalidates_stale_instance(test: &mut Test) -> Result<()> {
187    let mut db = setup(test).await;
188
189    let item = toasty::create!(Item { name: "hello" })
190        .exec(&mut db)
191        .await?;
192    assert_eq!(item.version, 1);
193
194    // Snapshot the record at version 1 before any update lands.
195    let mut stale = Item::filter_by_id(item.id).get(&mut db).await?;
196    assert_eq!(stale.version, 1);
197
198    // A query-based update advances the DB version 1 → 2.
199    Item::filter_by_id(item.id)
200        .update()
201        .name("query-updated")
202        .exec(&mut db)
203        .await?;
204
205    // The stale instance (still version 1) must fail rather than overwrite.
206    let result: Result<()> = stale.update().name("clobber").exec(&mut db).await;
207    assert!(
208        result.is_err(),
209        "expected stale instance update to fail after a concurrent query update"
210    );
211
212    // The query update's write is intact.
213    let reloaded = Item::filter_by_id(item.id).get(&mut db).await?;
214    assert_eq!(reloaded.name, "query-updated");
215    assert_eq!(reloaded.version, 2);
216
217    Ok(())
218}
219
220/// Query-based update on a versioned model matching multiple rows. On DynamoDB
221/// this exercises update_by_key path 2 (no unique index, N keys via
222/// transact_write_items); on SQL backends it is a single multi-row UPDATE.
223///
224/// The increment is applied atomically to every matched row, so each row's
225/// version advances independently. Verifies the multi-row path applies all
226/// assignments and bumps each version.
227#[driver_test(scenario(crate::scenarios::tagged_item))]
228pub async fn query_update_multi_key_works(test: &mut Test) -> Result<()> {
229    let mut db = setup(test).await;
230
231    // Create two items sharing the same tag
232    let items = toasty::create!(Item::[
233        { tag: "batch", status: "active", name: "alpha" },
234        { tag: "batch", status: "active", name: "beta" },
235    ])
236    .exec(&mut db)
237    .await?;
238    assert!(items.iter().all(|i| i.version == 1));
239
240    // Update all items with tag == "batch" in one query-based operation.
241    Item::filter_by_tag("batch")
242        .update()
243        .name("updated")
244        .exec(&mut db)
245        .await?;
246
247    let a2 = Item::filter_by_id(items[0].id).get(&mut db).await?;
248    let b2 = Item::filter_by_id(items[1].id).get(&mut db).await?;
249    assert_eq!(a2.name, "updated");
250    assert_eq!(a2.version, 2);
251    assert_eq!(b2.name, "updated");
252    assert_eq!(b2.version, 2);
253
254    Ok(())
255}
256
257/// Query-based update through the unique-index path (path 3) increments the
258/// version. The version is a non-unique column, so it rides along in the main
259/// update expression alongside the unique-index surgery.
260#[driver_test(scenario(crate::scenarios::versioned_user_unique_email))]
261pub async fn query_update_unique_index_increments_version(test: &mut Test) -> Result<()> {
262    let mut db = setup(test).await;
263
264    let user = toasty::create!(User {
265        email: "carol@example.com"
266    })
267    .exec(&mut db)
268    .await?;
269    assert_eq!(user.version, 1);
270
271    User::filter_by_id(user.id)
272        .update()
273        .email("carol2@example.com")
274        .exec(&mut db)
275        .await?;
276
277    let reloaded = User::filter_by_id(user.id).get(&mut db).await?;
278    assert_eq!(reloaded.email, "carol2@example.com");
279    assert_eq!(reloaded.version, 2);
280
281    Ok(())
282}
283
284/// Updating a record through the unique-index path (path 3) increments the
285/// version when the unique column changes.
286#[driver_test(scenario(crate::scenarios::versioned_user_unique_email))]
287pub async fn unique_index_update_increments_version(test: &mut Test) -> Result<()> {
288    let mut db = setup(test).await;
289
290    let mut user = toasty::create!(User {
291        email: "alice@example.com"
292    })
293    .exec(&mut db)
294    .await?;
295    assert_eq!(user.version, 1);
296
297    user.update()
298        .email("alice2@example.com")
299        .exec(&mut db)
300        .await?;
301    assert_eq!(user.version, 2);
302
303    user.update()
304        .email("alice3@example.com")
305        .exec(&mut db)
306        .await?;
307    assert_eq!(user.version, 3);
308
309    Ok(())
310}
311
312/// Stale update on a model with a unique index: the second update from a stale
313/// snapshot should fail.
314#[driver_test(scenario(crate::scenarios::versioned_user_unique_email))]
315pub async fn unique_index_stale_update_fails(test: &mut Test) -> Result<()> {
316    let mut db = setup(test).await;
317
318    let mut user = toasty::create!(User {
319        email: "bob@example.com"
320    })
321    .exec(&mut db)
322    .await?;
323    assert_eq!(user.version, 1);
324
325    let mut stale = User::filter_by_email("bob@example.com")
326        .get(&mut db)
327        .await?;
328    assert_eq!(stale.version, 1);
329
330    // Advance user.version to 2
331    user.update()
332        .email("bob2@example.com")
333        .exec(&mut db)
334        .await?;
335    assert_eq!(user.version, 2);
336
337    // Stale handle (version == 1) should fail
338    let result: Result<()> = stale.update().email("bob3@example.com").exec(&mut db).await;
339    assert!(
340        result.is_err(),
341        "expected stale unique-index update to fail"
342    );
343
344    Ok(())
345}
346
347/// The query-based counterpart to `relative_update_increments_value_and_version`:
348/// a relative assignment (`value += 1`) on a versioned model through a
349/// query-rooted update.
350///
351/// The user's relative column needs a driver round-trip, so it's in the
352/// returning column list; the engine-injected version bump is kept *out* of
353/// that list. The driver returns exactly the requested columns, so the version
354/// value never lands in the returned row and shifts `value` out of its slot.
355#[driver_test(scenario(crate::scenarios::versioned_counter))]
356pub async fn query_relative_update_increments_value_and_version(test: &mut Test) -> Result<()> {
357    let mut db = setup(test).await;
358
359    let counter = toasty::create!(Counter { value: 10 }).exec(&mut db).await?;
360    assert_struct!(counter, _ { value: 10, version: 1, .. });
361
362    Counter::filter_by_id(counter.id)
363        .update()
364        .value(toasty::stmt::increment())
365        .exec(&mut db)
366        .await?;
367
368    let reloaded = Counter::filter_by_id(counter.id).get(&mut db).await?;
369    assert_struct!(reloaded, _ { value: 11, version: 2, .. });
370
371    Ok(())
372}
373
374/// Deleting a record checks the version — a fresh handle succeeds.
375#[driver_test(scenario(crate::scenarios::versioned_item))]
376pub async fn delete_checks_version(test: &mut Test) -> Result<()> {
377    let mut db = setup(test).await;
378
379    let item = toasty::create!(Item { name: "hello" })
380        .exec(&mut db)
381        .await?;
382    assert_eq!(item.version, 1);
383    let id = item.id;
384
385    item.delete().exec(&mut db).await?;
386
387    // Item should be gone — get() should return not-found
388    let after_delete = Item::filter_by_id(id).get(&mut db).await;
389    assert!(after_delete.is_err(), "item should have been deleted");
390
391    Ok(())
392}
393
394/// An instance update whose row has been deleted fails instead of silently
395/// succeeding. SQL backends raise `record_not_found`.
396#[driver_test(scenario(crate::scenarios::versioned_item))]
397pub async fn update_after_delete_fails(test: &mut Test) -> Result<()> {
398    let mut db = setup(test).await;
399
400    let mut item = toasty::create!(Item { name: "hello" })
401        .exec(&mut db)
402        .await?;
403
404    // Delete the row out from under the instance.
405    Item::filter_by_id(item.id).delete().exec(&mut db).await?;
406
407    let result: Result<()> = item.update().name("stale").exec(&mut db).await;
408    let err = assert_err!(result);
409    if test.capability().sql {
410        assert!(
411            err.is_record_not_found(),
412            "expected record_not_found, got {err:?}"
413        );
414    }
415
416    Ok(())
417}
418
419/// The read-back variant of `update_after_delete_fails`: a relative update
420/// (`increment`) carries a `RETURNING`, so the conditional write goes through
421/// the row-returning path. With the row deleted there is nothing to return —
422/// the update fails rather than reloading the instance from a phantom row.
423#[driver_test(scenario(crate::scenarios::versioned_counter))]
424pub async fn relative_update_after_delete_fails(test: &mut Test) -> Result<()> {
425    let mut db = setup(test).await;
426
427    let mut counter = toasty::create!(Counter { value: 10 }).exec(&mut db).await?;
428
429    Counter::filter_by_id(counter.id)
430        .delete()
431        .exec(&mut db)
432        .await?;
433
434    let result: Result<()> = counter
435        .update()
436        .value(toasty::stmt::increment())
437        .exec(&mut db)
438        .await;
439    let err = assert_err!(result);
440    if test.capability().sql {
441        assert!(
442            err.is_record_not_found(),
443            "expected record_not_found, got {err:?}"
444        );
445    }
446
447    Ok(())
448}
449
450/// A conditional write binding a value whose database type cannot be inferred
451/// from the value alone (`f64`). Exercises bind-param type refinement through
452/// both SQL conditional-write plans (regression: the PostgreSQL CTE plan left
453/// `f64` assignment params untyped and panicked).
454#[driver_test]
455pub async fn update_with_float_field_works(test: &mut Test) -> Result<()> {
456    #[derive(Debug, toasty::Model)]
457    struct Gauge {
458        #[key]
459        #[auto]
460        id: uuid::Uuid,
461
462        value: f64,
463
464        #[version]
465        version: u64,
466    }
467
468    let mut db = test.setup_db(models!(Gauge)).await;
469
470    let mut gauge = toasty::create!(Gauge { value: 1.5 }).exec(&mut db).await?;
471
472    gauge.update().value(2.5).exec(&mut db).await?;
473    assert_struct!(gauge, _ { value: 2.5, version: 2, .. });
474
475    let reloaded = Gauge::filter_by_id(gauge.id).get(&mut db).await?;
476    assert_struct!(reloaded, _ { value: 2.5, version: 2, .. });
477
478    Ok(())
479}
480
481/// Deleting from a stale snapshot (wrong version) should fail.
482#[driver_test(scenario(crate::scenarios::versioned_item))]
483pub async fn stale_delete_fails(test: &mut Test) -> Result<()> {
484    let mut db = setup(test).await;
485
486    let mut item = toasty::create!(Item { name: "hello" })
487        .exec(&mut db)
488        .await?;
489    assert_eq!(item.version, 1);
490
491    // Load a stale copy and then advance item.version to 2.
492    let stale = Item::filter_by_id(item.id).get(&mut db).await?;
493    item.update().name("updated").exec(&mut db).await?;
494    assert_eq!(item.version, 2);
495
496    // stale.version == 1 — delete should fail.
497    let result: Result<()> = stale.delete().exec(&mut db).await;
498    assert!(result.is_err(), "expected stale delete to fail");
499
500    // Item should still exist.
501    let _ = Item::filter_by_id(item.id)
502        .get(&mut db)
503        .await
504        .expect("item should still exist");
505
506    Ok(())
507}