Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/cosmos/azure_data_cosmos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

### Breaking Changes

- `ContainerClient::patch_item()` and `PatchItemOptions` are now gated behind the new, non-default `preview_patch` feature. PATCH is not exactly-once under transport failures: an interrupted patch may re-apply non-idempotent operations (`increment`, `add` on an array, `move`), so it is no longer part of the supported default surface. Enable `preview_patch` to keep using it, and prefer idempotent operations (`set` on a caller-computed value) until the guarantee is fixed. ([#5130](https://github.com/Azure/azure-sdk-for-rust/pull/5130))
- `CosmosClient::database_client` and `DatabaseClient::container_client` now take `impl Into<ResourceIdentity>` instead of `&str`; call sites passing a deref-able string (for example a `Cow<str>` field) need `&*value` or `.as_ref()`. ([#4687](https://github.com/Azure/azure-sdk-for-rust/pull/4687))
- `DatabaseClient::id()` now returns `&ResourceIdentity` instead of `&str`. ([#4687](https://github.com/Azure/azure-sdk-for-rust/pull/4687))
- Control-plane APIs are now gated behind the new `control_plane` feature, which is **not** enabled by default. Code using database or container management (`CosmosClient::create_database`/`query_databases`, `DatabaseClient::read`/`create_container`/`query_containers`/`delete`, `ContainerClient::replace`/`delete`), throughput management (`read_throughput`/`begin_replace_throughput`, `ThroughputPoller`), or the associated model and options types (`DatabaseProperties`, `ThroughputProperties`, and the container create/replace/delete/query, database, and throughput option types) must now enable the `control_plane` feature. Reading container properties via `ContainerClient::read()` — along with `ContainerProperties`, `IndexingPolicy`, `ResourceResponse`, and `ReadContainerOptions` — remains available without the feature, since it works with Entra ID authentication and mirrors the metadata read the SDK already performs internally. ([#4854](https://github.com/Azure/azure-sdk-for-rust/pull/4854))
Expand Down
10 changes: 10 additions & 0 deletions sdk/cosmos/azure_data_cosmos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ metrics = [
preview_dtx = [
"azure_data_cosmos_driver/preview_dtx",
] # Enables preview Distributed Transaction APIs. Disabled by default and not production-ready.
preview_patch = [
"azure_data_cosmos_driver/preview_patch",
] # Enables the preview PATCH API. Disabled by default and not production-ready: an interrupted patch may re-apply non-idempotent operations.
Comment thread
FabianMeiswinkel marked this conversation as resolved.
Outdated
__internal_in_memory_emulator = [
"azure_data_cosmos_driver/__internal_in_memory_emulator",
"key_auth",
Expand Down Expand Up @@ -172,3 +175,10 @@ required-features = ["__internal_in_memory_emulator"]
name = "cosmos"
path = "examples/cosmos/main.rs"
required-features = ["control_plane"]

# The PATCH example calls the preview `patch_item` API, which only exists
# when `preview_patch` is enabled.
[[example]]
name = "cosmos_patch"
path = "examples/cosmos_patch.rs"
required-features = ["preview_patch"]
37 changes: 27 additions & 10 deletions sdk/cosmos/azure_data_cosmos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ container metadata read the SDK already performs internally.
```rust
use serde::{Serialize, Deserialize};
use azure_data_cosmos::CosmosClient;
use azure_data_cosmos::models::{PatchInstructions, PatchOperation};

#[derive(Serialize, Deserialize)]
struct Item {
Expand Down Expand Up @@ -137,21 +136,39 @@ async fn example(cosmos_client: CosmosClient) -> Result<(), Box<dyn std::error::
// Replace an item
container.replace_item("partition1", "1", item, None).await?;

let patch = PatchInstructions::from(vec![
PatchOperation::set("/value", serde_json::json!("4")),
]);
let patched: Item = container
.patch_item("partition1", "1", patch, None)
.await?
.into_model()?;
println!("patched value = {}", patched.value);

// Delete an item
container.delete_item("partition1", "1", None).await?;
Ok(())
}
```

### Partial updates with PATCH (preview)

`ContainerClient::patch_item()` applies JSON-Patch-style operations to a single item. It is
gated behind the `preview_patch` feature and is **not production-ready**:

```sh
cargo add azure_data_cosmos --features preview_patch
```

```rust,ignore
use azure_data_cosmos::models::{PatchInstructions, PatchOperation};

let patch = PatchInstructions::from(vec![
PatchOperation::set("/value", serde_json::json!("4")),
]);
let patched: Item = container
.patch_item("partition1", "1", patch, None)
.await?
.into_model()?;
```

PATCH is implemented client-side as a read, a local merge, and an ETag-guarded replace. If the
replace is interrupted after the service commits it, the pipeline may retry it and the
read-modify-write loop may re-apply the patch. Non-idempotent operations
(`increment`, `add` on an array, `move`) can therefore be applied **more than once**. Use
idempotent operations such as `set` with a caller-computed value until this limitation is fixed.

## Next steps

* [Resource Model of Azure Cosmos DB Service](https://learn.microsoft.com/azure/cosmos-db/sql-api-resources)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ pub struct ItemReadOptions {

### 5.2 `ItemWriteOptions`

Options for item write operations (`create_item`, `replace_item`, `upsert_item`, `delete_item`, `patch_item`).
Options for item write operations (`create_item`, `replace_item`, `upsert_item`, `delete_item`).

```rust
#[derive(Clone, Default)]
Expand Down
6 changes: 5 additions & 1 deletion sdk/cosmos/azure_data_cosmos/examples/cosmos_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

//! Partial updates with PATCH.
//!
//! **Preview.** Requires the `preview_patch` feature. PATCH is not
//! production-ready: an interrupted patch may re-apply non-idempotent
//! operations. See `ContainerClient::patch_item` rustdoc.
//!
//! Demonstrates `ContainerClient::patch_item` and the `PatchInstructions`
//! builder, exercising every variant of `PatchOperation` (`set`, `add`,
//! `replace`, `remove`, `increment`, `move_value`).
Expand All @@ -27,7 +31,7 @@
//! ## Running
//!
//! ```text
//! cargo run --example cosmos_patch -- \
//! cargo run --features preview_patch --example cosmos_patch -- \
//! https://<account>.documents.azure.com:443/ --region "East US" --use-entra
//! ```

Expand Down
22 changes: 16 additions & 6 deletions sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ use crate::{
clients::ClientContext,
diagnostics::CosmosOperationContext,
feed::{ChangeFeedPageIterator, FeedRange, FeedScope, QueryItemIterator},
models::{BatchResponse, ChangeFeedItem, ItemResponse, PatchInstructions, TransactionalBatch},
models::{BatchResponse, ChangeFeedItem, ItemResponse, TransactionalBatch},
options::{
BatchOptions, BinaryEncodingOptions, ChangeFeedMode, ChangeFeedOptions,
ChangeFeedStartFrom, ItemReadOptions, ItemWriteOptions, OperationOptions, PatchItemOptions,
Precondition, QueryOptions, ReadContainerOptions, ReadFeedRangesOptions, SessionToken,
ChangeFeedStartFrom, ItemReadOptions, ItemWriteOptions, OperationOptions, Precondition,
QueryOptions, ReadContainerOptions, ReadFeedRangesOptions, SessionToken,
},
PartitionKey, Query, ResourceIdentity,
};
#[cfg(feature = "preview_patch")]
use crate::{models::PatchInstructions, options::PatchItemOptions};

use azure_data_cosmos_driver::models::{
ContainerReference, CosmosOperation, ItemReference, PartitionKeyKind,
Expand Down Expand Up @@ -524,6 +526,9 @@ impl ContainerClient {
/// Applies a JSON-PATCH-style update to an item by reading it, applying
/// the [`PatchInstructions`] locally, and issuing an ETag-guarded Replace.
///
/// **Preview.** Requires the `preview_patch` feature. This API is not
/// production-ready — see [Failure Semantics](#failure-semantics) below.
///
/// The handler refuses to PATCH paths that overlap the container's
/// partition-key paths: rewriting the partition key would move the
/// document to a different physical partition, so such requests are
Expand Down Expand Up @@ -590,6 +595,7 @@ impl ContainerClient {
/// appends should either build idempotent ops (`PatchOperation::set` on a
/// caller-computed value) or detect duplicate-application via a
/// monotonic application-level sequence number.
#[cfg(feature = "preview_patch")]
pub async fn patch_item(
&self,
partition_key: impl Into<PartitionKey>,
Expand Down Expand Up @@ -1500,9 +1506,13 @@ fn _assert_futures_are_send() {
let client: &ContainerClient = todo!();
let partition_key: PartitionKey = todo!();
let item_id: &str = todo!();
let patch: PatchInstructions = todo!();
let options: Option<PatchItemOptions> = todo!();
assert_send(client.patch_item(partition_key, item_id, patch, options));
assert_send(client.read_item(partition_key.clone(), item_id, None));
#[cfg(feature = "preview_patch")]
{
let patch: PatchInstructions = todo!();
let options: Option<PatchItemOptions> = todo!();
assert_send(client.patch_item(partition_key, item_id, patch, options));
}
}

#[cfg(test)]
Expand Down
6 changes: 6 additions & 0 deletions sdk/cosmos/azure_data_cosmos/src/options/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ impl ItemWriteOptions {

/// Options for [`ContainerClient::patch_item()`](crate::clients::ContainerClient::patch_item()).
///
/// **Preview.** Requires the `preview_patch` feature. PATCH is not
/// production-ready: an interrupted patch may re-apply non-idempotent
/// operations. See [Failure Semantics](crate::clients::ContainerClient::patch_item()).
///
/// PATCH is implemented driver-side as a Read-Modify-Write (RMW) loop:
/// the driver reads the current item, applies your [`PatchInstructions`](crate::models::PatchInstructions)
/// locally, and issues an ETag-guarded Replace. If the Replace returns
Expand Down Expand Up @@ -143,6 +147,7 @@ impl ItemWriteOptions {
/// 412 retries you want to tolerate. Setting the budget too low can
/// cancel the RMW between the Read and the Replace, producing a
/// timeout error even when the service is healthy.
#[cfg(feature = "preview_patch")]
#[derive(Clone, Default)]
#[non_exhaustive]
pub struct PatchItemOptions {
Expand All @@ -158,6 +163,7 @@ pub struct PatchItemOptions {
pub max_attempts: Option<std::num::NonZeroU8>,
}

#[cfg(feature = "preview_patch")]
impl PatchItemOptions {
/// Sets the session token for this request.
pub fn with_session_token(mut self, session_token: impl Into<SessionToken>) -> Self {
Expand Down
4 changes: 3 additions & 1 deletion sdk/cosmos/azure_data_cosmos/src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ pub use database::{
};
pub use feed::{FeedOptions, QueryOptions};
pub use feed_ranges::ReadFeedRangesOptions;
pub use item::{ItemReadOptions, ItemWriteOptions, PatchItemOptions};
#[cfg(feature = "preview_patch")]
pub use item::PatchItemOptions;
pub use item::{ItemReadOptions, ItemWriteOptions};
pub use routing_strategy::RoutingStrategy;
#[cfg(feature = "control_plane")]
pub use throughput::ThroughputOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ pub async fn hpk_item_delete_full_key() -> Result<(), Box<dyn Error>> {
}

/// A5: patch a field on an item addressed by a full 2-level key.
#[cfg(feature = "preview_patch")]
#[tokio::test]
#[cfg_attr(
not(any(test_category = "emulator", test_category = "emulator_vnext")),
Expand Down
1 change: 1 addition & 0 deletions sdk/cosmos/azure_data_cosmos/tests/emulator_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod cosmos_hpk;
mod cosmos_items;
mod cosmos_offers;
mod cosmos_partition_key_types;
#[cfg(feature = "preview_patch")]
mod cosmos_patch;
mod cosmos_proxy;
mod cosmos_query;
Expand Down
1 change: 1 addition & 0 deletions sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

### Breaking Changes

- `CosmosOperation::patch_item()`, `with_patch_max_attempts()`, `patch_max_attempts()`, `as_patch_sub_operation()`, and the `OperationType::Patch` dispatch in `CosmosDriver::execute_operation` are now gated behind the new, non-default `preview_patch` feature. The read-modify-write loop is not exactly-once under transport failures: an interrupted inner Replace can be retried by the pipeline, observed as a lost race, and re-applied. See `docs/PATCH_HANDLER_SPEC.md`. ([#5130](https://github.com/Azure/azure-sdk-for-rust/pull/5130))
- Resource-reference accessors now return `Option` to account for RID-addressed references that have no name. `DatabaseReference::name_based_path`, `ContainerReference::database_name`, and `ContainerReference::name_based_path` return `None` when the reference is addressed by RID (previously they returned `&str`/`String` and assumed a name was always present). Use the new `ContainerReference::base_path` to obtain the addressing-appropriate path (RID-based or name-based) when building request URLs. ([#4640](https://github.com/Azure/azure-sdk-for-rust/pull/4640))
- `AccountReference`, `DatabaseReference`, `ContainerReference`, and `ItemReference` are now tuple structs wrapping private shared state, so wildcard struct patterns such as `AccountReference { .. }` no longer compile. All accessors are unchanged. `DatabaseReference::into_account` was removed; use `DatabaseReference::account` and clone. ([#4908](https://github.com/Azure/azure-sdk-for-rust/pull/4908))
- `CosmosDriver::plan_operation` now takes an additional `plan_options: &PlanOptions` argument (after `continuation`). The continuation token remains its own argument. ([#4855](https://github.com/Azure/azure-sdk-for-rust/pull/4855))
Expand Down
1 change: 1 addition & 0 deletions sdk/cosmos/azure_data_cosmos_driver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ rustls = ["reqwest", "reqwest/rustls", "__tls"]
native_tls = ["reqwest", "reqwest/native-tls", "__tls"]
fault_injection = []
preview_dtx = [] # Enables preview Distributed Transaction APIs. Disabled by default and not production-ready.
preview_patch = [] # Enables the preview PATCH API. Disabled by default and not production-ready: an interrupted patch may re-apply non-idempotent operations. See docs/PATCH_HANDLER_SPEC.md.
# `__internal_in_memory_emulator` exposes the in-memory query evaluator
# (`crate::query::eval`, `crate::query::value`) used by the in-memory Cosmos DB
# emulator. The evaluator intentionally trades full Cosmos parity for emulator
Expand Down
27 changes: 20 additions & 7 deletions sdk/cosmos/azure_data_cosmos_driver/docs/PATCH_HANDLER_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
This document describes the contract for `OperationType::Patch` in
`azure_data_cosmos_driver`.

## Status: preview, not production-ready

PATCH is gated behind the **`preview_patch`** Cargo feature in both
`azure_data_cosmos_driver` and `azure_data_cosmos`. It is off by default and
is not part of either crate's supported surface.

The gate exists because the handler does not deliver exactly-once semantics
under transport failures: an interrupted patch may re-apply non-idempotent
operations (`Increment`, `Add` on an array, `Move`). See
[Invariants](#invariants) for the exact interleaving. The feature will stay
gated until that hole is closed.

## Overview

`Patch` is a *virtual* operation type: the Cosmos DB REST endpoint does not
Expand Down Expand Up @@ -250,10 +262,11 @@ as a JSON number without precision loss.
already replicated the original commit returns 412, which the RMW loop
treats as a normal race-lost and recovers by re-Reading and re-applying.
Non-idempotent ops (`Increment`, `Add` on an array, `Move`) may therefore
be applied **more than once** under this scenario. Lifting this caveat
requires marking the internal Replace as non-idempotent for retry
purposes (e.g. a per-op idempotency override on `CosmosOperation`); that
is tracked as a follow-up because it interacts with PPAF write-retry
semantics. Callers needing exactly-once should either use idempotent ops
(`Set` on a caller-computed value) or detect duplicate-application via a
monotonic application-level sequence number.
be applied **more than once** under this scenario. This is the reason the
whole feature is gated behind `preview_patch`. Closing the hole requires
the RMW loop to be able to *recognize its own committed write* rather than
mistaking it for a concurrent writer — i.e. stamping each attempt with a
marker the loop can look for on the verification read. Until then, callers
needing exactly-once should either use idempotent ops (`Set` on a
caller-computed value) or detect duplicate-application via a monotonic
application-level sequence number.
Original file line number Diff line number Diff line change
Expand Up @@ -2382,6 +2382,7 @@ impl DiagnosticsContext {
/// `replace_item` for a PATCH's final Replace) instead of the virtual
/// operation's own name (`patch_item`). Consumes `self` before it is shared
/// via `Arc`, preserving the type's immutability contract.
#[cfg_attr(not(feature = "preview_patch"), allow(dead_code))]
pub(crate) fn with_operation_name(mut self, operation_name: Option<Arc<str>>) -> Self {
self.requests = Self::preserve_request_operation_names(
&self.requests,
Expand All @@ -2407,6 +2408,7 @@ impl DiagnosticsContext {
/// context that was itself an aggregate). When the name is unchanged, or
/// there is no displaced name to record, the existing `Arc` is shared
/// rather than the request list being cloned.
#[cfg_attr(not(feature = "preview_patch"), allow(dead_code))]
fn preserve_request_operation_names(
requests: &Arc<Vec<RequestDiagnostics>>,
previous: Option<&Arc<str>>,
Expand Down Expand Up @@ -2444,6 +2446,7 @@ impl DiagnosticsContext {
/// need to re-stamp the identity without taking ownership. The JSON caches
/// are intentionally not carried over: they may already have been rendered
/// with the old name.
#[cfg_attr(not(feature = "preview_patch"), allow(dead_code))]
pub(crate) fn clone_with_operation_name(&self, operation_name: Option<Arc<str>>) -> Self {
DiagnosticsContext {
activity_id: self.activity_id.clone(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2586,6 +2586,7 @@ impl CosmosDriver {
// Read-Modify-Write handler, which issues its own Read/Replace
// operations through this same entry point. `Box::pin` gives the
// recursive future a fixed size.
#[cfg(feature = "preview_patch")]
if operation.operation_type() == crate::models::OperationType::Patch {
let max_attempts = operation.patch_max_attempts();
return Box::pin(async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,20 @@
//! and pipeline stages are pure functions over those components.

pub(crate) mod components;
// Exists only to synthesize the PATCH handler's response from a local body.
#[cfg(feature = "preview_patch")]
pub(crate) mod from_local_body;
pub(crate) mod hedge_budget;
pub(crate) mod hedging_diagnostics;
pub(crate) mod hedging_eligibility;
pub(crate) mod operation_pipeline;
// Shared by the preview PATCH handler and the in-memory emulator's DTX patch
// handling, which is the only DTX consumer of local patch evaluation.
#[cfg(any(
feature = "preview_patch",
all(feature = "preview_dtx", feature = "__internal_in_memory_emulator")
))]
pub(crate) mod patch_eval;
#[cfg(feature = "preview_patch")]
pub(crate) mod patch_handler;
pub(crate) mod retry_evaluation;
1 change: 1 addition & 0 deletions sdk/cosmos/azure_data_cosmos_driver/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ impl CosmosError {
/// `None` for `Synthetic` errors. Used by internal pipeline code
/// that needs to inspect the wire body / headers regardless of
/// whether diagnostics finalization has happened yet.
#[cfg_attr(not(feature = "preview_patch"), allow(dead_code))]
pub(crate) fn wire_payload(&self) -> Option<&CosmosResponsePayload> {
match &self.inner.context {
ErrorContext::WirePending { payload } => Some(payload),
Expand Down
Loading
Loading