diff --git a/Cargo.toml b/Cargo.toml index 694c89ae73..6517dbda94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ base64 = "0.22" bytes = "1.0" cargo_metadata = "0.18.1" clap = { version = "4.4.16", features = ["derive"] } +cfg-if = "1.0.0" dyn-clone = "1.0" fe2o3-amqp = { version = "0.12", features = ["native-tls", "tracing", "uuid"] } fe2o3-amqp-ext = { version = "0.12", features = [] } diff --git a/sdk/cosmos/azure_data_cosmos/Cargo.toml b/sdk/cosmos/azure_data_cosmos/Cargo.toml index 7c6db29ba3..d0eb7a1fd4 100644 --- a/sdk/cosmos/azure_data_cosmos/Cargo.toml +++ b/sdk/cosmos/azure_data_cosmos/Cargo.toml @@ -13,9 +13,8 @@ documentation = "https://docs.rs/azure_data_cosmos" keywords = ["sdk", "azure", "rest", "cloud", "cosmos", "database"] categories = ["api-bindings"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +cfg-if.workspace = true async-trait.workspace = true azure_core.workspace = true typespec_client_core = { workspace = true, features = ["derive"] } @@ -44,6 +43,7 @@ default = ["hmac_rust"] hmac_rust = ["azure_core/hmac_rust"] hmac_openssl = ["azure_core/hmac_openssl"] key_auth = [] # Enables support for key-based authentication (Primary Keys and Resource Tokens) +control_plane = ["key_auth"] # Control-plane operations require key-based authentication. [package.metadata.docs.rs] -features = ["key_auth"] +features = ["control_plane", "key_auth"] diff --git a/sdk/cosmos/azure_data_cosmos/docs/control-plane-warning.md b/sdk/cosmos/azure_data_cosmos/docs/control-plane-warning.md new file mode 100644 index 0000000000..7a5562dd68 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos/docs/control-plane-warning.md @@ -0,0 +1,5 @@ +
+ +This is a control-plane API and requires that you authenticate using a key. To use Entra ID to perform this operation, you must use the [Azure Resource Manager APIs](https://learn.microsoft.com/en-us/azure/templates/microsoft.documentdb/databaseaccounts). + +
diff --git a/sdk/cosmos/azure_data_cosmos/examples/cosmos/create.rs b/sdk/cosmos/azure_data_cosmos/examples/cosmos/create.rs index 9a5a3d3ae5..f480267c7b 100644 --- a/sdk/cosmos/azure_data_cosmos/examples/cosmos/create.rs +++ b/sdk/cosmos/azure_data_cosmos/examples/cosmos/create.rs @@ -4,42 +4,129 @@ use azure_data_cosmos::{ clients::{ContainerClientMethods, DatabaseClientMethods}, CosmosClient, CosmosClientMethods, PartitionKey, }; -use clap::Args; +use clap::{Args, Subcommand}; -/// Creates a new item. +#[cfg(feature = "control_plane")] +use azure_data_cosmos::models::{ContainerProperties, PartitionKeyDefinition}; + +/// Creates a new item, database, or container. #[derive(Clone, Args)] pub struct CreateCommand { - /// The database in which to create the item. - database: String, + #[command(subcommand)] + subcommand: Subcommands, +} + +#[derive(Clone, Subcommand)] +pub enum Subcommands { + /// Create an item in a container. + Item { + /// The database in which to create the item. + database: String, + + /// The container in which to create the item. + container: String, + + /// The partition key of the new item. + #[clap(long, short)] + partition_key: String, + + /// The JSON of the new item. + #[clap(long, short)] + json: String, + }, - /// The container in which to create the item. - container: String, + /// Create a database (does not support Entra ID). + #[cfg(feature = "control_plane")] + Database { + /// The ID of the new database to create. + id: String, + }, - /// The partition key of the new item. - #[clap(long, short)] - partition_key: String, + /// Create a container (does not support Entra ID). + #[cfg(feature = "control_plane")] + Container { + /// The ID of the database to create the container in. + database: String, - /// The JSON of the new item. - #[clap(long, short)] - json: String, + /// The ID of the new container to create. + #[clap(long, short)] + id: Option, + + /// The path to the partition key properties (supports up to 3). + #[clap(long, short)] + partition_key: Vec, + + /// The JSON for a ContainerProperties value. The 'id' and 'partition key' options are ignored if this is set. + #[clap(long)] + json: Option, + }, } impl CreateCommand { pub async fn run(self, client: CosmosClient) -> Result<(), Box> { - let db_client = client.database_client(&self.database); - let container_client = db_client.container_client(&self.container); - - let pk = PartitionKey::from(&self.partition_key); - let item: serde_json::Value = serde_json::from_str(&self.json)?; - - let created = container_client - .create_item(pk, item, None) - .await? - .deserialize_body() - .await? - .unwrap(); - println!("Created item:"); - println!("{:#?}", created); - Ok(()) + match self.subcommand { + Subcommands::Item { + database, + container, + partition_key, + json, + } => { + let db_client = client.database_client(database); + let container_client = db_client.container_client(container); + + let pk = PartitionKey::from(&partition_key); + let item: serde_json::Value = serde_json::from_str(&json)?; + + let created = container_client + .create_item(pk, item, None) + .await? + .deserialize_body() + .await? + .unwrap(); + println!("Created item:"); + println!("{:#?}", created); + Ok(()) + } + + #[cfg(feature = "control_plane")] + Subcommands::Database { id } => { + let db = client + .create_database(id, None) + .await? + .deserialize_body() + .await? + .unwrap(); + println!("Created database:"); + println!("{:#?}", db); + Ok(()) + } + + #[cfg(feature = "control_plane")] + Subcommands::Container { + database, + id, + partition_key, + json, + } => { + let properties = match json { + Some(j) => serde_json::from_str(&j).unwrap(), + None => ContainerProperties { + id: id.expect("the ID is required when not using '--json'"), + partition_key: PartitionKeyDefinition::new(partition_key), + ..Default::default() + }, + }; + let container = client + .database_client(database) + .create_container(properties, None) + .await? + .deserialize_body() + .await? + .unwrap(); + println!("Created container:"); + println!("{:#?}", container); + Ok(()) + } + } } } diff --git a/sdk/cosmos/azure_data_cosmos/examples/cosmos/delete.rs b/sdk/cosmos/azure_data_cosmos/examples/cosmos/delete.rs index ccc6d22047..28695e7561 100644 --- a/sdk/cosmos/azure_data_cosmos/examples/cosmos/delete.rs +++ b/sdk/cosmos/azure_data_cosmos/examples/cosmos/delete.rs @@ -5,39 +5,91 @@ use azure_data_cosmos::{ clients::{ContainerClientMethods, DatabaseClientMethods}, CosmosClient, CosmosClientMethods, }; -use clap::Args; +use clap::{Args, Subcommand}; -/// Deletes an item. +/// Deletes an item, database, or container. #[derive(Clone, Args)] pub struct DeleteCommand { - /// The database containing the item. - database: String, + #[command(subcommand)] + subcommand: Subcommands, +} + +#[derive(Clone, Subcommand)] +pub enum Subcommands { + /// Delete an item in a container. + Item { + /// The database containing the item. + database: String, + + /// The container containing the item. + container: String, + + /// The ID of the item. + #[clap(long, short)] + item_id: String, - /// The container containing the item. - container: String, + /// The partition key of the item. + #[clap(long, short)] + partition_key: String, + }, - /// The ID of the item. - #[clap(long, short)] - item_id: String, + /// Create a database (does not support Entra ID). + #[cfg(feature = "control_plane")] + Database { + /// The ID of the database to delete. + id: String, + }, - /// The partition key of the item. - #[clap(long, short)] - partition_key: String, + /// Create a container (does not support Entra ID). + #[cfg(feature = "control_plane")] + Container { + /// The ID of the database the container is in. + database: String, + + /// The ID of the container to delete + id: String, + }, } impl DeleteCommand { pub async fn run(self, client: CosmosClient) -> Result<(), Box> { - let db_client = client.database_client(&self.database); - let container_client = db_client.container_client(&self.container); - - let response = container_client - .delete_item(&self.partition_key, &self.item_id, None) - .await; - match response { - Err(e) if e.http_status() == Some(StatusCode::NotFound) => println!("Item not found!"), - Ok(_) => println!("Item deleted"), - Err(e) => return Err(e.into()), - }; - Ok(()) + match self.subcommand { + Subcommands::Item { + database, + container, + item_id, + partition_key, + } => { + let db_client = client.database_client(database); + let container_client = db_client.container_client(container); + + let response = container_client + .delete_item(partition_key, item_id, None) + .await; + match response { + Err(e) if e.http_status() == Some(StatusCode::NotFound) => { + println!("Item not found!") + } + Ok(_) => println!("Item deleted"), + Err(e) => return Err(e.into()), + }; + Ok(()) + } + + #[cfg(feature = "control_plane")] + Subcommands::Database { id } => { + let db_client = client.database_client(id); + db_client.delete(None).await?; + Ok(()) + } + + #[cfg(feature = "control_plane")] + Subcommands::Container { database, id } => { + let db_client = client.database_client(database); + let container_client = db_client.container_client(id); + container_client.delete(None).await?; + Ok(()) + } + } } } diff --git a/sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs b/sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs index 00820e4286..fce90a7876 100644 --- a/sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs +++ b/sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs @@ -10,8 +10,12 @@ use crate::{ ItemOptions, PartitionKey, Query, QueryPartitionStrategy, }; -use azure_core::{Context, Pager, Request, Response}; +#[cfg(feature = "control_plane")] +use crate::DeleteContainerOptions; + +use azure_core::{Context, Method, Pager, Request, Response}; use serde::{de::DeserializeOwned, Serialize}; +use typespec_client_core::http::PagerResult; use url::Url; #[cfg(doc)] @@ -46,8 +50,16 @@ pub trait ContainerClientMethods { options: Option, ) -> azure_core::Result>; - /// Returns the identifier of the Cosmos container. - fn id(&self) -> &str; + /// Deletes this container. + /// + #[doc = include_str!("../../docs/control-plane-warning.md")] + /// + /// # Arguments + /// * `options` - Optional parameters for the request. + #[allow(async_fn_in_trait)] // REASON: See https://github.com/Azure/azure-sdk-for-rust/issues/1796 for detailed justification + #[cfg(feature = "control_plane")] + async fn delete(&self, options: Option) + -> azure_core::Result; /// Creates a new item in the container. /// @@ -314,18 +326,15 @@ pub trait ContainerClientMethods { /// /// You can get a `Container` by calling [`DatabaseClient::container_client()`](crate::clients::DatabaseClient::container_client()). pub struct ContainerClient { - container_id: String, container_url: Url, pipeline: CosmosPipeline, } impl ContainerClient { - pub(crate) fn new(pipeline: CosmosPipeline, database_url: &Url, container_id: &str) -> Self { - let container_id = container_id.to_string(); - let container_url = database_url.with_path_segments(["colls", &container_id]); + pub(crate) fn new(pipeline: CosmosPipeline, database_url: &Url, container_name: &str) -> Self { + let container_url = database_url.with_path_segments(["colls", container_name]); Self { - container_id, container_url, pipeline, } @@ -340,14 +349,23 @@ impl ContainerClientMethods for ContainerClient { // REASON: This is a documented public API so prefixing with '_' is undesirable. options: Option, ) -> azure_core::Result> { - let mut req = Request::new(self.container_url.clone(), azure_core::Method::Get); + let mut req = Request::new(self.container_url.clone(), Method::Get); self.pipeline .send(Context::new(), &mut req, ResourceType::Containers) .await } - fn id(&self) -> &str { - &self.container_id + #[cfg(feature = "control_plane")] + async fn delete( + &self, + #[allow(unused_variables)] + // REASON: This is a documented public API so prefixing with '_' is undesirable. + options: Option, + ) -> azure_core::Result { + let mut req = Request::new(self.container_url.clone(), Method::Delete); + self.pipeline + .send(Context::new(), &mut req, ResourceType::Containers) + .await } async fn create_item( @@ -360,7 +378,7 @@ impl ContainerClientMethods for ContainerClient { options: Option, ) -> azure_core::Result>> { let url = self.container_url.with_path_segments(["docs"]); - let mut req = Request::new(url, azure_core::Method::Post); + let mut req = Request::new(url, Method::Post); req.insert_headers(&partition_key.into())?; req.set_json(&item)?; self.pipeline @@ -381,7 +399,7 @@ impl ContainerClientMethods for ContainerClient { let url = self .container_url .with_path_segments(["docs", item_id.as_ref()]); - let mut req = Request::new(url, azure_core::Method::Put); + let mut req = Request::new(url, Method::Put); req.insert_headers(&partition_key.into())?; req.set_json(&item)?; self.pipeline @@ -399,7 +417,7 @@ impl ContainerClientMethods for ContainerClient { options: Option, ) -> azure_core::Result>> { let url = self.container_url.with_path_segments(["docs"]); - let mut req = Request::new(url, azure_core::Method::Post); + let mut req = Request::new(url, Method::Post); req.insert_header(constants::IS_UPSERT, "true"); req.insert_headers(&partition_key.into())?; req.set_json(&item)?; @@ -420,7 +438,7 @@ impl ContainerClientMethods for ContainerClient { let url = self .container_url .with_path_segments(["docs", item_id.as_ref()]); - let mut req = Request::new(url, azure_core::Method::Get); + let mut req = Request::new(url, Method::Get); req.insert_headers(&partition_key.into())?; self.pipeline .send(Context::new(), &mut req, ResourceType::Items) @@ -439,7 +457,7 @@ impl ContainerClientMethods for ContainerClient { let url = self .container_url .with_path_segments(["docs", item_id.as_ref()]); - let mut req = Request::new(url, azure_core::Method::Delete); + let mut req = Request::new(url, Method::Delete); req.insert_headers(&partition_key.into())?; self.pipeline .send(Context::new(), &mut req, ResourceType::Items) @@ -457,11 +475,38 @@ impl ContainerClientMethods for ContainerClient { ) -> azure_core::Result>> { let mut url = self.container_url.clone(); url.append_path_segments(["docs"]); - let mut base_request = Request::new(url, azure_core::Method::Post); + let mut base_req = Request::new(url, Method::Post); + + base_req.insert_header(constants::QUERY, "True"); + base_req.add_mandatory_header(&constants::QUERY_CONTENT_TYPE); + let QueryPartitionStrategy::SinglePartition(partition_key) = partition_key.into(); - base_request.insert_headers(&partition_key)?; + base_req.insert_headers(&partition_key)?; - self.pipeline - .send_query_request(query.into(), base_request, ResourceType::Items) + base_req.set_json(&query.into())?; + + // We have to double-clone here. + // First we clone the pipeline to pass it in to the closure + let pipeline = self.pipeline.clone(); + Ok(Pager::from_callback(move |continuation| { + // Then we have to clone it again to pass it in to the async block. + // This is because Pageable can't borrow any data, it has to own it all. + // That's probably good, because it means a Pageable can outlive the client that produced it, but it requires some extra cloning. + let pipeline = pipeline.clone(); + let mut req = base_req.clone(); + async move { + if let Some(continuation) = continuation { + req.insert_header(constants::CONTINUATION, continuation); + } + + let response = pipeline + .send(Context::new(), &mut req, ResourceType::Items) + .await?; + Ok(PagerResult::from_response_header( + response, + &constants::CONTINUATION, + )) + } + })) } } diff --git a/sdk/cosmos/azure_data_cosmos/src/clients/cosmos_client.rs b/sdk/cosmos/azure_data_cosmos/src/clients/cosmos_client.rs index 178379abab..a2c437209e 100644 --- a/sdk/cosmos/azure_data_cosmos/src/clients/cosmos_client.rs +++ b/sdk/cosmos/azure_data_cosmos/src/clients/cosmos_client.rs @@ -1,15 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::clients::DatabaseClient; -use crate::models::DatabaseQueryResults; -use crate::pipeline::{AuthorizationPolicy, CosmosPipeline, ResourceType}; -use crate::utils::AppendPathSegments; -use crate::{CosmosClientOptions, Query, QueryDatabasesOptions}; -use azure_core::credentials::TokenCredential; -use azure_core::{Request, Url}; +use crate::{ + clients::DatabaseClient, + models::DatabaseQueryResults, + pipeline::{AuthorizationPolicy, CosmosPipeline, ResourceType}, + utils::AppendPathSegments, + CosmosClientOptions, Query, QueryDatabasesOptions, +}; +use azure_core::{credentials::TokenCredential, Request, Url}; use std::sync::Arc; +#[cfg(feature = "control_plane")] +use crate::{ + models::{DatabaseProperties, Item}, + CreateDatabaseOptions, +}; +#[cfg(feature = "control_plane")] +use azure_core::{Context, Method, Response}; +#[cfg(feature = "control_plane")] +use serde::Serialize; + #[cfg(feature = "key_auth")] use azure_core::credentials::Secret; @@ -65,6 +76,21 @@ pub trait CosmosClientMethods { query: impl Into, options: Option, ) -> azure_core::Result>; + + /// Creates a new database. + /// + #[doc = include_str!("../../docs/control-plane-warning.md")] + /// + /// # Arguments + /// * `id` - The ID of the new database. + /// * `options` - Optional parameters for the request. + #[allow(async_fn_in_trait)] // REASON: See https://github.com/Azure/azure-sdk-for-rust/issues/1796 for detailed justification + #[cfg(feature = "control_plane")] + async fn create_database( + &self, + id: String, + options: Option, + ) -> azure_core::Result>>; } impl CosmosClient { @@ -156,11 +182,33 @@ impl CosmosClientMethods for CosmosClient { // REASON: This is a documented public API so prefixing with '_' is undesirable. options: Option, ) -> azure_core::Result> { - let mut url = self.endpoint.clone(); - url.append_path_segments(["dbs"]); + let url = self.endpoint.with_path_segments(["dbs"]); let base_request = Request::new(url, azure_core::Method::Post); self.pipeline .send_query_request(query.into(), base_request, ResourceType::Databases) } + + #[cfg(feature = "control_plane")] + async fn create_database( + &self, + id: String, + + #[allow(unused_variables)] + // REASON: This is a documented public API so prefixing with '_' is undesirable. + options: Option, + ) -> azure_core::Result>> { + #[derive(Serialize)] + struct RequestBody { + id: String, + } + + let url = self.endpoint.with_path_segments(["dbs"]); + let mut req = Request::new(url, Method::Post); + req.set_json(&RequestBody { id })?; + + self.pipeline + .send(Context::new(), &mut req, ResourceType::Databases) + .await + } } diff --git a/sdk/cosmos/azure_data_cosmos/src/clients/database_client.rs b/sdk/cosmos/azure_data_cosmos/src/clients/database_client.rs index c81735b6a2..af90844be1 100644 --- a/sdk/cosmos/azure_data_cosmos/src/clients/database_client.rs +++ b/sdk/cosmos/azure_data_cosmos/src/clients/database_client.rs @@ -1,14 +1,25 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::models::{ContainerQueryResults, DatabaseProperties}; -use crate::options::ReadDatabaseOptions; -use crate::pipeline::ResourceType; -use crate::utils::AppendPathSegments; -use crate::{clients::ContainerClient, pipeline::CosmosPipeline}; -use crate::{Query, QueryContainersOptions}; +use crate::{ + clients::ContainerClient, + models::{ContainerQueryResults, DatabaseProperties}, + pipeline::{CosmosPipeline, ResourceType}, + utils::AppendPathSegments, + Query, QueryContainersOptions, ReadDatabaseOptions, +}; + +#[cfg(feature = "control_plane")] +use crate::{ + models::{ContainerProperties, Item}, + CreateContainerOptions, DeleteDatabaseOptions, +}; use azure_core::{Context, Pager, Request, Response}; + +#[cfg(feature = "control_plane")] +use azure_core::Method; + use url::Url; #[cfg(doc)] @@ -80,6 +91,31 @@ pub trait DatabaseClientMethods { query: impl Into, options: Option, ) -> azure_core::Result>; + + /// Creates a new container. + /// + #[doc = include_str!("../../docs/control-plane-warning.md")] + /// + /// # Arguments + /// * `properties` - A [`ContainerProperties`] describing the new container. + /// * `options` - Optional parameters for the request. + #[allow(async_fn_in_trait)] // REASON: See https://github.com/Azure/azure-sdk-for-rust/issues/1796 for detailed justification + #[cfg(feature = "control_plane")] + async fn create_container( + &self, + properties: ContainerProperties, + options: Option, + ) -> azure_core::Result>>; + + /// Deletes this database. + /// + #[doc = include_str!("../../docs/control-plane-warning.md")] + /// + /// # Arguments + /// * `options` - Optional parameters for the request. + #[allow(async_fn_in_trait)] // REASON: See https://github.com/Azure/azure-sdk-for-rust/issues/1796 for detailed justification + #[cfg(feature = "control_plane")] + async fn delete(&self, options: Option) -> azure_core::Result; } /// A client for working with a specific database in a Cosmos DB account. @@ -141,4 +177,35 @@ impl DatabaseClientMethods for DatabaseClient { self.pipeline .send_query_request(query.into(), base_request, ResourceType::Containers) } + + #[cfg(feature = "control_plane")] + async fn create_container( + &self, + properties: ContainerProperties, + + #[allow(unused_variables)] + // REASON: This is a documented public API so prefixing with '_' is undesirable. + options: Option, + ) -> azure_core::Result>> { + let url = self.database_url.with_path_segments(["colls"]); + let mut req = Request::new(url, Method::Post); + req.set_json(&properties)?; + + self.pipeline + .send(Context::new(), &mut req, ResourceType::Containers) + .await + } + + #[cfg(feature = "control_plane")] + async fn delete( + &self, + #[allow(unused_variables)] + // REASON: This is a documented public API so prefixing with '_' is undesirable. + options: Option, + ) -> azure_core::Result { + let mut req = Request::new(self.database_url.clone(), Method::Delete); + self.pipeline + .send(Context::new(), &mut req, ResourceType::Databases) + .await + } } diff --git a/sdk/cosmos/azure_data_cosmos/src/constants.rs b/sdk/cosmos/azure_data_cosmos/src/constants.rs index e42f88926e..bf3e184775 100644 --- a/sdk/cosmos/azure_data_cosmos/src/constants.rs +++ b/sdk/cosmos/azure_data_cosmos/src/constants.rs @@ -4,6 +4,8 @@ // Don't spell-check header names (which should start with 'x-'). // cSpell:ignoreRegExp /x-[^\s]+/ +//! Constants defining HTTP headers and other values relevant to Azure Cosmos DB APIs. + use azure_core::{headers::HeaderName, request_options::ContentType}; pub const QUERY: HeaderName = HeaderName::from_static("x-ms-documentdb-query"); diff --git a/sdk/cosmos/azure_data_cosmos/src/lib.rs b/sdk/cosmos/azure_data_cosmos/src/lib.rs index 945847fabe..bda7be485b 100644 --- a/sdk/cosmos/azure_data_cosmos/src/lib.rs +++ b/sdk/cosmos/azure_data_cosmos/src/lib.rs @@ -20,7 +20,9 @@ pub(crate) mod utils; pub mod models; +#[doc(inline)] pub use clients::{CosmosClient, CosmosClientMethods}; + pub use options::*; pub use partition_key::*; pub use query::*; diff --git a/sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs b/sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs index f94e0a9d00..225e320dbe 100644 --- a/sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs +++ b/sdk/cosmos/azure_data_cosmos/src/models/container_properties.rs @@ -1,9 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + use std::time::Duration; use azure_core::Model; -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::models::SystemProperties; +use crate::models::{IndexingPolicy, PartitionKeyDefinition, SystemProperties}; #[cfg(doc)] use crate::clients::ContainerClientMethods; @@ -15,53 +18,89 @@ where Ok(Option::::deserialize(deserializer)?.map(Duration::from_secs)) } +fn serialize_ttl(duration: &Option, serializer: S) -> Result +where + S: Serializer, +{ + match duration { + Some(d) => serializer.serialize_some(&d.as_secs()), + None => serializer.serialize_none(), + } +} + /// Properties of a Cosmos DB container. /// -/// Returned by [`ContainerClient::read()`](crate::clients::ContainerClient::read()). -#[non_exhaustive] -#[derive(Model, Clone, Debug, Deserialize, PartialEq, Eq)] +/// # Constructing +/// +/// When constructing this type, you should **always** use [Struct Update] syntax using `..Default::default()`, for example: +/// +/// ```rust +/// # use azure_data_cosmos::models::ContainerProperties; +/// let properties = ContainerProperties { +/// id: "NewContainer".to_string(), +/// partition_key: "/partitionKey".into(), +/// ..Default::default() +/// }; +/// ``` +/// +/// Using this syntax has two purposes: +/// +/// 1. It allows you to construct the type even though [`SystemProperties`] is not constructable (these properties should always be empty when you send a request). +/// 2. It protects you if we add additional properties to this struct. +/// +/// Also, note that the `id` and `partition_key` values are **required** by the server. You will get an error from the server if you omit them. +/// +/// [Struct Update]: https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html?highlight=Struct#creating-instances-from-other-instances-with-struct-update-syntax +#[derive(Model, Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ContainerProperties { /// The ID of the container. pub id: String, - /// The time-to-live for items in the container. - /// - /// For more information see - #[serde(default)] - #[serde(deserialize_with = "deserialize_ttl")] - pub default_ttl: Option, - - /// The time-to-live for the analytical store in the container. - /// - /// For more information see - #[serde(default)] - #[serde(deserialize_with = "deserialize_ttl")] - pub analytical_storage_ttl: Option, - /// The definition of the partition key for the container. pub partition_key: PartitionKeyDefinition, /// The indexing policy for the container. + #[serde(skip_serializing_if = "Option::is_none")] pub indexing_policy: Option, /// The unique key policy for the container. + #[serde(skip_serializing_if = "Option::is_none")] pub unique_key_policy: Option, /// The conflict resolution policy for the container. + #[serde(skip_serializing_if = "Option::is_none")] pub conflict_resolution_policy: Option, /// The vector embedding policy for the container. + #[serde(skip_serializing_if = "Option::is_none")] pub vector_embedding_policy: Option, + /// The time-to-live for items in the container. + /// + /// For more information see + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(deserialize_with = "deserialize_ttl")] + #[serde(serialize_with = "serialize_ttl")] + pub default_ttl: Option, + + /// The time-to-live for the analytical store in the container. + /// + /// For more information see + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(deserialize_with = "deserialize_ttl")] + #[serde(serialize_with = "serialize_ttl")] + pub analytical_storage_ttl: Option, + /// A [`SystemProperties`] object containing common system properties for the container. #[serde(flatten)] pub system_properties: SystemProperties, } /// Represents the vector embedding policy for a container. -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct VectorEmbeddingPolicy { /// The [`VectorEmbedding`]s that describe the vector embeddings of items in the container. @@ -70,8 +109,7 @@ pub struct VectorEmbeddingPolicy { } /// Represents the vector embedding policy for a container. -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct VectorEmbedding { /// The path to the property containing the vector. @@ -88,8 +126,7 @@ pub struct VectorEmbedding { } /// Defines the data types of the elements of a vector. -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum VectorDataType { /// Represents the `float16` data type. @@ -106,8 +143,7 @@ pub enum VectorDataType { } /// Defines the distance functions that can be used to calculate the distance between vectors. -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum VectorDistanceFunction { /// Represents the `euclidian` distance function. @@ -121,136 +157,10 @@ pub enum VectorDistanceFunction { DotProduct, } -/// Represents the partition key definition for a container. -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct PartitionKeyDefinition { - /// The list of partition keys paths. - pub paths: Vec, - - /// The version of the partition key hash in use. - #[serde(default)] - pub version: i32, -} - -/// Represents the indexing policy for a container. -/// -/// For more information see -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct IndexingPolicy { - /// Indicates that the indexing policy is automatic. - #[serde(default)] - pub automatic: bool, - - /// The indexing mode in use. - #[serde(default)] - pub indexing_mode: IndexingMode, - - /// The paths to be indexed. - #[serde(default)] - pub included_paths: Vec, - - /// The paths to be excluded. - #[serde(default)] - pub excluded_paths: Vec, - - /// A list of spatial indexes in the container. - #[serde(default)] - pub spatial_indexes: Vec, - - /// A list of composite indexes in the container - #[serde(default)] - pub composite_indexes: Vec, - - /// A list of vector indexes in the container - #[serde(default)] - pub vector_indexes: Vec, -} - -/// Defines the indexing modes supported by Azure Cosmos DB. -#[non_exhaustive] -#[derive(Clone, Default, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum IndexingMode { - Consistent, - - #[default] - None, -} - -/// Represents a JSON path. -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct PropertyPath { - // The path to the property referenced in this index. - pub path: String, -} - -/// Represents a spatial index -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct SpatialIndex { - /// The path to the property referenced in this index. - pub path: String, - - /// The spatial types used in this index - pub types: Vec, -} - -/// Defines the types of spatial data that can be indexed. -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "PascalCase")] -pub enum SpatialType { - Point, - Polygon, - LineString, - MultiPolygon, -} - -/// Represents a composite index -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -#[serde(transparent)] -pub struct CompositeIndex { - /// The properties in this composite index - pub properties: Vec, -} - -/// Describes a single property in a composite index. -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct CompositeIndexProperty { - /// The path to the property referenced in this index. - pub path: String, - - /// The order of the composite index. - /// - /// For example, if you want to run the query "SELECT * FROM c ORDER BY c.age asc, c.height desc", - /// then you'd specify the order for "/asc" to be *ascending* and the order for "/height" to be *descending*. - pub order: CompositeIndexOrder, -} - -/// Ordering values available for composite indexes. -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum CompositeIndexOrder { - Ascending, - Descending, -} - /// Represents a unique key policy for a container. /// /// For more information see -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct UniqueKeyPolicy { /// The keys defined in this policy. @@ -258,8 +168,7 @@ pub struct UniqueKeyPolicy { } /// Represents a single unique key for a container. -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct UniqueKey { /// The set of paths which must be unique for each item. @@ -269,8 +178,7 @@ pub struct UniqueKey { /// Represents a conflict resolution policy for a container /// /// For more information, see -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ConflictResolutionPolicy { /// The conflict resolution mode. @@ -286,8 +194,7 @@ pub struct ConflictResolutionPolicy { } /// Defines conflict resolution types available in Azure Cosmos DB -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "PascalCase")] pub enum ConflictResolutionMode { /// Conflict resolution will be performed by using the highest value of the property specified by [`ConflictResolutionPolicy::resolution_path`]. @@ -297,172 +204,74 @@ pub enum ConflictResolutionMode { Custom, } -/// Represents a vector index -/// -/// For more information, see -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct VectorIndex { - /// The path to the property referenced in this index. - pub path: String, +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use std::time::Duration; + + use crate::models::ContainerProperties; + + #[cfg(test)] + #[derive(Debug, Deserialize, Serialize)] + struct DurationHolder { + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(deserialize_with = "super::deserialize_ttl")] + #[serde(serialize_with = "super::serialize_ttl")] + pub duration: Option, + } - /// The type of the vector index. - #[serde(rename = "type")] // "type" is a reserved word in Rust. - pub index_type: VectorIndexType, -} + #[test] + pub fn serialize_ttl() { + let value = DurationHolder { + duration: Some(Duration::from_secs(4200)), + }; + let json = serde_json::to_string(&value).unwrap(); + assert_eq!(r#"{"duration":4200}"#, json); + } -/// Types of vector indexes supported by Cosmos DB -#[non_exhaustive] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum VectorIndexType { - Flat, - QuantizedFlat, - DiskANN, -} + #[test] + pub fn serialize_missing_ttl() { + let value = DurationHolder { duration: None }; + let json = serde_json::to_string(&value).unwrap(); + assert_eq!(r#"{}"#, json); + } -#[cfg(test)] -mod tests { - use crate::models::{ - CompositeIndex, CompositeIndexOrder, CompositeIndexProperty, IndexingMode, PropertyPath, - SpatialIndex, SpatialType, VectorIndex, VectorIndexType, - }; + #[test] + pub fn deserialize_ttl() { + let value: DurationHolder = serde_json::from_str(r#"{"duration":4200}"#).unwrap(); + assert_eq!(Some(Duration::from_secs(4200)), value.duration); + } - use super::IndexingPolicy; + #[test] + pub fn deserialize_missing_ttl() { + let value: DurationHolder = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(None, value.duration); + } + + #[test] + pub fn deserialize_null_ttl() { + let value: DurationHolder = serde_json::from_str(r#"{"duration":null}"#).unwrap(); + assert_eq!(None, value.duration); + } #[test] - pub fn deserialize_indexing_policy() { - // A fairly complete deserialization test that covers most of the indexing policies described in our docs. - let policy = r#" - { - "indexingMode": "consistent", - "includedPaths": [ - { - "path": "/*" - } - ], - "excludedPaths": [ - { - "path": "/path/to/single/excluded/property/?" - }, - { - "path": "/path/to/root/of/multiple/excluded/properties/*" - } - ], - "spatialIndexes": [ - { - "path": "/path/to/geojson/property/?", - "types": [ - "Point", - "Polygon", - "MultiPolygon", - "LineString" - ] - } - ], - "vectorIndexes": [ - { - "path": "/vector1", - "type": "quantizedFlat" - }, - { - "path": "/vector2", - "type": "diskANN" - } - ], - "compositeIndexes":[ - [ - { - "path":"/name", - "order":"ascending" - }, - { - "path":"/age", - "order":"descending" - } - ], - [ - { - "path":"/name2", - "order":"descending" - }, - { - "path":"/age2", - "order":"ascending" - } - ] - ], - "extraValueNotCurrentlyPresentInModel": { - "this": "should not fail" - } - } - "#; - - let policy: IndexingPolicy = serde_json::from_str(policy).unwrap(); + pub fn container_properties_default_serialization() { + // This test asserts that the default value serializes the same way across SDK versions. + // When new properties are added to ContainerProperties, this test should not break. + // If it does, users who are using `..Default::default()` syntax will start sending an unexpected payload to the server. + // In rare cases, it's reasonable to update this test, if the new generated JSON is considered _equivalent_ to the original by the server. + // But in general, a failure in this test means that the same user code will send an unexpected value in a new version of the SDK. + let properties = ContainerProperties { + id: "MyContainer".to_string(), + partition_key: "/partitionKey".into(), + ..Default::default() + }; + let json = serde_json::to_string(&properties).unwrap(); assert_eq!( - IndexingPolicy { - automatic: false, - indexing_mode: IndexingMode::Consistent, - included_paths: vec![PropertyPath { - path: "/*".to_string(), - }], - excluded_paths: vec![ - PropertyPath { - path: "/path/to/single/excluded/property/?".to_string() - }, - PropertyPath { - path: "/path/to/root/of/multiple/excluded/properties/*".to_string() - }, - ], - spatial_indexes: vec![SpatialIndex { - path: "/path/to/geojson/property/?".to_string(), - types: vec![ - SpatialType::Point, - SpatialType::Polygon, - SpatialType::MultiPolygon, - SpatialType::LineString, - ] - }], - composite_indexes: vec![ - CompositeIndex { - properties: vec![ - CompositeIndexProperty { - path: "/name".to_string(), - order: CompositeIndexOrder::Ascending, - }, - CompositeIndexProperty { - path: "/age".to_string(), - order: CompositeIndexOrder::Descending, - }, - ] - }, - CompositeIndex { - properties: vec![ - CompositeIndexProperty { - path: "/name2".to_string(), - order: CompositeIndexOrder::Descending, - }, - CompositeIndexProperty { - path: "/age2".to_string(), - order: CompositeIndexOrder::Ascending, - }, - ] - }, - ], - vector_indexes: vec![ - VectorIndex { - path: "/vector1".to_string(), - index_type: VectorIndexType::QuantizedFlat, - }, - VectorIndex { - path: "/vector2".to_string(), - index_type: VectorIndexType::DiskANN, - } - ] - }, - policy + "{\"id\":\"MyContainer\",\"partitionKey\":{\"paths\":[\"/partitionKey\"],\"kind\":\"Hash\",\"version\":2}}", + json ); } } diff --git a/sdk/cosmos/azure_data_cosmos/src/models/indexing_policy.rs b/sdk/cosmos/azure_data_cosmos/src/models/indexing_policy.rs new file mode 100644 index 0000000000..dcab0ea197 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos/src/models/indexing_policy.rs @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use serde::{Deserialize, Serialize}; + +/// Represents the indexing policy for a container. +/// +/// For more information see +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct IndexingPolicy { + /// Indicates that the indexing policy is automatic. + #[serde(default)] + pub automatic: bool, + + /// The indexing mode in use. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub indexing_mode: Option, + + /// The paths to be indexed. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub included_paths: Vec, + + /// The paths to be excluded. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub excluded_paths: Vec, + + /// A list of spatial indexes in the container. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub spatial_indexes: Vec, + + /// A list of composite indexes in the container + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub composite_indexes: Vec, + + /// A list of vector indexes in the container + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub vector_indexes: Vec, +} + +/// Defines the indexing modes supported by Azure Cosmos DB. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum IndexingMode { + Consistent, + None, +} + +/// Represents a JSON path. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PropertyPath { + // The path to the property referenced in this index. + pub path: String, +} + +/// Represents a spatial index +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct SpatialIndex { + /// The path to the property referenced in this index. + pub path: String, + + /// The spatial types used in this index + pub types: Vec, +} + +/// Defines the types of spatial data that can be indexed. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "PascalCase")] +pub enum SpatialType { + Point, + Polygon, + LineString, + MultiPolygon, +} + +/// Represents a composite index +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(transparent)] +pub struct CompositeIndex { + /// The properties in this composite index + pub properties: Vec, +} + +/// Describes a single property in a composite index. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct CompositeIndexProperty { + /// The path to the property referenced in this index. + pub path: String, + + /// The order of the composite index. + /// + /// For example, if you want to run the query "SELECT * FROM c ORDER BY c.age asc, c.height desc", + /// then you'd specify the order for "/asc" to be *ascending* and the order for "/height" to be *descending*. + pub order: CompositeIndexOrder, +} + +/// Ordering values available for composite indexes. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum CompositeIndexOrder { + Ascending, + Descending, +} + +/// Represents a vector index +/// +/// For more information, see +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct VectorIndex { + /// The path to the property referenced in this index. + pub path: String, + + /// The type of the vector index. + #[serde(rename = "type")] // "type" is a reserved word in Rust. + pub index_type: VectorIndexType, +} + +/// Types of vector indexes supported by Cosmos DB +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum VectorIndexType { + /// Represents the `flat` vector index type. + Flat, + + /// Represents the `quantizedFlat` vector index type. + QuantizedFlat, + + /// Represents the `diskANN` vector index type. + DiskANN, +} + +#[cfg(test)] +mod tests { + use crate::models::{ + CompositeIndex, CompositeIndexOrder, CompositeIndexProperty, IndexingMode, IndexingPolicy, + PropertyPath, SpatialIndex, SpatialType, VectorIndex, VectorIndexType, + }; + + #[test] + pub fn deserialize_indexing_policy() { + // A fairly complete deserialization test that covers most of the indexing policies described in our docs. + let policy = r#" + { + "indexingMode": "consistent", + "includedPaths": [ + { + "path": "/*" + } + ], + "excludedPaths": [ + { + "path": "/path/to/single/excluded/property/?" + }, + { + "path": "/path/to/root/of/multiple/excluded/properties/*" + } + ], + "spatialIndexes": [ + { + "path": "/path/to/geojson/property/?", + "types": [ + "Point", + "Polygon", + "MultiPolygon", + "LineString" + ] + } + ], + "vectorIndexes": [ + { + "path": "/vector1", + "type": "quantizedFlat" + }, + { + "path": "/vector2", + "type": "diskANN" + } + ], + "compositeIndexes":[ + [ + { + "path":"/name", + "order":"ascending" + }, + { + "path":"/age", + "order":"descending" + } + ], + [ + { + "path":"/name2", + "order":"descending" + }, + { + "path":"/age2", + "order":"ascending" + } + ] + ], + "extraValueNotCurrentlyPresentInModel": { + "this": "should not fail" + } + } + "#; + + let policy: IndexingPolicy = serde_json::from_str(policy).unwrap(); + + assert_eq!( + IndexingPolicy { + automatic: false, + indexing_mode: Some(IndexingMode::Consistent), + included_paths: vec![PropertyPath { + path: "/*".to_string(), + }], + excluded_paths: vec![ + PropertyPath { + path: "/path/to/single/excluded/property/?".to_string() + }, + PropertyPath { + path: "/path/to/root/of/multiple/excluded/properties/*".to_string() + }, + ], + spatial_indexes: vec![SpatialIndex { + path: "/path/to/geojson/property/?".to_string(), + types: vec![ + SpatialType::Point, + SpatialType::Polygon, + SpatialType::MultiPolygon, + SpatialType::LineString, + ] + }], + composite_indexes: vec![ + CompositeIndex { + properties: vec![ + CompositeIndexProperty { + path: "/name".to_string(), + order: CompositeIndexOrder::Ascending, + }, + CompositeIndexProperty { + path: "/age".to_string(), + order: CompositeIndexOrder::Descending, + }, + ] + }, + CompositeIndex { + properties: vec![ + CompositeIndexProperty { + path: "/name2".to_string(), + order: CompositeIndexOrder::Descending, + }, + CompositeIndexProperty { + path: "/age2".to_string(), + order: CompositeIndexOrder::Ascending, + }, + ] + }, + ], + vector_indexes: vec![ + VectorIndex { + path: "/vector1".to_string(), + index_type: VectorIndexType::QuantizedFlat, + }, + VectorIndex { + path: "/vector2".to_string(), + index_type: VectorIndexType::DiskANN, + } + ] + }, + policy + ); + } + + #[test] + pub fn serialize_indexing_policy() { + let policy = IndexingPolicy { + automatic: true, + indexing_mode: None, + included_paths: vec![PropertyPath { + path: "/*".to_string(), + }], + excluded_paths: vec![ + PropertyPath { + path: "/path/to/single/excluded/property/?".to_string(), + }, + PropertyPath { + path: "/path/to/root/of/multiple/excluded/properties/*".to_string(), + }, + ], + spatial_indexes: vec![ + SpatialIndex { + path: "/path/to/geojson/property/?".to_string(), + types: vec![ + SpatialType::Point, + SpatialType::Polygon, + SpatialType::MultiPolygon, + SpatialType::LineString, + ], + }, + SpatialIndex { + path: "/path/to/geojson/property2/?".to_string(), + types: vec![], + }, + ], + composite_indexes: vec![ + CompositeIndex { + properties: vec![ + CompositeIndexProperty { + path: "/name".to_string(), + order: CompositeIndexOrder::Ascending, + }, + CompositeIndexProperty { + path: "/age".to_string(), + order: CompositeIndexOrder::Descending, + }, + ], + }, + CompositeIndex { properties: vec![] }, + ], + vector_indexes: vec![ + VectorIndex { + path: "/vector1".to_string(), + index_type: VectorIndexType::QuantizedFlat, + }, + VectorIndex { + path: "/vector2".to_string(), + index_type: VectorIndexType::DiskANN, + }, + ], + }; + let json = serde_json::to_string(&policy).unwrap(); + + assert_eq!( + "{\"automatic\":true,\"includedPaths\":[{\"path\":\"/*\"}],\"excludedPaths\":[{\"path\":\"/path/to/single/excluded/property/?\"},{\"path\":\"/path/to/root/of/multiple/excluded/properties/*\"}],\"spatialIndexes\":[{\"path\":\"/path/to/geojson/property/?\",\"types\":[\"Point\",\"Polygon\",\"MultiPolygon\",\"LineString\"]},{\"path\":\"/path/to/geojson/property2/?\",\"types\":[]}],\"compositeIndexes\":[[{\"path\":\"/name\",\"order\":\"ascending\"},{\"path\":\"/age\",\"order\":\"descending\"}],[]],\"vectorIndexes\":[{\"path\":\"/vector1\",\"type\":\"quantizedFlat\"},{\"path\":\"/vector2\",\"type\":\"diskANN\"}]}", + json + ); + } +} diff --git a/sdk/cosmos/azure_data_cosmos/src/models/mod.rs b/sdk/cosmos/azure_data_cosmos/src/models/mod.rs index 75c9699612..ebe4553e02 100644 --- a/sdk/cosmos/azure_data_cosmos/src/models/mod.rs +++ b/sdk/cosmos/azure_data_cosmos/src/models/mod.rs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -//! Model types sent to and received from the Cosmos DB API. +//! Model types sent to and received from the Azure Cosmos DB API. use azure_core::{date::OffsetDateTime, Model}; -use serde::{de::DeserializeOwned, Deserialize, Deserializer}; +use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize}; #[cfg(doc)] use crate::{ @@ -13,10 +13,14 @@ use crate::{ }; mod container_properties; +mod indexing_policy; mod item; +mod partition_key_definition; pub use container_properties::*; +pub use indexing_policy::*; pub use item::*; +pub use partition_key_definition::*; fn deserialize_cosmos_timestamp<'de, D>(deserializer: D) -> Result, D::Error> where @@ -71,22 +75,30 @@ pub struct ContainerQueryResults { /// Common system properties returned for most Cosmos DB resources. #[non_exhaustive] -#[derive(Clone, Default, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct SystemProperties { /// The entity tag associated with the resource. + #[serde(default)] + #[serde(skip_serializing)] #[serde(rename = "_etag")] pub etag: Option, /// The self-link associated with the resource. + #[serde(default)] + #[serde(skip_serializing)] #[serde(rename = "_self")] pub self_link: Option, /// The system-generated unique identifier associated with the resource. + #[serde(default)] + #[serde(skip_serializing)] #[serde(rename = "_rid")] pub resource_id: Option, /// A [`OffsetDateTime`] representing the last modified time of the resource. + #[serde(default)] #[serde(rename = "_ts")] + #[serde(skip_serializing)] #[serde(deserialize_with = "deserialize_cosmos_timestamp")] pub last_modified: Option, } @@ -104,3 +116,40 @@ pub struct DatabaseProperties { #[serde(flatten)] pub system_properties: SystemProperties, } + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use time::{Date, Month, OffsetDateTime, Time}; + + #[cfg(test)] + #[derive(Debug, Deserialize, Serialize)] + struct TimestampHolder { + #[serde(default)] + #[serde(deserialize_with = "super::deserialize_cosmos_timestamp")] + pub ts: Option, + } + + #[test] + pub fn deserialize_timestamp() { + let value: TimestampHolder = serde_json::from_str(r#"{"ts":1729036800}"#).unwrap(); + let expected = OffsetDateTime::new_utc( + Date::from_calendar_date(2024, Month::October, 16).unwrap(), // Can't be a const because Result::unwrap isn't const. + Time::MIDNIGHT, + ); + + assert_eq!(Some(expected), value.ts); + } + + #[test] + pub fn deserialize_missing_timestamp() { + let value: TimestampHolder = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(None, value.ts); + } + + #[test] + pub fn deserialize_null_timestamp() { + let value: TimestampHolder = serde_json::from_str(r#"{"ts":null}"#).unwrap(); + assert_eq!(None, value.ts); + } +} diff --git a/sdk/cosmos/azure_data_cosmos/src/models/partition_key_definition.rs b/sdk/cosmos/azure_data_cosmos/src/models/partition_key_definition.rs new file mode 100644 index 0000000000..fd8c0de2bf --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos/src/models/partition_key_definition.rs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use serde::{Deserialize, Serialize}; + +/// Represents the partition key definition for a container. +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PartitionKeyDefinition { + /// The list of partition keys paths. + pub paths: Vec, + + /// The partition key kind. + pub kind: PartitionKeyKind, + + /// The version of the partition key hash in use. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +impl PartitionKeyDefinition { + /// Creates a new [`PartitionKeyDefinition`] from the provided list of partition key paths. + /// + /// The [`PartitionKeyDefinition::kind`] will be set automatically, depending on how many paths are provided. + pub fn new(paths: impl Into>) -> Self { + let paths = paths.into(); + let kind = if paths.len() > 1 { + PartitionKeyKind::MultiHash + } else { + PartitionKeyKind::Hash + }; + PartitionKeyDefinition { + paths, + kind, + version: Some(2), + } + } +} + +impl From<&str> for PartitionKeyDefinition { + fn from(value: &str) -> Self { + PartitionKeyDefinition::new(vec![value.into()]) + } +} + +impl From for PartitionKeyDefinition { + fn from(value: String) -> Self { + PartitionKeyDefinition::new(vec![value]) + } +} + +impl, S2: Into> From<(S1, S2)> for PartitionKeyDefinition { + fn from(value: (S1, S2)) -> Self { + PartitionKeyDefinition::new(vec![value.0.into(), value.1.into()]) + } +} + +impl, S2: Into, S3: Into> From<(S1, S2, S3)> + for PartitionKeyDefinition +{ + fn from(value: (S1, S2, S3)) -> Self { + PartitionKeyDefinition::new(vec![value.0.into(), value.1.into(), value.2.into()]) + } +} + +/// Represents the kind of a partition key. +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "PascalCase")] +pub enum PartitionKeyKind { + /// The container is partitioned by hashing the value of a single partition key. + #[default] + Hash, + + /// The container is partitioned by hashing multiple, hierarchical, partition keys. + MultiHash, +} + +#[cfg(test)] +mod tests { + use crate::models::{PartitionKeyDefinition, PartitionKeyKind}; + + #[test] + pub fn from_single() { + assert_eq!( + PartitionKeyDefinition { + paths: vec!["/a".to_string()], + kind: PartitionKeyKind::Hash, + version: Some(2), + }, + "/a".into() + ); + assert_eq!( + PartitionKeyDefinition { + paths: vec!["/a".to_string()], + kind: PartitionKeyKind::Hash, + version: Some(2), + }, + "/a".to_string().into() + ); + } + + #[test] + pub fn from_pair() { + assert_eq!( + PartitionKeyDefinition { + paths: vec!["/a".to_string(), "/b".to_string()], + kind: PartitionKeyKind::MultiHash, + version: Some(2), + }, + ("/a", "/b").into() + ); + } + + #[test] + pub fn from_triple() { + assert_eq!( + PartitionKeyDefinition { + paths: vec!["/a".to_string(), "/b".to_string(), "/c".to_string()], + kind: PartitionKeyKind::MultiHash, + version: Some(2), + }, + ("/a", "/b", "/c").into() + ); + } +} diff --git a/sdk/cosmos/azure_data_cosmos/src/options/create_container_options.rs b/sdk/cosmos/azure_data_cosmos/src/options/create_container_options.rs new file mode 100644 index 0000000000..9a7c0fa668 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos/src/options/create_container_options.rs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#[cfg(doc)] +use crate::clients::DatabaseClientMethods; + +/// Options to be passed to [`DatabaseClient::create_container()`](crate::clients::DatabaseClient::create_container()). +#[derive(Clone, Debug, Default)] +pub struct CreateContainerOptions {} + +impl CreateContainerOptions { + /// Creates a new [`CreateContainerOptionsBuilder`](CreateContainerOptionsBuilder) that can be used to construct a [`CreateContainerOptions`]. + /// + /// # Examples + /// + /// ```rust + /// let options = azure_data_cosmos::CreateContainerOptions::builder().build(); + /// ``` + pub fn builder() -> CreateContainerOptionsBuilder { + CreateContainerOptionsBuilder::default() + } +} + +/// Builder used to construct a [`CreateContainerOptions`]. +/// +/// Obtain a [`CreateContainerOptionsBuilder`] by calling [`CreateContainerOptions::builder()`] +#[derive(Default)] +pub struct CreateContainerOptionsBuilder(CreateContainerOptions); + +impl CreateContainerOptionsBuilder { + /// Builds a [`CreateContainerOptions`] from the builder. + /// + /// This does not consume the builder, and can be called multiple times. + pub fn build(&self) -> CreateContainerOptions { + self.0.clone() + } +} diff --git a/sdk/cosmos/azure_data_cosmos/src/options/create_database_options.rs b/sdk/cosmos/azure_data_cosmos/src/options/create_database_options.rs new file mode 100644 index 0000000000..ecc8ae68b8 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos/src/options/create_database_options.rs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#[cfg(doc)] +use crate::CosmosClientMethods; + +/// Options to be passed to [`CosmosClient::create_database()`](crate::CosmosClient::create_database()). +#[derive(Clone, Debug, Default)] +pub struct CreateDatabaseOptions {} + +impl CreateDatabaseOptions { + /// Creates a new [`CreateDatabaseOptionsBuilder`](CreateDatabaseOptionsBuilder) that can be used to construct a [`CreateDatabaseOptions`]. + /// + /// # Examples + /// + /// ```rust + /// let options = azure_data_cosmos::CreateDatabaseOptions::builder().build(); + /// ``` + pub fn builder() -> CreateDatabaseOptionsBuilder { + CreateDatabaseOptionsBuilder::default() + } +} + +/// Builder used to construct a [`CreateDatabaseOptions`]. +/// +/// Obtain a [`CreateDatabaseOptionsBuilder`] by calling [`CreateDatabaseOptions::builder()`] +#[derive(Default)] +pub struct CreateDatabaseOptionsBuilder(CreateDatabaseOptions); + +impl CreateDatabaseOptionsBuilder { + /// Builds a [`CreateDatabaseOptions`] from the builder. + /// + /// This does not consume the builder, and can be called multiple times. + pub fn build(&self) -> CreateDatabaseOptions { + self.0.clone() + } +} diff --git a/sdk/cosmos/azure_data_cosmos/src/options/delete_container_options.rs b/sdk/cosmos/azure_data_cosmos/src/options/delete_container_options.rs new file mode 100644 index 0000000000..62ab8c0562 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos/src/options/delete_container_options.rs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#[cfg(doc)] +use crate::clients::ContainerClientMethods; + +/// Options to be passed to [`ContainerClient::delete()`](crate::clients::ContainerClient::delete()). +#[derive(Clone, Debug, Default)] +pub struct DeleteContainerOptions {} + +impl DeleteContainerOptions { + /// Creates a new [`DeleteContainerOptionsBuilder`](DeleteContainerOptionsBuilder) that can be used to construct a [`DeleteContainerOptions`]. + /// + /// # Examples + /// + /// ```rust + /// let options = azure_data_cosmos::DeleteContainerOptions::builder().build(); + /// ``` + pub fn builder() -> DeleteContainerOptionsBuilder { + DeleteContainerOptionsBuilder::default() + } +} + +/// Builder used to construct a [`DeleteContainerOptions`]. +/// +/// Obtain a [`DeleteContainerOptionsBuilder`] by calling [`DeleteContainerOptions::builder()`] +#[derive(Default)] +pub struct DeleteContainerOptionsBuilder(DeleteContainerOptions); + +impl DeleteContainerOptionsBuilder { + /// Builds a [`DeleteContainerOptions`] from the builder. + /// + /// This does not consume the builder, and can be called multiple times. + pub fn build(&self) -> DeleteContainerOptions { + self.0.clone() + } +} diff --git a/sdk/cosmos/azure_data_cosmos/src/options/delete_database_options.rs b/sdk/cosmos/azure_data_cosmos/src/options/delete_database_options.rs new file mode 100644 index 0000000000..01b18e5062 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos/src/options/delete_database_options.rs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#[cfg(doc)] +use crate::clients::DatabaseClientMethods; + +/// Options to be passed to [`DatabaseClient::delete()`](crate::clients::DatabaseClient::delete()). +#[derive(Clone, Debug, Default)] +pub struct DeleteDatabaseOptions {} + +impl DeleteDatabaseOptions { + /// Creates a new [`DeleteDatabaseOptionsBuilder`](DeleteDatabaseOptionsBuilder) that can be used to construct a [`DeleteDatabaseOptions`]. + /// + /// # Examples + /// + /// ```rust + /// let options = azure_data_cosmos::DeleteDatabaseOptions::builder().build(); + /// ``` + pub fn builder() -> DeleteDatabaseOptionsBuilder { + DeleteDatabaseOptionsBuilder::default() + } +} + +/// Builder used to construct a [`DeleteDatabaseOptions`]. +/// +/// Obtain a [`DeleteDatabaseOptionsBuilder`] by calling [`DeleteDatabaseOptions::builder()`] +#[derive(Default)] +pub struct DeleteDatabaseOptionsBuilder(DeleteDatabaseOptions); + +impl DeleteDatabaseOptionsBuilder { + /// Builds a [`DeleteDatabaseOptions`] from the builder. + /// + /// This does not consume the builder, and can be called multiple times. + pub fn build(&self) -> DeleteDatabaseOptions { + self.0.clone() + } +} diff --git a/sdk/cosmos/azure_data_cosmos/src/options/item_options.rs b/sdk/cosmos/azure_data_cosmos/src/options/item_options.rs index e9059cfc19..415c8675c5 100644 --- a/sdk/cosmos/azure_data_cosmos/src/options/item_options.rs +++ b/sdk/cosmos/azure_data_cosmos/src/options/item_options.rs @@ -4,7 +4,7 @@ #[cfg(doc)] use crate::clients::ContainerClientMethods; -/// Options to be passed to [`ContainerClient::create_item()`](crate::clients::ContainerClient::create_item()). +/// Options to be passed to APIs that manipulate items. #[derive(Clone, Debug, Default)] pub struct ItemOptions {} diff --git a/sdk/cosmos/azure_data_cosmos/src/options/mod.rs b/sdk/cosmos/azure_data_cosmos/src/options/mod.rs index eff0b6f433..62674696d9 100644 --- a/sdk/cosmos/azure_data_cosmos/src/options/mod.rs +++ b/sdk/cosmos/azure_data_cosmos/src/options/mod.rs @@ -9,6 +9,19 @@ mod query_options; mod read_container_options; mod read_database_options; +cfg_if::cfg_if! { + if #[cfg(feature = "control_plane")] { + mod create_container_options; + mod create_database_options; + mod delete_container_options; + mod delete_database_options; + pub use create_container_options::CreateContainerOptions; + pub use create_database_options::CreateDatabaseOptions; + pub use delete_container_options::DeleteContainerOptions; + pub use delete_database_options::DeleteDatabaseOptions; + } +} + pub use cosmos_client_options::CosmosClientOptions; pub use item_options::ItemOptions; pub use query_containers_options::QueryContainersOptions; @@ -29,4 +42,13 @@ pub mod builders { pub use super::query_options::QueryOptionsBuilder; pub use super::read_container_options::ReadContainerOptionsBuilder; pub use super::read_database_options::ReadDatabaseOptionsBuilder; + + cfg_if::cfg_if! { + if #[cfg(feature = "control_plane")] { + pub use super::create_container_options::CreateContainerOptionsBuilder; + pub use super::create_database_options::CreateDatabaseOptionsBuilder; + pub use super::delete_container_options::DeleteContainerOptionsBuilder; + pub use super::delete_database_options::DeleteDatabaseOptionsBuilder; + } + } } diff --git a/sdk/cosmos/azure_data_cosmos/src/partition_key.rs b/sdk/cosmos/azure_data_cosmos/src/partition_key.rs index cba139c9e0..49abc07f20 100644 --- a/sdk/cosmos/azure_data_cosmos/src/partition_key.rs +++ b/sdk/cosmos/azure_data_cosmos/src/partition_key.rs @@ -12,7 +12,6 @@ use crate::constants; /// [`QueryPartitionStrategy`] implements [`From`] for any type that is convertible to a `PartitionKey`. /// This allows you to use any of the syntaxes specified in the [`PartitionKey`] docs any place an [`Into`] is expected. #[derive(Debug, Clone)] -#[non_exhaustive] pub enum QueryPartitionStrategy { SinglePartition(PartitionKey), } diff --git a/sdk/cosmos/azure_data_cosmos/src/pipeline/authorization_policy.rs b/sdk/cosmos/azure_data_cosmos/src/pipeline/authorization_policy.rs index 0a8febb762..f6e6e013b5 100644 --- a/sdk/cosmos/azure_data_cosmos/src/pipeline/authorization_policy.rs +++ b/sdk/cosmos/azure_data_cosmos/src/pipeline/authorization_policy.rs @@ -21,7 +21,7 @@ use url::form_urlencoded; #[cfg(feature = "key_auth")] use azure_core::{credentials::Secret, hmac::hmac_sha256}; -const AZURE_VERSION: &str = "2018-12-31"; +const AZURE_VERSION: &str = "2020-07-15"; const VERSION_NUMBER: &str = "1.0"; #[derive(Debug, Clone, Copy, PartialEq, Eq)]