diff --git a/sdk/cosmos/assets.json b/sdk/cosmos/assets.json index 7708dc8a42..0e743ffd6e 100644 --- a/sdk/cosmos/assets.json +++ b/sdk/cosmos/assets.json @@ -1,6 +1,6 @@ { "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "rust", - "Tag": "rust/azure_data_cosmos_ff23846344", + "Tag": "rust/azure_data_cosmos_a39b424a5b", "TagPrefix": "rust/azure_data_cosmos" } \ No newline at end of file diff --git a/sdk/cosmos/azure_data_cosmos/CHANGELOG.md b/sdk/cosmos/azure_data_cosmos/CHANGELOG.md index 5be47ed21c..44adcf0acd 100644 --- a/sdk/cosmos/azure_data_cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure_data_cosmos/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.25.0 (Unreleased) ### Features Added +* Added `if_match_etag` to `ItemOptions` ([#2705](https://github.com/Azure/azure-sdk-for-rust/pull/2705)) ### Breaking Changes diff --git a/sdk/cosmos/azure_data_cosmos/README.md b/sdk/cosmos/azure_data_cosmos/README.md index fdf8cf175b..2b96d4fa1e 100644 --- a/sdk/cosmos/azure_data_cosmos/README.md +++ b/sdk/cosmos/azure_data_cosmos/README.md @@ -131,8 +131,6 @@ async fn example(cosmos_client: CosmosClient) -> Result<(), Box { #[derive(Clone, Default)] pub struct ItemOptions<'a> { pub method_options: ClientMethodOptions<'a>, - + /// If specified, the operation will only be performed if the item matches the provided Etag. + /// + /// See [Optimistic Concurrency Control](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/database-transactions-optimistic-concurrency#optimistic-concurrency-control) for more. + pub if_match_etag: Option, /// When this value is true, write operations will respond with the new value of the resource being written. /// /// The default for this is `false`, which reduces the network and CPU burden that comes from serializing and deserializing the response. diff --git a/sdk/cosmos/azure_data_cosmos/tests/cosmos_items.rs b/sdk/cosmos/azure_data_cosmos/tests/cosmos_items.rs index 2c30058bc0..187642b43c 100644 --- a/sdk/cosmos/azure_data_cosmos/tests/cosmos_items.rs +++ b/sdk/cosmos/azure_data_cosmos/tests/cosmos_items.rs @@ -2,6 +2,7 @@ mod framework; +use azure_core::http::Etag; use azure_core_test::{recorded, TestContext}; use azure_data_cosmos::{ clients::ContainerClient, @@ -201,6 +202,7 @@ pub async fn item_read_system_properties(context: TestContext) -> Result<(), Box read_item.get("_rid").is_some(), "expected _rid to be present" ); + assert!( read_item.get("_etag").is_some(), "expected _etag to be present" @@ -405,3 +407,293 @@ pub async fn item_null_partition_key(context: TestContext) -> Result<(), Box Result<(), Box> { + let account = TestAccount::from_env(context, None).await?; + let cosmos_client = account.connect_with_key(None)?; + let container_client = create_container(&account, &cosmos_client).await?; + + //Create an item + let mut item = TestItem { + id: "Item1".into(), + partition_key: Some("Partition1".into()), + value: 42, + nested: NestedItem { + nested_value: "Nested".into(), + }, + bool_value: true, + }; + + let response = container_client + .create_item("Partition1", &item, None) + .await?; + + //Store Etag from response + let etag: Etag = response + .headers() + .get_str(&azure_core::http::headers::ETAG) + .expect("expected the etag to be returned") + .into(); + + //Replace item with correct Etag + item.value = 24; + item.nested.nested_value = "Updated".into(); + + container_client + .replace_item( + "Partition1", + "Item1", + &item, + Some(ItemOptions { + if_match_etag: Some(etag), + ..Default::default() + }), + ) + .await?; + + //Replace item with incorrect Etag + item.value = 52; + item.nested.nested_value = "UpdatedAgain".into(); + + let response = container_client + .replace_item( + "Partition1", + "Item1", + &item, + Some(ItemOptions { + if_match_etag: Some("incorrectEtag".into()), + ..Default::default() + }), + ) + .await; + + assert_eq!( + Some(azure_core::http::StatusCode::PreconditionFailed), + response + .expect_err("expected the server to return an error") + .http_status() + ); + + account.cleanup().await?; + Ok(()) +} + +#[recorded::test] +pub async fn item_upsert_if_match_etag(context: TestContext) -> Result<(), Box> { + let account = TestAccount::from_env(context, None).await?; + let cosmos_client = account.connect_with_key(None)?; + let container_client = create_container(&account, &cosmos_client).await?; + + //Create an item + let mut item = TestItem { + id: "Item1".into(), + partition_key: Some("Partition1".into()), + value: 42, + nested: NestedItem { + nested_value: "Nested".into(), + }, + bool_value: true, + }; + + let response = container_client + .create_item("Partition1", &item, None) + .await?; + + //Store Etag from response + let etag: Etag = response + .headers() + .get_str(&azure_core::http::headers::ETAG) + .expect("expected the etag to be returned") + .into(); + + //Upsert item with correct Etag + item.value = 24; + item.nested.nested_value = "Updated".into(); + + container_client + .upsert_item( + "Partition1", + &item, + Some(ItemOptions { + if_match_etag: Some(etag), + ..Default::default() + }), + ) + .await?; + + //Upsert item with incorrect Etag + item.value = 52; + item.nested.nested_value = "UpdatedAgain".into(); + + let response = container_client + .upsert_item( + "Partition1", + &item, + Some(ItemOptions { + if_match_etag: Some("incorrectEtag".into()), + ..Default::default() + }), + ) + .await; + + assert_eq!( + Some(azure_core::http::StatusCode::PreconditionFailed), + response + .expect_err("expected the server to return an error") + .http_status() + ); + + account.cleanup().await?; + Ok(()) +} + +#[recorded::test] +pub async fn item_delete_if_match_etag(context: TestContext) -> Result<(), Box> { + let account = TestAccount::from_env(context, None).await?; + let cosmos_client = account.connect_with_key(None)?; + let container_client = create_container(&account, &cosmos_client).await?; + + //Create an item + let item = TestItem { + id: "Item1".into(), + partition_key: Some("Partition1".into()), + value: 42, + nested: NestedItem { + nested_value: "Nested".into(), + }, + bool_value: true, + }; + + let response = container_client + .create_item("Partition1", &item, None) + .await?; + + //Store Etag from response + let etag: Etag = response + .headers() + .get_str(&azure_core::http::headers::ETAG) + .expect("expected the etag to be returned") + .into(); + + //Delete item with correct Etag + container_client + .delete_item( + "Partition1", + "Item1", + Some(ItemOptions { + if_match_etag: Some(etag), + ..Default::default() + }), + ) + .await?; + + //Add item again for second delete test + container_client + .create_item("Partition1", &item, None) + .await?; + + //Delete item with incorrect Etag + let response = container_client + .delete_item( + "Partition1", + "Item1", + Some(ItemOptions { + if_match_etag: Some("incorrectEtag".into()), + ..Default::default() + }), + ) + .await; + + assert_eq!( + Some(azure_core::http::StatusCode::PreconditionFailed), + response + .expect_err("expected the server to return an error") + .http_status() + ); + + account.cleanup().await?; + Ok(()) +} + +#[recorded::test] +pub async fn item_patch_if_match_etag(context: TestContext) -> Result<(), Box> { + let account = TestAccount::from_env(context, None).await?; + let cosmos_client = account.connect_with_key(None)?; + let container_client = create_container(&account, &cosmos_client).await?; + + //Create an item + let item = TestItem { + id: "Item1".into(), + partition_key: Some("Partition1".into()), + value: 42, + nested: NestedItem { + nested_value: "Nested".into(), + }, + bool_value: true, + }; + + let response = container_client + .create_item("Partition1", &item, None) + .await?; + + //Store Etag from response + let etag: Etag = response + .headers() + .get_str(&azure_core::http::headers::ETAG) + .expect("expected the etag to be returned") + .into(); + + //Patch item with correct Etag + let patch = PatchDocument::default() + .with_replace("/nested/nested_value", "Patched")? + .with_increment("/value", 10)?; + + container_client + .patch_item( + "Partition1", + "Item1", + patch, + Some(ItemOptions { + if_match_etag: Some(etag), + ..Default::default() + }), + ) + .await?; + + let patched_item: TestItem = container_client + .read_item("Partition1", "Item1", None) + .await? + .into_body() + .await?; + + assert_eq!("Patched", patched_item.nested.nested_value); + assert_eq!(52, patched_item.value); + + //Patch item with incorrect Etag + let patch = PatchDocument::default() + .with_replace("/nested/nested_value", "PatchedIncorrect")? + .with_increment("/value", 15)?; + + let response = container_client + .patch_item( + "Partition1", + "Item1", + patch, + Some(ItemOptions { + if_match_etag: Some("incorrectEtag".into()), + ..Default::default() + }), + ) + .await; + + assert_eq!( + Some(azure_core::http::StatusCode::PreconditionFailed), + response + .expect_err("expected the server to return an error") + .http_status() + ); + + account.cleanup().await?; + Ok(()) +}