Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 6 additions & 1 deletion sdk/cosmos/azure_data_cosmos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@

### Features Added

- `CosmosClient::database_client` and `DatabaseClient::container_client` now accept anything convertible into the new `ResourceIdentity` type, so databases and containers can be addressed either by name (a `&str`/`String`, as before) or by resource ID (RID) via the new `ResourceId` newtype. Addressing modes cannot be mixed across the hierarchy: a RID-addressed database yields only RID-addressed containers, and a name-addressed database yields only name-addressed containers; mixing them fails fast with a client-side error. `DatabaseClient` gains `id()` (returning the `ResourceIdentity`), `name()`, and `rid()` accessors, and skips the extra database read when resolving throughput for a RID-addressed database. ([#4613](https://github.com/Azure/azure-sdk-for-rust/pull/4613))

### Breaking Changes

- `CosmosClient::database_client` and `DatabaseClient::container_client` now take `impl Into<ResourceIdentity>` instead of `&str`. Passing a string literal, `&str`, `String`, or `&String` is unchanged (it still selects name addressing), but call sites that previously relied on deref coercion to `&str` — e.g. passing a `&Cow<str>`, `&Box<str>`, or another `Deref<Target = str>` smart string — no longer compile against the generic bound and must dereference explicitly (`&*value` or `value.as_ref()`). This is a compile-time-only source change with no runtime behavior difference. ([#4613](https://github.com/Azure/azure-sdk-for-rust/pull/4613))

### Bugs Fixed

- RID-addressed data-plane requests are now authenticated correctly. The driver signs RID-addressed databases, containers, and their feeds/children over the lowercased resource RID (matching the service's `is_name_based = false` rule) and sends the RID raw in the request URL instead of percent-encoding it. Previously the driver always signed the full name-style resource link and percent-encoded the RID, which made the gateway reject RID reads (e.g. `401` for a database or container read by RID). Reading a database or container by RID, listing/querying containers under a RID database, and querying items, reading throughput, and creating items on a RID-addressed container now work end-to-end. ([#4613](https://github.com/Azure/azure-sdk-for-rust/pull/4613))

### Other Changes

## 0.36.0 (2026-06-19)
Expand Down Expand Up @@ -49,7 +55,6 @@
- The `allow_invalid_certificates` Cargo feature has been removed. The capability is now in the default feature set but requires explicit opt-in via `CosmosRuntimeBuilder::with_connection_pool(ConnectionPoolOptionsBuilder::new().with_server_certificate_validation(ServerCertificateValidation::RequiredUnlessEmulator).build())`. The new `RequiredUnlessEmulator` policy is not a blanket "disable validation" knob — it validates the server certificate normally and only relaxes validation for detected Cosmos DB emulator hosts (via `AccountEndpoint` + `Region` heuristics, or the `AZURE_COSMOS_EMULATOR_HOST` environment variable). See the driver CHANGELOG for the underlying `EmulatorServerCertValidation` → `ServerCertificateValidation` rename.
- Per-account driver caching has been removed from the underlying runtime — each `CosmosClient::build(...)` now constructs a fresh `CosmosDriver`. Clients sharing the same `CosmosRuntime` continue to share transport pools, sampler, account cache, etc.; only the per-account `CosmosDriver` instance is no longer reused. ([#4588](https://github.com/Azure/azure-sdk-for-rust/pull/4588))


### Bugs Fixed

- `403/1008 (DatabaseAccountNotFound)` and `403/3 (WriteForbidden)` now trigger an account-topology refresh and retry against the refreshed endpoints instead of bubbling up. ([#4590](https://github.com/Azure/azure-sdk-for-rust/pull/4590))
Expand Down
79 changes: 65 additions & 14 deletions sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
Precondition, QueryOptions, ReadContainerOptions, ReadFeedRangesOptions,
ReplaceContainerOptions, SessionToken, ThroughputOptions,
},
PartitionKey, Query,
PartitionKey, Query, ResourceIdentity,
};

use super::ThroughputPoller;
Expand All @@ -33,21 +33,72 @@ pub struct ContainerClient {
impl ContainerClient {
pub(crate) async fn new(
context: ClientContext,
container_id: &str,
database_id: &str,
database: &ResourceIdentity,
container: ResourceIdentity,
) -> crate::Result<Self> {
// Eagerly resolve immutable container metadata from the driver.
let container_ref = context
.driver
.resolve_container(database_id, container_id)
.await
.map_err(|e| {
azure_data_cosmos_driver::error::CosmosErrorBuilder::from_error(e)
.with_context(format!(
"failed to resolve container metadata for '{database_id}/{container_id}'"
))
// The container's addressing mode must match the database's: name-with-name
// or RID-with-RID. Mixing the two is not supported by the service routing.
let container_ref = match (database, &container) {
(ResourceIdentity::Name(db_name), ResourceIdentity::Name(container_name)) => context
.driver
.resolve_container(db_name, container_name)
.await
.map_err(|e| {
azure_data_cosmos_driver::error::CosmosErrorBuilder::from_error(e)
.with_context(format!(
"failed to resolve container metadata for '{db_name}/{container_name}'"
))
.build()
})?,
(ResourceIdentity::Rid(db_rid), ResourceIdentity::Rid(container_rid)) => {
let resolved = context
.driver
.resolve_container_by_rid(container_rid.as_str())
.await
.map_err(|e| {
azure_data_cosmos_driver::error::CosmosErrorBuilder::from_error(e)
.with_context(format!(
"failed to resolve container metadata for RID '{}'",
container_rid.as_str()
))
.build()
})?;

// The parent database RID is derived from the container RID, not
// taken from this `DatabaseClient`. Reject a container whose parent
// database does not match the addressed database so callers can't
// accidentally reach into a different database.
if resolved.database_rid() != db_rid.as_str() {
return Err(azure_data_cosmos_driver::error::CosmosError::builder()
.with_status(
azure_data_cosmos_driver::error::CosmosStatus::CLIENT_INVALID_RESOURCE_ID,
)
.with_message(format!(
"container RID '{}' belongs to database '{}', not the addressed database '{}'",
container_rid.as_str(),
resolved.database_rid(),
db_rid.as_str()
))
.build()
.into());
}

resolved
}
(ResourceIdentity::Name(_), ResourceIdentity::Rid(_))
| (ResourceIdentity::Rid(_), ResourceIdentity::Name(_)) => {
return Err(azure_data_cosmos_driver::error::CosmosError::builder()
.with_status(
azure_data_cosmos_driver::error::CosmosStatus::CLIENT_MIXED_NAME_RID_ADDRESSING,
)
.with_message(
"database and container must use the same addressing mode: \
address both by name or both by RID",
)
.build()
})?;
.into());
}
};

Ok(Self {
container_ref,
Expand Down
27 changes: 22 additions & 5 deletions sdk/cosmos/azure_data_cosmos/src/clients/cosmos_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
models::DatabaseProperties,
models::ResourceResponse,
options::{CreateDatabaseOptions, QueryDatabasesOptions},
Query,
Query, ResourceIdentity,
};
use azure_core::http::Url;
use azure_data_cosmos_driver::models::CosmosOperation;
Expand Down Expand Up @@ -95,12 +95,29 @@ impl CosmosClient {
CosmosClientBuilder::new()
}

/// Gets a [`DatabaseClient`] that can be used to access the database with the specified ID.
/// Gets a [`DatabaseClient`] that can be used to access the database with the
/// specified identity.
///
/// The database may be addressed either by name or by [`ResourceId`](crate::ResourceId)
/// (RID). Anything that converts into a [`ResourceIdentity`](crate::ResourceIdentity)
/// is accepted — a `&str`/`String` selects name addressing, a `ResourceId`
/// selects RID addressing.
///
/// # Arguments
/// * `id` - The ID of the database.
pub fn database_client(&self, id: &str) -> DatabaseClient {
DatabaseClient::new(self.context.clone(), id)
/// * `database` - The name or RID of the database.
///
/// # Examples
///
/// ```rust,no_run
/// # use azure_data_cosmos::{CosmosClient, ResourceId};
/// # let client: CosmosClient = panic!("this is a non-running example");
/// // By name:
/// let db = client.database_client("my-database");
/// // By RID:
/// let db = client.database_client(ResourceId::from("abc123=="));
/// ```
pub fn database_client(&self, database: impl Into<ResourceIdentity>) -> DatabaseClient {
DatabaseClient::new(self.context.clone(), database.into())
}

/// Gets the endpoint of the database account this client is connected to.
Expand Down
77 changes: 55 additions & 22 deletions sdk/cosmos/azure_data_cosmos/src/clients/database_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
CreateContainerOptions, DeleteDatabaseOptions, QueryContainersOptions, ReadDatabaseOptions,
ThroughputOptions,
},
Query,
Query, ResourceId, ResourceIdentity,
};
use azure_data_cosmos_driver::models::{CosmosOperation, DatabaseReference};

Expand All @@ -20,43 +20,68 @@ use super::ThroughputPoller;
///
/// You can get a `DatabaseClient` by calling [`CosmosClient::database_client()`](crate::CosmosClient::database_client()).
pub struct DatabaseClient {
database_id: String,
identity: ResourceIdentity,
context: ClientContext,
database_ref: DatabaseReference,
}

impl DatabaseClient {
pub(crate) fn new(context: ClientContext, database_id: &str) -> Self {
let database_id = database_id.to_string();
let database_ref =
DatabaseReference::from_name(context.driver.account().clone(), database_id.clone());
pub(crate) fn new(context: ClientContext, identity: ResourceIdentity) -> Self {
let account = context.driver.account().clone();
let database_ref = match &identity {
ResourceIdentity::Name(name) => {
DatabaseReference::from_name(account, name.clone().into_owned())
}
ResourceIdentity::Rid(rid) => {
DatabaseReference::from_rid(account, rid.as_str().to_owned())
}
};

Self {
database_id,
identity,
context,
database_ref,
}
}

/// Gets a [`ContainerClient`] that can be used to access the collection with the specified name.
/// Gets a [`ContainerClient`] that can be used to access the container with the
/// specified identity.
///
/// This method eagerly resolves immutable container metadata (resource ID and partition key
/// definition) from the service, so the returned client is ready for immediate use without
/// per-operation cache lookups.
///
/// The container's addressing mode must match this database's: a name-addressed
/// database accepts only name-addressed containers, and a RID-addressed database
/// accepts only [`ResourceId`](crate::ResourceId)-addressed containers.
///
/// # Arguments
/// * `name` - The name of the container.
/// * `container` - The name or RID of the container.
///
/// # Errors
///
/// Returns an error if the container does not exist or the metadata cannot be resolved.
pub async fn container_client(&self, name: &str) -> crate::Result<ContainerClient> {
ContainerClient::new(self.context.clone(), name, &self.database_id).await
/// Returns an error if the container does not exist, the metadata cannot be
/// resolved, or the addressing mode does not match this database's.
pub async fn container_client(
&self,
container: impl Into<ResourceIdentity>,
) -> crate::Result<ContainerClient> {
ContainerClient::new(self.context.clone(), &self.identity, container.into()).await
}

/// Returns the identity (name or RID) used to construct this client.
pub fn id(&self) -> &ResourceIdentity {
&self.identity
}

/// Returns the database name, or `None` if this client was addressed by RID.
pub fn name(&self) -> Option<&str> {
self.identity.as_name()
}

/// Returns the identifier of the Cosmos database.
pub fn id(&self) -> &str {
&self.database_id
/// Returns the database RID, or `None` if this client was addressed by name.
pub fn rid(&self) -> Option<&ResourceId> {
self.identity.as_rid()
}

/// Reads the properties of the database.
Expand Down Expand Up @@ -208,6 +233,17 @@ impl DatabaseClient {
))
}

/// Returns the database RID, using the client's identity directly when it is
/// already RID-addressed, or reading the database from the service to obtain
/// the `_rid` when addressed by name.
async fn resource_id(&self) -> crate::Result<String> {
if let Some(rid) = self.rid() {
return Ok(rid.as_str().to_owned());
}
let db = self.read(None).await?.into_model()?;
resource_id_or_error(db.system_properties.resource_id, "database")
}

/// Reads database throughput properties, if any.
///
/// This will return `None` if the database does not have a throughput offer configured.
Expand All @@ -219,9 +255,7 @@ impl DatabaseClient {
options: Option<ThroughputOptions>,
) -> crate::Result<Option<ThroughputProperties>> {
let options = options.unwrap_or_default();
// We need to get the RID for the database.
let db = self.read(None).await?.into_model()?;
let resource_id = resource_id_or_error(db.system_properties.resource_id, "database")?;
let resource_id = self.resource_id().await?;

offers_client::find_offer(
&self.context.driver,
Expand Down Expand Up @@ -263,9 +297,7 @@ impl DatabaseClient {
options: Option<ThroughputOptions>,
) -> crate::Result<ThroughputPoller> {
let options = options.unwrap_or_default();
// We need to get the RID for the database.
let db = self.read(None).await?.into_model()?;
let resource_id = resource_id_or_error(db.system_properties.resource_id, "database")?;
let resource_id = self.resource_id().await?;

offers_client::begin_replace(
self.context.driver.clone(),
Expand Down Expand Up @@ -312,7 +344,8 @@ mod tests {
fn _assert_futures_are_send() {
fn assert_send<T: Send>(_: T) {}
let client: &DatabaseClient = todo!();
assert_send(client.container_client(todo!()));
let container_identity: ResourceIdentity = todo!();
assert_send(client.container_client(container_identity));
assert_send(client.read(todo!()));
assert_send(client.query_containers(Query::from("SELECT * FROM c"), todo!()));
Comment on lines 345 to 350

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be a valid cast, it'll just fail at runtime (and this is just testing compile-time conditions), but we should use something explicit rather than as ResourceIdentity.

assert_send(client.create_container(todo!(), todo!()));
Expand Down
2 changes: 2 additions & 0 deletions sdk/cosmos/azure_data_cosmos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub use error::{CosmosError, CosmosStatus, Result, SubStatusCode};
pub use feed::{FeedScope, Query};
pub use models::{PartitionKey, TransactionalBatch};
pub use options::RoutingStrategy;
pub use resource_identity::{ResourceId, ResourceIdentity};
pub use runtime::{CosmosRuntime, CosmosRuntimeBuilder};

// =========================================================================
Expand All @@ -42,6 +43,7 @@ mod constants;
mod credential;
mod driver_bridge;
mod region_proximity;
mod resource_identity;
mod runtime;
mod session_helpers;

Expand Down
Loading
Loading