From 3007cb06b57d5ef88724c4a42e6870adeb8e29f4 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Thu, 13 Nov 2025 23:37:28 +0000 Subject: [PATCH 01/21] initial port of c bindings --- Cargo.lock | 4 + sdk/cosmos/.dict.txt | 3 + .../azure_data_cosmos/src/models/mod.rs | 2 +- .../azure_data_cosmos_native/Cargo.toml | 10 +- sdk/cosmos/azure_data_cosmos_native/build.rs | 6 +- .../azure_data_cosmos_native/src/blocking.rs | 51 ++ .../src/clients/container_client.rs | 522 ++++++++++++++ .../src/clients/cosmos_client.rs | 341 +++++++++ .../src/clients/database_client.rs | 394 +++++++++++ .../src/clients/mod.rs | 93 +++ .../azure_data_cosmos_native/src/error.rs | 663 ++++++++++++++++++ .../azure_data_cosmos_native/src/lib.rs | 15 +- .../azure_data_cosmos_native/src/macros.rs | 7 + .../azure_data_cosmos_native/src/string.rs | 127 ++++ 14 files changed, 2233 insertions(+), 5 deletions(-) create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/blocking.rs create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/error.rs create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/string.rs diff --git a/Cargo.lock b/Cargo.lock index fe1abf0d8f6..3537d80a75d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,8 +335,12 @@ dependencies = [ name = "azure_data_cosmos_native" version = "0.27.0" dependencies = [ + "azure_core", "azure_data_cosmos", "cbindgen", + "futures", + "serde_json", + "tokio", ] [[package]] diff --git a/sdk/cosmos/.dict.txt b/sdk/cosmos/.dict.txt index 00b93cefe42..a11c42c68c5 100644 --- a/sdk/cosmos/.dict.txt +++ b/sdk/cosmos/.dict.txt @@ -8,6 +8,8 @@ udfs backoff pluggable cloneable +upsert +upserts # Cosmos' docs all use "Autoscale" as a single word, rather than a compound "AutoScale" or "Auto Scale" autoscale @@ -15,3 +17,4 @@ autoscale # Words used within the Cosmos Native Client (azure_data_cosmos_native) azurecosmos cosmosclient +cstring diff --git a/sdk/cosmos/azure_data_cosmos/src/models/mod.rs b/sdk/cosmos/azure_data_cosmos/src/models/mod.rs index 56facf60564..8d5d0d22ee2 100644 --- a/sdk/cosmos/azure_data_cosmos/src/models/mod.rs +++ b/sdk/cosmos/azure_data_cosmos/src/models/mod.rs @@ -98,7 +98,7 @@ pub struct SystemProperties { /// /// Returned by [`DatabaseClient::read()`](crate::clients::DatabaseClient::read()). #[non_exhaustive] -#[derive(Clone, Default, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct DatabaseProperties { /// The ID of the database. pub id: String, diff --git a/sdk/cosmos/azure_data_cosmos_native/Cargo.toml b/sdk/cosmos/azure_data_cosmos_native/Cargo.toml index 504ff194432..e2c8065dc2e 100644 --- a/sdk/cosmos/azure_data_cosmos_native/Cargo.toml +++ b/sdk/cosmos/azure_data_cosmos_native/Cargo.toml @@ -14,7 +14,15 @@ name = "azurecosmos" crate-type = ["cdylib", "staticlib"] [dependencies] -azure_data_cosmos = { path = "../azure_data_cosmos" } +futures.workspace = true +tokio = { workspace = true, optional = true, features = ["rt-multi-thread", "macros"] } +serde_json = { workspace = true, features = ["raw_value"] } +azure_core.workspace = true +azure_data_cosmos = { path = "../azure_data_cosmos", features = [ "key_auth", "preview_query_engine" ] } + +[features] +default = ["tokio"] +tokio = ["dep:tokio"] [build-dependencies] cbindgen = "0.29.0" diff --git a/sdk/cosmos/azure_data_cosmos_native/build.rs b/sdk/cosmos/azure_data_cosmos_native/build.rs index 95466994886..4e9eae7ed3e 100644 --- a/sdk/cosmos/azure_data_cosmos_native/build.rs +++ b/sdk/cosmos/azure_data_cosmos_native/build.rs @@ -36,9 +36,13 @@ fn main() { "\n// Specifies the version of cosmosclient this header file was generated from.\n// This should match the version of libcosmosclient you are referencing.\n#define COSMOSCLIENT_H_VERSION \"{}\"", env!("CARGO_PKG_VERSION") )) + .with_style(cbindgen::Style::Both) + .rename_item("CosmosClientHandle", "CosmosClient") + .rename_item("DatabaseClientHandle", "DatabaseClient") + .rename_item("ContainerClientHandle", "ContainerClient") .with_cpp_compat(true) .with_header(header) .generate() .expect("unable to generate bindings") - .write_to_file("include/cosmosclient.h"); + .write_to_file("include/azurecosmos.h"); } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs b/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs new file mode 100644 index 00000000000..0e2e70c8e6f --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs @@ -0,0 +1,51 @@ +use std::{future::Future, sync::OnceLock}; +use tokio::runtime::Runtime; + +// Centralized runtime - single instance per process (following Azure SDK pattern) +// https://github.com/Azure/azure-sdk-for-cpp/blob/main/sdk/core/azure-core-amqp/src/impl/rust_amqp/rust_amqp/rust_wrapper/src/amqp/connection.rs#L100-L107 +pub static RUNTIME: OnceLock = OnceLock::new(); + +pub fn block_on(future: F) -> F::Output +where + F: Future, +{ + let runtime = RUNTIME.get_or_init(|| Runtime::new().expect("Failed to create Tokio runtime")); + runtime.block_on(future) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_blocking_runtime_initialization() { + let result1 = block_on(async { 42 }); + let result2 = block_on(async { 24 }); + + assert_eq!(result1, 42); + assert_eq!(result2, 24); + } + + #[test] + fn test_blocking_async_operation() { + let result = block_on(async { + tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; + "completed" + }); + + assert_eq!(result, "completed"); + } + + #[test] + fn test_runtime_singleton() { + block_on(async { 1 }); + let runtime1 = RUNTIME.get(); + + block_on(async { 2 }); + let runtime2 = RUNTIME.get(); + + assert!(runtime1.is_some()); + assert!(runtime2.is_some()); + assert!(std::ptr::eq(runtime1.unwrap(), runtime2.unwrap())); + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs new file mode 100644 index 00000000000..e5394aa52b5 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs @@ -0,0 +1,522 @@ +use std::os::raw::c_char; + +use azure_data_cosmos::clients::ContainerClient; +use azure_data_cosmos::query::Query; +use futures::TryStreamExt; +use serde_json::value::RawValue; + +use crate::blocking::block_on; +use crate::error::{self, marshal_result, CosmosError, CosmosErrorCode}; +use crate::string::{parse_cstr, safe_cstring_into_raw}; +use crate::ContainerClientHandle; + +#[no_mangle] +pub extern "C" fn cosmos_container_free(container: *mut ContainerClientHandle) { + if !container.is_null() { + unsafe { ContainerClientHandle::free_ptr(container) } + } +} + +fn create_item_inner( + container: &ContainerClient, + partition_key: &str, + json_str: &str, +) -> Result<(), CosmosError> { + let raw_value = RawValue::from_string(json_str.to_string())?; + + // Clone for async - Azure SDK needs owned String for async block + let pk = partition_key.to_string(); + block_on(container.create_item(pk, raw_value, None))?; + Ok(()) +} + +/// Creates a new item in the specified container. +/// +/// # Arguments +/// * `container` - Pointer to the `ContainerClient`. +/// * `partition_key` - The partition key value as a nul-terminated C string. +/// * `json_data` - The item data as a raw JSON nul-terminated C string. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_container_create_item( + container: *const ContainerClientHandle, + partition_key: *const c_char, + json_data: *const c_char, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if container.is_null() || partition_key.is_null() || json_data.is_null() || out_error.is_null() + { + return CosmosErrorCode::InvalidArgument; + } + + let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + + let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + let json_str = match parse_cstr(json_data, error::CSTR_INVALID_JSON_DATA) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + marshal_result( + create_item_inner(container_handle, partition_key_str, json_str), + out_error, + |_| {}, + ) +} + +fn upsert_item_inner( + container: &ContainerClient, + partition_key: &str, + json_str: &str, +) -> Result<(), CosmosError> { + let raw_value: Box = serde_json::from_str(json_str)?; + let pk = partition_key.to_string(); + block_on(container.upsert_item(pk, raw_value, None))?; + Ok(()) +} + +/// Upserts an item in the specified container. +/// +/// # Arguments +/// * `container` - Pointer to the `ContainerClient`. +/// * `partition_key` - The partition key value as a nul-terminated C string. +/// * `json_data` - The item data as a raw JSON nul-terminated C string. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_container_upsert_item( + container: *const ContainerClientHandle, + partition_key: *const c_char, + json_data: *const c_char, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if container.is_null() || partition_key.is_null() || json_data.is_null() || out_error.is_null() + { + return CosmosErrorCode::InvalidArgument; + } + + let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + + let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + let json_str = match parse_cstr(json_data, error::CSTR_INVALID_JSON_DATA) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + marshal_result( + upsert_item_inner(container_handle, partition_key_str, json_str), + out_error, + |_| {}, + ) +} + +// Inner function: Returns JSON string +fn read_item_inner( + container: &ContainerClient, + partition_key: &str, + item_id: &str, +) -> Result { + let pk = partition_key.to_string(); + + // The type we read into doesn't matter, because we'll extract the raw string instead of deserializing. + let response = block_on(container.read_item::<()>(pk, item_id, None))?; + Ok(response.into_body().into_string()?) +} + +/// Reads an item from the specified container. +/// +/// # Arguments +/// * `container` - Pointer to the `ContainerClient`. +/// * `partition_key` - The partition key value as a nul-terminated C string. +/// * `item_id` - The ID of the item to read as a nul-terminated C string. +/// * `out_json` - Output parameter that will receive the item data as a raw JSON nul-terminated C string. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_container_read_item( + container: *const ContainerClientHandle, + partition_key: *const c_char, + item_id: *const c_char, + out_json: *mut *mut c_char, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if container.is_null() + || partition_key.is_null() + || item_id.is_null() + || out_json.is_null() + || out_error.is_null() + { + return CosmosErrorCode::InvalidArgument; + } + + let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + + let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + let item_id_str = match parse_cstr(item_id, error::CSTR_INVALID_ITEM_ID) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + marshal_result( + read_item_inner(container_handle, partition_key_str, item_id_str), + out_error, + |json_string| unsafe { + let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); + }, + ) +} + +fn replace_item_inner( + container: &ContainerClient, + partition_key: &str, + item_id: &str, + json_str: &str, +) -> Result<(), CosmosError> { + let raw_value = RawValue::from_string(json_str.to_string())?; + let pk = partition_key.to_string(); + block_on(container.replace_item(pk, item_id, raw_value, None))?; + Ok(()) +} + +/// Replaces an existing item in the specified container. +/// +/// # Arguments +/// * `container` - Pointer to the `ContainerClient`. +/// * `partition_key` - The partition key value as a nul-terminated C string. +/// * `item_id` - The ID of the item to replace as a nul-terminated C string. +/// * `json_data` - The new item data as a raw JSON nul-terminated C string. +/// * `out_error` - Output parameter that will receive error information if the function fails +#[no_mangle] +pub extern "C" fn cosmos_container_replace_item( + container: *const ContainerClientHandle, + partition_key: *const c_char, + item_id: *const c_char, + json_data: *const c_char, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if container.is_null() + || partition_key.is_null() + || item_id.is_null() + || json_data.is_null() + || out_error.is_null() + { + return CosmosErrorCode::InvalidArgument; + } + + let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + + let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + let item_id_str = match parse_cstr(item_id, error::CSTR_INVALID_ITEM_ID) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + let json_str = match parse_cstr(json_data, error::CSTR_INVALID_JSON_DATA) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + marshal_result( + replace_item_inner(container_handle, partition_key_str, item_id_str, json_str), + out_error, + |_| {}, + ) +} + +fn delete_item_inner( + container: &ContainerClient, + partition_key: &str, + item_id: &str, +) -> Result<(), CosmosError> { + let pk = partition_key.to_string(); + block_on(container.delete_item(pk, item_id, None))?; + Ok(()) +} + +/// Deletes an item from the specified container. +/// +/// # Arguments +/// * `container` - Pointer to the `ContainerClient`. +/// * `partition_key` - The partition key value as a nul-terminated C string. +/// * `item_id` - The ID of the item to delete as a nul-terminated C string. +/// * `out_error` - Output parameter that will receive error information if the function fails +#[no_mangle] +pub extern "C" fn cosmos_container_delete_item( + container: *const ContainerClientHandle, + partition_key: *const c_char, + item_id: *const c_char, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if container.is_null() || partition_key.is_null() || item_id.is_null() || out_error.is_null() { + return CosmosErrorCode::InvalidArgument; + } + + let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + + let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + let item_id_str = match parse_cstr(item_id, error::CSTR_INVALID_ITEM_ID) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + marshal_result( + delete_item_inner(container_handle, partition_key_str, item_id_str), + out_error, + |_| {}, + ) +} + +// TODO: Patch + +fn read_container_inner(container: &ContainerClient) -> Result { + let response = block_on(container.read(None))?; + Ok(response.into_body().into_string()?) +} + +/// Reads the properties of the specified container. +/// +/// # Arguments +/// * `container` - Pointer to the `ContainerClient`. +/// * `out_json` - Output parameter that will receive the container properties as a raw JSON nul-terminated C string. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_container_read( + container: *const ContainerClientHandle, + out_json: *mut *mut c_char, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if container.is_null() || out_json.is_null() || out_error.is_null() { + return CosmosErrorCode::InvalidArgument; + } + + let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + + marshal_result( + read_container_inner(container_handle), + out_error, + |json_string| unsafe { + let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); + }, + ) +} + +fn query_items_inner( + container: &ContainerClient, + query_str: &str, + partition_key_opt: Option<&str>, +) -> Result { + let cosmos_query = Query::from(query_str); + let pk_owned = partition_key_opt.map(|s| s.to_string()); + + let pager = if let Some(pk) = pk_owned { + container.query_items::>(cosmos_query, pk, None)? + } else { + container.query_items::>(cosmos_query, (), None)? + }; + + // We don't expose the raw string in a FeedPage, so we need to collect and serialize. + // We'll evaluate optimizing this later if needed. + let results = block_on(pager.try_collect::>())?; + serde_json::to_string(&results).map_err(|_| { + CosmosError::from_static_cstr(CosmosErrorCode::DataConversion, error::CSTR_INVALID_JSON) + }) +} + +/// Queries items in the specified container. +/// +/// # Arguments +/// * `container` - Pointer to the `ContainerClient`. +/// * `query` - The query to execute as a nul-terminated C string. +/// * `partition_key` - Optional partition key value as a nul-terminated C string. Specify a null pointer for a cross-partition query. +/// * `out_json` - Output parameter that will receive the query results as a raw JSON nul-terminated C string. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_container_query_items( + container: *const ContainerClientHandle, + query: *const c_char, + partition_key: *const c_char, + out_json: *mut *mut c_char, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if container.is_null() || query.is_null() || out_json.is_null() || out_error.is_null() { + return CosmosErrorCode::InvalidArgument; + } + + let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + + let query_str = match parse_cstr(query, error::CSTR_INVALID_QUERY) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + let partition_key_opt = if partition_key.is_null() { + None + } else { + match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { + Ok("") => None, + Ok(s) => Some(s), + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + } + }; + + marshal_result( + query_items_inner(container_handle, query_str, partition_key_opt), + out_error, + |json_string| unsafe { + let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ptr; + + #[test] + fn test_container_crud_operations_null_validation() { + assert_eq!( + cosmos_container_create_item(ptr::null(), ptr::null(), ptr::null(), ptr::null_mut()), + CosmosErrorCode::InvalidArgument + ); + + assert_eq!( + cosmos_container_read_item( + ptr::null(), + ptr::null(), + ptr::null(), + ptr::null_mut(), + ptr::null_mut() + ), + CosmosErrorCode::InvalidArgument + ); + + assert_eq!( + cosmos_container_replace_item( + ptr::null(), + ptr::null(), + ptr::null(), + ptr::null(), + ptr::null_mut() + ), + CosmosErrorCode::InvalidArgument + ); + + assert_eq!( + cosmos_container_delete_item(ptr::null(), ptr::null(), ptr::null(), ptr::null_mut()), + CosmosErrorCode::InvalidArgument + ); + + assert_eq!( + cosmos_container_query_items( + ptr::null(), + ptr::null(), + ptr::null(), + ptr::null_mut(), + ptr::null_mut() + ), + CosmosErrorCode::InvalidArgument + ); + + assert_eq!( + cosmos_container_read(ptr::null(), ptr::null_mut(), ptr::null_mut()), + CosmosErrorCode::InvalidArgument + ); + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs new file mode 100644 index 00000000000..1f8e6a6392a --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs @@ -0,0 +1,341 @@ +use std::os::raw::c_char; + +use azure_core::credentials::Secret; +use azure_data_cosmos::clients::DatabaseClient; +use azure_data_cosmos::query::Query; +use azure_data_cosmos::CosmosClient; +use futures::TryStreamExt; + +use crate::blocking::block_on; +use crate::error::{self, marshal_result, CosmosError, CosmosErrorCode}; +use crate::string::{parse_cstr, safe_cstring_into_raw}; +use crate::{CosmosClientHandle, DatabaseClientHandle}; + +fn create_client_inner( + endpoint_str: &str, + key_str: &str, +) -> Result, CosmosError> { + let key_owned = key_str.to_string(); + let client = azure_data_cosmos::CosmosClient::with_key( + endpoint_str, + Secret::new(key_owned.clone()), + None, + )?; + Ok(Box::new(client)) +} + +/// Creates a new CosmosClient and returns a pointer to it via the out parameter. +/// +/// # Arguments +/// * `endpoint` - The Cosmos DB account endpoint, as a nul-terminated C string. +/// * `key` - The Cosmos DB account key, as a nul-terminated C string +/// * `out_client` - Output parameter that will receive a pointer to the created CosmosClientHandle. +/// * `out_error` - Output parameter that will receive error information if the function fails. +/// +/// # Returns +/// * Returns [`CosmosErrorCode::Success`] on success. +/// * Returns [`CosmosErrorCode::InvalidArgument`] if any input pointer is null or if the input strings are invalid. +#[no_mangle] +pub extern "C" fn cosmos_client_create( + endpoint: *const c_char, + key: *const c_char, + out_client: *mut *mut CosmosClientHandle, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if endpoint.is_null() || key.is_null() || out_client.is_null() || out_error.is_null() { + return CosmosErrorCode::InvalidArgument; + } + + let endpoint_str = match parse_cstr(endpoint, error::CSTR_INVALID_ENDPOINT) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + let key_str = match parse_cstr(key, error::CSTR_INVALID_KEY) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + marshal_result( + create_client_inner(endpoint_str, key_str), + out_error, + |handle| unsafe { + *out_client = CosmosClientHandle::wrap_ptr(handle); + }, + ) +} + +/// Releases the memory associated with a [`CosmosClient`]. +#[no_mangle] +pub extern "C" fn cosmos_client_free(client: *mut CosmosClientHandle) { + if !client.is_null() { + unsafe { CosmosClientHandle::free_ptr(client) } + } +} + +fn database_client_inner( + client: &CosmosClient, + database_id_str: &str, +) -> Result, CosmosError> { + let database_client = client.database_client(database_id_str); + Ok(Box::new(database_client)) +} + +/// Gets a [`DatabaseClient`] from the given [`CosmosClient`] for the specified database ID. +/// +/// # Arguments +/// * `client` - Pointer to the [`CosmosClient`]. +/// * `database_id` - The database ID as a nul-terminated C string. +/// * `out_database` - Output parameter that will receive a pointer to the created [`DatabaseClient`]. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_client_database_client( + client: *const CosmosClientHandle, + database_id: *const c_char, + out_database: *mut *mut DatabaseClientHandle, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if client.is_null() || database_id.is_null() || out_database.is_null() || out_error.is_null() { + return CosmosErrorCode::InvalidArgument; + } + + let client_handle = unsafe { CosmosClientHandle::unwrap_ptr(client) }; + + let database_id_str = match parse_cstr(database_id, error::CSTR_INVALID_DATABASE_ID) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + marshal_result( + database_client_inner(client_handle, database_id_str), + out_error, + |db_handle| unsafe { + *out_database = DatabaseClientHandle::wrap_ptr(db_handle); + }, + ) +} + +fn query_databases_inner(client: &CosmosClient, query_str: &str) -> Result { + let cosmos_query = Query::from(query_str); + let pager = client.query_databases(cosmos_query, None)?; + + // We don't expose the raw string in a FeedPage, so we need to collect and serialize. + // We'll evaluate optimizing this later if needed. + let results = block_on(pager.try_collect::>())?; + serde_json::to_string(&results).map_err(|_| { + CosmosError::from_static_cstr(CosmosErrorCode::DataConversion, error::CSTR_INVALID_JSON) + }) +} + +/// Queries the databases in the Cosmos DB account using the provided SQL query string. +/// +/// # Arguments +/// * `client` - Pointer to the [`CosmosClient`]. +/// * `query` - The SQL query string as a nul-terminated C string. +/// * `out_json` - Output parameter that will receive a pointer to the resulting JSON string +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_client_query_databases( + client: *const CosmosClientHandle, + query: *const c_char, + out_json: *mut *mut c_char, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if client.is_null() || query.is_null() || out_json.is_null() || out_error.is_null() { + return CosmosErrorCode::InvalidArgument; + } + + let client_handle = unsafe { CosmosClientHandle::unwrap_ptr(client) }; + + let query_str = match parse_cstr(query, error::CSTR_INVALID_QUERY) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + marshal_result( + query_databases_inner(client_handle, query_str), + out_error, + |json_string| unsafe { + let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); + }, + ) +} + +fn create_database_inner( + client: &CosmosClient, + database_id_str: &str, +) -> Result, CosmosError> { + block_on(client.create_database(database_id_str, None))?; + + let database_client = client.database_client(database_id_str); + + Ok(Box::new(database_client.into())) +} + +/// Creates a new database in the Cosmos DB account with the specified database ID. +/// +/// # Arguments +/// * `client` - Pointer to the [`CosmosClient`]. +/// * `database_id` - The database ID as a nul-terminated C string. +/// * `out_database` - Output parameter that will receive a pointer to the created [`DatabaseClient`]. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_client_create_database( + client: *const CosmosClientHandle, + database_id: *const c_char, + out_database: *mut *mut DatabaseClient, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if client.is_null() || database_id.is_null() || out_database.is_null() || out_error.is_null() { + return CosmosErrorCode::InvalidArgument; + } + + let client_handle = unsafe { CosmosClientHandle::unwrap_ptr(client) }; + + let database_id_str = match parse_cstr(database_id, error::CSTR_INVALID_DATABASE_ID) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + *out_database = std::ptr::null_mut(); + } + return code; + } + }; + + marshal_result( + create_database_inner(client_handle, database_id_str), + out_error, + |db_handle| unsafe { + *out_database = Box::into_raw(db_handle); + }, + ) +} + +#[cfg(test)] +mod tests { + use crate::cosmos_database_free; + + use super::*; + use std::ffi::CString; + use std::ptr; + + #[test] + fn test_cosmos_client_create_valid_params() { + let endpoint = CString::new("https://test.documents.azure.com") + .expect("test string should not contain NUL"); + let key = CString::new("test-key").expect("test string should not contain NUL"); + let mut client_ptr: *mut CosmosClientHandle = ptr::null_mut(); + let mut error = CosmosError::success(); + + let result = + cosmos_client_create(endpoint.as_ptr(), key.as_ptr(), &mut client_ptr, &mut error); + + assert_eq!(result, CosmosErrorCode::Success); + assert!(!client_ptr.is_null()); + assert_eq!(error.code, CosmosErrorCode::Success); + + cosmos_client_free(client_ptr); + } + + #[test] + fn test_cosmos_client_create_null_params() { + let mut client_ptr: *mut CosmosClientHandle = ptr::null_mut(); + let mut error = CosmosError::success(); + + let result = cosmos_client_create(ptr::null(), ptr::null(), &mut client_ptr, &mut error); + + assert_eq!(result, CosmosErrorCode::InvalidArgument); + assert!(client_ptr.is_null()); + } + + #[test] + fn test_cosmos_client_database_client() { + let endpoint = CString::new("https://test.documents.azure.com") + .expect("test string should not contain NUL"); + let key = CString::new("test-key").expect("test string should not contain NUL"); + let db_id = CString::new("test-db").expect("test string should not contain NUL"); + + let mut client_ptr: *mut CosmosClientHandle = ptr::null_mut(); + let mut db_ptr: *mut DatabaseClientHandle = ptr::null_mut(); + let mut error = CosmosError::success(); + + cosmos_client_create( + endpoint.as_ptr(), + key.as_ptr(), + &raw mut client_ptr, + &mut error, + ); + assert!(!client_ptr.is_null()); + + let result = + cosmos_client_database_client(client_ptr, db_id.as_ptr(), &mut db_ptr, &mut error); + + assert_eq!(result, CosmosErrorCode::Success); + assert!(!db_ptr.is_null()); + + cosmos_database_free(db_ptr); + cosmos_client_free(client_ptr); + } + + #[test] + fn test_cosmos_client_query_databases_null_params() { + let mut json_ptr: *mut c_char = ptr::null_mut(); + let mut error = CosmosError::success(); + + let result = + cosmos_client_query_databases(ptr::null(), ptr::null(), &mut json_ptr, &mut error); + + assert_eq!(result, CosmosErrorCode::InvalidArgument); + assert!(json_ptr.is_null()); + } + + #[test] + fn test_cosmos_client_create_database_null_params() { + let mut db_ptr: *mut DatabaseClient = ptr::null_mut(); + let mut error = CosmosError::success(); + + // Test null client + let result = + cosmos_client_create_database(ptr::null(), ptr::null(), &mut db_ptr, &mut error); + + assert_eq!(result, CosmosErrorCode::InvalidArgument); + assert!(db_ptr.is_null()); + + // Reset for next test + db_ptr = ptr::null_mut(); + error = CosmosError::success(); + + // Test null database_id + let result = + cosmos_client_create_database(ptr::null(), ptr::null(), &mut db_ptr, &mut error); + + assert_eq!(result, CosmosErrorCode::InvalidArgument); + assert!(db_ptr.is_null()); + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs new file mode 100644 index 00000000000..f6d2beaf7d8 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs @@ -0,0 +1,394 @@ +use std::os::raw::c_char; + +use azure_data_cosmos::clients::{ContainerClient, DatabaseClient}; +use azure_data_cosmos::models::ContainerProperties; +use azure_data_cosmos::query::Query; +use futures::TryStreamExt; + +use crate::blocking::block_on; +use crate::error::{self, marshal_result, CosmosError, CosmosErrorCode}; +use crate::string::{parse_cstr, safe_cstring_into_raw}; +use crate::{ContainerClientHandle, DatabaseClientHandle}; + +/// Releases the memory associated with a [`DatabaseClient`]. +#[no_mangle] +pub extern "C" fn cosmos_database_free(database: *mut DatabaseClientHandle) { + if !database.is_null() { + unsafe { DatabaseClientHandle::free_ptr(database) } + } +} + +fn container_client_inner( + database: &DatabaseClient, + container_id_str: &str, +) -> Result, CosmosError> { + let container_client = database.container_client(container_id_str); + Ok(Box::new(container_client.into())) +} + +/// Retrieves a pointer to a [`ContainerClient`] for the specified container ID within the given database. +/// +/// # Arguments +/// * `database` - Pointer to the [`DatabaseClient`]. +/// * `container_id` - The container ID as a nul-terminated C string. +/// * `out_container` - Output parameter that will receive a pointer to the [`ContainerClient`]. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_database_container_client( + database: *const DatabaseClientHandle, + container_id: *const c_char, + out_container: *mut *mut ContainerClientHandle, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if database.is_null() + || container_id.is_null() + || out_container.is_null() + || out_error.is_null() + { + return CosmosErrorCode::InvalidArgument; + } + + let database_handle = unsafe { DatabaseClientHandle::unwrap_ptr(database) }; + + let container_id_str = match parse_cstr(container_id, error::CSTR_INVALID_CONTAINER_ID) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + marshal_result( + container_client_inner(database_handle, container_id_str), + out_error, + |container_handle| unsafe { + *out_container = ContainerClientHandle::wrap_ptr(container_handle); + }, + ) +} + +fn read_database_inner(database: &DatabaseClient) -> Result { + let response = block_on(database.read(None))?; + Ok(response.into_body().into_string()?) +} + +/// Reads the properties of the specified database and returns them as a JSON string. +/// +/// # Arguments +/// * `database` - Pointer to the [`DatabaseClient`]. +/// * `out_json` - Output parameter that will receive a pointer to the JSON string. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_database_read( + database: *const DatabaseClient, + out_json: *mut *mut c_char, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if database.is_null() || out_json.is_null() || out_error.is_null() { + return CosmosErrorCode::InvalidArgument; + } + + let database_handle = unsafe { &*database }; + + marshal_result( + read_database_inner(database_handle), + out_error, + |json_string| unsafe { + let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); + }, + ) +} + +fn delete_database_inner(database: &DatabaseClient) -> Result<(), CosmosError> { + block_on(database.delete(None))?; + Ok(()) +} + +/// Deletes the specified database. +/// +/// # Arguments +/// * `database` - Pointer to the [`DatabaseClient`]. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_database_delete( + database: *const DatabaseClient, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if database.is_null() || out_error.is_null() { + return CosmosErrorCode::InvalidArgument; + } + + let database_handle = unsafe { &*database }; + + marshal_result(delete_database_inner(database_handle), out_error, |_| {}) +} + +fn create_container_inner( + database: &DatabaseClient, + container_id_str: &str, + partition_key_path_str: &str, +) -> Result, CosmosError> { + let container_id_owned = container_id_str.to_string(); + let partition_key_owned = partition_key_path_str.to_string(); + + let properties = ContainerProperties { + id: container_id_owned.clone().into(), + partition_key: partition_key_owned.clone().into(), + ..Default::default() + }; + + block_on(database.create_container(properties, None))?; + + let container_client = database.container_client(&container_id_owned); + + Ok(Box::new(container_client.into())) +} + +/// Creates a new container within the specified database. +/// +/// # Arguments +/// * `database` - Pointer to the [`DatabaseClient`]. +/// * `container_id` - The container ID as a nul-terminated C string. +/// * `partition_key_path` - The partition key path as a nul-terminated C string. +/// * `out_container` - Output parameter that will receive a pointer to the newly created [`ContainerClient`]. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_database_create_container( + database: *const DatabaseClient, + container_id: *const c_char, + partition_key_path: *const c_char, + out_container: *mut *mut ContainerClient, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if database.is_null() + || container_id.is_null() + || partition_key_path.is_null() + || out_container.is_null() + || out_error.is_null() + { + return CosmosErrorCode::InvalidArgument; + } + + let database_handle = unsafe { &*database }; + + let container_id_str = match parse_cstr(container_id, error::CSTR_INVALID_CONTAINER_ID) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + *out_container = std::ptr::null_mut(); + } + return code; + } + }; + + let partition_key_path_str = + match parse_cstr(partition_key_path, error::CSTR_INVALID_PARTITION_KEY) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + *out_container = std::ptr::null_mut(); + } + return code; + } + }; + + marshal_result( + create_container_inner(database_handle, container_id_str, partition_key_path_str), + out_error, + |container_handle| unsafe { + *out_container = Box::into_raw(container_handle); + }, + ) +} + +fn query_containers_inner( + database: &DatabaseClient, + query_str: &str, +) -> Result { + let cosmos_query = Query::from(query_str); + let pager = database.query_containers(cosmos_query, None)?; + + // We don't expose the raw string in a FeedPage, so we need to collect and serialize. + // We'll evaluate optimizing this later if needed. + let results = block_on(pager.try_collect::>())?; + serde_json::to_string(&results).map_err(|_| { + CosmosError::from_static_cstr(CosmosErrorCode::DataConversion, error::CSTR_INVALID_JSON) + }) +} + +/// Queries the containers within the specified database and returns the results as a JSON string. +/// +/// # Arguments +/// * `database` - Pointer to the [`DatabaseClient`]. +/// * `query` - The query string as a nul-terminated C string. +/// * `out_json` - Output parameter that will receive a pointer to the JSON string. +/// * `out_error` - Output parameter that will receive error information if the function fails. +#[no_mangle] +pub extern "C" fn cosmos_database_query_containers( + database: *const DatabaseClient, + query: *const c_char, + out_json: *mut *mut c_char, + out_error: *mut CosmosError, +) -> CosmosErrorCode { + if database.is_null() || query.is_null() || out_json.is_null() || out_error.is_null() { + return CosmosErrorCode::InvalidArgument; + } + + let database_handle = unsafe { &*database }; + + let query_str = match parse_cstr(query, error::CSTR_INVALID_QUERY) { + Ok(s) => s, + Err(e) => { + let code = e.code; + unsafe { + *out_error = e; + } + return code; + } + }; + + marshal_result( + query_containers_inner(database_handle, query_str), + out_error, + |json_string| unsafe { + let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); + }, + ) +} + +// TODO: Add more database operations following Azure SDK pattern: +// - Additional advanced database operations if needed + +#[cfg(test)] +mod tests { + use crate::{ + cosmos_client_create, cosmos_client_database_client, cosmos_client_free, + cosmos_container_free, ContainerClientHandle, CosmosClientHandle, + }; + + use super::*; + use std::{ffi::CString, ptr}; + + #[test] + fn test_database_container_client_null_params() { + let mut container_ptr: *mut ContainerClientHandle = ptr::null_mut(); + let mut error = CosmosError::success(); + + let result = cosmos_database_container_client( + ptr::null(), + ptr::null(), + &mut container_ptr, + &mut error, + ); + + assert_eq!(result, CosmosErrorCode::InvalidArgument); + assert!(container_ptr.is_null()); + } + + #[test] + fn test_database_read_null_params() { + let mut json_ptr: *mut c_char = ptr::null_mut(); + let mut error = CosmosError::success(); + + let result = cosmos_database_read(ptr::null(), &mut json_ptr, &mut error); + + assert_eq!(result, CosmosErrorCode::InvalidArgument); + assert!(json_ptr.is_null()); + } + + #[test] + fn test_database_delete_null_params() { + let mut error = CosmosError::success(); + + let result = cosmos_database_delete(ptr::null(), &mut error); + + assert_eq!(result, CosmosErrorCode::InvalidArgument); + } + + #[test] + fn test_database_create_container_null_params() { + let mut container_ptr: *mut ContainerClient = ptr::null_mut(); + let mut error = CosmosError::success(); + + // Test null database + let result = cosmos_database_create_container( + ptr::null(), + ptr::null(), + ptr::null(), + &mut container_ptr, + &mut error, + ); + + assert_eq!(result, CosmosErrorCode::InvalidArgument); + assert!(container_ptr.is_null()); + + // Reset for next test + container_ptr = ptr::null_mut(); + error = CosmosError::success(); + + // Test with valid database but null container_id + // Note: We can't create a real DatabaseClientHandle without Azure SDK setup, + // so we test the null parameter validation only + let result = cosmos_database_create_container( + ptr::null(), + ptr::null(), + c"/test".as_ptr() as *const c_char, + &mut container_ptr, + &mut error, + ); + + assert_eq!(result, CosmosErrorCode::InvalidArgument); + assert!(container_ptr.is_null()); + } + + #[test] + fn test_database_query_containers_null_params() { + let mut json_ptr: *mut c_char = ptr::null_mut(); + let mut error = CosmosError::success(); + + let result = + cosmos_database_query_containers(ptr::null(), ptr::null(), &mut json_ptr, &mut error); + + assert_eq!(result, CosmosErrorCode::InvalidArgument); + assert!(json_ptr.is_null()); + } + + #[test] + fn test_database_container_client() { + let endpoint = CString::new("https://test.documents.azure.com") + .expect("test string should not contain NUL"); + let key = CString::new("test-key").expect("test string should not contain NUL"); + let db_id = CString::new("test-db").expect("test string should not contain NUL"); + + let mut client_ptr: *mut CosmosClientHandle = ptr::null_mut(); + let mut db_ptr: *mut DatabaseClientHandle = ptr::null_mut(); + let mut container_ptr: *mut ContainerClientHandle = ptr::null_mut(); + let mut error = CosmosError::success(); + + cosmos_client_create(endpoint.as_ptr(), key.as_ptr(), &mut client_ptr, &mut error); + assert!(!client_ptr.is_null()); + + cosmos_client_database_client(client_ptr, db_id.as_ptr(), &mut db_ptr, &mut error); + assert!(!db_ptr.is_null()); + + let result = cosmos_database_container_client( + db_ptr, + c"test-container".as_ptr() as *const c_char, + &mut container_ptr, + &mut error, + ); + assert_eq!(result, CosmosErrorCode::Success); + assert!(!container_ptr.is_null()); + + cosmos_container_free(container_ptr); + cosmos_database_free(db_ptr); + cosmos_client_free(client_ptr); + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs new file mode 100644 index 00000000000..79173a25a49 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs @@ -0,0 +1,93 @@ +#![allow( + clippy::missing_safety_doc, + reason = "We're operating on raw pointers received from FFI." +)] + +pub mod container_client; +pub mod cosmos_client; +pub mod database_client; + +pub use container_client::*; +pub use cosmos_client::*; +pub use database_client::*; + +// Below are opaque handle types for FFI. +// These types are used as the type names for pointers passed across the FFI boundary, but they are zero-sized in Rust. +// The actual data is stored in the corresponding Azure SDK for Rust types and we provide functions to transmute pointers from these handle types to the actual types. +// This pattern allows cbindgen to see the types for generating headers, while keeping the actual implementation details hidden from the C side. +// +// You might be tempted to use a macro to generate them, but cbindgen does not handle macros well, so we define them manually. + +pub struct CosmosClientHandle; + +impl CosmosClientHandle { + pub unsafe fn unwrap_ptr<'a>( + ptr: *const CosmosClientHandle, + ) -> &'a azure_data_cosmos::CosmosClient { + (ptr as *const azure_data_cosmos::CosmosClient) + .as_ref() + .unwrap() + } + + pub unsafe fn wrap_ptr(value: Box) -> *mut CosmosClientHandle { + Box::into_raw(value) as *mut CosmosClientHandle + } + + pub unsafe fn free_ptr(ptr: *mut CosmosClientHandle) { + if !ptr.is_null() { + drop(Box::from_raw(ptr as *mut azure_data_cosmos::CosmosClient)); + } + } +} + +pub struct DatabaseClientHandle; + +impl DatabaseClientHandle { + pub unsafe fn unwrap_ptr<'a>( + ptr: *const DatabaseClientHandle, + ) -> &'a azure_data_cosmos::clients::DatabaseClient { + (ptr as *const azure_data_cosmos::clients::DatabaseClient) + .as_ref() + .unwrap() + } + + pub unsafe fn wrap_ptr( + value: Box, + ) -> *mut DatabaseClientHandle { + Box::into_raw(value) as *mut DatabaseClientHandle + } + + pub unsafe fn free_ptr(ptr: *mut DatabaseClientHandle) { + if !ptr.is_null() { + drop(Box::from_raw( + ptr as *mut azure_data_cosmos::clients::DatabaseClient, + )); + } + } +} + +pub struct ContainerClientHandle; + +impl ContainerClientHandle { + pub unsafe fn unwrap_ptr<'a>( + ptr: *const ContainerClientHandle, + ) -> &'a azure_data_cosmos::clients::ContainerClient { + (ptr as *const azure_data_cosmos::clients::ContainerClient) + .as_ref() + .unwrap() + } + + pub unsafe fn wrap_ptr( + value: Box, + ) -> *mut ContainerClientHandle { + Box::into_raw(value) as *mut ContainerClientHandle + } + + pub unsafe fn free_ptr(ptr: *mut ContainerClientHandle) { + if !ptr.is_null() { + drop(Box::from_raw( + ptr as *mut azure_data_cosmos::clients::ContainerClient, + )); + } + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/error.rs b/sdk/cosmos/azure_data_cosmos_native/src/error.rs new file mode 100644 index 00000000000..9cfddc69b5e --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/error.rs @@ -0,0 +1,663 @@ +use azure_core::error::ErrorKind; +use std::ffi::CStr; +use std::os::raw::c_char; + +pub static CSTR_NUL_BYTES_ERROR: &CStr = c"String contains NUL bytes"; +pub static CSTR_INVALID_CHARS_ERROR: &CStr = c"Error message contains invalid characters"; +pub static CSTR_UNKNOWN_ERROR: &CStr = c"Unknown error"; +pub static CSTR_INVALID_JSON: &CStr = c"Invalid JSON data"; +pub static CSTR_CLIENT_CREATION_FAILED: &CStr = c"Failed to create Azure Cosmos client"; + +pub static CSTR_INVALID_ENDPOINT: &CStr = c"Invalid endpoint string"; +pub static CSTR_INVALID_KEY: &CStr = c"Invalid key string"; +pub static CSTR_INVALID_DATABASE_ID: &CStr = c"Invalid database ID string"; +pub static CSTR_INVALID_CONTAINER_ID: &CStr = c"Invalid container ID string"; +pub static CSTR_INVALID_PARTITION_KEY: &CStr = c"Invalid partition key string"; +pub static CSTR_INVALID_ITEM_ID: &CStr = c"Invalid item ID string"; +pub static CSTR_INVALID_JSON_DATA: &CStr = c"Invalid JSON data string"; +pub static CSTR_INVALID_QUERY: &CStr = c"Invalid query string"; + +pub static CSTR_QUERY_NOT_IMPLEMENTED: &CStr = + c"Query operations not yet implemented - requires stream handling"; + +// Helper function to check if a pointer is one of our static constants +fn is_static_error_message(ptr: *const c_char) -> bool { + if ptr.is_null() { + return true; + } + + ptr == CSTR_INVALID_CHARS_ERROR.as_ptr() + || ptr == CSTR_UNKNOWN_ERROR.as_ptr() + || ptr == CSTR_NUL_BYTES_ERROR.as_ptr() + || ptr == CSTR_INVALID_JSON.as_ptr() + || ptr == CSTR_CLIENT_CREATION_FAILED.as_ptr() + || ptr == CSTR_INVALID_ENDPOINT.as_ptr() + || ptr == CSTR_INVALID_KEY.as_ptr() + || ptr == CSTR_INVALID_DATABASE_ID.as_ptr() + || ptr == CSTR_INVALID_CONTAINER_ID.as_ptr() + || ptr == CSTR_INVALID_PARTITION_KEY.as_ptr() + || ptr == CSTR_INVALID_ITEM_ID.as_ptr() + || ptr == CSTR_INVALID_JSON_DATA.as_ptr() + || ptr == CSTR_QUERY_NOT_IMPLEMENTED.as_ptr() +} + +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CosmosErrorCode { + Success = 0, + InvalidArgument = 1, + ConnectionFailed = 2, + UnknownError = 999, + + BadRequest = 400, + Unauthorized = 401, + Forbidden = 403, + NotFound = 404, + Conflict = 409, + PreconditionFailed = 412, + RequestTimeout = 408, + TooManyRequests = 429, + InternalServerError = 500, + BadGateway = 502, + ServiceUnavailable = 503, + + // Additional Azure SDK specific error codes + AuthenticationFailed = 1001, + DataConversion = 1002, + + PartitionKeyMismatch = 2001, + ResourceQuotaExceeded = 2002, + RequestRateTooLarge = 2003, + ItemSizeTooLarge = 2004, + PartitionKeyNotFound = 2005, + + // FFI boundary error codes - for infrastructure issues at the boundary + FFIInvalidUTF8 = 3001, // Invalid UTF-8 in string parameters crossing FFI + FFIInvalidHandle = 3002, // Corrupted/invalid handle passed across FFI + FFIMemoryError = 3003, // Memory allocation/deallocation issues at FFI boundary + FFIMarshalingError = 3004, // Data marshaling/unmarshaling failed at FFI boundary +} + +// CosmosError struct with hybrid memory management +// +// MEMORY MANAGEMENT STRATEGY: +// - message pointer can be either: +// 1. STATIC: Points to compile-time constants (never freed) +// 2. OWNED: Points to heap-allocated CString (must be freed) +// +// SAFETY: +// - Static messages: Created via from_static_cstr(), pointer lives forever +// - Owned messages: Created via new(), caller responsible for cleanup +// - Mixed usage: free_message() safely distinguishes between static/owned +// +// This approach follows Azure SDK pattern for zero-cost static errors +// while maintaining compatibility with dynamic error messages. +#[repr(C)] +pub struct CosmosError { + pub code: CosmosErrorCode, + pub message: *const c_char, +} + +impl CosmosError { + // Safe cleanup that handles both static and owned messages + pub fn free_message(&mut self) { + if !is_static_error_message(self.message) && !self.message.is_null() { + unsafe { + let _ = std::ffi::CString::from_raw(self.message as *mut c_char); + } + } + self.message = std::ptr::null(); + } + + pub fn success() -> Self { + Self { + code: CosmosErrorCode::Success, + message: std::ptr::null(), + } + } + + // Create error from static CStr (zero allocation) + pub fn from_static_cstr(code: CosmosErrorCode, static_cstr: &'static std::ffi::CStr) -> Self { + Self { + code, + message: static_cstr.as_ptr(), + } + } + + pub fn new(code: CosmosErrorCode, message: String) -> Self { + let c_message = std::ffi::CString::new(message) + .expect("SDK-generated error message should not contain NUL bytes") + .into_raw() as *const c_char; + + Self { + code, + message: c_message, + } + } +} + +pub fn http_status_to_error_code(status_code: u16) -> CosmosErrorCode { + match status_code { + 400 => CosmosErrorCode::BadRequest, + 401 => CosmosErrorCode::Unauthorized, + 403 => CosmosErrorCode::Forbidden, + 404 => CosmosErrorCode::NotFound, + 408 => CosmosErrorCode::RequestTimeout, + 409 => CosmosErrorCode::Conflict, + 412 => CosmosErrorCode::PreconditionFailed, + 429 => CosmosErrorCode::TooManyRequests, + 500 => CosmosErrorCode::InternalServerError, + 502 => CosmosErrorCode::BadGateway, + 503 => CosmosErrorCode::ServiceUnavailable, + _ => CosmosErrorCode::UnknownError, + } +} + +// Extract Cosmos DB specific error information from error messages +fn extract_cosmos_db_error_info(error_message: &str) -> (CosmosErrorCode, String) { + if error_message.contains("PartitionKeyMismatch") + || error_message.contains("partition key mismatch") + { + ( + CosmosErrorCode::PartitionKeyMismatch, + error_message.to_string(), + ) + } else if error_message.contains("Resource quota exceeded") + || error_message.contains("Request rate is large") + { + ( + CosmosErrorCode::ResourceQuotaExceeded, + error_message.to_string(), + ) + } else if error_message.contains("429") && error_message.contains("Request rate is large") { + ( + CosmosErrorCode::RequestRateTooLarge, + error_message.to_string(), + ) + } else if error_message.contains("Entity is too large") + || error_message.contains("Request entity too large") + { + (CosmosErrorCode::ItemSizeTooLarge, error_message.to_string()) + } else if error_message.contains("Partition key") && error_message.contains("not found") { + ( + CosmosErrorCode::PartitionKeyNotFound, + error_message.to_string(), + ) + } else { + (CosmosErrorCode::UnknownError, error_message.to_string()) + } +} + +// Native Azure SDK error conversion using structured error data +pub fn convert_azure_error_native(azure_error: &azure_core::Error) -> CosmosError { + let error_string = azure_error.to_string(); + + if let Some(status_code) = azure_error.http_status() { + let (cosmos_error_code, refined_message) = extract_cosmos_db_error_info(&error_string); + + if cosmos_error_code != CosmosErrorCode::UnknownError { + CosmosError::new(cosmos_error_code, refined_message) + } else { + let error_code = http_status_to_error_code(u16::from(status_code)); + CosmosError::new(error_code, error_string) + } + } else { + match azure_error.kind() { + ErrorKind::Credential => CosmosError::new( + CosmosErrorCode::AuthenticationFailed, + format!("Authentication failed: {}", azure_error), + ), + ErrorKind::Io => CosmosError::new( + CosmosErrorCode::ConnectionFailed, + format!("IO error: {}", azure_error), + ), + ErrorKind::DataConversion => { + if error_string.contains("Not Found") || error_string.contains("not found") { + CosmosError::new( + CosmosErrorCode::NotFound, + format!("Resource not found: {}", azure_error), + ) + } else { + CosmosError::new( + CosmosErrorCode::DataConversion, + format!("Data conversion error: {}", azure_error), + ) + } + } + _ => CosmosError::new( + CosmosErrorCode::UnknownError, + format!("Unknown error: {}", azure_error), + ), + } + } +} + +impl From for CosmosError { + fn from(error: azure_core::Error) -> Self { + convert_azure_error_native(&error) + } +} + +impl From for CosmosError { + fn from(error: serde_json::Error) -> Self { + CosmosError::new( + CosmosErrorCode::DataConversion, + format!("JSON error: {}", error), + ) + } +} + +fn free_non_static_error_message(message: *const c_char) { + if message.is_null() { + return; + } + if !is_static_error_message(message) { + unsafe { + let _ = std::ffi::CString::from_raw(message as *mut c_char); + } + } +} + +/// Releases the memory associated with a [`CosmosError`]. +#[no_mangle] +pub extern "C" fn cosmos_error_free(error: *mut CosmosError) { + if error.is_null() { + return; + } + unsafe { + let err = Box::from_raw(error); + free_non_static_error_message(err.message); + } +} + +pub fn marshal_result( + result: Result, + out_error: *mut CosmosError, + on_success: impl FnOnce(T), +) -> CosmosErrorCode +where + E: Into, +{ + match result { + Ok(value) => { + on_success(value); + unsafe { + *out_error = CosmosError::success(); + } + CosmosErrorCode::Success + } + Err(err) => { + let cosmos_error: CosmosError = err.into(); + let code = cosmos_error.code; + unsafe { + *out_error = cosmos_error; + } + code + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use azure_core::{error::ErrorKind, Error}; + + #[test] + fn test_convert_azure_error_native_io_error() { + let azure_error = Error::new(ErrorKind::Io, "Network connection failed"); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!(cosmos_error.code, CosmosErrorCode::ConnectionFailed); + free_non_static_error_message(cosmos_error.message); + } + + #[test] + fn test_convert_azure_error_native_credential_error() { + let azure_error = Error::new(ErrorKind::Credential, "Invalid credentials"); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!(cosmos_error.code, CosmosErrorCode::AuthenticationFailed); + free_non_static_error_message(cosmos_error.message); + } + + #[test] + fn test_convert_azure_error_native_data_conversion_error() { + let azure_error = Error::new(ErrorKind::DataConversion, "Failed to convert data"); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!(cosmos_error.code, CosmosErrorCode::DataConversion); + free_non_static_error_message(cosmos_error.message); + } + + #[test] + fn test_convert_azure_error_native_missing_field_id_without_404_remains_dataconversion() { + // Missing field without explicit 404 indicator should remain DataConversion + let azure_error = Error::new( + ErrorKind::DataConversion, + "missing field `id` at line 1 column 49", + ); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!(cosmos_error.code, CosmosErrorCode::DataConversion); + free_non_static_error_message(cosmos_error.message); + } + + #[test] + fn test_convert_azure_error_native_missing_field_id_with_404_maps_to_notfound() { + // Missing field WITH explicit 404 indicator should map to NotFound + let azure_error = Error::new( + ErrorKind::DataConversion, + "404 Not Found: missing field `id` at line 1 column 49", + ); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!(cosmos_error.code, CosmosErrorCode::NotFound); + free_non_static_error_message(cosmos_error.message); + } + + #[test] + fn test_convert_azure_error_native_invalid_value_remains_dataconversion() { + let azure_error = Error::new( + ErrorKind::DataConversion, + "invalid value: integer `-2`, expected u64 at line 1 column 305", + ); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!(cosmos_error.code, CosmosErrorCode::DataConversion); + free_non_static_error_message(cosmos_error.message); + } + + #[test] + fn test_convert_azure_error_native_404_in_dataconversion() { + let azure_error = Error::new( + ErrorKind::DataConversion, + "404 Not Found - Resource does not exist", + ); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!(cosmos_error.code, CosmosErrorCode::NotFound); + free_non_static_error_message(cosmos_error.message); + } + + // Comprehensive unit tests for conservative error mapping + #[test] + fn test_error_mapping_explicit_http_status_codes() { + // Test explicit HTTP status codes in DataConversion errors + let test_cases = vec![ + ("401 Unauthorized access", CosmosErrorCode::DataConversion), // No longer http + ("403 Forbidden resource", CosmosErrorCode::DataConversion), // No longer http + ("409 Conflict detected", CosmosErrorCode::DataConversion), // No longer http + ("404 Not Found", CosmosErrorCode::NotFound), + ("not found resource", CosmosErrorCode::NotFound), + ("Not Found: resource missing", CosmosErrorCode::NotFound), + ]; + + for (message, expected_code) in test_cases { + let azure_error = Error::new(ErrorKind::DataConversion, message); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!( + cosmos_error.code, expected_code, + "Failed for message: {}", + message + ); + free_non_static_error_message(cosmos_error.message); + } + } + + #[test] + fn test_error_mapping_missing_field_patterns() { + // Test missing field patterns with and without 404 indicators + let should_remain_dataconversion = vec![ + "missing field `id` at line 1 column 49", + "missing field `name` at line 2 column 10", + "missing required field `partition_key`", + "field `id` missing from JSON", // This should stay DataConversion - no "Not Found" phrase + ]; + + for message in should_remain_dataconversion { + let azure_error = Error::new(ErrorKind::DataConversion, message); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!( + cosmos_error.code, + CosmosErrorCode::DataConversion, + "Should remain DataConversion for: {}", + message + ); + free_non_static_error_message(cosmos_error.message); + } + + // Test missing field WITH explicit "Not Found" indicator (should map to NotFound) + let should_map_to_notfound = vec![ + "Not Found - missing field `id` in response", + "not found: missing field `name` at line 2", + ]; + + for message in should_map_to_notfound { + let azure_error = Error::new(ErrorKind::DataConversion, message); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!( + cosmos_error.code, + CosmosErrorCode::NotFound, + "Should map to NotFound for: {}", + message + ); + free_non_static_error_message(cosmos_error.message); + } + } + + #[test] + fn test_error_mapping_json_parsing_errors() { + // Test various JSON parsing errors that should remain DataConversion + let json_parsing_errors = vec![ + "invalid value: integer `-2`, expected u64 at line 1 column 305", + "expected `,` or `}` at line 1 column 15", + "invalid type: string \"hello\", expected u64 at line 2 column 8", + "EOF while parsing a value at line 1 column 0", + "invalid escape sequence at line 1 column 20", + ]; + + for message in json_parsing_errors { + let azure_error = Error::new(ErrorKind::DataConversion, message); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!( + cosmos_error.code, + CosmosErrorCode::DataConversion, + "JSON parsing error should remain DataConversion for: {}", + message + ); + free_non_static_error_message(cosmos_error.message); + } + } + + #[test] + fn test_error_mapping_http_response_errors() { + use azure_core::http::StatusCode; + + let http_test_cases = vec![ + ( + StatusCode::NotFound, + "Resource not found", + CosmosErrorCode::NotFound, + ), + ( + StatusCode::Unauthorized, + "Authentication failed", + CosmosErrorCode::Unauthorized, + ), + ( + StatusCode::Forbidden, + "Access denied", + CosmosErrorCode::Forbidden, + ), + ( + StatusCode::Conflict, + "Resource already exists", + CosmosErrorCode::Conflict, + ), + ( + StatusCode::InternalServerError, + "Internal server error", + CosmosErrorCode::InternalServerError, + ), + ( + StatusCode::BadRequest, + "Bad request", + CosmosErrorCode::BadRequest, + ), + ( + StatusCode::RequestTimeout, + "Request timeout", + CosmosErrorCode::RequestTimeout, + ), + ( + StatusCode::TooManyRequests, + "Too many requests", + CosmosErrorCode::TooManyRequests, + ), + ( + StatusCode::BadGateway, + "Bad gateway", + CosmosErrorCode::BadGateway, + ), + ( + StatusCode::ServiceUnavailable, + "Service unavailable", + CosmosErrorCode::ServiceUnavailable, + ), + ]; + + for (status_code, message, expected_code) in http_test_cases { + let error_kind = ErrorKind::HttpResponse { + status: status_code, + error_code: None, + raw_response: None, + }; + let azure_error = + Error::with_error(error_kind, std::io::Error::other(message), message); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!( + cosmos_error.code, + expected_code, + "Failed for HTTP status {}: {}", + u16::from(status_code), + message + ); + free_non_static_error_message(cosmos_error.message); + } + } + + #[test] + fn test_error_mapping_fallback_without_http_status() { + let fallback_test_cases = vec![ + ( + ErrorKind::Other, + "Generic error without HTTP status", + CosmosErrorCode::UnknownError, + ), + ( + ErrorKind::Other, + "Some other error", + CosmosErrorCode::UnknownError, + ), + ]; + + for (kind, message, expected_code) in fallback_test_cases { + let azure_error = Error::new(kind, message); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!( + cosmos_error.code, expected_code, + "Failed for fallback case: {}", + message + ); + free_non_static_error_message(cosmos_error.message); + } + } + + #[test] + fn test_error_mapping_edge_cases() { + // Test edge cases and boundary conditions + let edge_cases = vec![ + // Case sensitivity (our logic checks for "Not Found" and "not found", but not "NOT FOUND") + ("NOT FOUND", CosmosErrorCode::DataConversion), + // Empty/whitespace + ("", CosmosErrorCode::DataConversion), + (" ", CosmosErrorCode::DataConversion), + // Real Azure error patterns + ( + "Entity with the specified id does not exist", + CosmosErrorCode::DataConversion, + ), + ( + "Not Found: Entity with the specified id does not exist", + CosmosErrorCode::NotFound, + ), + ]; + + for (message, expected_code) in edge_cases { + let azure_error = Error::new(ErrorKind::DataConversion, message); + let cosmos_error = convert_azure_error_native(&azure_error); + assert_eq!( + cosmos_error.code, expected_code, + "Failed for edge case: {}", + message + ); + free_non_static_error_message(cosmos_error.message); + } + } + + #[test] + fn test_http_status_to_error_code_mapping() { + assert_eq!(http_status_to_error_code(400), CosmosErrorCode::BadRequest); + assert_eq!( + http_status_to_error_code(401), + CosmosErrorCode::Unauthorized + ); + assert_eq!(http_status_to_error_code(403), CosmosErrorCode::Forbidden); + assert_eq!(http_status_to_error_code(404), CosmosErrorCode::NotFound); + assert_eq!(http_status_to_error_code(409), CosmosErrorCode::Conflict); + assert_eq!( + http_status_to_error_code(412), + CosmosErrorCode::PreconditionFailed + ); + assert_eq!( + http_status_to_error_code(408), + CosmosErrorCode::RequestTimeout + ); + assert_eq!( + http_status_to_error_code(429), + CosmosErrorCode::TooManyRequests + ); + assert_eq!( + http_status_to_error_code(500), + CosmosErrorCode::InternalServerError + ); + assert_eq!(http_status_to_error_code(502), CosmosErrorCode::BadGateway); + assert_eq!( + http_status_to_error_code(503), + CosmosErrorCode::ServiceUnavailable + ); + assert_eq!( + http_status_to_error_code(999), + CosmosErrorCode::UnknownError + ); + } + + #[test] + fn test_cosmos_error_memory_management() { + let mut error = CosmosError::new(CosmosErrorCode::BadRequest, "Test error".to_string()); + + // Message should be allocated + assert!(!error.message.is_null()); + + // Free should work without crashing + error.free_message(); + assert!(error.message.is_null()); + } + + #[test] + fn test_cosmos_error_static_message() { + let error = CosmosError::from_static_cstr( + CosmosErrorCode::InvalidArgument, + CSTR_INVALID_CHARS_ERROR, + ); + + // Static message should be set + assert!(!error.message.is_null()); + + // Should be recognized as static + assert!(is_static_error_message(error.message)); + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs index 6ad18528ce2..6d5095bd358 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs @@ -1,19 +1,30 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#![allow(clippy::not_unsafe_ptr_arg_deref, reason = "We do that a lot here.")] + use std::ffi::{c_char, CStr}; #[macro_use] mod macros; +pub mod blocking; +pub mod clients; +pub mod error; +pub mod string; + +pub use clients::*; + +// We just want this value to be present as a string in the compiled binary. +// But in order to prevent the compiler from optimizing it away, we expose it as a non-mangled static variable. /// cbindgen:ignore -#[no_mangle] // Necessary to prevent the compiler from stripping it when optimizing +#[no_mangle] pub static BUILD_IDENTIFIER: &CStr = c_str!(env!("BUILD_IDENTIFIER")); const VERSION: &CStr = c_str!(env!("CARGO_PKG_VERSION")); /// Returns a constant C string containing the version of the Cosmos Client library. #[no_mangle] -pub extern "C" fn cosmosclient_version() -> *const c_char { +pub extern "C" fn cosmos_version() -> *const c_char { VERSION.as_ptr() } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/macros.rs b/sdk/cosmos/azure_data_cosmos_native/src/macros.rs index a5ba7d43603..15ddfdff2d2 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/macros.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/macros.rs @@ -24,3 +24,10 @@ macro_rules! c_str { } }; } + +#[macro_export] +macro_rules! block_on { + ($async_expr:expr) => { + $crate::blocking::block_on($async_expr) + }; +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/string.rs b/sdk/cosmos/azure_data_cosmos_native/src/string.rs new file mode 100644 index 00000000000..e1cf0188992 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/string.rs @@ -0,0 +1,127 @@ +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +use crate::error::{CosmosError, CosmosErrorCode}; + +// Safe CString conversion helper that handles NUL bytes gracefully +pub fn safe_cstring_new(s: &str) -> CString { + CString::new(s).expect("FFI boundary strings must not contain NUL bytes") +} + +// Safe CString conversion that returns raw pointer and error code +pub fn safe_cstring_into_raw( + s: &str, + out_ptr: &mut *mut c_char, + out_error: &mut CosmosError, +) -> CosmosErrorCode { + let c_string = safe_cstring_new(s); + *out_ptr = c_string.into_raw(); + *out_error = CosmosError::success(); + CosmosErrorCode::Success +} + +pub fn parse_cstr<'a>( + ptr: *const c_char, + error_msg: &'static CStr, +) -> Result<&'a str, CosmosError> { + if ptr.is_null() { + return Err(CosmosError::from_static_cstr( + CosmosErrorCode::InvalidArgument, + error_msg, + )); + } + unsafe { CStr::from_ptr(ptr) } + .to_str() + .map_err(|_| CosmosError::from_static_cstr(CosmosErrorCode::InvalidArgument, error_msg)) +} + +/// Releases the memory associated with a C string obtained from Rust. +#[no_mangle] +pub extern "C" fn cosmos_string_free(ptr: *const c_char) { + if !ptr.is_null() { + unsafe { + let _ = CString::from_raw(ptr as *mut c_char); + } + } +} + +#[cfg(test)] +mod tests { + use crate::cosmos_version; + use crate::error::CSTR_INVALID_JSON; + + use super::*; + use std::ffi::CStr; + use std::ptr; + + #[test] + fn test_cosmos_version() { + let version_ptr = cosmos_version(); + assert!(!version_ptr.is_null()); + + let version_str = unsafe { CStr::from_ptr(version_ptr).to_str().unwrap() }; + + assert!(version_str.contains("cosmos-cpp-wrapper")); + assert!(version_str.contains("v0.1.0")); + + cosmos_string_free(version_ptr); + } + + #[test] + fn test_cosmos_string_free_null_safety() { + cosmos_string_free(ptr::null()); + } + + #[test] + fn test_safe_cstring_new() { + let result = safe_cstring_new("hello world"); + assert_eq!(result.to_str().unwrap(), "hello world"); + + let panic_result = std::panic::catch_unwind(|| { + safe_cstring_new("hello\0world"); + }); + assert!(panic_result.is_err()); + } + + #[test] + fn test_safe_cstring_into_raw() { + let mut ptr: *mut c_char = ptr::null_mut(); + let mut error = CosmosError::success(); + let code = safe_cstring_into_raw("test", &mut ptr, &mut error); + assert_eq!(code, CosmosErrorCode::Success); + assert!(!ptr.is_null()); + assert_eq!(error.code, CosmosErrorCode::Success); + + cosmos_string_free(ptr); + + let panic_result = std::panic::catch_unwind(|| { + let mut ptr2: *mut c_char = ptr::null_mut(); + let mut error2 = CosmosError::success(); + safe_cstring_into_raw("test\0fail", &mut ptr2, &mut error2); + }); + assert!(panic_result.is_err()); + } + + #[test] + fn test_static_vs_owned_error_messages() { + let static_error = + CosmosError::from_static_cstr(CosmosErrorCode::DataConversion, CSTR_INVALID_JSON); + assert_eq!(static_error.code, CosmosErrorCode::DataConversion); + assert!(!static_error.message.is_null()); + assert_eq!( + static_error.message, + CSTR_INVALID_JSON.as_ptr() as *mut c_char + ); + + let owned_error = CosmosError::new( + CosmosErrorCode::BadRequest, + "Dynamic error message".to_string(), + ); + assert_eq!(owned_error.code, CosmosErrorCode::BadRequest); + assert!(!owned_error.message.is_null()); + assert_ne!( + owned_error.message, + CSTR_INVALID_JSON.as_ptr() as *mut c_char + ); + } +} From d16e00dc41939b011e0405e57aaa11e2cfe27c1a Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Thu, 13 Nov 2025 23:54:46 +0000 Subject: [PATCH 02/21] initial crud c test --- Cargo.lock | 2 + .../azure_data_cosmos_native/CMakeLists.txt | 4 +- .../azure_data_cosmos_native/Cargo.toml | 6 +- .../c_tests/item_crud.c | 131 ++++++++++++++++++ .../c_tests/version.c | 4 +- .../azure_data_cosmos_native/src/blocking.rs | 5 +- .../azure_data_cosmos_native/src/lib.rs | 15 ++ 7 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c diff --git a/Cargo.lock b/Cargo.lock index 3537d80a75d..2eb2d0e54ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,6 +341,8 @@ dependencies = [ "futures", "serde_json", "tokio", + "tracing", + "tracing-subscriber", ] [[package]] diff --git a/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt b/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt index e1b8b7289b8..93183335f71 100644 --- a/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt +++ b/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt @@ -19,10 +19,12 @@ FetchContent_MakeAvailable(Corrosion) corrosion_import_crate( MANIFEST_PATH ./Cargo.toml CRATETYPES staticlib cdylib + PROFILE dev ) set(TEST_FILES - ./c_tests/version.c) + ./c_tests/version.c + ./c_tests/item_crud.c) foreach(test_file ${TEST_FILES}) get_filename_component(test_name ${test_file} NAME_WE) diff --git a/sdk/cosmos/azure_data_cosmos_native/Cargo.toml b/sdk/cosmos/azure_data_cosmos_native/Cargo.toml index e2c8065dc2e..10b0524d8be 100644 --- a/sdk/cosmos/azure_data_cosmos_native/Cargo.toml +++ b/sdk/cosmos/azure_data_cosmos_native/Cargo.toml @@ -19,10 +19,14 @@ tokio = { workspace = true, optional = true, features = ["rt-multi-thread", "mac serde_json = { workspace = true, features = ["raw_value"] } azure_core.workspace = true azure_data_cosmos = { path = "../azure_data_cosmos", features = [ "key_auth", "preview_query_engine" ] } +tracing.workspace = true +tracing-subscriber = { workspace = true, optional = true, features = ["fmt", "env-filter"] } [features] -default = ["tokio"] +default = ["tokio", "reqwest", "tracing"] tokio = ["dep:tokio"] +reqwest = ["azure_core/reqwest"] +tracing = ["dep:tracing-subscriber"] [build-dependencies] cbindgen = "0.29.0" diff --git a/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c new file mode 100644 index 00000000000..d84a653b0be --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include +#include +#include +#include "../include/azurecosmos.h" + +#define SENTINEL_VALUE "test-sentinel-12345" +#define ITEM_ID "test-item-id" +#define PARTITION_KEY_VALUE "test-partition" + +int main() { + cosmos_enable_tracing(); + + // Get environment variables + const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); + const char *key = getenv("AZURE_COSMOS_KEY"); + const char *database_name = getenv("AZURE_COSMOS_DATABASE"); + const char *container_name = getenv("AZURE_COSMOS_CONTAINER"); + + if (!endpoint || !key || !database_name || !container_name) { + printf("Error: Missing required environment variables.\n"); + printf("Required: AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_KEY, AZURE_COSMOS_DATABASE, AZURE_COSMOS_CONTAINER\n"); + return 1; + } + + printf("Running Cosmos DB item CRUD test...\n"); + printf("Endpoint: %s\n", endpoint); + printf("Database: %s\n", database_name); + printf("Container: %s\n", container_name); + + struct CosmosError error = {0}; + struct CosmosClient *client = NULL; + struct DatabaseClient *database = NULL; + struct ContainerClient *container = NULL; + char *read_json = NULL; + int result = 0; + + // Create Cosmos client + CosmosErrorCode code = cosmos_client_create(endpoint, key, &client, &error); + if (code != Success) { + printf("Failed to create Cosmos client: %s (code: %d)\n", error.message, error.code); + result = 1; + goto cleanup; + } + printf("✓ Created Cosmos client\n"); + + // Get database client + code = cosmos_client_database_client(client, database_name, &database, &error); + if (code != Success) { + printf("Failed to get database client: %s (code: %d)\n", error.message, error.code); + result = 1; + goto cleanup; + } + printf("✓ Got database client\n"); + + // Get container client + code = cosmos_database_container_client(database, container_name, &container, &error); + if (code != Success) { + printf("Failed to get container client: %s (code: %d)\n", error.message, error.code); + result = 1; + goto cleanup; + } + printf("✓ Got container client\n"); + + // Construct JSON document with sentinel value + char json_data[512]; + snprintf(json_data, sizeof(json_data), + "{\"id\":\"%s\",\"partitionKey\":\"%s\",\"name\":\"Test Document\",\"sentinel\":\"%s\",\"description\":\"This is a test document for CRUD operations\"}", + ITEM_ID, PARTITION_KEY_VALUE, SENTINEL_VALUE); + + printf("Upserting document: %s\n", json_data); + + // Upsert the item + code = cosmos_container_upsert_item(container, PARTITION_KEY_VALUE, json_data, &error); + if (code != Success) { + printf("Failed to upsert item: %s (code: %d)\n", error.message, error.code); + result = 1; + goto cleanup; + } + printf("✓ Upserted item successfully\n"); + + // Read the item back + code = cosmos_container_read_item(container, PARTITION_KEY_VALUE, ITEM_ID, &read_json, &error); + if (code != Success) { + printf("Failed to read item: %s (code: %d)\n", error.message, error.code); + result = 1; + goto cleanup; + } + printf("✓ Read item successfully\n"); + + printf("Read back JSON: %s\n", read_json); + + // Verify the sentinel value is present in the returned JSON + if (strstr(read_json, SENTINEL_VALUE) == NULL) { + printf("❌ FAIL: Sentinel value '%s' not found in returned JSON\n", SENTINEL_VALUE); + result = 1; + goto cleanup; + } + + // Verify the item ID is present + if (strstr(read_json, ITEM_ID) == NULL) { + printf("❌ FAIL: Item ID '%s' not found in returned JSON\n", ITEM_ID); + result = 1; + goto cleanup; + } + + printf("✓ All assertions passed!\n"); + printf("SUCCESS: Item CRUD test completed successfully.\n"); + +cleanup: +// // Free all allocated resources +// if (read_json) { +// cosmos_string_free(read_json); +// } +// if (container) { +// cosmos_container_free(container); +// } +// if (database) { +// cosmos_database_free(database); +// } +// if (client) { +// cosmos_client_free(client); +// } +// if (error.message) { +// cosmos_error_free(&error); +// } + + return result; +} diff --git a/sdk/cosmos/azure_data_cosmos_native/c_tests/version.c b/sdk/cosmos/azure_data_cosmos_native/c_tests/version.c index bf193c926d6..f8e8b79d17a 100644 --- a/sdk/cosmos/azure_data_cosmos_native/c_tests/version.c +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/version.c @@ -3,10 +3,10 @@ #include #include -#include "../include/cosmosclient.h" +#include "../include/azurecosmos.h" int main() { - const char *version = cosmosclient_version(); + const char *version = cosmos_version(); const char *header_version = COSMOSCLIENT_H_VERSION; printf("Cosmos Client Version: %s\n", version); printf("Header Version: %s\n", header_version); diff --git a/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs b/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs index 0e2e70c8e6f..7e397bcfe11 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs @@ -9,7 +9,10 @@ pub fn block_on(future: F) -> F::Output where F: Future, { - let runtime = RUNTIME.get_or_init(|| Runtime::new().expect("Failed to create Tokio runtime")); + let runtime = RUNTIME.get_or_init(|| { + tracing::trace!("Initializing blocking Tokio runtime"); + Runtime::new().expect("Failed to create Tokio runtime") + }); runtime.block_on(future) } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs index 6d5095bd358..9cd1f9c5208 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs @@ -28,3 +28,18 @@ const VERSION: &CStr = c_str!(env!("CARGO_PKG_VERSION")); pub extern "C" fn cosmos_version() -> *const c_char { VERSION.as_ptr() } + +/// Installs tracing listeners that output to stdout/stderr based on the `COSMOS_LOG` environment variable. +/// +/// Just calling this function isn't sufficient to get logging output. You must also set the `COSMOS_LOG` environment variable +/// to specify the desired log level and targets. See +/// for details on the syntax for this variable. +#[no_mangle] +#[cfg(feature = "tracing")] +pub extern "C" fn cosmos_enable_tracing() { + use tracing_subscriber::EnvFilter; + + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_env("COSMOS_LOG")) + .init(); +} From 2c901fd18d42ef78d9662aa6ccdf65ba4fe72d0a Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Fri, 14 Nov 2025 00:16:53 +0000 Subject: [PATCH 03/21] IT WORKS --- sdk/core/typespec/src/error/mod.rs | 6 +++++- sdk/core/typespec_client_core/src/http/clients/reqwest.rs | 5 +---- sdk/cosmos/azure_data_cosmos_native/Cargo.toml | 3 ++- .../src/clients/container_client.rs | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/sdk/core/typespec/src/error/mod.rs b/sdk/core/typespec/src/error/mod.rs index 5691c8ebfe9..3822bb672a6 100644 --- a/sdk/core/typespec/src/error/mod.rs +++ b/sdk/core/typespec/src/error/mod.rs @@ -304,7 +304,11 @@ impl Display for Error { Repr::Simple(kind) => std::fmt::Display::fmt(&kind, f), Repr::SimpleMessage(_, message) => f.write_str(message), Repr::Custom(Custom { error, .. }) => std::fmt::Display::fmt(&error, f), - Repr::CustomMessage(_, message) => f.write_str(message), + Repr::CustomMessage(Custom { error, .. }, message) => { + f.write_str(message)?; + f.write_str(": ")?; + std::fmt::Display::fmt(&error, f) + } } } } diff --git a/sdk/core/typespec_client_core/src/http/clients/reqwest.rs b/sdk/core/typespec_client_core/src/http/clients/reqwest.rs index cc4f1248a07..8388ff1477f 100644 --- a/sdk/core/typespec_client_core/src/http/clients/reqwest.rs +++ b/sdk/core/typespec_client_core/src/http/clients/reqwest.rs @@ -58,10 +58,7 @@ impl HttpClient for ::reqwest::Client { "performing request {method} '{}' with `reqwest`", url.sanitize(&DEFAULT_ALLOWED_QUERY_PARAMETERS) ); - let rsp = self - .execute(reqwest_request) - .await - .with_context(ErrorKind::Io, "failed to execute `reqwest` request")?; + let rsp = self.execute(reqwest_request).await.unwrap(); let status = rsp.status(); let headers = to_headers(rsp.headers()); diff --git a/sdk/cosmos/azure_data_cosmos_native/Cargo.toml b/sdk/cosmos/azure_data_cosmos_native/Cargo.toml index 10b0524d8be..0a93afd786b 100644 --- a/sdk/cosmos/azure_data_cosmos_native/Cargo.toml +++ b/sdk/cosmos/azure_data_cosmos_native/Cargo.toml @@ -23,9 +23,10 @@ tracing.workspace = true tracing-subscriber = { workspace = true, optional = true, features = ["fmt", "env-filter"] } [features] -default = ["tokio", "reqwest", "tracing"] +default = ["tokio", "reqwest", "reqwest_native_tls", "tracing"] tokio = ["dep:tokio"] reqwest = ["azure_core/reqwest"] +reqwest_native_tls = ["azure_core/reqwest_native_tls"] tracing = ["dep:tracing-subscriber"] [build-dependencies] diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs index e5394aa52b5..d83333e496d 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs @@ -87,6 +87,7 @@ fn upsert_item_inner( ) -> Result<(), CosmosError> { let raw_value: Box = serde_json::from_str(json_str)?; let pk = partition_key.to_string(); + tracing::trace!(raw_value = %raw_value.get(), pk = %pk, "Upserting item"); block_on(container.upsert_item(pk, raw_value, None))?; Ok(()) } From 2ba38979bf5273721ce4edee1e2fa21431cfbea9 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Fri, 14 Nov 2025 00:20:48 +0000 Subject: [PATCH 04/21] remove unnecessary changes to error --- sdk/core/typespec/src/error/mod.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sdk/core/typespec/src/error/mod.rs b/sdk/core/typespec/src/error/mod.rs index 3822bb672a6..5691c8ebfe9 100644 --- a/sdk/core/typespec/src/error/mod.rs +++ b/sdk/core/typespec/src/error/mod.rs @@ -304,11 +304,7 @@ impl Display for Error { Repr::Simple(kind) => std::fmt::Display::fmt(&kind, f), Repr::SimpleMessage(_, message) => f.write_str(message), Repr::Custom(Custom { error, .. }) => std::fmt::Display::fmt(&error, f), - Repr::CustomMessage(Custom { error, .. }, message) => { - f.write_str(message)?; - f.write_str(": ")?; - std::fmt::Display::fmt(&error, f) - } + Repr::CustomMessage(_, message) => f.write_str(message), } } } From 94c74cf85038a4549cc736d92a4995d6e56174af Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Fri, 14 Nov 2025 20:42:38 +0000 Subject: [PATCH 05/21] cbindgen fixes --- sdk/cosmos/azure_data_cosmos_native/build.rs | 50 +++++++-- .../c_tests/item_crud.c | 102 +++++++++++------- .../src/clients/container_client.rs | 35 +++--- .../src/clients/cosmos_client.rs | 52 ++++----- .../src/clients/database_client.rs | 33 +++--- .../src/clients/mod.rs | 81 -------------- 6 files changed, 163 insertions(+), 190 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/build.rs b/sdk/cosmos/azure_data_cosmos_native/build.rs index 4e9eae7ed3e..32fa026672e 100644 --- a/sdk/cosmos/azure_data_cosmos_native/build.rs +++ b/sdk/cosmos/azure_data_cosmos_native/build.rs @@ -3,6 +3,8 @@ // cSpell:ignore SOURCEVERSION, SOURCEBRANCH, BUILDID, BUILDNUMBER, COSMOSCLIENT, cosmosclient, libcosmosclient, cbindgen +use std::collections::HashMap; + fn main() { let build_id = format!( "$Id: {}, Version: {}, Commit: {}, Branch: {}, Build ID: {}, Build Number: {}, Timestamp: {}$", @@ -28,20 +30,46 @@ fn main() { .to_string(); header.push_str(&format!("// Build identifier: {}\n", build_id)); + let config = cbindgen::Config { + language: cbindgen::Language::C, + header: Some(header), + after_includes: Some( + "\n// Specifies the version of cosmosclient this header file was generated from.\n// This should match the version of libcosmosclient you are referencing.\n#define COSMOSCLIENT_H_VERSION \"".to_string() + + env!("CARGO_PKG_VERSION") + + "\"", + ), + cpp_compat: true, + parse: cbindgen::ParseConfig { + parse_deps: true, + include: Some(vec!["azure_data_cosmos".into()]), + ..Default::default() + }, + style: cbindgen::Style::Both, + enumeration: cbindgen::EnumConfig { + rename_variants: cbindgen::RenameRule::QualifiedScreamingSnakeCase, + ..Default::default() + }, + documentation_length: cbindgen::DocumentationLength::Full, + documentation_style: cbindgen::DocumentationStyle::Doxy, + export: cbindgen::ExportConfig { + prefix: Some("cosmos_".into()), + exclude: vec!["PartitionKeyValue".into()], + rename: HashMap::from([ + ("CosmosError".into(), "error".into()), + ("CosmosErrorCode".into(), "error_code".into()), + ("CosmosClient".into(), "client".into()), + ("DatabaseClient".into(), "database_client".into()), + ("ContainerClient".into(), "container_client".into()), + ]), + ..Default::default() + }, + ..Default::default() + }; + let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); cbindgen::Builder::new() .with_crate(crate_dir) - .with_language(cbindgen::Language::C) - .with_after_include(format!( - "\n// Specifies the version of cosmosclient this header file was generated from.\n// This should match the version of libcosmosclient you are referencing.\n#define COSMOSCLIENT_H_VERSION \"{}\"", - env!("CARGO_PKG_VERSION") - )) - .with_style(cbindgen::Style::Both) - .rename_item("CosmosClientHandle", "CosmosClient") - .rename_item("DatabaseClientHandle", "DatabaseClient") - .rename_item("ContainerClientHandle", "ContainerClient") - .with_cpp_compat(true) - .with_header(header) + .with_config(config) .generate() .expect("unable to generate bindings") .write_to_file("include/azurecosmos.h"); diff --git a/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c index d84a653b0be..df2398381d9 100644 --- a/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c @@ -4,65 +4,74 @@ #include #include #include +#include #include "../include/azurecosmos.h" #define SENTINEL_VALUE "test-sentinel-12345" #define ITEM_ID "test-item-id" #define PARTITION_KEY_VALUE "test-partition" +#define PARTITION_KEY_PATH "/partitionKey" int main() { cosmos_enable_tracing(); - // Get environment variables + // Get environment variables (only endpoint and key required) const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); const char *key = getenv("AZURE_COSMOS_KEY"); - const char *database_name = getenv("AZURE_COSMOS_DATABASE"); - const char *container_name = getenv("AZURE_COSMOS_CONTAINER"); - if (!endpoint || !key || !database_name || !container_name) { + if (!endpoint || !key) { printf("Error: Missing required environment variables.\n"); - printf("Required: AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_KEY, AZURE_COSMOS_DATABASE, AZURE_COSMOS_CONTAINER\n"); + printf("Required: AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_KEY\n"); return 1; } + // Generate unique database and container names using timestamp + time_t current_time = time(NULL); + char database_name[64]; + snprintf(database_name, sizeof(database_name), "auto-test-db-%ld", current_time); + printf("Running Cosmos DB item CRUD test...\n"); printf("Endpoint: %s\n", endpoint); printf("Database: %s\n", database_name); - printf("Container: %s\n", container_name); + printf("Container: test-container\n"); - struct CosmosError error = {0}; - struct CosmosClient *client = NULL; - struct DatabaseClient *database = NULL; - struct ContainerClient *container = NULL; + cosmos_error error = {0}; + cosmos_client *client = NULL; + cosmos_database_client *database = NULL; + cosmos_container_client *container = NULL; char *read_json = NULL; int result = 0; + int database_created = 0; + int container_created = 0; // Create Cosmos client - CosmosErrorCode code = cosmos_client_create(endpoint, key, &client, &error); - if (code != Success) { + cosmos_error_code code = cosmos_client_create_with_key(endpoint, key, &client, &error); + if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create Cosmos client: %s (code: %d)\n", error.message, error.code); result = 1; goto cleanup; } printf("✓ Created Cosmos client\n"); - // Get database client - code = cosmos_client_database_client(client, database_name, &database, &error); - if (code != Success) { - printf("Failed to get database client: %s (code: %d)\n", error.message, error.code); + // Create database + code = cosmos_client_create_database(client, database_name, &database, &error); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create database: %s (code: %d)\n", error.message, error.code); result = 1; goto cleanup; } - printf("✓ Got database client\n"); + database_created = 1; + printf("✓ Created database: %s\n", database_name); - // Get container client - code = cosmos_database_container_client(database, container_name, &container, &error); - if (code != Success) { - printf("Failed to get container client: %s (code: %d)\n", error.message, error.code); + // Create container with partition key + code = cosmos_database_create_container(database, "test-container", PARTITION_KEY_PATH, &container, &error); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create container: %s (code: %d)\n", error.message, error.code); result = 1; goto cleanup; } - printf("✓ Got container client\n"); + container_created = 1; + printf("✓ Created container: %s with partition key: %s\n", "test-container", PARTITION_KEY_PATH); // Construct JSON document with sentinel value char json_data[512]; @@ -74,7 +83,7 @@ int main() { // Upsert the item code = cosmos_container_upsert_item(container, PARTITION_KEY_VALUE, json_data, &error); - if (code != Success) { + if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to upsert item: %s (code: %d)\n", error.message, error.code); result = 1; goto cleanup; @@ -83,7 +92,7 @@ int main() { // Read the item back code = cosmos_container_read_item(container, PARTITION_KEY_VALUE, ITEM_ID, &read_json, &error); - if (code != Success) { + if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to read item: %s (code: %d)\n", error.message, error.code); result = 1; goto cleanup; @@ -110,22 +119,35 @@ int main() { printf("SUCCESS: Item CRUD test completed successfully.\n"); cleanup: -// // Free all allocated resources -// if (read_json) { -// cosmos_string_free(read_json); -// } -// if (container) { -// cosmos_container_free(container); -// } -// if (database) { -// cosmos_database_free(database); -// } -// if (client) { -// cosmos_client_free(client); -// } -// if (error.message) { -// cosmos_error_free(&error); -// } + // Clean up resources in reverse order, even on failure + if (read_json) { + cosmos_string_free(read_json); + } + + // Delete database (this will also delete the container) + if (database && database_created) { + printf("Deleting database: %s\n", database_name); + cosmos_error delete_error = {0}; + cosmos_error_code delete_code = cosmos_database_delete(database, &delete_error); + if (delete_code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to delete database: %s (code: %d)\n", delete_error.message, delete_error.code); + } else { + printf("✓ Deleted database successfully\n"); + } + } + + if (container) { + cosmos_container_free(container); + } + if (database) { + cosmos_database_free(database); + } + if (client) { + cosmos_client_free(client); + } + if (error.message) { + cosmos_error_free(&error); + } return result; } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs index d83333e496d..d9a3c117c41 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs @@ -8,12 +8,12 @@ use serde_json::value::RawValue; use crate::blocking::block_on; use crate::error::{self, marshal_result, CosmosError, CosmosErrorCode}; use crate::string::{parse_cstr, safe_cstring_into_raw}; -use crate::ContainerClientHandle; +/// Releases the memory associated with a [`ContainerClient`]. #[no_mangle] -pub extern "C" fn cosmos_container_free(container: *mut ContainerClientHandle) { +pub extern "C" fn cosmos_container_free(container: *mut ContainerClient) { if !container.is_null() { - unsafe { ContainerClientHandle::free_ptr(container) } + unsafe { drop(Box::from_raw(container)) } } } @@ -39,7 +39,7 @@ fn create_item_inner( /// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_container_create_item( - container: *const ContainerClientHandle, + container: *const ContainerClient, partition_key: *const c_char, json_data: *const c_char, out_error: *mut CosmosError, @@ -49,7 +49,7 @@ pub extern "C" fn cosmos_container_create_item( return CosmosErrorCode::InvalidArgument; } - let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + let container_handle = unsafe { &*container }; let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { Ok(s) => s, @@ -87,7 +87,6 @@ fn upsert_item_inner( ) -> Result<(), CosmosError> { let raw_value: Box = serde_json::from_str(json_str)?; let pk = partition_key.to_string(); - tracing::trace!(raw_value = %raw_value.get(), pk = %pk, "Upserting item"); block_on(container.upsert_item(pk, raw_value, None))?; Ok(()) } @@ -101,7 +100,7 @@ fn upsert_item_inner( /// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_container_upsert_item( - container: *const ContainerClientHandle, + container: *const ContainerClient, partition_key: *const c_char, json_data: *const c_char, out_error: *mut CosmosError, @@ -111,7 +110,7 @@ pub extern "C" fn cosmos_container_upsert_item( return CosmosErrorCode::InvalidArgument; } - let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + let container_handle = unsafe { &*container }; let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { Ok(s) => s, @@ -165,7 +164,7 @@ fn read_item_inner( /// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_container_read_item( - container: *const ContainerClientHandle, + container: *const ContainerClient, partition_key: *const c_char, item_id: *const c_char, out_json: *mut *mut c_char, @@ -180,7 +179,7 @@ pub extern "C" fn cosmos_container_read_item( return CosmosErrorCode::InvalidArgument; } - let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + let container_handle = unsafe { &*container }; let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { Ok(s) => s, @@ -235,7 +234,7 @@ fn replace_item_inner( /// * `out_error` - Output parameter that will receive error information if the function fails #[no_mangle] pub extern "C" fn cosmos_container_replace_item( - container: *const ContainerClientHandle, + container: *const ContainerClient, partition_key: *const c_char, item_id: *const c_char, json_data: *const c_char, @@ -250,7 +249,7 @@ pub extern "C" fn cosmos_container_replace_item( return CosmosErrorCode::InvalidArgument; } - let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + let container_handle = unsafe { &*container }; let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { Ok(s) => s, @@ -311,7 +310,7 @@ fn delete_item_inner( /// * `out_error` - Output parameter that will receive error information if the function fails #[no_mangle] pub extern "C" fn cosmos_container_delete_item( - container: *const ContainerClientHandle, + container: *const ContainerClient, partition_key: *const c_char, item_id: *const c_char, out_error: *mut CosmosError, @@ -320,7 +319,7 @@ pub extern "C" fn cosmos_container_delete_item( return CosmosErrorCode::InvalidArgument; } - let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + let container_handle = unsafe { &*container }; let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { Ok(s) => s, @@ -366,7 +365,7 @@ fn read_container_inner(container: &ContainerClient) -> Result CosmosErrorCode { @@ -374,7 +373,7 @@ pub extern "C" fn cosmos_container_read( return CosmosErrorCode::InvalidArgument; } - let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + let container_handle = unsafe { &*container }; marshal_result( read_container_inner(container_handle), @@ -417,7 +416,7 @@ fn query_items_inner( /// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_container_query_items( - container: *const ContainerClientHandle, + container: *const ContainerClient, query: *const c_char, partition_key: *const c_char, out_json: *mut *mut c_char, @@ -427,7 +426,7 @@ pub extern "C" fn cosmos_container_query_items( return CosmosErrorCode::InvalidArgument; } - let container_handle = unsafe { ContainerClientHandle::unwrap_ptr(container) }; + let container_handle = unsafe { &*container }; let query_str = match parse_cstr(query, error::CSTR_INVALID_QUERY) { Ok(s) => s, diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs index 1f8e6a6392a..2febe6ced72 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs @@ -9,7 +9,6 @@ use futures::TryStreamExt; use crate::blocking::block_on; use crate::error::{self, marshal_result, CosmosError, CosmosErrorCode}; use crate::string::{parse_cstr, safe_cstring_into_raw}; -use crate::{CosmosClientHandle, DatabaseClientHandle}; fn create_client_inner( endpoint_str: &str, @@ -29,17 +28,17 @@ fn create_client_inner( /// # Arguments /// * `endpoint` - The Cosmos DB account endpoint, as a nul-terminated C string. /// * `key` - The Cosmos DB account key, as a nul-terminated C string -/// * `out_client` - Output parameter that will receive a pointer to the created CosmosClientHandle. +/// * `out_client` - Output parameter that will receive a pointer to the created CosmosClient. /// * `out_error` - Output parameter that will receive error information if the function fails. /// /// # Returns /// * Returns [`CosmosErrorCode::Success`] on success. /// * Returns [`CosmosErrorCode::InvalidArgument`] if any input pointer is null or if the input strings are invalid. #[no_mangle] -pub extern "C" fn cosmos_client_create( +pub extern "C" fn cosmos_client_create_with_key( endpoint: *const c_char, key: *const c_char, - out_client: *mut *mut CosmosClientHandle, + out_client: *mut *mut CosmosClient, out_error: *mut CosmosError, ) -> CosmosErrorCode { if endpoint.is_null() || key.is_null() || out_client.is_null() || out_error.is_null() { @@ -72,16 +71,16 @@ pub extern "C" fn cosmos_client_create( create_client_inner(endpoint_str, key_str), out_error, |handle| unsafe { - *out_client = CosmosClientHandle::wrap_ptr(handle); + *out_client = Box::into_raw(handle); }, ) } /// Releases the memory associated with a [`CosmosClient`]. #[no_mangle] -pub extern "C" fn cosmos_client_free(client: *mut CosmosClientHandle) { +pub extern "C" fn cosmos_client_free(client: *mut CosmosClient) { if !client.is_null() { - unsafe { CosmosClientHandle::free_ptr(client) } + unsafe { drop(Box::from_raw(client)) } } } @@ -102,16 +101,16 @@ fn database_client_inner( /// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_client_database_client( - client: *const CosmosClientHandle, + client: *const CosmosClient, database_id: *const c_char, - out_database: *mut *mut DatabaseClientHandle, + out_database: *mut *mut DatabaseClient, out_error: *mut CosmosError, ) -> CosmosErrorCode { if client.is_null() || database_id.is_null() || out_database.is_null() || out_error.is_null() { return CosmosErrorCode::InvalidArgument; } - let client_handle = unsafe { CosmosClientHandle::unwrap_ptr(client) }; + let client_handle = unsafe { &*client }; let database_id_str = match parse_cstr(database_id, error::CSTR_INVALID_DATABASE_ID) { Ok(s) => s, @@ -128,7 +127,7 @@ pub extern "C" fn cosmos_client_database_client( database_client_inner(client_handle, database_id_str), out_error, |db_handle| unsafe { - *out_database = DatabaseClientHandle::wrap_ptr(db_handle); + *out_database = Box::into_raw(db_handle); }, ) } @@ -154,7 +153,7 @@ fn query_databases_inner(client: &CosmosClient, query_str: &str) -> Result s, @@ -205,16 +204,16 @@ fn create_database_inner( /// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_client_create_database( - client: *const CosmosClientHandle, + client: *const CosmosClient, database_id: *const c_char, out_database: *mut *mut DatabaseClient, out_error: *mut CosmosError, ) -> CosmosErrorCode { - if client.is_null() || database_id.is_null() || out_database.is_null() || out_error.is_null() { + if client.is_null() || database_id.is_null() || out_database.is_null() { return CosmosErrorCode::InvalidArgument; } - let client_handle = unsafe { CosmosClientHandle::unwrap_ptr(client) }; + let client_handle = unsafe { &*client }; let database_id_str = match parse_cstr(database_id, error::CSTR_INVALID_DATABASE_ID) { Ok(s) => s, @@ -250,11 +249,15 @@ mod tests { let endpoint = CString::new("https://test.documents.azure.com") .expect("test string should not contain NUL"); let key = CString::new("test-key").expect("test string should not contain NUL"); - let mut client_ptr: *mut CosmosClientHandle = ptr::null_mut(); + let mut client_ptr: *mut CosmosClient = ptr::null_mut(); let mut error = CosmosError::success(); - let result = - cosmos_client_create(endpoint.as_ptr(), key.as_ptr(), &mut client_ptr, &mut error); + let result = cosmos_client_create_with_key( + endpoint.as_ptr(), + key.as_ptr(), + &mut client_ptr, + &mut error, + ); assert_eq!(result, CosmosErrorCode::Success); assert!(!client_ptr.is_null()); @@ -265,10 +268,11 @@ mod tests { #[test] fn test_cosmos_client_create_null_params() { - let mut client_ptr: *mut CosmosClientHandle = ptr::null_mut(); + let mut client_ptr: *mut CosmosClient = ptr::null_mut(); let mut error = CosmosError::success(); - let result = cosmos_client_create(ptr::null(), ptr::null(), &mut client_ptr, &mut error); + let result = + cosmos_client_create_with_key(ptr::null(), ptr::null(), &mut client_ptr, &mut error); assert_eq!(result, CosmosErrorCode::InvalidArgument); assert!(client_ptr.is_null()); @@ -281,11 +285,11 @@ mod tests { let key = CString::new("test-key").expect("test string should not contain NUL"); let db_id = CString::new("test-db").expect("test string should not contain NUL"); - let mut client_ptr: *mut CosmosClientHandle = ptr::null_mut(); - let mut db_ptr: *mut DatabaseClientHandle = ptr::null_mut(); + let mut client_ptr: *mut CosmosClient = ptr::null_mut(); + let mut db_ptr: *mut DatabaseClient = ptr::null_mut(); let mut error = CosmosError::success(); - cosmos_client_create( + cosmos_client_create_with_key( endpoint.as_ptr(), key.as_ptr(), &raw mut client_ptr, diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs index f6d2beaf7d8..5a270ee0d76 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs @@ -8,13 +8,12 @@ use futures::TryStreamExt; use crate::blocking::block_on; use crate::error::{self, marshal_result, CosmosError, CosmosErrorCode}; use crate::string::{parse_cstr, safe_cstring_into_raw}; -use crate::{ContainerClientHandle, DatabaseClientHandle}; /// Releases the memory associated with a [`DatabaseClient`]. #[no_mangle] -pub extern "C" fn cosmos_database_free(database: *mut DatabaseClientHandle) { +pub extern "C" fn cosmos_database_free(database: *mut DatabaseClient) { if !database.is_null() { - unsafe { DatabaseClientHandle::free_ptr(database) } + unsafe { drop(Box::from_raw(database)) } } } @@ -23,7 +22,7 @@ fn container_client_inner( container_id_str: &str, ) -> Result, CosmosError> { let container_client = database.container_client(container_id_str); - Ok(Box::new(container_client.into())) + Ok(Box::new(container_client)) } /// Retrieves a pointer to a [`ContainerClient`] for the specified container ID within the given database. @@ -35,9 +34,9 @@ fn container_client_inner( /// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_database_container_client( - database: *const DatabaseClientHandle, + database: *const DatabaseClient, container_id: *const c_char, - out_container: *mut *mut ContainerClientHandle, + out_container: *mut *mut ContainerClient, out_error: *mut CosmosError, ) -> CosmosErrorCode { if database.is_null() @@ -48,7 +47,7 @@ pub extern "C" fn cosmos_database_container_client( return CosmosErrorCode::InvalidArgument; } - let database_handle = unsafe { DatabaseClientHandle::unwrap_ptr(database) }; + let database_handle = unsafe { &*database }; let container_id_str = match parse_cstr(container_id, error::CSTR_INVALID_CONTAINER_ID) { Ok(s) => s, @@ -65,7 +64,7 @@ pub extern "C" fn cosmos_database_container_client( container_client_inner(database_handle, container_id_str), out_error, |container_handle| unsafe { - *out_container = ContainerClientHandle::wrap_ptr(container_handle); + *out_container = Box::into_raw(container_handle); }, ) } @@ -268,9 +267,11 @@ pub extern "C" fn cosmos_database_query_containers( #[cfg(test)] mod tests { + use azure_data_cosmos::CosmosClient; + use crate::{ - cosmos_client_create, cosmos_client_database_client, cosmos_client_free, - cosmos_container_free, ContainerClientHandle, CosmosClientHandle, + cosmos_client_create_with_key, cosmos_client_database_client, cosmos_client_free, + cosmos_container_free, }; use super::*; @@ -278,7 +279,7 @@ mod tests { #[test] fn test_database_container_client_null_params() { - let mut container_ptr: *mut ContainerClientHandle = ptr::null_mut(); + let mut container_ptr: *mut ContainerClient = ptr::null_mut(); let mut error = CosmosError::success(); let result = cosmos_database_container_client( @@ -334,7 +335,7 @@ mod tests { error = CosmosError::success(); // Test with valid database but null container_id - // Note: We can't create a real DatabaseClientHandle without Azure SDK setup, + // Note: We can't create a real DatabaseClient without Azure SDK setup, // so we test the null parameter validation only let result = cosmos_database_create_container( ptr::null(), @@ -367,12 +368,12 @@ mod tests { let key = CString::new("test-key").expect("test string should not contain NUL"); let db_id = CString::new("test-db").expect("test string should not contain NUL"); - let mut client_ptr: *mut CosmosClientHandle = ptr::null_mut(); - let mut db_ptr: *mut DatabaseClientHandle = ptr::null_mut(); - let mut container_ptr: *mut ContainerClientHandle = ptr::null_mut(); + let mut client_ptr: *mut CosmosClient = ptr::null_mut(); + let mut db_ptr: *mut DatabaseClient = ptr::null_mut(); + let mut container_ptr: *mut ContainerClient = ptr::null_mut(); let mut error = CosmosError::success(); - cosmos_client_create(endpoint.as_ptr(), key.as_ptr(), &mut client_ptr, &mut error); + cosmos_client_create_with_key(endpoint.as_ptr(), key.as_ptr(), &mut client_ptr, &mut error); assert!(!client_ptr.is_null()); cosmos_client_database_client(client_ptr, db_id.as_ptr(), &mut db_ptr, &mut error); diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs index 79173a25a49..413b847dfc7 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs @@ -10,84 +10,3 @@ pub mod database_client; pub use container_client::*; pub use cosmos_client::*; pub use database_client::*; - -// Below are opaque handle types for FFI. -// These types are used as the type names for pointers passed across the FFI boundary, but they are zero-sized in Rust. -// The actual data is stored in the corresponding Azure SDK for Rust types and we provide functions to transmute pointers from these handle types to the actual types. -// This pattern allows cbindgen to see the types for generating headers, while keeping the actual implementation details hidden from the C side. -// -// You might be tempted to use a macro to generate them, but cbindgen does not handle macros well, so we define them manually. - -pub struct CosmosClientHandle; - -impl CosmosClientHandle { - pub unsafe fn unwrap_ptr<'a>( - ptr: *const CosmosClientHandle, - ) -> &'a azure_data_cosmos::CosmosClient { - (ptr as *const azure_data_cosmos::CosmosClient) - .as_ref() - .unwrap() - } - - pub unsafe fn wrap_ptr(value: Box) -> *mut CosmosClientHandle { - Box::into_raw(value) as *mut CosmosClientHandle - } - - pub unsafe fn free_ptr(ptr: *mut CosmosClientHandle) { - if !ptr.is_null() { - drop(Box::from_raw(ptr as *mut azure_data_cosmos::CosmosClient)); - } - } -} - -pub struct DatabaseClientHandle; - -impl DatabaseClientHandle { - pub unsafe fn unwrap_ptr<'a>( - ptr: *const DatabaseClientHandle, - ) -> &'a azure_data_cosmos::clients::DatabaseClient { - (ptr as *const azure_data_cosmos::clients::DatabaseClient) - .as_ref() - .unwrap() - } - - pub unsafe fn wrap_ptr( - value: Box, - ) -> *mut DatabaseClientHandle { - Box::into_raw(value) as *mut DatabaseClientHandle - } - - pub unsafe fn free_ptr(ptr: *mut DatabaseClientHandle) { - if !ptr.is_null() { - drop(Box::from_raw( - ptr as *mut azure_data_cosmos::clients::DatabaseClient, - )); - } - } -} - -pub struct ContainerClientHandle; - -impl ContainerClientHandle { - pub unsafe fn unwrap_ptr<'a>( - ptr: *const ContainerClientHandle, - ) -> &'a azure_data_cosmos::clients::ContainerClient { - (ptr as *const azure_data_cosmos::clients::ContainerClient) - .as_ref() - .unwrap() - } - - pub unsafe fn wrap_ptr( - value: Box, - ) -> *mut ContainerClientHandle { - Box::into_raw(value) as *mut ContainerClientHandle - } - - pub unsafe fn free_ptr(ptr: *mut ContainerClientHandle) { - if !ptr.is_null() { - drop(Box::from_raw( - ptr as *mut azure_data_cosmos::clients::ContainerClient, - )); - } - } -} From 498ef09ec46332d5f72e7ef092416ad65965e40a Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 00:24:16 +0000 Subject: [PATCH 06/21] introduce runtime/call context --- .../azure_data_cosmos_native/CMakeLists.txt | 4 +- sdk/cosmos/azure_data_cosmos_native/build.rs | 2 + .../c_tests/item_crud.c | 51 +- .../src/clients/container_client.rs | 514 +++----------- .../src/clients/cosmos_client.rs | 319 ++------- .../src/clients/database_client.rs | 376 ++-------- .../azure_data_cosmos_native/src/context.rs | 210 ++++++ .../azure_data_cosmos_native/src/error.rs | 672 ++++-------------- .../azure_data_cosmos_native/src/lib.rs | 7 +- .../azure_data_cosmos_native/src/macros.rs | 33 - .../src/runtime/mod.rs | 56 ++ .../src/runtime/tokio.rs | 33 + .../azure_data_cosmos_native/src/string.rs | 134 +--- 13 files changed, 739 insertions(+), 1672 deletions(-) create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/context.rs delete mode 100644 sdk/cosmos/azure_data_cosmos_native/src/macros.rs create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs diff --git a/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt b/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt index 93183335f71..0166b131659 100644 --- a/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt +++ b/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt @@ -1,8 +1,9 @@ # cSpell:ignore cosmosctest CRATETYPES endforeach -project(cosmosctest C) cmake_minimum_required(VERSION 4.1) +project(cosmosctest C) + # CMake automatically uses this option, but we should define it. option(BUILD_SHARED_LIBS "Build using shared libraries" ON) @@ -18,7 +19,6 @@ FetchContent_MakeAvailable(Corrosion) corrosion_import_crate( MANIFEST_PATH ./Cargo.toml - CRATETYPES staticlib cdylib PROFILE dev ) diff --git a/sdk/cosmos/azure_data_cosmos_native/build.rs b/sdk/cosmos/azure_data_cosmos_native/build.rs index 32fa026672e..ac04bd930ec 100644 --- a/sdk/cosmos/azure_data_cosmos_native/build.rs +++ b/sdk/cosmos/azure_data_cosmos_native/build.rs @@ -55,6 +55,8 @@ fn main() { prefix: Some("cosmos_".into()), exclude: vec!["PartitionKeyValue".into()], rename: HashMap::from([ + ("RuntimeContext".into(), "runtime_context".into()), + ("CallContext".into(), "call_context".into()), ("CosmosError".into(), "error".into()), ("CosmosErrorCode".into(), "error_code".into()), ("CosmosClient".into(), "client".into()), diff --git a/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c index df2398381d9..76c50d359da 100644 --- a/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c @@ -12,6 +12,17 @@ #define PARTITION_KEY_VALUE "test-partition" #define PARTITION_KEY_PATH "/partitionKey" +void display_error(const cosmos_error *error) { + printf("Error Code: %d\n", error->code); + if (error->message) { + printf("Error Message: %s\n", error->message); + } + if (error->detail) { + printf("Error Details: %s\n", error->detail); + cosmos_string_free(error->detail); + } +} + int main() { cosmos_enable_tracing(); @@ -35,28 +46,36 @@ int main() { printf("Database: %s\n", database_name); printf("Container: test-container\n"); - cosmos_error error = {0}; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return 1; + } + cosmos_call_context ctx; + ctx.runtime_context = runtime; + ctx.include_error_details = true; + cosmos_client *client = NULL; cosmos_database_client *database = NULL; cosmos_container_client *container = NULL; - char *read_json = NULL; + const char *read_json = NULL; int result = 0; int database_created = 0; int container_created = 0; // Create Cosmos client - cosmos_error_code code = cosmos_client_create_with_key(endpoint, key, &client, &error); + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { - printf("Failed to create Cosmos client: %s (code: %d)\n", error.message, error.code); + display_error(&ctx.error); result = 1; goto cleanup; } printf("✓ Created Cosmos client\n"); // Create database - code = cosmos_client_create_database(client, database_name, &database, &error); + code = cosmos_client_create_database(&ctx, client, database_name, &database); if (code != COSMOS_ERROR_CODE_SUCCESS) { - printf("Failed to create database: %s (code: %d)\n", error.message, error.code); + display_error(&ctx.error); result = 1; goto cleanup; } @@ -64,9 +83,9 @@ int main() { printf("✓ Created database: %s\n", database_name); // Create container with partition key - code = cosmos_database_create_container(database, "test-container", PARTITION_KEY_PATH, &container, &error); + code = cosmos_database_create_container(&ctx, database, "test-container", PARTITION_KEY_PATH, &container); if (code != COSMOS_ERROR_CODE_SUCCESS) { - printf("Failed to create container: %s (code: %d)\n", error.message, error.code); + display_error(&ctx.error); result = 1; goto cleanup; } @@ -82,18 +101,18 @@ int main() { printf("Upserting document: %s\n", json_data); // Upsert the item - code = cosmos_container_upsert_item(container, PARTITION_KEY_VALUE, json_data, &error); + code = cosmos_container_upsert_item(&ctx, container, PARTITION_KEY_VALUE, json_data); if (code != COSMOS_ERROR_CODE_SUCCESS) { - printf("Failed to upsert item: %s (code: %d)\n", error.message, error.code); + display_error(&ctx.error); result = 1; goto cleanup; } printf("✓ Upserted item successfully\n"); // Read the item back - code = cosmos_container_read_item(container, PARTITION_KEY_VALUE, ITEM_ID, &read_json, &error); + code = cosmos_container_read_item(&ctx, container, PARTITION_KEY_VALUE, ITEM_ID, &read_json); if (code != COSMOS_ERROR_CODE_SUCCESS) { - printf("Failed to read item: %s (code: %d)\n", error.message, error.code); + display_error(&ctx.error); result = 1; goto cleanup; } @@ -127,10 +146,9 @@ int main() { // Delete database (this will also delete the container) if (database && database_created) { printf("Deleting database: %s\n", database_name); - cosmos_error delete_error = {0}; - cosmos_error_code delete_code = cosmos_database_delete(database, &delete_error); + cosmos_error_code delete_code = cosmos_database_delete(&ctx, database); if (delete_code != COSMOS_ERROR_CODE_SUCCESS) { - printf("Failed to delete database: %s (code: %d)\n", delete_error.message, delete_error.code); + display_error(&ctx.error); } else { printf("✓ Deleted database successfully\n"); } @@ -145,9 +163,6 @@ int main() { if (client) { cosmos_client_free(client); } - if (error.message) { - cosmos_error_free(&error); - } return result; } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs index d9a3c117c41..832a5ffad81 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs @@ -1,3 +1,4 @@ +use std::ffi::CString; use std::os::raw::c_char; use azure_data_cosmos::clients::ContainerClient; @@ -5,9 +6,9 @@ use azure_data_cosmos::query::Query; use futures::TryStreamExt; use serde_json::value::RawValue; -use crate::blocking::block_on; -use crate::error::{self, marshal_result, CosmosError, CosmosErrorCode}; -use crate::string::{parse_cstr, safe_cstring_into_raw}; +use crate::context::CallContext; +use crate::error::{self, CosmosErrorCode, Error}; +use crate::string::parse_cstr; /// Releases the memory associated with a [`ContainerClient`]. #[no_mangle] @@ -17,506 +18,209 @@ pub extern "C" fn cosmos_container_free(container: *mut ContainerClient) { } } -fn create_item_inner( - container: &ContainerClient, - partition_key: &str, - json_str: &str, -) -> Result<(), CosmosError> { - let raw_value = RawValue::from_string(json_str.to_string())?; - - // Clone for async - Azure SDK needs owned String for async block - let pk = partition_key.to_string(); - block_on(container.create_item(pk, raw_value, None))?; - Ok(()) -} - /// Creates a new item in the specified container. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `container` - Pointer to the `ContainerClient`. /// * `partition_key` - The partition key value as a nul-terminated C string. /// * `json_data` - The item data as a raw JSON nul-terminated C string. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_container_create_item( + ctx: *mut CallContext, container: *const ContainerClient, partition_key: *const c_char, json_data: *const c_char, - out_error: *mut CosmosError, ) -> CosmosErrorCode { - if container.is_null() || partition_key.is_null() || json_data.is_null() || out_error.is_null() - { - return CosmosErrorCode::InvalidArgument; - } - - let container_handle = unsafe { &*container }; - - let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - let json_str = match parse_cstr(json_data, error::CSTR_INVALID_JSON_DATA) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - marshal_result( - create_item_inner(container_handle, partition_key_str, json_str), - out_error, - |_| {}, - ) -} - -fn upsert_item_inner( - container: &ContainerClient, - partition_key: &str, - json_str: &str, -) -> Result<(), CosmosError> { - let raw_value: Box = serde_json::from_str(json_str)?; - let pk = partition_key.to_string(); - block_on(container.upsert_item(pk, raw_value, None))?; - Ok(()) + context!(ctx).run_async(async { + let container = unsafe { &*container }; + let partition_key = + parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string(); + let json = parse_cstr(json_data, error::messages::INVALID_JSON)?.to_string(); + let raw_value = RawValue::from_string(json)?; + container + .create_item(partition_key, raw_value, None) + .await?; + Ok(()) + }) } /// Upserts an item in the specified container. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `container` - Pointer to the `ContainerClient`. /// * `partition_key` - The partition key value as a nul-terminated C string. /// * `json_data` - The item data as a raw JSON nul-terminated C string. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_container_upsert_item( + ctx: *mut CallContext, container: *const ContainerClient, partition_key: *const c_char, json_data: *const c_char, - out_error: *mut CosmosError, ) -> CosmosErrorCode { - if container.is_null() || partition_key.is_null() || json_data.is_null() || out_error.is_null() - { - return CosmosErrorCode::InvalidArgument; - } - - let container_handle = unsafe { &*container }; - - let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - let json_str = match parse_cstr(json_data, error::CSTR_INVALID_JSON_DATA) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - marshal_result( - upsert_item_inner(container_handle, partition_key_str, json_str), - out_error, - |_| {}, - ) -} - -// Inner function: Returns JSON string -fn read_item_inner( - container: &ContainerClient, - partition_key: &str, - item_id: &str, -) -> Result { - let pk = partition_key.to_string(); - - // The type we read into doesn't matter, because we'll extract the raw string instead of deserializing. - let response = block_on(container.read_item::<()>(pk, item_id, None))?; - Ok(response.into_body().into_string()?) + context!(ctx).run_async(async { + let container = unsafe { &*container }; + let partition_key = + parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string(); + let json = parse_cstr(json_data, error::messages::INVALID_JSON)?.to_string(); + let raw_value = RawValue::from_string(json)?; + container + .upsert_item(partition_key, raw_value, None) + .await?; + Ok(()) + }) } /// Reads an item from the specified container. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `container` - Pointer to the `ContainerClient`. /// * `partition_key` - The partition key value as a nul-terminated C string. /// * `item_id` - The ID of the item to read as a nul-terminated C string. /// * `out_json` - Output parameter that will receive the item data as a raw JSON nul-terminated C string. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_container_read_item( + ctx: *mut CallContext, container: *const ContainerClient, partition_key: *const c_char, item_id: *const c_char, - out_json: *mut *mut c_char, - out_error: *mut CosmosError, + out_json: *mut *const c_char, ) -> CosmosErrorCode { - if container.is_null() - || partition_key.is_null() - || item_id.is_null() - || out_json.is_null() - || out_error.is_null() - { - return CosmosErrorCode::InvalidArgument; - } - - let container_handle = unsafe { &*container }; - - let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - let item_id_str = match parse_cstr(item_id, error::CSTR_INVALID_ITEM_ID) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - marshal_result( - read_item_inner(container_handle, partition_key_str, item_id_str), - out_error, - |json_string| unsafe { - let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); - }, - ) -} - -fn replace_item_inner( - container: &ContainerClient, - partition_key: &str, - item_id: &str, - json_str: &str, -) -> Result<(), CosmosError> { - let raw_value = RawValue::from_string(json_str.to_string())?; - let pk = partition_key.to_string(); - block_on(container.replace_item(pk, item_id, raw_value, None))?; - Ok(()) + context!(ctx).run_async_with_output(out_json, async { + let container = unsafe { &*container }; + let partition_key = + parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string(); + let item_id = parse_cstr(item_id, error::messages::INVALID_ITEM_ID)?; + + // We can specify '()' as the type parameter because we only want the raw JSON string. + let response = container + .read_item::<()>(partition_key, item_id, None) + .await?; + let body = response.into_body().into_string()?; + + Ok(CString::new(body)?) + }) } /// Replaces an existing item in the specified container. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `container` - Pointer to the `ContainerClient`. /// * `partition_key` - The partition key value as a nul-terminated C string. /// * `item_id` - The ID of the item to replace as a nul-terminated C string. /// * `json_data` - The new item data as a raw JSON nul-terminated C string. -/// * `out_error` - Output parameter that will receive error information if the function fails #[no_mangle] pub extern "C" fn cosmos_container_replace_item( + ctx: *mut CallContext, container: *const ContainerClient, partition_key: *const c_char, item_id: *const c_char, json_data: *const c_char, - out_error: *mut CosmosError, ) -> CosmosErrorCode { - if container.is_null() - || partition_key.is_null() - || item_id.is_null() - || json_data.is_null() - || out_error.is_null() - { - return CosmosErrorCode::InvalidArgument; - } - - let container_handle = unsafe { &*container }; - - let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - let item_id_str = match parse_cstr(item_id, error::CSTR_INVALID_ITEM_ID) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - let json_str = match parse_cstr(json_data, error::CSTR_INVALID_JSON_DATA) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - marshal_result( - replace_item_inner(container_handle, partition_key_str, item_id_str, json_str), - out_error, - |_| {}, - ) -} - -fn delete_item_inner( - container: &ContainerClient, - partition_key: &str, - item_id: &str, -) -> Result<(), CosmosError> { - let pk = partition_key.to_string(); - block_on(container.delete_item(pk, item_id, None))?; - Ok(()) + context!(ctx).run_async(async { + let container = unsafe { &*container }; + let partition_key = + parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string(); + let item_id = parse_cstr(item_id, error::messages::INVALID_ITEM_ID)?; + let json = parse_cstr(json_data, error::messages::INVALID_JSON)?.to_string(); + + let raw_value = RawValue::from_string(json)?; + let pk = partition_key.to_string(); + container.replace_item(pk, item_id, raw_value, None).await?; + Ok(()) + }) } /// Deletes an item from the specified container. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `container` - Pointer to the `ContainerClient`. /// * `partition_key` - The partition key value as a nul-terminated C string. /// * `item_id` - The ID of the item to delete as a nul-terminated C string. -/// * `out_error` - Output parameter that will receive error information if the function fails #[no_mangle] pub extern "C" fn cosmos_container_delete_item( + ctx: *mut CallContext, container: *const ContainerClient, partition_key: *const c_char, item_id: *const c_char, - out_error: *mut CosmosError, ) -> CosmosErrorCode { - if container.is_null() || partition_key.is_null() || item_id.is_null() || out_error.is_null() { - return CosmosErrorCode::InvalidArgument; - } - - let container_handle = unsafe { &*container }; - - let partition_key_str = match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - let item_id_str = match parse_cstr(item_id, error::CSTR_INVALID_ITEM_ID) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - marshal_result( - delete_item_inner(container_handle, partition_key_str, item_id_str), - out_error, - |_| {}, - ) + context!(ctx).run_async(async { + let container = unsafe { &*container }; + let partition_key = + parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string(); + let item_id = parse_cstr(item_id, error::messages::INVALID_ITEM_ID)?; + container.delete_item(partition_key, item_id, None).await?; + Ok(()) + }) } // TODO: Patch -fn read_container_inner(container: &ContainerClient) -> Result { - let response = block_on(container.read(None))?; - Ok(response.into_body().into_string()?) -} - /// Reads the properties of the specified container. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `container` - Pointer to the `ContainerClient`. /// * `out_json` - Output parameter that will receive the container properties as a raw JSON nul-terminated C string. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_container_read( + ctx: *mut CallContext, container: *const ContainerClient, - out_json: *mut *mut c_char, - out_error: *mut CosmosError, + out_json: *mut *const c_char, ) -> CosmosErrorCode { - if container.is_null() || out_json.is_null() || out_error.is_null() { - return CosmosErrorCode::InvalidArgument; - } - - let container_handle = unsafe { &*container }; - - marshal_result( - read_container_inner(container_handle), - out_error, - |json_string| unsafe { - let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); - }, - ) -} - -fn query_items_inner( - container: &ContainerClient, - query_str: &str, - partition_key_opt: Option<&str>, -) -> Result { - let cosmos_query = Query::from(query_str); - let pk_owned = partition_key_opt.map(|s| s.to_string()); - - let pager = if let Some(pk) = pk_owned { - container.query_items::>(cosmos_query, pk, None)? - } else { - container.query_items::>(cosmos_query, (), None)? - }; - - // We don't expose the raw string in a FeedPage, so we need to collect and serialize. - // We'll evaluate optimizing this later if needed. - let results = block_on(pager.try_collect::>())?; - serde_json::to_string(&results).map_err(|_| { - CosmosError::from_static_cstr(CosmosErrorCode::DataConversion, error::CSTR_INVALID_JSON) + context!(ctx).run_async_with_output(out_json, async { + let container = unsafe { &*container }; + let response = container.read(None).await?; + let body = response.into_body().into_string()?; + Ok(CString::new(body)?) }) } /// Queries items in the specified container. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `container` - Pointer to the `ContainerClient`. /// * `query` - The query to execute as a nul-terminated C string. /// * `partition_key` - Optional partition key value as a nul-terminated C string. Specify a null pointer for a cross-partition query. /// * `out_json` - Output parameter that will receive the query results as a raw JSON nul-terminated C string. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_container_query_items( + ctx: *mut CallContext, container: *const ContainerClient, query: *const c_char, partition_key: *const c_char, - out_json: *mut *mut c_char, - out_error: *mut CosmosError, + out_json: *mut *const c_char, ) -> CosmosErrorCode { - if container.is_null() || query.is_null() || out_json.is_null() || out_error.is_null() { - return CosmosErrorCode::InvalidArgument; - } - - let container_handle = unsafe { &*container }; - - let query_str = match parse_cstr(query, error::CSTR_INVALID_QUERY) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - let partition_key_opt = if partition_key.is_null() { - None - } else { - match parse_cstr(partition_key, error::CSTR_INVALID_PARTITION_KEY) { - Ok("") => None, - Ok(s) => Some(s), - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - } - }; - - marshal_result( - query_items_inner(container_handle, query_str, partition_key_opt), - out_error, - |json_string| unsafe { - let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); - }, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::ptr; - - #[test] - fn test_container_crud_operations_null_validation() { - assert_eq!( - cosmos_container_create_item(ptr::null(), ptr::null(), ptr::null(), ptr::null_mut()), - CosmosErrorCode::InvalidArgument - ); - - assert_eq!( - cosmos_container_read_item( - ptr::null(), - ptr::null(), - ptr::null(), - ptr::null_mut(), - ptr::null_mut() - ), - CosmosErrorCode::InvalidArgument - ); - - assert_eq!( - cosmos_container_replace_item( - ptr::null(), - ptr::null(), - ptr::null(), - ptr::null(), - ptr::null_mut() - ), - CosmosErrorCode::InvalidArgument - ); - - assert_eq!( - cosmos_container_delete_item(ptr::null(), ptr::null(), ptr::null(), ptr::null_mut()), - CosmosErrorCode::InvalidArgument - ); - - assert_eq!( - cosmos_container_query_items( - ptr::null(), - ptr::null(), - ptr::null(), - ptr::null_mut(), - ptr::null_mut() - ), - CosmosErrorCode::InvalidArgument - ); - - assert_eq!( - cosmos_container_read(ptr::null(), ptr::null_mut(), ptr::null_mut()), - CosmosErrorCode::InvalidArgument - ); - } + context!(ctx).run_async_with_output(out_json, async { + let container = unsafe { &*container }; + let query = Query::from(parse_cstr(query, error::messages::INVALID_QUERY)?); + + let partition_key = if partition_key.is_null() { + None + } else { + Some(parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string()) + }; + + let pager = if let Some(pk) = partition_key { + container.query_items::>(query, pk, None)? + } else { + container.query_items::>(query, (), None)? + }; + + // We don't expose the raw string in a FeedPage, so we need to collect and serialize. + // We'll evaluate optimizing this later if needed. + let results = pager.try_collect::>().await?; + let json = serde_json::to_string(&results).map_err(|_| { + Error::new( + CosmosErrorCode::DataConversion, + error::messages::INVALID_JSON, + ) + })?; + Ok(CString::new(json)?) + }) } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs index 2febe6ced72..8720acd0f7d 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs @@ -1,3 +1,4 @@ +use std::ffi::CString; use std::os::raw::c_char; use azure_core::credentials::Secret; @@ -6,74 +7,35 @@ use azure_data_cosmos::query::Query; use azure_data_cosmos::CosmosClient; use futures::TryStreamExt; -use crate::blocking::block_on; -use crate::error::{self, marshal_result, CosmosError, CosmosErrorCode}; -use crate::string::{parse_cstr, safe_cstring_into_raw}; - -fn create_client_inner( - endpoint_str: &str, - key_str: &str, -) -> Result, CosmosError> { - let key_owned = key_str.to_string(); - let client = azure_data_cosmos::CosmosClient::with_key( - endpoint_str, - Secret::new(key_owned.clone()), - None, - )?; - Ok(Box::new(client)) -} +use crate::context::CallContext; +use crate::error::{self, CosmosErrorCode, Error}; +use crate::string::parse_cstr; /// Creates a new CosmosClient and returns a pointer to it via the out parameter. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `endpoint` - The Cosmos DB account endpoint, as a nul-terminated C string. /// * `key` - The Cosmos DB account key, as a nul-terminated C string /// * `out_client` - Output parameter that will receive a pointer to the created CosmosClient. -/// * `out_error` - Output parameter that will receive error information if the function fails. /// /// # Returns /// * Returns [`CosmosErrorCode::Success`] on success. /// * Returns [`CosmosErrorCode::InvalidArgument`] if any input pointer is null or if the input strings are invalid. #[no_mangle] pub extern "C" fn cosmos_client_create_with_key( + ctx: *mut CallContext, endpoint: *const c_char, key: *const c_char, out_client: *mut *mut CosmosClient, - out_error: *mut CosmosError, ) -> CosmosErrorCode { - if endpoint.is_null() || key.is_null() || out_client.is_null() || out_error.is_null() { - return CosmosErrorCode::InvalidArgument; - } - - let endpoint_str = match parse_cstr(endpoint, error::CSTR_INVALID_ENDPOINT) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; + context!(ctx).run_sync_with_output(out_client, || { + let endpoint = parse_cstr(endpoint, error::messages::INVALID_ENDPOINT)?; + let key = parse_cstr(key, error::messages::INVALID_KEY)?.to_string(); + let client = azure_data_cosmos::CosmosClient::with_key(endpoint, Secret::new(key), None)?; - let key_str = match parse_cstr(key, error::CSTR_INVALID_KEY) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - marshal_result( - create_client_inner(endpoint_str, key_str), - out_error, - |handle| unsafe { - *out_client = Box::into_raw(handle); - }, - ) + Ok(Box::new(client)) + }) } /// Releases the memory associated with a [`CosmosClient`]. @@ -84,262 +46,81 @@ pub extern "C" fn cosmos_client_free(client: *mut CosmosClient) { } } -fn database_client_inner( - client: &CosmosClient, - database_id_str: &str, -) -> Result, CosmosError> { - let database_client = client.database_client(database_id_str); - Ok(Box::new(database_client)) -} - /// Gets a [`DatabaseClient`] from the given [`CosmosClient`] for the specified database ID. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `client` - Pointer to the [`CosmosClient`]. /// * `database_id` - The database ID as a nul-terminated C string. /// * `out_database` - Output parameter that will receive a pointer to the created [`DatabaseClient`]. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_client_database_client( + ctx: *mut CallContext, client: *const CosmosClient, database_id: *const c_char, out_database: *mut *mut DatabaseClient, - out_error: *mut CosmosError, ) -> CosmosErrorCode { - if client.is_null() || database_id.is_null() || out_database.is_null() || out_error.is_null() { - return CosmosErrorCode::InvalidArgument; - } - - let client_handle = unsafe { &*client }; - - let database_id_str = match parse_cstr(database_id, error::CSTR_INVALID_DATABASE_ID) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - marshal_result( - database_client_inner(client_handle, database_id_str), - out_error, - |db_handle| unsafe { - *out_database = Box::into_raw(db_handle); - }, - ) -} - -fn query_databases_inner(client: &CosmosClient, query_str: &str) -> Result { - let cosmos_query = Query::from(query_str); - let pager = client.query_databases(cosmos_query, None)?; - - // We don't expose the raw string in a FeedPage, so we need to collect and serialize. - // We'll evaluate optimizing this later if needed. - let results = block_on(pager.try_collect::>())?; - serde_json::to_string(&results).map_err(|_| { - CosmosError::from_static_cstr(CosmosErrorCode::DataConversion, error::CSTR_INVALID_JSON) + context!(ctx).run_sync_with_output(out_database, || { + let client = unsafe { &*client }; + let database_id = parse_cstr(database_id, error::messages::INVALID_DATABASE_ID)?; + let database_client = client.database_client(database_id); + Ok(Box::new(database_client)) }) } /// Queries the databases in the Cosmos DB account using the provided SQL query string. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `client` - Pointer to the [`CosmosClient`]. /// * `query` - The SQL query string as a nul-terminated C string. /// * `out_json` - Output parameter that will receive a pointer to the resulting JSON string -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_client_query_databases( + ctx: *mut CallContext, client: *const CosmosClient, query: *const c_char, - out_json: *mut *mut c_char, - out_error: *mut CosmosError, + out_json: *mut *const c_char, ) -> CosmosErrorCode { - if client.is_null() || query.is_null() || out_json.is_null() || out_error.is_null() { - return CosmosErrorCode::InvalidArgument; - } - - let client_handle = unsafe { &*client }; - - let query_str = match parse_cstr(query, error::CSTR_INVALID_QUERY) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - marshal_result( - query_databases_inner(client_handle, query_str), - out_error, - |json_string| unsafe { - let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); - }, - ) -} - -fn create_database_inner( - client: &CosmosClient, - database_id_str: &str, -) -> Result, CosmosError> { - block_on(client.create_database(database_id_str, None))?; - - let database_client = client.database_client(database_id_str); - - Ok(Box::new(database_client.into())) + context!(ctx).run_async_with_output(out_json, async { + let client = unsafe { &*client }; + let query_str = parse_cstr(query, error::messages::INVALID_QUERY)?; + + let cosmos_query = Query::from(query_str); + let pager = client.query_databases(cosmos_query, None)?; + + // We don't expose the raw string in a FeedPage, so we need to collect and serialize. + // We'll evaluate optimizing this later if needed. + let results = pager.try_collect::>().await?; + let json = serde_json::to_string(&results).map_err(|_| { + Error::new( + CosmosErrorCode::DataConversion, + error::messages::INVALID_JSON, + ) + })?; + let json = CString::new(json)?; + Ok(json) + }) } -/// Creates a new database in the Cosmos DB account with the specified database ID. +/// Creates a new database in the Cosmos DB account with the specified database ID, and returns a pointer to the created [`DatabaseClient`]. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `client` - Pointer to the [`CosmosClient`]. /// * `database_id` - The database ID as a nul-terminated C string. -/// * `out_database` - Output parameter that will receive a pointer to the created [`DatabaseClient`]. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_client_create_database( + ctx: *mut CallContext, client: *const CosmosClient, database_id: *const c_char, out_database: *mut *mut DatabaseClient, - out_error: *mut CosmosError, ) -> CosmosErrorCode { - if client.is_null() || database_id.is_null() || out_database.is_null() { - return CosmosErrorCode::InvalidArgument; - } - - let client_handle = unsafe { &*client }; - - let database_id_str = match parse_cstr(database_id, error::CSTR_INVALID_DATABASE_ID) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - *out_database = std::ptr::null_mut(); - } - return code; - } - }; - - marshal_result( - create_database_inner(client_handle, database_id_str), - out_error, - |db_handle| unsafe { - *out_database = Box::into_raw(db_handle); - }, - ) -} + context!(ctx).run_async_with_output(out_database, async { + let client = unsafe { &*client }; -#[cfg(test)] -mod tests { - use crate::cosmos_database_free; - - use super::*; - use std::ffi::CString; - use std::ptr; - - #[test] - fn test_cosmos_client_create_valid_params() { - let endpoint = CString::new("https://test.documents.azure.com") - .expect("test string should not contain NUL"); - let key = CString::new("test-key").expect("test string should not contain NUL"); - let mut client_ptr: *mut CosmosClient = ptr::null_mut(); - let mut error = CosmosError::success(); - - let result = cosmos_client_create_with_key( - endpoint.as_ptr(), - key.as_ptr(), - &mut client_ptr, - &mut error, - ); - - assert_eq!(result, CosmosErrorCode::Success); - assert!(!client_ptr.is_null()); - assert_eq!(error.code, CosmosErrorCode::Success); - - cosmos_client_free(client_ptr); - } - - #[test] - fn test_cosmos_client_create_null_params() { - let mut client_ptr: *mut CosmosClient = ptr::null_mut(); - let mut error = CosmosError::success(); - - let result = - cosmos_client_create_with_key(ptr::null(), ptr::null(), &mut client_ptr, &mut error); - - assert_eq!(result, CosmosErrorCode::InvalidArgument); - assert!(client_ptr.is_null()); - } - - #[test] - fn test_cosmos_client_database_client() { - let endpoint = CString::new("https://test.documents.azure.com") - .expect("test string should not contain NUL"); - let key = CString::new("test-key").expect("test string should not contain NUL"); - let db_id = CString::new("test-db").expect("test string should not contain NUL"); - - let mut client_ptr: *mut CosmosClient = ptr::null_mut(); - let mut db_ptr: *mut DatabaseClient = ptr::null_mut(); - let mut error = CosmosError::success(); - - cosmos_client_create_with_key( - endpoint.as_ptr(), - key.as_ptr(), - &raw mut client_ptr, - &mut error, - ); - assert!(!client_ptr.is_null()); - - let result = - cosmos_client_database_client(client_ptr, db_id.as_ptr(), &mut db_ptr, &mut error); - - assert_eq!(result, CosmosErrorCode::Success); - assert!(!db_ptr.is_null()); - - cosmos_database_free(db_ptr); - cosmos_client_free(client_ptr); - } - - #[test] - fn test_cosmos_client_query_databases_null_params() { - let mut json_ptr: *mut c_char = ptr::null_mut(); - let mut error = CosmosError::success(); - - let result = - cosmos_client_query_databases(ptr::null(), ptr::null(), &mut json_ptr, &mut error); - - assert_eq!(result, CosmosErrorCode::InvalidArgument); - assert!(json_ptr.is_null()); - } - - #[test] - fn test_cosmos_client_create_database_null_params() { - let mut db_ptr: *mut DatabaseClient = ptr::null_mut(); - let mut error = CosmosError::success(); - - // Test null client - let result = - cosmos_client_create_database(ptr::null(), ptr::null(), &mut db_ptr, &mut error); - - assert_eq!(result, CosmosErrorCode::InvalidArgument); - assert!(db_ptr.is_null()); - - // Reset for next test - db_ptr = ptr::null_mut(); - error = CosmosError::success(); - - // Test null database_id - let result = - cosmos_client_create_database(ptr::null(), ptr::null(), &mut db_ptr, &mut error); - - assert_eq!(result, CosmosErrorCode::InvalidArgument); - assert!(db_ptr.is_null()); - } + let database_id = parse_cstr(database_id, error::messages::INVALID_DATABASE_ID)?; + client.create_database(database_id, None).await?; + Ok(Box::new(client.database_client(database_id))) + }) } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs index 5a270ee0d76..b7e72f08886 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs @@ -1,3 +1,4 @@ +use std::ffi::CString; use std::os::raw::c_char; use azure_data_cosmos::clients::{ContainerClient, DatabaseClient}; @@ -5,9 +6,9 @@ use azure_data_cosmos::models::ContainerProperties; use azure_data_cosmos::query::Query; use futures::TryStreamExt; -use crate::blocking::block_on; -use crate::error::{self, marshal_result, CosmosError, CosmosErrorCode}; -use crate::string::{parse_cstr, safe_cstring_into_raw}; +use crate::context::CallContext; +use crate::error::{self, CosmosErrorCode, Error}; +use crate::string::parse_cstr; /// Releases the memory associated with a [`DatabaseClient`]. #[no_mangle] @@ -17,379 +18,132 @@ pub extern "C" fn cosmos_database_free(database: *mut DatabaseClient) { } } -fn container_client_inner( - database: &DatabaseClient, - container_id_str: &str, -) -> Result, CosmosError> { - let container_client = database.container_client(container_id_str); - Ok(Box::new(container_client)) -} - /// Retrieves a pointer to a [`ContainerClient`] for the specified container ID within the given database. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `database` - Pointer to the [`DatabaseClient`]. /// * `container_id` - The container ID as a nul-terminated C string. /// * `out_container` - Output parameter that will receive a pointer to the [`ContainerClient`]. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_database_container_client( + ctx: *mut CallContext, database: *const DatabaseClient, container_id: *const c_char, out_container: *mut *mut ContainerClient, - out_error: *mut CosmosError, ) -> CosmosErrorCode { - if database.is_null() - || container_id.is_null() - || out_container.is_null() - || out_error.is_null() - { - return CosmosErrorCode::InvalidArgument; - } - - let database_handle = unsafe { &*database }; - - let container_id_str = match parse_cstr(container_id, error::CSTR_INVALID_CONTAINER_ID) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - marshal_result( - container_client_inner(database_handle, container_id_str), - out_error, - |container_handle| unsafe { - *out_container = Box::into_raw(container_handle); - }, - ) -} - -fn read_database_inner(database: &DatabaseClient) -> Result { - let response = block_on(database.read(None))?; - Ok(response.into_body().into_string()?) + context!(ctx).run_sync_with_output(out_container, || { + let database = unsafe { &*database }; + let container_id = parse_cstr(container_id, error::messages::INVALID_CONTAINER_ID)?; + let container_client = database.container_client(container_id); + Ok(Box::new(container_client)) + }) } /// Reads the properties of the specified database and returns them as a JSON string. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `database` - Pointer to the [`DatabaseClient`]. /// * `out_json` - Output parameter that will receive a pointer to the JSON string. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_database_read( + ctx: *mut CallContext, database: *const DatabaseClient, - out_json: *mut *mut c_char, - out_error: *mut CosmosError, + out_json: *mut *const c_char, ) -> CosmosErrorCode { - if database.is_null() || out_json.is_null() || out_error.is_null() { - return CosmosErrorCode::InvalidArgument; - } - - let database_handle = unsafe { &*database }; - - marshal_result( - read_database_inner(database_handle), - out_error, - |json_string| unsafe { - let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); - }, - ) -} - -fn delete_database_inner(database: &DatabaseClient) -> Result<(), CosmosError> { - block_on(database.delete(None))?; - Ok(()) + context!(ctx).run_async_with_output(out_json, async { + let database = unsafe { &*database }; + let response = database.read(None).await?; + let json = response.into_body().into_string()?; + Ok(CString::new(json)?) + }) } /// Deletes the specified database. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `database` - Pointer to the [`DatabaseClient`]. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_database_delete( + ctx: *mut CallContext, database: *const DatabaseClient, - out_error: *mut CosmosError, ) -> CosmosErrorCode { - if database.is_null() || out_error.is_null() { - return CosmosErrorCode::InvalidArgument; - } - - let database_handle = unsafe { &*database }; - - marshal_result(delete_database_inner(database_handle), out_error, |_| {}) -} - -fn create_container_inner( - database: &DatabaseClient, - container_id_str: &str, - partition_key_path_str: &str, -) -> Result, CosmosError> { - let container_id_owned = container_id_str.to_string(); - let partition_key_owned = partition_key_path_str.to_string(); - - let properties = ContainerProperties { - id: container_id_owned.clone().into(), - partition_key: partition_key_owned.clone().into(), - ..Default::default() - }; - - block_on(database.create_container(properties, None))?; - - let container_client = database.container_client(&container_id_owned); - - Ok(Box::new(container_client.into())) + context!(ctx).run_async(async { + let database = unsafe { &*database }; + database.delete(None).await?; + Ok(()) + }) } /// Creates a new container within the specified database. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `database` - Pointer to the [`DatabaseClient`]. /// * `container_id` - The container ID as a nul-terminated C string. /// * `partition_key_path` - The partition key path as a nul-terminated C string. /// * `out_container` - Output parameter that will receive a pointer to the newly created [`ContainerClient`]. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_database_create_container( + ctx: *mut CallContext, database: *const DatabaseClient, container_id: *const c_char, partition_key_path: *const c_char, out_container: *mut *mut ContainerClient, - out_error: *mut CosmosError, ) -> CosmosErrorCode { - if database.is_null() - || container_id.is_null() - || partition_key_path.is_null() - || out_container.is_null() - || out_error.is_null() - { - return CosmosErrorCode::InvalidArgument; - } - - let database_handle = unsafe { &*database }; - - let container_id_str = match parse_cstr(container_id, error::CSTR_INVALID_CONTAINER_ID) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - *out_container = std::ptr::null_mut(); - } - return code; - } - }; - - let partition_key_path_str = - match parse_cstr(partition_key_path, error::CSTR_INVALID_PARTITION_KEY) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - *out_container = std::ptr::null_mut(); - } - return code; - } + context!(ctx).run_async_with_output(out_container, async { + let database = unsafe { &*database }; + + let container_id = + parse_cstr(container_id, error::messages::INVALID_CONTAINER_ID)?.to_string(); + let partition_key_path = + parse_cstr(partition_key_path, error::messages::INVALID_PARTITION_KEY)?.to_string(); + let properties = ContainerProperties { + id: container_id.clone().into(), + partition_key: partition_key_path.clone().into(), + ..Default::default() }; - marshal_result( - create_container_inner(database_handle, container_id_str, partition_key_path_str), - out_error, - |container_handle| unsafe { - *out_container = Box::into_raw(container_handle); - }, - ) -} + database.create_container(properties, None).await?; -fn query_containers_inner( - database: &DatabaseClient, - query_str: &str, -) -> Result { - let cosmos_query = Query::from(query_str); - let pager = database.query_containers(cosmos_query, None)?; + let container_client = database.container_client(&container_id); - // We don't expose the raw string in a FeedPage, so we need to collect and serialize. - // We'll evaluate optimizing this later if needed. - let results = block_on(pager.try_collect::>())?; - serde_json::to_string(&results).map_err(|_| { - CosmosError::from_static_cstr(CosmosErrorCode::DataConversion, error::CSTR_INVALID_JSON) + Ok(Box::new(container_client.into())) }) } /// Queries the containers within the specified database and returns the results as a JSON string. /// /// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `database` - Pointer to the [`DatabaseClient`]. /// * `query` - The query string as a nul-terminated C string. /// * `out_json` - Output parameter that will receive a pointer to the JSON string. -/// * `out_error` - Output parameter that will receive error information if the function fails. #[no_mangle] pub extern "C" fn cosmos_database_query_containers( + ctx: *mut CallContext, database: *const DatabaseClient, query: *const c_char, - out_json: *mut *mut c_char, - out_error: *mut CosmosError, + out_json: *mut *const c_char, ) -> CosmosErrorCode { - if database.is_null() || query.is_null() || out_json.is_null() || out_error.is_null() { - return CosmosErrorCode::InvalidArgument; - } - - let database_handle = unsafe { &*database }; - - let query_str = match parse_cstr(query, error::CSTR_INVALID_QUERY) { - Ok(s) => s, - Err(e) => { - let code = e.code; - unsafe { - *out_error = e; - } - return code; - } - }; - - marshal_result( - query_containers_inner(database_handle, query_str), - out_error, - |json_string| unsafe { - let _ = safe_cstring_into_raw(&json_string, &mut *out_json, &mut *out_error); - }, - ) -} - -// TODO: Add more database operations following Azure SDK pattern: -// - Additional advanced database operations if needed - -#[cfg(test)] -mod tests { - use azure_data_cosmos::CosmosClient; - - use crate::{ - cosmos_client_create_with_key, cosmos_client_database_client, cosmos_client_free, - cosmos_container_free, - }; - - use super::*; - use std::{ffi::CString, ptr}; - - #[test] - fn test_database_container_client_null_params() { - let mut container_ptr: *mut ContainerClient = ptr::null_mut(); - let mut error = CosmosError::success(); - - let result = cosmos_database_container_client( - ptr::null(), - ptr::null(), - &mut container_ptr, - &mut error, - ); - - assert_eq!(result, CosmosErrorCode::InvalidArgument); - assert!(container_ptr.is_null()); - } - - #[test] - fn test_database_read_null_params() { - let mut json_ptr: *mut c_char = ptr::null_mut(); - let mut error = CosmosError::success(); - - let result = cosmos_database_read(ptr::null(), &mut json_ptr, &mut error); - - assert_eq!(result, CosmosErrorCode::InvalidArgument); - assert!(json_ptr.is_null()); - } - - #[test] - fn test_database_delete_null_params() { - let mut error = CosmosError::success(); - - let result = cosmos_database_delete(ptr::null(), &mut error); - - assert_eq!(result, CosmosErrorCode::InvalidArgument); - } - - #[test] - fn test_database_create_container_null_params() { - let mut container_ptr: *mut ContainerClient = ptr::null_mut(); - let mut error = CosmosError::success(); - - // Test null database - let result = cosmos_database_create_container( - ptr::null(), - ptr::null(), - ptr::null(), - &mut container_ptr, - &mut error, - ); - - assert_eq!(result, CosmosErrorCode::InvalidArgument); - assert!(container_ptr.is_null()); - - // Reset for next test - container_ptr = ptr::null_mut(); - error = CosmosError::success(); - - // Test with valid database but null container_id - // Note: We can't create a real DatabaseClient without Azure SDK setup, - // so we test the null parameter validation only - let result = cosmos_database_create_container( - ptr::null(), - ptr::null(), - c"/test".as_ptr() as *const c_char, - &mut container_ptr, - &mut error, - ); - - assert_eq!(result, CosmosErrorCode::InvalidArgument); - assert!(container_ptr.is_null()); - } - - #[test] - fn test_database_query_containers_null_params() { - let mut json_ptr: *mut c_char = ptr::null_mut(); - let mut error = CosmosError::success(); - - let result = - cosmos_database_query_containers(ptr::null(), ptr::null(), &mut json_ptr, &mut error); - - assert_eq!(result, CosmosErrorCode::InvalidArgument); - assert!(json_ptr.is_null()); - } - - #[test] - fn test_database_container_client() { - let endpoint = CString::new("https://test.documents.azure.com") - .expect("test string should not contain NUL"); - let key = CString::new("test-key").expect("test string should not contain NUL"); - let db_id = CString::new("test-db").expect("test string should not contain NUL"); - - let mut client_ptr: *mut CosmosClient = ptr::null_mut(); - let mut db_ptr: *mut DatabaseClient = ptr::null_mut(); - let mut container_ptr: *mut ContainerClient = ptr::null_mut(); - let mut error = CosmosError::success(); - - cosmos_client_create_with_key(endpoint.as_ptr(), key.as_ptr(), &mut client_ptr, &mut error); - assert!(!client_ptr.is_null()); - - cosmos_client_database_client(client_ptr, db_id.as_ptr(), &mut db_ptr, &mut error); - assert!(!db_ptr.is_null()); - - let result = cosmos_database_container_client( - db_ptr, - c"test-container".as_ptr() as *const c_char, - &mut container_ptr, - &mut error, - ); - assert_eq!(result, CosmosErrorCode::Success); - assert!(!container_ptr.is_null()); - - cosmos_container_free(container_ptr); - cosmos_database_free(db_ptr); - cosmos_client_free(client_ptr); - } + context!(ctx).run_async_with_output(out_json, async { + let database = unsafe { &*database }; + + let query = parse_cstr(query, error::messages::INVALID_QUERY)?; + let cosmos_query = Query::from(query); + let pager = database.query_containers(cosmos_query, None)?; + + // We don't expose the raw string in a FeedPage, so we need to collect and serialize. + // We'll evaluate optimizing this later if needed. + let results = pager.try_collect::>().await?; + let s = serde_json::to_string(&results).map_err(|_| { + Error::new( + CosmosErrorCode::DataConversion, + error::messages::INVALID_JSON, + ) + })?; + Ok(CString::new(s)?) + }) } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/context.rs b/sdk/cosmos/azure_data_cosmos_native/src/context.rs new file mode 100644 index 00000000000..c744c784b46 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/context.rs @@ -0,0 +1,210 @@ +use crate::{ + error::{CosmosError, CosmosErrorCode, Error}, + runtime::RuntimeContext, +}; + +/// Represents the context for a call into the Cosmos DB native SDK. +/// +/// This structure can be created on the caller side, as long as the caller is able to create a C-compatible struct. +/// The `runtime_context` field must be set to a pointer to a `RuntimeContext` created by the +/// [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create) function. +/// +/// The structure can also be created using [`cosmos_call_context_create`](crate::context::cosmos_call_context_create), +/// in which case Rust will manage the memory for the structure, and it must be freed using [`cosmos_call_context_free`](crate::context::cosmos_call_context_free). +/// +/// This structure must remain active and at the memory address specified in the function call for the duration of the call into the SDK. +/// If calling an async function, that may mean it must be allocated on the heap to ensure it remains live (depending on the caller's language/runtime). +/// +/// A single [`CallContext`] may be reused for muliple calls, but cannot be used concurrently from multiple threads. +/// When reusing a [`CallContext`] the [`CallContext::error`] field will be overwritten with the error from the most recent call. +/// Error details will NOT be freed if the context is reused; the caller is responsible for freeing any error details if needed. +#[repr(C)] +#[derive(Default)] +pub struct CallContext { + /// Pointer to a RuntimeContext created by [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create). + pub runtime_context: *const RuntimeContext, + + /// Indicates whether detailed case-specific error information should be included in error responses. + /// + /// Normally, a [`CosmosError`] contains only a static error message, which does not need to be freed. + /// However, this also means that the error message may not contain detailed information about the specific error that occurred. + /// If this field is set to true, the SDK will allocate a detailed error message string for each error that occurs, + /// which must be freed by the caller using [`cosmos_string_free`](crate::string::cosmos_string_free) after each error is handled. + pub include_error_details: bool, + + /// Holds the error information for the last operation performed using this context. + /// + /// The value of this is ignored on input; it is only set by the SDK to report errors. + /// The [`CosmosError::code`] field will always match the returned error code from the function. + /// The string associated with the error (if any) will be allocated by the SDK and must be freed + /// by the caller using the appropriate function. + pub error: CosmosError, +} + +/// Creates a new [`CallContext`] and returns a pointer to it. +/// This must be freed using [`cosmos_call_context_free`] when no longer needed. +/// +/// A [`CallContext`] may be reused for multiple calls, but cannot be used concurrently from multiple threads. +#[no_mangle] +pub extern "C" fn cosmos_call_context_create( + runtime_ctx: *const RuntimeContext, + include_error_details: bool, +) -> *mut CallContext { + let ctx = CallContext { + runtime_context: runtime_ctx, + include_error_details, + error: CosmosError { + code: CosmosErrorCode::Success, + message: crate::error::messages::OPERATION_SUCCEEDED.as_ptr(), + detail: std::ptr::null(), + }, + }; + Box::into_raw(Box::new(ctx)) +} + +/// Frees a [`CallContext`] created by [`cosmos_call_context_create`]. +#[no_mangle] +pub extern "C" fn cosmos_call_context_free(ctx: *mut CallContext) { + if !ctx.is_null() { + unsafe { drop(Box::from_raw(ctx)) } + } +} + +impl CallContext { + pub fn from_ptr<'a>(ptr: *mut CallContext) -> &'a mut CallContext { + debug_assert!(!ptr.is_null()); + unsafe { &mut *ptr } + } + + pub fn runtime(&mut self) -> &crate::runtime::RuntimeContext { + assert!(!self.runtime_context.is_null()); + unsafe { &*self.runtime_context } + } + + /// Runs a synchronous operation with no outputs, capturing any error into the CallContext. + pub fn run_sync(&mut self, f: impl FnOnce() -> Result<(), Error>) -> CosmosErrorCode { + match f() { + Ok(()) => { + self.error = Error::SUCCESS.into_ffi(self.include_error_details); + CosmosErrorCode::Success + } + Err(err) => self.set_error_and_return_code(err), + } + } + + /// Runs a synchronous operation with a single output, capturing any error into the CallContext. + pub fn run_sync_with_output( + &mut self, + out: *mut T::Output, + f: impl FnOnce() -> Result, + ) -> CosmosErrorCode { + if out.is_null() { + self.error = Error::new( + CosmosErrorCode::InvalidArgument, + crate::error::messages::NULL_OUTPUT_POINTER, + ) + .into_ffi(self.include_error_details); + return CosmosErrorCode::InvalidArgument; + } + + match f() { + Ok(value) => { + unsafe { + *out = value.into_raw(); + } + self.error = Error::SUCCESS.into_ffi(self.include_error_details); + CosmosErrorCode::Success + } + Err(err) => self.set_error_and_return_code(err), + } + } + + /// Runs an asynchronous operation with no outputs, capturing any error into the CallContext. + pub fn run_async( + &mut self, + f: impl std::future::Future>, + ) -> CosmosErrorCode { + let r = self.runtime().block_on(f); + match r { + Ok(()) => { + self.error = Error::SUCCESS.into_ffi(self.include_error_details); + CosmosErrorCode::Success + } + Err(err) => self.set_error_and_return_code(err), + } + } + + /// Runs an asynchronous operation with a single output, capturing any error into the CallContext. + pub fn run_async_with_output( + &mut self, + out: *mut T::Output, + f: impl std::future::Future>, + ) -> CosmosErrorCode { + if out.is_null() { + self.error = Error::new( + CosmosErrorCode::InvalidArgument, + crate::error::messages::NULL_OUTPUT_POINTER, + ) + .into_ffi(self.include_error_details); + return CosmosErrorCode::InvalidArgument; + } + + let r = self.runtime().block_on(f); + match r { + Ok(value) => { + unsafe { + *out = value.into_raw(); + } + self.error = Error::SUCCESS.into_ffi(self.include_error_details); + CosmosErrorCode::Success + } + Err(err) => self.set_error_and_return_code(err), + } + } + + fn set_error_and_return_code(&mut self, err: Error) -> CosmosErrorCode { + let err = err.into_ffi(self.include_error_details); + let code = err.code; + self.error = err; + code + } +} + +#[macro_export] +macro_rules! context { + ($param: expr) => { + if $param.is_null() { + return $crate::error::CosmosErrorCode::CallContextMissing; + } else { + let ctx = $crate::context::CallContext::from_ptr($param); + if ctx.runtime_context.is_null() { + return $crate::error::CosmosErrorCode::RuntimeContextMissing; + } else { + ctx + } + } + }; +} + +/// Marker trait that indicates that a type can be converted into a pointer type for FFI output parameters. +pub trait IntoRaw { + type Output; + + fn into_raw(self) -> Self::Output; +} + +impl IntoRaw for Box { + type Output = *mut T; + + fn into_raw(self) -> *mut T { + Box::into_raw(self) + } +} + +impl IntoRaw for std::ffi::CString { + type Output = *const std::ffi::c_char; + + fn into_raw(self) -> *const std::ffi::c_char { + self.into_raw() + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/error.rs b/sdk/cosmos/azure_data_cosmos_native/src/error.rs index 9cfddc69b5e..a4e7e540ed8 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/error.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/error.rs @@ -1,49 +1,33 @@ use azure_core::error::ErrorKind; -use std::ffi::CStr; -use std::os::raw::c_char; - -pub static CSTR_NUL_BYTES_ERROR: &CStr = c"String contains NUL bytes"; -pub static CSTR_INVALID_CHARS_ERROR: &CStr = c"Error message contains invalid characters"; -pub static CSTR_UNKNOWN_ERROR: &CStr = c"Unknown error"; -pub static CSTR_INVALID_JSON: &CStr = c"Invalid JSON data"; -pub static CSTR_CLIENT_CREATION_FAILED: &CStr = c"Failed to create Azure Cosmos client"; - -pub static CSTR_INVALID_ENDPOINT: &CStr = c"Invalid endpoint string"; -pub static CSTR_INVALID_KEY: &CStr = c"Invalid key string"; -pub static CSTR_INVALID_DATABASE_ID: &CStr = c"Invalid database ID string"; -pub static CSTR_INVALID_CONTAINER_ID: &CStr = c"Invalid container ID string"; -pub static CSTR_INVALID_PARTITION_KEY: &CStr = c"Invalid partition key string"; -pub static CSTR_INVALID_ITEM_ID: &CStr = c"Invalid item ID string"; -pub static CSTR_INVALID_JSON_DATA: &CStr = c"Invalid JSON data string"; -pub static CSTR_INVALID_QUERY: &CStr = c"Invalid query string"; - -pub static CSTR_QUERY_NOT_IMPLEMENTED: &CStr = - c"Query operations not yet implemented - requires stream handling"; - -// Helper function to check if a pointer is one of our static constants -fn is_static_error_message(ptr: *const c_char) -> bool { - if ptr.is_null() { - return true; - } - - ptr == CSTR_INVALID_CHARS_ERROR.as_ptr() - || ptr == CSTR_UNKNOWN_ERROR.as_ptr() - || ptr == CSTR_NUL_BYTES_ERROR.as_ptr() - || ptr == CSTR_INVALID_JSON.as_ptr() - || ptr == CSTR_CLIENT_CREATION_FAILED.as_ptr() - || ptr == CSTR_INVALID_ENDPOINT.as_ptr() - || ptr == CSTR_INVALID_KEY.as_ptr() - || ptr == CSTR_INVALID_DATABASE_ID.as_ptr() - || ptr == CSTR_INVALID_CONTAINER_ID.as_ptr() - || ptr == CSTR_INVALID_PARTITION_KEY.as_ptr() - || ptr == CSTR_INVALID_ITEM_ID.as_ptr() - || ptr == CSTR_INVALID_JSON_DATA.as_ptr() - || ptr == CSTR_QUERY_NOT_IMPLEMENTED.as_ptr() +use std::ffi::{CStr, CString, NulError}; + +/// Collection of static C strings for error messages. +pub mod messages { + use std::ffi::CStr; + + pub static OPERATION_SUCCEEDED: &CStr = c"Operation completed successfully"; + pub static NULL_OUTPUT_POINTER: &CStr = c"Output pointer is null"; + + pub static CSTR_NUL_BYTES_ERROR: &CStr = c"String contains NUL bytes"; + pub static CSTR_INVALID_CHARS_ERROR: &CStr = c"Error message contains invalid characters"; + pub static CSTR_UNKNOWN_ERROR: &CStr = c"Unknown error"; + pub static INVALID_JSON: &CStr = c"Invalid JSON data"; + pub static CSTR_CLIENT_CREATION_FAILED: &CStr = c"Failed to create Azure Cosmos client"; + + pub static INVALID_ENDPOINT: &CStr = c"Invalid endpoint string"; + pub static INVALID_KEY: &CStr = c"Invalid key string"; + pub static INVALID_DATABASE_ID: &CStr = c"Invalid database ID string"; + pub static INVALID_CONTAINER_ID: &CStr = c"Invalid container ID string"; + pub static INVALID_PARTITION_KEY: &CStr = c"Invalid partition key string"; + pub static INVALID_ITEM_ID: &CStr = c"Invalid item ID string"; + pub static CSTR_INVALID_JSON_DATA: &CStr = c"Invalid JSON data string"; + pub static INVALID_QUERY: &CStr = c"Invalid query string"; } #[repr(i32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum CosmosErrorCode { + #[default] Success = 0, InvalidArgument = 1, ConnectionFailed = 2, @@ -71,71 +55,109 @@ pub enum CosmosErrorCode { ItemSizeTooLarge = 2004, PartitionKeyNotFound = 2005, - // FFI boundary error codes - for infrastructure issues at the boundary - FFIInvalidUTF8 = 3001, // Invalid UTF-8 in string parameters crossing FFI - FFIInvalidHandle = 3002, // Corrupted/invalid handle passed across FFI - FFIMemoryError = 3003, // Memory allocation/deallocation issues at FFI boundary - FFIMarshalingError = 3004, // Data marshaling/unmarshaling failed at FFI boundary + InvalidUTF8 = 3001, // Invalid UTF-8 in string parameters crossing FFI + InvalidHandle = 3002, // Corrupted/invalid handle passed across FFI + MemoryError = 3003, // Memory allocation/deallocation issues at FFI boundary + MarshalingError = 3004, // Data marshaling/unmarshaling failed at FFI boundary + CallContextMissing = 3005, // CallContext not provided where required + RuntimeContextMissing = 3006, // RuntimeContext not provided where required + InvalidCString = 3007, // Invalid C string (not null-terminated or malformed) } -// CosmosError struct with hybrid memory management -// -// MEMORY MANAGEMENT STRATEGY: -// - message pointer can be either: -// 1. STATIC: Points to compile-time constants (never freed) -// 2. OWNED: Points to heap-allocated CString (must be freed) -// -// SAFETY: -// - Static messages: Created via from_static_cstr(), pointer lives forever -// - Owned messages: Created via new(), caller responsible for cleanup -// - Mixed usage: free_message() safely distinguishes between static/owned -// -// This approach follows Azure SDK pattern for zero-cost static errors -// while maintaining compatibility with dynamic error messages. -#[repr(C)] -pub struct CosmosError { - pub code: CosmosErrorCode, - pub message: *const c_char, +/// Internal structure for representing errors. +/// +/// This structure is not exposed across the FFI boundary directly. +/// Instead, the [`CallContext`](crate::context::CallContext) receives this error and then marshals it +/// to an appropriate representation for the caller. +pub struct Error { + /// The error code representing the type of error. + code: CosmosErrorCode, + + /// A static C string message describing the error. This value does not need to be freed. + message: &'static CStr, + + /// An optional error detail object that can provide additional context about the error. + /// This is held as a boxed trait so that it only allocates the string if the user requested detailed errors. + detail: Option>, } -impl CosmosError { - // Safe cleanup that handles both static and owned messages - pub fn free_message(&mut self) { - if !is_static_error_message(self.message) && !self.message.is_null() { - unsafe { - let _ = std::ffi::CString::from_raw(self.message as *mut c_char); - } - } - self.message = std::ptr::null(); - } +impl Error { + /// Creates a success [`CosmosError`] with a static message and no detail. + pub const SUCCESS: Self = Self { + code: CosmosErrorCode::Success, + message: messages::OPERATION_SUCCEEDED, + detail: None, + }; - pub fn success() -> Self { + /// Creates a new [`CosmosError`] with a static C string message that does not need to be freed. + pub fn new(code: CosmosErrorCode, message: &'static CStr) -> Self { Self { - code: CosmosErrorCode::Success, - message: std::ptr::null(), + code, + message, + detail: None, } } - // Create error from static CStr (zero allocation) - pub fn from_static_cstr(code: CosmosErrorCode, static_cstr: &'static std::ffi::CStr) -> Self { + /// Creates a new [`CosmosError`] with both a static message, and a detailed dynamic message that must be freed with [`cosmos_string_free`](crate::string::cosmos_string_free). + pub fn with_detail( + code: CosmosErrorCode, + message: &'static CStr, + detail: impl ToString + 'static, + ) -> Self { Self { code, - message: static_cstr.as_ptr(), + message, + detail: Some(Box::new(detail)), } } - pub fn new(code: CosmosErrorCode, message: String) -> Self { - let c_message = std::ffi::CString::new(message) - .expect("SDK-generated error message should not contain NUL bytes") - .into_raw() as *const c_char; + pub fn into_ffi(self, include_details: bool) -> CosmosError { + let detail_ptr = if include_details { + if let Some(detail) = self.detail { + let detail_string = detail.to_string(); + CString::new(detail_string) + .map(|c| c.into_raw() as *const _) + .unwrap_or_else(|_| std::ptr::null()) + } else { + std::ptr::null() + } + } else { + std::ptr::null() + }; - Self { - code, - message: c_message, + CosmosError { + code: self.code, + message: self.message.as_ptr(), + detail: detail_ptr, } } } +/// External representation of an error across the FFI boundary. +#[repr(C)] +#[derive(Default)] +pub struct CosmosError { + /// The error code representing the type of error. + pub code: CosmosErrorCode, + + /// A static C string message describing the error. This value does not need to be freed. + pub message: *const std::ffi::c_char, + + /// An optional detailed C string message providing additional context about the error. + /// This is only set if [`include_error_details`](crate::context::CallContext::include_error_details) is true. + /// If this pointer is non-null, it must be freed by the caller using [`cosmos_string_free`](crate::string::cosmos_string_free). + pub detail: *const std::ffi::c_char, +} + +impl CosmosError { + // /// cbindgen:ignore + // pub static SUCCESS: Self = Self { + // code: CosmosErrorCode::Success, + // message: messages::OPERATION_SUCCEEDED.as_ptr(), + // detail: std::ptr::null(), + // }; +} + pub fn http_status_to_error_code(status_code: u16) -> CosmosErrorCode { match status_code { 400 => CosmosErrorCode::BadRequest, @@ -154,510 +176,106 @@ pub fn http_status_to_error_code(status_code: u16) -> CosmosErrorCode { } // Extract Cosmos DB specific error information from error messages -fn extract_cosmos_db_error_info(error_message: &str) -> (CosmosErrorCode, String) { +fn extract_cosmos_db_error_info(error_message: &str) -> (CosmosErrorCode, &'static CStr) { if error_message.contains("PartitionKeyMismatch") || error_message.contains("partition key mismatch") { ( CosmosErrorCode::PartitionKeyMismatch, - error_message.to_string(), + c"Partition key mismatch", ) } else if error_message.contains("Resource quota exceeded") || error_message.contains("Request rate is large") { ( CosmosErrorCode::ResourceQuotaExceeded, - error_message.to_string(), + c"Resource quota exceeded", ) } else if error_message.contains("429") && error_message.contains("Request rate is large") { ( CosmosErrorCode::RequestRateTooLarge, - error_message.to_string(), + c"Request rate too large", ) } else if error_message.contains("Entity is too large") || error_message.contains("Request entity too large") { - (CosmosErrorCode::ItemSizeTooLarge, error_message.to_string()) + (CosmosErrorCode::ItemSizeTooLarge, c"Item size too large") } else if error_message.contains("Partition key") && error_message.contains("not found") { ( CosmosErrorCode::PartitionKeyNotFound, - error_message.to_string(), + c"Partition key not found", ) } else { - (CosmosErrorCode::UnknownError, error_message.to_string()) + (CosmosErrorCode::UnknownError, c"Unknown error") } } // Native Azure SDK error conversion using structured error data -pub fn convert_azure_error_native(azure_error: &azure_core::Error) -> CosmosError { +pub fn convert_azure_error_native(azure_error: azure_core::Error) -> Error { let error_string = azure_error.to_string(); if let Some(status_code) = azure_error.http_status() { - let (cosmos_error_code, refined_message) = extract_cosmos_db_error_info(&error_string); + let (cosmos_error_code, message) = extract_cosmos_db_error_info(&error_string); if cosmos_error_code != CosmosErrorCode::UnknownError { - CosmosError::new(cosmos_error_code, refined_message) + Error::with_detail(cosmos_error_code, message, azure_error) } else { let error_code = http_status_to_error_code(u16::from(status_code)); - CosmosError::new(error_code, error_string) + Error::with_detail(error_code, c"HTTP error", azure_error) } } else { match azure_error.kind() { - ErrorKind::Credential => CosmosError::new( + ErrorKind::Credential => Error::with_detail( CosmosErrorCode::AuthenticationFailed, - format!("Authentication failed: {}", azure_error), + c"Authentication failed", + azure_error, ), - ErrorKind::Io => CosmosError::new( + ErrorKind::Io => Error::with_detail( CosmosErrorCode::ConnectionFailed, - format!("IO error: {}", azure_error), + c"Connection failed", + azure_error, ), ErrorKind::DataConversion => { if error_string.contains("Not Found") || error_string.contains("not found") { - CosmosError::new( + Error::with_detail( CosmosErrorCode::NotFound, - format!("Resource not found: {}", azure_error), + c"Resource not found", + azure_error, ) } else { - CosmosError::new( + Error::with_detail( CosmosErrorCode::DataConversion, - format!("Data conversion error: {}", azure_error), + c"Data conversion failed", + azure_error, ) } } - _ => CosmosError::new( - CosmosErrorCode::UnknownError, - format!("Unknown error: {}", azure_error), - ), + _ => Error::with_detail(CosmosErrorCode::UnknownError, c"Unknown error", azure_error), } } } -impl From for CosmosError { +impl From for Error { fn from(error: azure_core::Error) -> Self { - convert_azure_error_native(&error) + convert_azure_error_native(error) } } -impl From for CosmosError { +impl From for Error { fn from(error: serde_json::Error) -> Self { - CosmosError::new( + Error::with_detail( CosmosErrorCode::DataConversion, - format!("JSON error: {}", error), + c"JSON serialization/deserialization error", + error.to_string(), ) } } -fn free_non_static_error_message(message: *const c_char) { - if message.is_null() { - return; - } - if !is_static_error_message(message) { - unsafe { - let _ = std::ffi::CString::from_raw(message as *mut c_char); - } - } -} - -/// Releases the memory associated with a [`CosmosError`]. -#[no_mangle] -pub extern "C" fn cosmos_error_free(error: *mut CosmosError) { - if error.is_null() { - return; - } - unsafe { - let err = Box::from_raw(error); - free_non_static_error_message(err.message); - } -} - -pub fn marshal_result( - result: Result, - out_error: *mut CosmosError, - on_success: impl FnOnce(T), -) -> CosmosErrorCode -where - E: Into, -{ - match result { - Ok(value) => { - on_success(value); - unsafe { - *out_error = CosmosError::success(); - } - CosmosErrorCode::Success - } - Err(err) => { - let cosmos_error: CosmosError = err.into(); - let code = cosmos_error.code; - unsafe { - *out_error = cosmos_error; - } - code - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use azure_core::{error::ErrorKind, Error}; - - #[test] - fn test_convert_azure_error_native_io_error() { - let azure_error = Error::new(ErrorKind::Io, "Network connection failed"); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!(cosmos_error.code, CosmosErrorCode::ConnectionFailed); - free_non_static_error_message(cosmos_error.message); - } - - #[test] - fn test_convert_azure_error_native_credential_error() { - let azure_error = Error::new(ErrorKind::Credential, "Invalid credentials"); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!(cosmos_error.code, CosmosErrorCode::AuthenticationFailed); - free_non_static_error_message(cosmos_error.message); - } - - #[test] - fn test_convert_azure_error_native_data_conversion_error() { - let azure_error = Error::new(ErrorKind::DataConversion, "Failed to convert data"); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!(cosmos_error.code, CosmosErrorCode::DataConversion); - free_non_static_error_message(cosmos_error.message); - } - - #[test] - fn test_convert_azure_error_native_missing_field_id_without_404_remains_dataconversion() { - // Missing field without explicit 404 indicator should remain DataConversion - let azure_error = Error::new( - ErrorKind::DataConversion, - "missing field `id` at line 1 column 49", - ); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!(cosmos_error.code, CosmosErrorCode::DataConversion); - free_non_static_error_message(cosmos_error.message); - } - - #[test] - fn test_convert_azure_error_native_missing_field_id_with_404_maps_to_notfound() { - // Missing field WITH explicit 404 indicator should map to NotFound - let azure_error = Error::new( - ErrorKind::DataConversion, - "404 Not Found: missing field `id` at line 1 column 49", - ); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!(cosmos_error.code, CosmosErrorCode::NotFound); - free_non_static_error_message(cosmos_error.message); - } - - #[test] - fn test_convert_azure_error_native_invalid_value_remains_dataconversion() { - let azure_error = Error::new( - ErrorKind::DataConversion, - "invalid value: integer `-2`, expected u64 at line 1 column 305", - ); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!(cosmos_error.code, CosmosErrorCode::DataConversion); - free_non_static_error_message(cosmos_error.message); - } - - #[test] - fn test_convert_azure_error_native_404_in_dataconversion() { - let azure_error = Error::new( - ErrorKind::DataConversion, - "404 Not Found - Resource does not exist", - ); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!(cosmos_error.code, CosmosErrorCode::NotFound); - free_non_static_error_message(cosmos_error.message); - } - - // Comprehensive unit tests for conservative error mapping - #[test] - fn test_error_mapping_explicit_http_status_codes() { - // Test explicit HTTP status codes in DataConversion errors - let test_cases = vec![ - ("401 Unauthorized access", CosmosErrorCode::DataConversion), // No longer http - ("403 Forbidden resource", CosmosErrorCode::DataConversion), // No longer http - ("409 Conflict detected", CosmosErrorCode::DataConversion), // No longer http - ("404 Not Found", CosmosErrorCode::NotFound), - ("not found resource", CosmosErrorCode::NotFound), - ("Not Found: resource missing", CosmosErrorCode::NotFound), - ]; - - for (message, expected_code) in test_cases { - let azure_error = Error::new(ErrorKind::DataConversion, message); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!( - cosmos_error.code, expected_code, - "Failed for message: {}", - message - ); - free_non_static_error_message(cosmos_error.message); - } - } - - #[test] - fn test_error_mapping_missing_field_patterns() { - // Test missing field patterns with and without 404 indicators - let should_remain_dataconversion = vec![ - "missing field `id` at line 1 column 49", - "missing field `name` at line 2 column 10", - "missing required field `partition_key`", - "field `id` missing from JSON", // This should stay DataConversion - no "Not Found" phrase - ]; - - for message in should_remain_dataconversion { - let azure_error = Error::new(ErrorKind::DataConversion, message); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!( - cosmos_error.code, - CosmosErrorCode::DataConversion, - "Should remain DataConversion for: {}", - message - ); - free_non_static_error_message(cosmos_error.message); - } - - // Test missing field WITH explicit "Not Found" indicator (should map to NotFound) - let should_map_to_notfound = vec![ - "Not Found - missing field `id` in response", - "not found: missing field `name` at line 2", - ]; - - for message in should_map_to_notfound { - let azure_error = Error::new(ErrorKind::DataConversion, message); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!( - cosmos_error.code, - CosmosErrorCode::NotFound, - "Should map to NotFound for: {}", - message - ); - free_non_static_error_message(cosmos_error.message); - } - } - - #[test] - fn test_error_mapping_json_parsing_errors() { - // Test various JSON parsing errors that should remain DataConversion - let json_parsing_errors = vec![ - "invalid value: integer `-2`, expected u64 at line 1 column 305", - "expected `,` or `}` at line 1 column 15", - "invalid type: string \"hello\", expected u64 at line 2 column 8", - "EOF while parsing a value at line 1 column 0", - "invalid escape sequence at line 1 column 20", - ]; - - for message in json_parsing_errors { - let azure_error = Error::new(ErrorKind::DataConversion, message); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!( - cosmos_error.code, - CosmosErrorCode::DataConversion, - "JSON parsing error should remain DataConversion for: {}", - message - ); - free_non_static_error_message(cosmos_error.message); - } - } - - #[test] - fn test_error_mapping_http_response_errors() { - use azure_core::http::StatusCode; - - let http_test_cases = vec![ - ( - StatusCode::NotFound, - "Resource not found", - CosmosErrorCode::NotFound, - ), - ( - StatusCode::Unauthorized, - "Authentication failed", - CosmosErrorCode::Unauthorized, - ), - ( - StatusCode::Forbidden, - "Access denied", - CosmosErrorCode::Forbidden, - ), - ( - StatusCode::Conflict, - "Resource already exists", - CosmosErrorCode::Conflict, - ), - ( - StatusCode::InternalServerError, - "Internal server error", - CosmosErrorCode::InternalServerError, - ), - ( - StatusCode::BadRequest, - "Bad request", - CosmosErrorCode::BadRequest, - ), - ( - StatusCode::RequestTimeout, - "Request timeout", - CosmosErrorCode::RequestTimeout, - ), - ( - StatusCode::TooManyRequests, - "Too many requests", - CosmosErrorCode::TooManyRequests, - ), - ( - StatusCode::BadGateway, - "Bad gateway", - CosmosErrorCode::BadGateway, - ), - ( - StatusCode::ServiceUnavailable, - "Service unavailable", - CosmosErrorCode::ServiceUnavailable, - ), - ]; - - for (status_code, message, expected_code) in http_test_cases { - let error_kind = ErrorKind::HttpResponse { - status: status_code, - error_code: None, - raw_response: None, - }; - let azure_error = - Error::with_error(error_kind, std::io::Error::other(message), message); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!( - cosmos_error.code, - expected_code, - "Failed for HTTP status {}: {}", - u16::from(status_code), - message - ); - free_non_static_error_message(cosmos_error.message); - } - } - - #[test] - fn test_error_mapping_fallback_without_http_status() { - let fallback_test_cases = vec![ - ( - ErrorKind::Other, - "Generic error without HTTP status", - CosmosErrorCode::UnknownError, - ), - ( - ErrorKind::Other, - "Some other error", - CosmosErrorCode::UnknownError, - ), - ]; - - for (kind, message, expected_code) in fallback_test_cases { - let azure_error = Error::new(kind, message); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!( - cosmos_error.code, expected_code, - "Failed for fallback case: {}", - message - ); - free_non_static_error_message(cosmos_error.message); - } - } - - #[test] - fn test_error_mapping_edge_cases() { - // Test edge cases and boundary conditions - let edge_cases = vec![ - // Case sensitivity (our logic checks for "Not Found" and "not found", but not "NOT FOUND") - ("NOT FOUND", CosmosErrorCode::DataConversion), - // Empty/whitespace - ("", CosmosErrorCode::DataConversion), - (" ", CosmosErrorCode::DataConversion), - // Real Azure error patterns - ( - "Entity with the specified id does not exist", - CosmosErrorCode::DataConversion, - ), - ( - "Not Found: Entity with the specified id does not exist", - CosmosErrorCode::NotFound, - ), - ]; - - for (message, expected_code) in edge_cases { - let azure_error = Error::new(ErrorKind::DataConversion, message); - let cosmos_error = convert_azure_error_native(&azure_error); - assert_eq!( - cosmos_error.code, expected_code, - "Failed for edge case: {}", - message - ); - free_non_static_error_message(cosmos_error.message); - } - } - - #[test] - fn test_http_status_to_error_code_mapping() { - assert_eq!(http_status_to_error_code(400), CosmosErrorCode::BadRequest); - assert_eq!( - http_status_to_error_code(401), - CosmosErrorCode::Unauthorized - ); - assert_eq!(http_status_to_error_code(403), CosmosErrorCode::Forbidden); - assert_eq!(http_status_to_error_code(404), CosmosErrorCode::NotFound); - assert_eq!(http_status_to_error_code(409), CosmosErrorCode::Conflict); - assert_eq!( - http_status_to_error_code(412), - CosmosErrorCode::PreconditionFailed - ); - assert_eq!( - http_status_to_error_code(408), - CosmosErrorCode::RequestTimeout - ); - assert_eq!( - http_status_to_error_code(429), - CosmosErrorCode::TooManyRequests - ); - assert_eq!( - http_status_to_error_code(500), - CosmosErrorCode::InternalServerError - ); - assert_eq!(http_status_to_error_code(502), CosmosErrorCode::BadGateway); - assert_eq!( - http_status_to_error_code(503), - CosmosErrorCode::ServiceUnavailable - ); - assert_eq!( - http_status_to_error_code(999), - CosmosErrorCode::UnknownError - ); - } - - #[test] - fn test_cosmos_error_memory_management() { - let mut error = CosmosError::new(CosmosErrorCode::BadRequest, "Test error".to_string()); - - // Message should be allocated - assert!(!error.message.is_null()); - - // Free should work without crashing - error.free_message(); - assert!(error.message.is_null()); - } - - #[test] - fn test_cosmos_error_static_message() { - let error = CosmosError::from_static_cstr( - CosmosErrorCode::InvalidArgument, - CSTR_INVALID_CHARS_ERROR, - ); - - // Static message should be set - assert!(!error.message.is_null()); - - // Should be recognized as static - assert!(is_static_error_message(error.message)); +impl From for Error { + fn from(_error: NulError) -> Self { + Error::new( + CosmosErrorCode::InvalidCString, + c"String contains NUL bytes", + ) } } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs index 9cd1f9c5208..343d26906fa 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs @@ -6,12 +6,13 @@ use std::ffi::{c_char, CStr}; #[macro_use] -mod macros; - +pub mod string; +#[macro_use] +pub mod context; pub mod blocking; pub mod clients; pub mod error; -pub mod string; +pub mod runtime; pub use clients::*; diff --git a/sdk/cosmos/azure_data_cosmos_native/src/macros.rs b/sdk/cosmos/azure_data_cosmos_native/src/macros.rs deleted file mode 100644 index 15ddfdff2d2..00000000000 --- a/sdk/cosmos/azure_data_cosmos_native/src/macros.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -macro_rules! c_str { - ($s:expr) => { - const { - // This does a few funky things to make sure we can stay in a const context - // Which ensures the string is generated as a c-str at compile time - const STR: &str = $s; - const BYTES: [u8; STR.len() + 1] = const { - let mut cstr_buf: [u8; STR.len() + 1] = [0; STR.len() + 1]; - let mut i = 0; - // For loops over ranges don't work in const contexts yet. - while i < STR.len() { - cstr_buf[i] = STR.as_bytes()[i]; - i += 1; - } - cstr_buf - }; - match CStr::from_bytes_with_nul(&BYTES) { - Ok(cstr) => cstr, - Err(_) => panic!("failed to convert value to C string"), - } - } - }; -} - -#[macro_export] -macro_rules! block_on { - ($async_expr:expr) => { - $crate::blocking::block_on($async_expr) - }; -} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs b/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs new file mode 100644 index 00000000000..b2766dd0240 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs @@ -0,0 +1,56 @@ +//! This module provides runtime abstractions and implementations for different async runtimes. +//! When compiling the C library, a feature is used to select which runtime implementation to include. +//! Currently, only the Tokio runtime is supported. +//! +//! All callers to the Cosmos DB Client API must first create a RuntimeContext object appropriate for their chosen runtime +//! using the [`cosmos_runtime_context_create`] function. +//! This object must then be passed to all other API functions, within a `CallContext` structure. + +#[cfg(feature = "tokio")] +mod tokio; + +#[cfg(feature = "tokio")] +pub use tokio::*; + +use crate::error::CosmosError; + +/// Creates a new [`RuntimeContext`] for Cosmos DB Client API calls. +/// +/// This must be called before any other Cosmos DB Client API functions are used, +/// and the returned pointer must be passed within a `CallContext` structure to those functions. +/// +/// When the `RuntimeContext` is no longer needed, it should be freed using the +/// [`cosmos_runtime_context_free`] function. However, if the program is terminating, +/// it is not strictly necessary to free it. +/// +/// If this function fails, it will return a null pointer, and the `out_error` parameter +/// (if not null) will be set to contain the error details. +/// +/// The error will contain a dynamically-allocated [`CosmosError::detail`] string that must be +/// freed by the caller using the [`cosmos_string_free`](crate::string::cosmos_string_free) function. +#[no_mangle] +pub extern "C" fn cosmos_runtime_context_create( + out_error: *mut CosmosError, +) -> *mut RuntimeContext { + let c = match RuntimeContext::new() { + Ok(c) => c, + Err(e) => { + unsafe { + if !out_error.is_null() { + *out_error = e.into_ffi(true); + } + } + return std::ptr::null_mut(); + } + }; + Box::into_raw(Box::new(c)) +} + +/// Destroys a [`RuntimeContext`] created by [`cosmos_runtime_context_create`]. +/// This frees the memory associated with the `RuntimeContext`. +#[no_mangle] +pub extern "C" fn cosmos_runtime_context_free(ctx: *mut RuntimeContext) { + if !ctx.is_null() { + unsafe { drop(Box::from_raw(ctx)) } + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs b/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs new file mode 100644 index 00000000000..f61c8341e04 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs @@ -0,0 +1,33 @@ +use tokio::runtime::{Builder, Runtime}; + +use crate::error::{CosmosErrorCode, Error}; + +/// Provides a RuntimeContext (see [`crate::runtime`]) implementation using the Tokio runtime. +pub struct RuntimeContext { + runtime: Runtime, +} + +impl RuntimeContext { + pub fn new() -> Result { + let runtime = Builder::new_multi_thread() + .enable_all() + .build() + .map_err(|e| { + Error::with_detail( + CosmosErrorCode::UnknownError, + c"Unknown error initializing Cosmos SDK runtime", + e, + ) + })?; + Ok(Self { runtime }) + } +} + +impl RuntimeContext { + pub fn block_on(&self, future: F) -> R + where + F: std::future::Future, + { + self.runtime.block_on(future) + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/string.rs b/sdk/cosmos/azure_data_cosmos_native/src/string.rs index e1cf0188992..02ee26b6e56 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/string.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/string.rs @@ -1,38 +1,45 @@ use std::ffi::{CStr, CString}; use std::os::raw::c_char; -use crate::error::{CosmosError, CosmosErrorCode}; +use crate::error::{CosmosErrorCode, Error}; + +#[macro_export] +macro_rules! c_str { + ($s:expr) => { + const { + // This does a few funky things to make sure we can stay in a const context + // Which ensures the string is generated as a c-str at compile time + const STR: &str = $s; + const BYTES: [u8; STR.len() + 1] = const { + let mut cstr_buf: [u8; STR.len() + 1] = [0; STR.len() + 1]; + let mut i = 0; + // For loops over ranges don't work in const contexts yet. + while i < STR.len() { + cstr_buf[i] = STR.as_bytes()[i]; + i += 1; + } + cstr_buf + }; + match CStr::from_bytes_with_nul(&BYTES) { + Ok(cstr) => cstr, + Err(_) => panic!("failed to convert value to C string"), + } + } + }; +} // Safe CString conversion helper that handles NUL bytes gracefully pub fn safe_cstring_new(s: &str) -> CString { CString::new(s).expect("FFI boundary strings must not contain NUL bytes") } -// Safe CString conversion that returns raw pointer and error code -pub fn safe_cstring_into_raw( - s: &str, - out_ptr: &mut *mut c_char, - out_error: &mut CosmosError, -) -> CosmosErrorCode { - let c_string = safe_cstring_new(s); - *out_ptr = c_string.into_raw(); - *out_error = CosmosError::success(); - CosmosErrorCode::Success -} - -pub fn parse_cstr<'a>( - ptr: *const c_char, - error_msg: &'static CStr, -) -> Result<&'a str, CosmosError> { +pub fn parse_cstr<'a>(ptr: *const c_char, error_msg: &'static CStr) -> Result<&'a str, Error> { if ptr.is_null() { - return Err(CosmosError::from_static_cstr( - CosmosErrorCode::InvalidArgument, - error_msg, - )); + return Err(Error::new(CosmosErrorCode::InvalidArgument, error_msg)); } unsafe { CStr::from_ptr(ptr) } .to_str() - .map_err(|_| CosmosError::from_static_cstr(CosmosErrorCode::InvalidArgument, error_msg)) + .map_err(|_| Error::new(CosmosErrorCode::InvalidArgument, error_msg)) } /// Releases the memory associated with a C string obtained from Rust. @@ -40,88 +47,7 @@ pub fn parse_cstr<'a>( pub extern "C" fn cosmos_string_free(ptr: *const c_char) { if !ptr.is_null() { unsafe { - let _ = CString::from_raw(ptr as *mut c_char); + drop(CString::from_raw(ptr as *mut c_char)); } } } - -#[cfg(test)] -mod tests { - use crate::cosmos_version; - use crate::error::CSTR_INVALID_JSON; - - use super::*; - use std::ffi::CStr; - use std::ptr; - - #[test] - fn test_cosmos_version() { - let version_ptr = cosmos_version(); - assert!(!version_ptr.is_null()); - - let version_str = unsafe { CStr::from_ptr(version_ptr).to_str().unwrap() }; - - assert!(version_str.contains("cosmos-cpp-wrapper")); - assert!(version_str.contains("v0.1.0")); - - cosmos_string_free(version_ptr); - } - - #[test] - fn test_cosmos_string_free_null_safety() { - cosmos_string_free(ptr::null()); - } - - #[test] - fn test_safe_cstring_new() { - let result = safe_cstring_new("hello world"); - assert_eq!(result.to_str().unwrap(), "hello world"); - - let panic_result = std::panic::catch_unwind(|| { - safe_cstring_new("hello\0world"); - }); - assert!(panic_result.is_err()); - } - - #[test] - fn test_safe_cstring_into_raw() { - let mut ptr: *mut c_char = ptr::null_mut(); - let mut error = CosmosError::success(); - let code = safe_cstring_into_raw("test", &mut ptr, &mut error); - assert_eq!(code, CosmosErrorCode::Success); - assert!(!ptr.is_null()); - assert_eq!(error.code, CosmosErrorCode::Success); - - cosmos_string_free(ptr); - - let panic_result = std::panic::catch_unwind(|| { - let mut ptr2: *mut c_char = ptr::null_mut(); - let mut error2 = CosmosError::success(); - safe_cstring_into_raw("test\0fail", &mut ptr2, &mut error2); - }); - assert!(panic_result.is_err()); - } - - #[test] - fn test_static_vs_owned_error_messages() { - let static_error = - CosmosError::from_static_cstr(CosmosErrorCode::DataConversion, CSTR_INVALID_JSON); - assert_eq!(static_error.code, CosmosErrorCode::DataConversion); - assert!(!static_error.message.is_null()); - assert_eq!( - static_error.message, - CSTR_INVALID_JSON.as_ptr() as *mut c_char - ); - - let owned_error = CosmosError::new( - CosmosErrorCode::BadRequest, - "Dynamic error message".to_string(), - ); - assert_eq!(owned_error.code, CosmosErrorCode::BadRequest); - assert!(!owned_error.message.is_null()); - assert_ne!( - owned_error.message, - CSTR_INVALID_JSON.as_ptr() as *mut c_char - ); - } -} From aeaba9202027fb632642a4bd57ad7dbabca4a5a6 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 00:40:06 +0000 Subject: [PATCH 07/21] more c tests --- .../azure_data_cosmos_native/CMakeLists.txt | 4 +- .../c_tests/context_memory_management.c | 308 +++++++++ .../c_tests/error_handling.c | 606 ++++++++++++++++++ .../c_tests/item_crud.c | 2 +- .../src/clients/container_client.rs | 15 +- .../src/clients/cosmos_client.rs | 7 +- .../src/clients/database_client.rs | 13 +- .../azure_data_cosmos_native/src/error.rs | 10 +- .../azure_data_cosmos_native/src/lib.rs | 27 + 9 files changed, 967 insertions(+), 25 deletions(-) create mode 100644 sdk/cosmos/azure_data_cosmos_native/c_tests/context_memory_management.c create mode 100644 sdk/cosmos/azure_data_cosmos_native/c_tests/error_handling.c diff --git a/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt b/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt index 0166b131659..5136d218f56 100644 --- a/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt +++ b/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt @@ -24,7 +24,9 @@ corrosion_import_crate( set(TEST_FILES ./c_tests/version.c - ./c_tests/item_crud.c) + ./c_tests/item_crud.c + ./c_tests/context_memory_management.c + ./c_tests/error_handling.c) foreach(test_file ${TEST_FILES}) get_filename_component(test_name ${test_file} NAME_WE) diff --git a/sdk/cosmos/azure_data_cosmos_native/c_tests/context_memory_management.c b/sdk/cosmos/azure_data_cosmos_native/c_tests/context_memory_management.c new file mode 100644 index 00000000000..64c253a0fba --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/context_memory_management.c @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include +#include +#include +#include +#include "../include/azurecosmos.h" + +#define TEST_PASS 0 +#define TEST_FAIL 1 + +// Test counter +static int tests_run = 0; +static int tests_passed = 0; + +void report_test(const char *test_name, int passed) { + tests_run++; + if (passed) { + tests_passed++; + printf("✓ PASS: %s\n", test_name); + } else { + printf("✗ FAIL: %s\n", test_name); + } +} + +// Test 1: Runtime context lifecycle +int test_runtime_context_lifecycle() { + printf("\n--- Test: runtime_context_lifecycle ---\n"); + + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return TEST_FAIL; + } + printf("Created runtime context successfully\n"); + + // Free it + cosmos_runtime_context_free(runtime); + printf("Freed runtime context successfully\n"); + + return TEST_PASS; +} + +// Test 2: Stack-allocated call context +int test_call_context_stack_allocated() { + printf("\n--- Test: call_context_stack_allocated ---\n"); + + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return TEST_FAIL; + } + + // Stack-allocated context + cosmos_call_context ctx; + ctx.runtime_context = runtime; + ctx.include_error_details = false; + + printf("Created stack-allocated call context\n"); + + // Use it for a simple operation (get version) + const char *version = cosmos_version(); + if (!version) { + printf("Failed to get version\n"); + cosmos_runtime_context_free(runtime); + return TEST_FAIL; + } + printf("Successfully used stack-allocated context (version: %s)\n", version); + + cosmos_runtime_context_free(runtime); + return TEST_PASS; +} + +// Test 3: Heap-allocated call context +int test_call_context_heap_allocated() { + printf("\n--- Test: call_context_heap_allocated ---\n"); + + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return TEST_FAIL; + } + + // Heap-allocated context + cosmos_call_context *ctx = cosmos_call_context_create(runtime, false); + if (!ctx) { + printf("Failed to create heap-allocated call context\n"); + cosmos_runtime_context_free(runtime); + return TEST_FAIL; + } + printf("Created heap-allocated call context\n"); + + // Use it for a simple operation (get version) + const char *version = cosmos_version(); + if (!version) { + printf("Failed to get version\n"); + cosmos_call_context_free(ctx); + cosmos_runtime_context_free(runtime); + return TEST_FAIL; + } + printf("Successfully used heap-allocated context (version: %s)\n", version); + + cosmos_call_context_free(ctx); + cosmos_runtime_context_free(runtime); + return TEST_PASS; +} + +// Test 4: Call context reuse +int test_call_context_reuse() { + printf("\n--- Test: call_context_reuse ---\n"); + + const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); + const char *key = getenv("AZURE_COSMOS_KEY"); + + if (!endpoint || !key) { + printf("Skipping test - requires AZURE_COSMOS_ENDPOINT and AZURE_COSMOS_KEY\n"); + return TEST_PASS; // Not a failure, just skipped + } + + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return TEST_FAIL; + } + + cosmos_call_context ctx; + ctx.runtime_context = runtime; + ctx.include_error_details = true; + + cosmos_client *client = NULL; + + // First call - create client + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("First call failed with code: %d\n", code); + cosmos_runtime_context_free(runtime); + return TEST_FAIL; + } + printf("First call succeeded (client created)\n"); + + cosmos_database_client *database = NULL; + + // Reuse context for second call - try to get a database client + code = cosmos_client_database_client(&ctx, client, "nonexistent-db", &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Second call failed with code: %d (expected, just testing reuse)\n", code); + // This is okay - we're testing context reuse, not that the operation succeeds + } else { + printf("Second call succeeded (database client retrieved)\n"); + cosmos_database_free(database); + } + + // Reuse context for third call - try again + database = NULL; + code = cosmos_client_database_client(&ctx, client, "another-nonexistent-db", &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Third call failed with code: %d (expected, just testing reuse)\n", code); + } else { + printf("Third call succeeded (database client retrieved)\n"); + cosmos_database_free(database); + } + + printf("Successfully reused call context for multiple operations\n"); + + cosmos_client_free(client); + cosmos_runtime_context_free(runtime); + return TEST_PASS; +} + +// Test 5: String memory management +int test_string_memory_management() { + printf("\n--- Test: string_memory_management ---\n"); + + const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); + const char *key = getenv("AZURE_COSMOS_KEY"); + + if (!endpoint || !key) { + printf("Skipping test - requires AZURE_COSMOS_ENDPOINT and AZURE_COSMOS_KEY\n"); + return TEST_PASS; + } + + time_t current_time = time(NULL); + char database_name[64]; + snprintf(database_name, sizeof(database_name), "auto-test-db-str-mem-%ld", current_time); + + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return TEST_FAIL; + } + + cosmos_call_context ctx; + ctx.runtime_context = runtime; + ctx.include_error_details = true; + + cosmos_client *client = NULL; + cosmos_database_client *database = NULL; + cosmos_container_client *container = NULL; + const char *read_json = NULL; + int result = TEST_PASS; + int database_created = 0; + + // Create client + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create client\n"); + result = TEST_FAIL; + goto cleanup; + } + + // Create database + code = cosmos_client_create_database(&ctx, client, database_name, &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create database\n"); + result = TEST_FAIL; + goto cleanup; + } + database_created = 1; + printf("Created database: %s\n", database_name); + + // Create container + code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", &container); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create container\n"); + result = TEST_FAIL; + goto cleanup; + } + printf("Created container\n"); + + // Create an item + const char *json_data = "{\"id\":\"item1\",\"pk\":\"pk1\",\"value\":\"test\"}"; + code = cosmos_container_upsert_item(&ctx, container, "pk1", json_data); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to upsert item\n"); + result = TEST_FAIL; + goto cleanup; + } + printf("Upserted item\n"); + + // Read the item - this returns a string that must be freed + code = cosmos_container_read_item(&ctx, container, "pk1", "item1", &read_json); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to read item\n"); + result = TEST_FAIL; + goto cleanup; + } + printf("Read item: %s\n", read_json); + + // Test freeing the string + if (read_json) { + cosmos_string_free(read_json); + read_json = NULL; + printf("Successfully freed JSON string\n"); + } + + // Test freeing error details (trigger an error) + code = cosmos_container_read_item(&ctx, container, "pk1", "nonexistent-item", &read_json); + if (code == COSMOS_ERROR_CODE_NOT_FOUND) { + printf("Got expected NOT_FOUND error\n"); + if (ctx.error.detail) { + printf("Error detail present: %s\n", ctx.error.detail); + cosmos_string_free(ctx.error.detail); + printf("Successfully freed error detail string\n"); + } + } + +cleanup: + if (database && database_created) { + cosmos_database_delete(&ctx, database); + } + + if (container) { + cosmos_container_free(container); + } + if (database) { + cosmos_database_free(database); + } + if (client) { + cosmos_client_free(client); + } + cosmos_runtime_context_free(runtime); + + return result; +} + +int main() { + printf("=== Test Suite 1: Context and Memory Management ===\n"); + + report_test("runtime_context_lifecycle", test_runtime_context_lifecycle() == TEST_PASS); + report_test("call_context_stack_allocated", test_call_context_stack_allocated() == TEST_PASS); + report_test("call_context_heap_allocated", test_call_context_heap_allocated() == TEST_PASS); + report_test("call_context_reuse", test_call_context_reuse() == TEST_PASS); + report_test("string_memory_management", test_string_memory_management() == TEST_PASS); + + printf("\n=== Test Summary ===\n"); + printf("Tests run: %d\n", tests_run); + printf("Tests passed: %d\n", tests_passed); + printf("Tests failed: %d\n", tests_run - tests_passed); + + if (tests_passed == tests_run) { + printf("\n✓ All tests passed!\n"); + return 0; + } else { + printf("\n✗ Some tests failed\n"); + return 1; + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/c_tests/error_handling.c b/sdk/cosmos/azure_data_cosmos_native/c_tests/error_handling.c new file mode 100644 index 00000000000..86e1bca6d37 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/error_handling.c @@ -0,0 +1,606 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include +#include +#include +#include +#include "../include/azurecosmos.h" + +#define TEST_PASS 0 +#define TEST_FAIL 1 + +// Test counter +static int tests_run = 0; +static int tests_passed = 0; + +void report_test(const char *test_name, int passed) { + tests_run++; + if (passed) { + tests_passed++; + printf("✓ PASS: %s\n", test_name); + } else { + printf("✗ FAIL: %s\n", test_name); + } +} + +// Test 1: NULL pointer handling +int test_null_pointer_handling() { + printf("\n--- Test: null_pointer_handling ---\n"); + + const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); + const char *key = getenv("AZURE_COSMOS_KEY"); + + if (!endpoint || !key) { + printf("Skipping test - requires AZURE_COSMOS_ENDPOINT and AZURE_COSMOS_KEY\n"); + return TEST_PASS; + } + + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return TEST_FAIL; + } + + cosmos_call_context ctx; + ctx.runtime_context = runtime; + ctx.include_error_details = false; + + cosmos_client *client = NULL; + int result = TEST_PASS; + + // Test 1a: NULL context + cosmos_error_code code = cosmos_client_create_with_key(NULL, endpoint, key, &client); + if (code == COSMOS_ERROR_CODE_CALL_CONTEXT_MISSING) { + printf("✓ NULL context correctly rejected with CALL_CONTEXT_MISSING\n"); + } else { + printf("✗ NULL context should return CALL_CONTEXT_MISSING, got: %d\n", code); + result = TEST_FAIL; + } + + // Test 1b: NULL endpoint + code = cosmos_client_create_with_key(&ctx, NULL, key, &client); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ NULL endpoint correctly rejected with error code: %d\n", code); + } else { + printf("✗ NULL endpoint should return error\n"); + if (client) cosmos_client_free(client); + result = TEST_FAIL; + } + + // Test 1c: NULL key + code = cosmos_client_create_with_key(&ctx, endpoint, NULL, &client); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ NULL key correctly rejected with error code: %d\n", code); + } else { + printf("✗ NULL key should return error\n"); + if (client) cosmos_client_free(client); + result = TEST_FAIL; + } + + // Test 1d: NULL output pointer + code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ NULL output pointer correctly rejected with error code: %d\n", code); + } else { + printf("✗ NULL output pointer should return error\n"); + result = TEST_FAIL; + } + + // Create a valid client for further tests + code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create valid client for remaining tests\n"); + cosmos_runtime_context_free(runtime); + return TEST_FAIL; + } + + // Test 1e: NULL client pointer in operation + cosmos_database_client *database = NULL; + code = cosmos_client_database_client(&ctx, NULL, "test-db", &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ NULL client pointer correctly rejected with error code: %d\n", code); + } else { + printf("✗ NULL client pointer should return error\n"); + if (database) cosmos_database_free(database); + result = TEST_FAIL; + } + + // Test 1f: NULL database name + code = cosmos_client_database_client(&ctx, client, NULL, &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ NULL database name correctly rejected with error code: %d\n", code); + } else { + printf("✗ NULL database name should return error\n"); + if (database) cosmos_database_free(database); + result = TEST_FAIL; + } + + cosmos_client_free(client); + cosmos_runtime_context_free(runtime); + return result; +} + +// Test 2: Invalid runtime context +int test_invalid_runtime_context() { + printf("\n--- Test: invalid_runtime_context ---\n"); + + const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); + const char *key = getenv("AZURE_COSMOS_KEY"); + + if (!endpoint || !key) { + printf("Skipping test - requires AZURE_COSMOS_ENDPOINT and AZURE_COSMOS_KEY\n"); + return TEST_PASS; + } + + cosmos_call_context ctx; + ctx.runtime_context = NULL; + ctx.include_error_details = false; + + // Now try to use the invalid context + cosmos_client *client = NULL; + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ Invalid/freed runtime context correctly rejected with error code: %d\n", code); + return TEST_PASS; + } else { + printf("✗ Invalid/freed runtime context should return error\n"); + if (client) cosmos_client_free(client); + return TEST_FAIL; + } +} + +// Test 3: Error details with flag enabled +int test_error_detail_with_flag() { + printf("\n--- Test: error_detail_with_flag ---\n"); + + const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); + const char *key = getenv("AZURE_COSMOS_KEY"); + + if (!endpoint || !key) { + printf("Skipping test - requires AZURE_COSMOS_ENDPOINT and AZURE_COSMOS_KEY\n"); + return TEST_PASS; + } + + time_t current_time = time(NULL); + char database_name[64]; + snprintf(database_name, sizeof(database_name), "test-err-dtl-%ld", current_time); + + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return TEST_FAIL; + } + + cosmos_call_context ctx; + ctx.runtime_context = runtime; + ctx.include_error_details = true; // Enable error details + + cosmos_client *client = NULL; + cosmos_database_client *database = NULL; + cosmos_container_client *container = NULL; + int result = TEST_PASS; + int database_created = 0; + + // Create client + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create client\n"); + result = TEST_FAIL; + goto cleanup; + } + + // Create database + code = cosmos_client_create_database(&ctx, client, database_name, &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create database\n"); + result = TEST_FAIL; + goto cleanup; + } + database_created = 1; + + // Create container + code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", &container); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create container\n"); + result = TEST_FAIL; + goto cleanup; + } + + // Trigger an error - try to read non-existent item + const char *read_json = NULL; + code = cosmos_container_read_item(&ctx, container, "pk1", "nonexistent-item", &read_json); + + if (code == COSMOS_ERROR_CODE_NOT_FOUND) { + printf("✓ Got expected NOT_FOUND error code\n"); + + // Check if error details are present + if (ctx.error.detail != NULL) { + printf("✓ Error detail is populated: %s\n", ctx.error.detail); + cosmos_string_free(ctx.error.detail); + ctx.error.detail = NULL; + } else { + printf("✗ Error detail should be populated when include_error_details=true\n"); + result = TEST_FAIL; + } + + if (ctx.error.message != NULL) { + printf("✓ Error message is populated: %s\n", ctx.error.message); + } + } else { + printf("✗ Expected NOT_FOUND error, got: %d\n", code); + result = TEST_FAIL; + } + +cleanup: + if (database && database_created) { + cosmos_database_delete(&ctx, database); + } + + if (container) { + cosmos_container_free(container); + } + if (database) { + cosmos_database_free(database); + } + if (client) { + cosmos_client_free(client); + } + cosmos_runtime_context_free(runtime); + + return result; +} + +// Test 4: Error details with flag disabled +int test_error_detail_without_flag() { + printf("\n--- Test: error_detail_without_flag ---\n"); + + const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); + const char *key = getenv("AZURE_COSMOS_KEY"); + + if (!endpoint || !key) { + printf("Skipping test - requires AZURE_COSMOS_ENDPOINT and AZURE_COSMOS_KEY\n"); + return TEST_PASS; + } + + time_t current_time = time(NULL); + char database_name[64]; + snprintf(database_name, sizeof(database_name), "test-no-dtl-%ld", current_time); + + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return TEST_FAIL; + } + + cosmos_call_context ctx; + ctx.runtime_context = runtime; + ctx.include_error_details = false; // Disable error details + + cosmos_client *client = NULL; + cosmos_database_client *database = NULL; + cosmos_container_client *container = NULL; + int result = TEST_PASS; + int database_created = 0; + + // Create client + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create client\n"); + result = TEST_FAIL; + goto cleanup; + } + + // Create database + code = cosmos_client_create_database(&ctx, client, database_name, &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create database\n"); + result = TEST_FAIL; + goto cleanup; + } + database_created = 1; + + // Create container + code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", &container); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create container\n"); + result = TEST_FAIL; + goto cleanup; + } + + // Trigger an error - try to read non-existent item + const char *read_json = NULL; + code = cosmos_container_read_item(&ctx, container, "pk1", "nonexistent-item", &read_json); + + if (code == COSMOS_ERROR_CODE_NOT_FOUND) { + printf("✓ Got expected NOT_FOUND error code\n"); + + // Check that error details are NOT present + if (ctx.error.detail == NULL) { + printf("✓ Error detail is NULL (as expected when include_error_details=false)\n"); + } else { + printf("✗ Error detail should be NULL when include_error_details=false, but got: %s\n", + ctx.error.detail); + cosmos_string_free(ctx.error.detail); + result = TEST_FAIL; + } + + if (ctx.error.message != NULL) { + printf("✓ Error message is still populated: %s\n", ctx.error.message); + } + } else { + printf("✗ Expected NOT_FOUND error, got: %d\n", code); + result = TEST_FAIL; + } + +cleanup: + if (database && database_created) { + cosmos_database_delete(&ctx, database); + } + + if (container) { + cosmos_container_free(container); + } + if (database) { + cosmos_database_free(database); + } + if (client) { + cosmos_client_free(client); + } + cosmos_runtime_context_free(runtime); + + return result; +} + +// Test 5: Invalid UTF-8 strings +int test_invalid_utf8_strings() { + printf("\n--- Test: invalid_utf8_strings ---\n"); + + const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); + const char *key = getenv("AZURE_COSMOS_KEY"); + + if (!endpoint || !key) { + printf("Skipping test - requires AZURE_COSMOS_ENDPOINT and AZURE_COSMOS_KEY\n"); + return TEST_PASS; + } + + time_t current_time = time(NULL); + char database_name[64]; + snprintf(database_name, sizeof(database_name), "test-utf8-%ld", current_time); + + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return TEST_FAIL; + } + + cosmos_call_context ctx; + ctx.runtime_context = runtime; + ctx.include_error_details = false; + + cosmos_client *client = NULL; + cosmos_database_client *database = NULL; + cosmos_container_client *container = NULL; + int result = TEST_PASS; + int database_created = 0; + + // Create client + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create client\n"); + result = TEST_FAIL; + goto cleanup; + } + + // Create database + code = cosmos_client_create_database(&ctx, client, database_name, &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create database\n"); + result = TEST_FAIL; + goto cleanup; + } + database_created = 1; + + // Create container + code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", &container); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create container\n"); + result = TEST_FAIL; + goto cleanup; + } + + // Test with invalid UTF-8 sequence in JSON data + // Note: In C, we can create invalid UTF-8 by using raw byte sequences + char invalid_json[128]; + // Start with valid JSON structure + strcpy(invalid_json, "{\"id\":\"item1\",\"pk\":\"pk1\",\"value\":\""); + // Append an invalid UTF-8 sequence (lone continuation byte) + size_t len = strlen(invalid_json); + invalid_json[len] = (char)0x80; // Invalid UTF-8 continuation byte without start byte + invalid_json[len + 1] = '\0'; + strcat(invalid_json, "\"}"); + + code = cosmos_container_upsert_item(&ctx, container, "pk1", invalid_json); + + if (code == COSMOS_ERROR_CODE_INVALID_UTF8) { + printf("✓ Invalid UTF-8 correctly rejected with INVALID_UTF8 error code\n"); + } else if (code != COSMOS_ERROR_CODE_SUCCESS) { + // Some other error - also acceptable as the invalid UTF-8 was caught + printf("✓ Invalid UTF-8 rejected with error code: %d\n", code); + } else { + printf("⚠ Invalid UTF-8 was not rejected (may have been sanitized or JSON parsing caught it)\n"); + // Not necessarily a failure - the system may have other validation layers + } + +cleanup: + if (database && database_created) { + cosmos_database_delete(&ctx, database); + } + + if (container) { + cosmos_container_free(container); + } + if (database) { + cosmos_database_free(database); + } + if (client) { + cosmos_client_free(client); + } + cosmos_runtime_context_free(runtime); + + return result; +} + +// Test 6: Empty string handling +int test_empty_string_handling() { + printf("\n--- Test: empty_string_handling ---\n"); + + const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); + const char *key = getenv("AZURE_COSMOS_KEY"); + + if (!endpoint || !key) { + printf("Skipping test - requires AZURE_COSMOS_ENDPOINT and AZURE_COSMOS_KEY\n"); + return TEST_PASS; + } + + time_t current_time = time(NULL); + char database_name[64]; + snprintf(database_name, sizeof(database_name), "test-empty-%ld", current_time); + + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + if (!runtime) { + printf("Failed to create runtime context\n"); + return TEST_FAIL; + } + + cosmos_call_context ctx; + ctx.runtime_context = runtime; + ctx.include_error_details = false; + + cosmos_client *client = NULL; + cosmos_database_client *database = NULL; + cosmos_container_client *container = NULL; + int result = TEST_PASS; + int database_created = 0; + + // Create client + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create client\n"); + result = TEST_FAIL; + goto cleanup; + } + + // Test 6a: Empty database name + code = cosmos_client_create_database(&ctx, client, "", &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ Empty database name correctly rejected with error code: %d\n", code); + } else { + printf("✗ Empty database name should return error\n"); + cosmos_database_free(database); + database = NULL; + result = TEST_FAIL; + } + + // Create valid database for remaining tests + code = cosmos_client_create_database(&ctx, client, database_name, &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create valid database\n"); + result = TEST_FAIL; + goto cleanup; + } + database_created = 1; + + // Test 6b: Empty container name + code = cosmos_database_create_container(&ctx, database, "", "/pk", &container); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ Empty container name correctly rejected with error code: %d\n", code); + } else { + printf("✗ Empty container name should return error\n"); + cosmos_container_free(container); + container = NULL; + result = TEST_FAIL; + } + + // Test 6c: Empty partition key path + code = cosmos_database_create_container(&ctx, database, "test-container", "", &container); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ Empty partition key path correctly rejected with error code: %d\n", code); + } else { + printf("✗ Empty partition key path should return error\n"); + cosmos_container_free(container); + container = NULL; + result = TEST_FAIL; + } + + // Create valid container for remaining tests + code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", &container); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("Failed to create valid container\n"); + result = TEST_FAIL; + goto cleanup; + } + + // Test 6d: Empty item ID in JSON + const char *json_with_empty_id = "{\"id\":\"\",\"pk\":\"pk1\",\"value\":\"test\"}"; + code = cosmos_container_upsert_item(&ctx, container, "pk1", json_with_empty_id); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ Empty item ID correctly rejected with error code: %d\n", code); + } else { + printf("✗ Empty item ID should return error\n"); + result = TEST_FAIL; + } + + // Test 6e: Empty partition key value + const char *json_data = "{\"id\":\"item1\",\"pk\":\"pk1\",\"value\":\"test\"}"; + code = cosmos_container_upsert_item(&ctx, container, "", json_data); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + printf("✓ Empty partition key value correctly rejected with error code: %d\n", code); + } else { + printf("✗ Empty partition key value should return error\n"); + result = TEST_FAIL; + } + +cleanup: + if (database && database_created) { + cosmos_database_delete(&ctx, database); + } + + if (container) { + cosmos_container_free(container); + } + if (database) { + cosmos_database_free(database); + } + if (client) { + cosmos_client_free(client); + } + cosmos_runtime_context_free(runtime); + + return result; +} + +int main() { + printf("=== Test Suite 2: Error Handling and Validation ===\n"); + + report_test("null_pointer_handling", test_null_pointer_handling() == TEST_PASS); + report_test("invalid_runtime_context", test_invalid_runtime_context() == TEST_PASS); + report_test("error_detail_with_flag", test_error_detail_with_flag() == TEST_PASS); + report_test("error_detail_without_flag", test_error_detail_without_flag() == TEST_PASS); + report_test("invalid_utf8_strings", test_invalid_utf8_strings() == TEST_PASS); + report_test("empty_string_handling", test_empty_string_handling() == TEST_PASS); + + printf("\n=== Test Summary ===\n"); + printf("Tests run: %d\n", tests_run); + printf("Tests passed: %d\n", tests_passed); + printf("Tests failed: %d\n", tests_run - tests_passed); + + if (tests_passed == tests_run) { + printf("\n✓ All tests passed!\n"); + return 0; + } else { + printf("\n✗ Some tests failed\n"); + return 1; + } +} diff --git a/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c index 76c50d359da..8a9b76cc9c5 100644 --- a/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c @@ -39,7 +39,7 @@ int main() { // Generate unique database and container names using timestamp time_t current_time = time(NULL); char database_name[64]; - snprintf(database_name, sizeof(database_name), "auto-test-db-%ld", current_time); + snprintf(database_name, sizeof(database_name), "auto-test-db-item-crud-%ld", current_time); printf("Running Cosmos DB item CRUD test...\n"); printf("Endpoint: %s\n", endpoint); diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs index 832a5ffad81..793cc0732b1 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs @@ -9,6 +9,7 @@ use serde_json::value::RawValue; use crate::context::CallContext; use crate::error::{self, CosmosErrorCode, Error}; use crate::string::parse_cstr; +use crate::unwrap_required_ptr; /// Releases the memory associated with a [`ContainerClient`]. #[no_mangle] @@ -33,7 +34,7 @@ pub extern "C" fn cosmos_container_create_item( json_data: *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async(async { - let container = unsafe { &*container }; + let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; let partition_key = parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string(); let json = parse_cstr(json_data, error::messages::INVALID_JSON)?.to_string(); @@ -60,7 +61,7 @@ pub extern "C" fn cosmos_container_upsert_item( json_data: *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async(async { - let container = unsafe { &*container }; + let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; let partition_key = parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string(); let json = parse_cstr(json_data, error::messages::INVALID_JSON)?.to_string(); @@ -89,7 +90,7 @@ pub extern "C" fn cosmos_container_read_item( out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { - let container = unsafe { &*container }; + let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; let partition_key = parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string(); let item_id = parse_cstr(item_id, error::messages::INVALID_ITEM_ID)?; @@ -121,7 +122,7 @@ pub extern "C" fn cosmos_container_replace_item( json_data: *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async(async { - let container = unsafe { &*container }; + let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; let partition_key = parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string(); let item_id = parse_cstr(item_id, error::messages::INVALID_ITEM_ID)?; @@ -149,7 +150,7 @@ pub extern "C" fn cosmos_container_delete_item( item_id: *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async(async { - let container = unsafe { &*container }; + let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; let partition_key = parse_cstr(partition_key, error::messages::INVALID_PARTITION_KEY)?.to_string(); let item_id = parse_cstr(item_id, error::messages::INVALID_ITEM_ID)?; @@ -173,7 +174,7 @@ pub extern "C" fn cosmos_container_read( out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { - let container = unsafe { &*container }; + let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; let response = container.read(None).await?; let body = response.into_body().into_string()?; Ok(CString::new(body)?) @@ -197,7 +198,7 @@ pub extern "C" fn cosmos_container_query_items( out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { - let container = unsafe { &*container }; + let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; let query = Query::from(parse_cstr(query, error::messages::INVALID_QUERY)?); let partition_key = if partition_key.is_null() { diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs index 8720acd0f7d..a9268ddda15 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs @@ -10,6 +10,7 @@ use futures::TryStreamExt; use crate::context::CallContext; use crate::error::{self, CosmosErrorCode, Error}; use crate::string::parse_cstr; +use crate::unwrap_required_ptr; /// Creates a new CosmosClient and returns a pointer to it via the out parameter. /// @@ -61,7 +62,7 @@ pub extern "C" fn cosmos_client_database_client( out_database: *mut *mut DatabaseClient, ) -> CosmosErrorCode { context!(ctx).run_sync_with_output(out_database, || { - let client = unsafe { &*client }; + let client = unwrap_required_ptr(client, error::messages::INVALID_CLIENT_POINTER)?; let database_id = parse_cstr(database_id, error::messages::INVALID_DATABASE_ID)?; let database_client = client.database_client(database_id); Ok(Box::new(database_client)) @@ -83,7 +84,7 @@ pub extern "C" fn cosmos_client_query_databases( out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { - let client = unsafe { &*client }; + let client = unwrap_required_ptr(client, error::messages::INVALID_CLIENT_POINTER)?; let query_str = parse_cstr(query, error::messages::INVALID_QUERY)?; let cosmos_query = Query::from(query_str); @@ -117,7 +118,7 @@ pub extern "C" fn cosmos_client_create_database( out_database: *mut *mut DatabaseClient, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_database, async { - let client = unsafe { &*client }; + let client = unwrap_required_ptr(client, error::messages::INVALID_CLIENT_POINTER)?; let database_id = parse_cstr(database_id, error::messages::INVALID_DATABASE_ID)?; client.create_database(database_id, None).await?; diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs index b7e72f08886..a4fb3c9bebc 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs @@ -9,6 +9,7 @@ use futures::TryStreamExt; use crate::context::CallContext; use crate::error::{self, CosmosErrorCode, Error}; use crate::string::parse_cstr; +use crate::unwrap_required_ptr; /// Releases the memory associated with a [`DatabaseClient`]. #[no_mangle] @@ -33,7 +34,7 @@ pub extern "C" fn cosmos_database_container_client( out_container: *mut *mut ContainerClient, ) -> CosmosErrorCode { context!(ctx).run_sync_with_output(out_container, || { - let database = unsafe { &*database }; + let database = unwrap_required_ptr(database, error::messages::INVALID_DATABASE_POINTER)?; let container_id = parse_cstr(container_id, error::messages::INVALID_CONTAINER_ID)?; let container_client = database.container_client(container_id); Ok(Box::new(container_client)) @@ -53,7 +54,7 @@ pub extern "C" fn cosmos_database_read( out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { - let database = unsafe { &*database }; + let database = unwrap_required_ptr(database, error::messages::INVALID_DATABASE_POINTER)?; let response = database.read(None).await?; let json = response.into_body().into_string()?; Ok(CString::new(json)?) @@ -71,7 +72,7 @@ pub extern "C" fn cosmos_database_delete( database: *const DatabaseClient, ) -> CosmosErrorCode { context!(ctx).run_async(async { - let database = unsafe { &*database }; + let database = unwrap_required_ptr(database, error::messages::INVALID_DATABASE_POINTER)?; database.delete(None).await?; Ok(()) }) @@ -94,7 +95,7 @@ pub extern "C" fn cosmos_database_create_container( out_container: *mut *mut ContainerClient, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_container, async { - let database = unsafe { &*database }; + let database = unwrap_required_ptr(database, error::messages::INVALID_DATABASE_POINTER)?; let container_id = parse_cstr(container_id, error::messages::INVALID_CONTAINER_ID)?.to_string(); @@ -110,7 +111,7 @@ pub extern "C" fn cosmos_database_create_container( let container_client = database.container_client(&container_id); - Ok(Box::new(container_client.into())) + Ok(Box::new(container_client)) }) } @@ -129,7 +130,7 @@ pub extern "C" fn cosmos_database_query_containers( out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { - let database = unsafe { &*database }; + let database = unwrap_required_ptr(database, error::messages::INVALID_DATABASE_POINTER)?; let query = parse_cstr(query, error::messages::INVALID_QUERY)?; let cosmos_query = Query::from(query); diff --git a/sdk/cosmos/azure_data_cosmos_native/src/error.rs b/sdk/cosmos/azure_data_cosmos_native/src/error.rs index a4e7e540ed8..387e6ec2574 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/error.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/error.rs @@ -7,21 +7,17 @@ pub mod messages { pub static OPERATION_SUCCEEDED: &CStr = c"Operation completed successfully"; pub static NULL_OUTPUT_POINTER: &CStr = c"Output pointer is null"; - - pub static CSTR_NUL_BYTES_ERROR: &CStr = c"String contains NUL bytes"; - pub static CSTR_INVALID_CHARS_ERROR: &CStr = c"Error message contains invalid characters"; - pub static CSTR_UNKNOWN_ERROR: &CStr = c"Unknown error"; pub static INVALID_JSON: &CStr = c"Invalid JSON data"; - pub static CSTR_CLIENT_CREATION_FAILED: &CStr = c"Failed to create Azure Cosmos client"; - pub static INVALID_ENDPOINT: &CStr = c"Invalid endpoint string"; pub static INVALID_KEY: &CStr = c"Invalid key string"; pub static INVALID_DATABASE_ID: &CStr = c"Invalid database ID string"; pub static INVALID_CONTAINER_ID: &CStr = c"Invalid container ID string"; pub static INVALID_PARTITION_KEY: &CStr = c"Invalid partition key string"; pub static INVALID_ITEM_ID: &CStr = c"Invalid item ID string"; - pub static CSTR_INVALID_JSON_DATA: &CStr = c"Invalid JSON data string"; pub static INVALID_QUERY: &CStr = c"Invalid query string"; + pub static INVALID_CLIENT_POINTER: &CStr = c"Invalid client pointer"; + pub static INVALID_DATABASE_POINTER: &CStr = c"Invalid database client pointer"; + pub static INVALID_CONTAINER_POINTER: &CStr = c"Invalid container client pointer"; } #[repr(i32)] diff --git a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs index 343d26906fa..1800aec2f7d 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs @@ -16,6 +16,33 @@ pub mod runtime; pub use clients::*; +/// Helper function to safely unwrap a required pointer, returning an error if it's null. +/// +/// # Arguments +/// * `ptr` - The pointer to check and dereference. +/// * `msg` - A static error message to use if the pointer is null. +/// +/// # Returns +/// * `Ok(&T)` if the pointer is non-null. +/// * `Err(Error)` with code `InvalidArgument` if the pointer is null. +/// +/// # Safety +/// This function assumes that if the pointer is non-null, it points to a valid `T`. +/// The caller must ensure the pointer was created properly and has not been freed. +pub fn unwrap_required_ptr<'a, T>( + ptr: *const T, + msg: &'static CStr, +) -> Result<&'a T, error::Error> { + if ptr.is_null() { + Err(error::Error::new( + error::CosmosErrorCode::InvalidArgument, + msg, + )) + } else { + Ok(unsafe { &*ptr }) + } +} + // We just want this value to be present as a string in the compiled binary. // But in order to prevent the compiler from optimizing it away, we expose it as a non-mangled static variable. /// cbindgen:ignore From 9a1298bb92ffcf524430c5db2e9eaaadbea84d24 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 00:47:19 +0000 Subject: [PATCH 08/21] remove debugging code --- sdk/core/typespec_client_core/src/http/clients/reqwest.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/core/typespec_client_core/src/http/clients/reqwest.rs b/sdk/core/typespec_client_core/src/http/clients/reqwest.rs index 8388ff1477f..cc4f1248a07 100644 --- a/sdk/core/typespec_client_core/src/http/clients/reqwest.rs +++ b/sdk/core/typespec_client_core/src/http/clients/reqwest.rs @@ -58,7 +58,10 @@ impl HttpClient for ::reqwest::Client { "performing request {method} '{}' with `reqwest`", url.sanitize(&DEFAULT_ALLOWED_QUERY_PARAMETERS) ); - let rsp = self.execute(reqwest_request).await.unwrap(); + let rsp = self + .execute(reqwest_request) + .await + .with_context(ErrorKind::Io, "failed to execute `reqwest` request")?; let status = rsp.status(); let headers = to_headers(rsp.headers()); From 0d18fa9df2f4a51b63626532b09761a9f98bea06 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 20:04:01 +0000 Subject: [PATCH 09/21] options types and tracing --- sdk/cosmos/azure_data_cosmos_native/build.rs | 22 +- .../c_tests/context_memory_management.c | 41 +- .../c_tests/error_handling.c | 75 +-- .../c_tests/item_crud.c | 17 +- .../include/.gitignore | 3 - .../include/azurecosmos.h | 547 ++++++++++++++++++ .../src/clients/container_client.rs | 76 +++ .../src/clients/cosmos_client.rs | 29 +- .../src/clients/database_client.rs | 34 ++ .../azure_data_cosmos_native/src/context.rs | 62 +- .../azure_data_cosmos_native/src/error.rs | 41 +- .../azure_data_cosmos_native/src/lib.rs | 3 + .../src/options/mod.rs | 44 ++ .../src/runtime/mod.rs | 19 +- .../src/runtime/tokio.rs | 16 +- .../azure_data_cosmos_native/src/string.rs | 7 +- 16 files changed, 941 insertions(+), 95 deletions(-) delete mode 100644 sdk/cosmos/azure_data_cosmos_native/include/.gitignore create mode 100644 sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h create mode 100644 sdk/cosmos/azure_data_cosmos_native/src/options/mod.rs diff --git a/sdk/cosmos/azure_data_cosmos_native/build.rs b/sdk/cosmos/azure_data_cosmos_native/build.rs index ac04bd930ec..5de976f223f 100644 --- a/sdk/cosmos/azure_data_cosmos_native/build.rs +++ b/sdk/cosmos/azure_data_cosmos_native/build.rs @@ -54,6 +54,8 @@ fn main() { export: cbindgen::ExportConfig { prefix: Some("cosmos_".into()), exclude: vec!["PartitionKeyValue".into()], + + // From what I can tell, there's no way to set a rename rule for types :( rename: HashMap::from([ ("RuntimeContext".into(), "runtime_context".into()), ("CallContext".into(), "call_context".into()), @@ -62,6 +64,17 @@ fn main() { ("CosmosClient".into(), "client".into()), ("DatabaseClient".into(), "database_client".into()), ("ContainerClient".into(), "container_client".into()), + ("ClientOptions".into(), "client_options".into()), + ("QueryOptions".into(), "query_options".into()), + ("CreateDatabaseOptions".into(), "create_database_options".into()), + ("ReadDatabaseOptions".into(), "read_database_options".into()), + ("DeleteDatabaseOptions".into(), "delete_database_options".into()), + ("CreateContainerOptions".into(), "create_container_options".into()), + ("ReadContainerOptions".into(), "read_container_options".into()), + ("DeleteContainerOptions".into(), "delete_container_options".into()), + ("ItemOptions".into(), "item_options".into()), + ("RuntimeOptions".into(), "runtime_options".into()), + ("CallContextOptions".into(), "call_context_options".into()), ]), ..Default::default() }, @@ -69,10 +82,13 @@ fn main() { }; let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - cbindgen::Builder::new() + let Ok(bindings) = cbindgen::Builder::new() .with_crate(crate_dir) .with_config(config) .generate() - .expect("unable to generate bindings") - .write_to_file("include/azurecosmos.h"); + else { + println!("cargo:error=Failed to generate C bindings for azure_data_cosmos_native"); + return; + }; + bindings.write_to_file("include/azurecosmos.h"); } diff --git a/sdk/cosmos/azure_data_cosmos_native/c_tests/context_memory_management.c b/sdk/cosmos/azure_data_cosmos_native/c_tests/context_memory_management.c index 64c253a0fba..3d56e095e99 100644 --- a/sdk/cosmos/azure_data_cosmos_native/c_tests/context_memory_management.c +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/context_memory_management.c @@ -28,7 +28,8 @@ void report_test(const char *test_name, int passed) { int test_runtime_context_lifecycle() { printf("\n--- Test: runtime_context_lifecycle ---\n"); - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { printf("Failed to create runtime context\n"); return TEST_FAIL; @@ -46,7 +47,8 @@ int test_runtime_context_lifecycle() { int test_call_context_stack_allocated() { printf("\n--- Test: call_context_stack_allocated ---\n"); - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { printf("Failed to create runtime context\n"); return TEST_FAIL; @@ -76,7 +78,8 @@ int test_call_context_stack_allocated() { int test_call_context_heap_allocated() { printf("\n--- Test: call_context_heap_allocated ---\n"); - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { printf("Failed to create runtime context\n"); return TEST_FAIL; @@ -114,11 +117,13 @@ int test_call_context_reuse() { const char *key = getenv("AZURE_COSMOS_KEY"); if (!endpoint || !key) { - printf("Skipping test - requires AZURE_COSMOS_ENDPOINT and AZURE_COSMOS_KEY\n"); - return TEST_PASS; // Not a failure, just skipped + printf("Error: Missing required environment variables.\n"); + printf("Required: AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_KEY\n"); + return TEST_FAIL; } - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { printf("Failed to create runtime context\n"); return TEST_FAIL; @@ -131,7 +136,7 @@ int test_call_context_reuse() { cosmos_client *client = NULL; // First call - create client - cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("First call failed with code: %d\n", code); cosmos_runtime_context_free(runtime); @@ -176,15 +181,17 @@ int test_string_memory_management() { const char *key = getenv("AZURE_COSMOS_KEY"); if (!endpoint || !key) { - printf("Skipping test - requires AZURE_COSMOS_ENDPOINT and AZURE_COSMOS_KEY\n"); - return TEST_PASS; + printf("Error: Missing required environment variables.\n"); + printf("Required: AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_KEY\n"); + return TEST_FAIL; } time_t current_time = time(NULL); char database_name[64]; snprintf(database_name, sizeof(database_name), "auto-test-db-str-mem-%ld", current_time); - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { printf("Failed to create runtime context\n"); return TEST_FAIL; @@ -202,7 +209,7 @@ int test_string_memory_management() { int database_created = 0; // Create client - cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create client\n"); result = TEST_FAIL; @@ -210,7 +217,7 @@ int test_string_memory_management() { } // Create database - code = cosmos_client_create_database(&ctx, client, database_name, &database); + code = cosmos_client_create_database(&ctx, client, database_name, NULL, &database); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create database\n"); result = TEST_FAIL; @@ -220,7 +227,7 @@ int test_string_memory_management() { printf("Created database: %s\n", database_name); // Create container - code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", &container); + code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", NULL, &container); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create container\n"); result = TEST_FAIL; @@ -230,7 +237,7 @@ int test_string_memory_management() { // Create an item const char *json_data = "{\"id\":\"item1\",\"pk\":\"pk1\",\"value\":\"test\"}"; - code = cosmos_container_upsert_item(&ctx, container, "pk1", json_data); + code = cosmos_container_upsert_item(&ctx, container, "pk1", json_data, NULL); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to upsert item\n"); result = TEST_FAIL; @@ -239,7 +246,7 @@ int test_string_memory_management() { printf("Upserted item\n"); // Read the item - this returns a string that must be freed - code = cosmos_container_read_item(&ctx, container, "pk1", "item1", &read_json); + code = cosmos_container_read_item(&ctx, container, "pk1", "item1", NULL, &read_json); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to read item\n"); result = TEST_FAIL; @@ -255,7 +262,7 @@ int test_string_memory_management() { } // Test freeing error details (trigger an error) - code = cosmos_container_read_item(&ctx, container, "pk1", "nonexistent-item", &read_json); + code = cosmos_container_read_item(&ctx, container, "pk1", "nonexistent-item", NULL, &read_json); if (code == COSMOS_ERROR_CODE_NOT_FOUND) { printf("Got expected NOT_FOUND error\n"); if (ctx.error.detail) { @@ -267,7 +274,7 @@ int test_string_memory_management() { cleanup: if (database && database_created) { - cosmos_database_delete(&ctx, database); + cosmos_database_delete(&ctx, database, NULL); } if (container) { diff --git a/sdk/cosmos/azure_data_cosmos_native/c_tests/error_handling.c b/sdk/cosmos/azure_data_cosmos_native/c_tests/error_handling.c index 86e1bca6d37..36d00fb6ecc 100644 --- a/sdk/cosmos/azure_data_cosmos_native/c_tests/error_handling.c +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/error_handling.c @@ -36,7 +36,8 @@ int test_null_pointer_handling() { return TEST_PASS; } - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { printf("Failed to create runtime context\n"); return TEST_FAIL; @@ -50,7 +51,7 @@ int test_null_pointer_handling() { int result = TEST_PASS; // Test 1a: NULL context - cosmos_error_code code = cosmos_client_create_with_key(NULL, endpoint, key, &client); + cosmos_error_code code = cosmos_client_create_with_key(NULL, endpoint, key, NULL, &client); if (code == COSMOS_ERROR_CODE_CALL_CONTEXT_MISSING) { printf("✓ NULL context correctly rejected with CALL_CONTEXT_MISSING\n"); } else { @@ -59,7 +60,7 @@ int test_null_pointer_handling() { } // Test 1b: NULL endpoint - code = cosmos_client_create_with_key(&ctx, NULL, key, &client); + code = cosmos_client_create_with_key(&ctx, NULL, key, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("✓ NULL endpoint correctly rejected with error code: %d\n", code); } else { @@ -69,7 +70,7 @@ int test_null_pointer_handling() { } // Test 1c: NULL key - code = cosmos_client_create_with_key(&ctx, endpoint, NULL, &client); + code = cosmos_client_create_with_key(&ctx, endpoint, NULL, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("✓ NULL key correctly rejected with error code: %d\n", code); } else { @@ -79,7 +80,7 @@ int test_null_pointer_handling() { } // Test 1d: NULL output pointer - code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL); + code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL, NULL); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("✓ NULL output pointer correctly rejected with error code: %d\n", code); } else { @@ -88,7 +89,7 @@ int test_null_pointer_handling() { } // Create a valid client for further tests - code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create valid client for remaining tests\n"); cosmos_runtime_context_free(runtime); @@ -139,7 +140,7 @@ int test_invalid_runtime_context() { // Now try to use the invalid context cosmos_client *client = NULL; - cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("✓ Invalid/freed runtime context correctly rejected with error code: %d\n", code); @@ -167,7 +168,8 @@ int test_error_detail_with_flag() { char database_name[64]; snprintf(database_name, sizeof(database_name), "test-err-dtl-%ld", current_time); - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { printf("Failed to create runtime context\n"); return TEST_FAIL; @@ -184,7 +186,7 @@ int test_error_detail_with_flag() { int database_created = 0; // Create client - cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create client\n"); result = TEST_FAIL; @@ -192,7 +194,7 @@ int test_error_detail_with_flag() { } // Create database - code = cosmos_client_create_database(&ctx, client, database_name, &database); + code = cosmos_client_create_database(&ctx, client, database_name, NULL, &database); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create database\n"); result = TEST_FAIL; @@ -201,7 +203,7 @@ int test_error_detail_with_flag() { database_created = 1; // Create container - code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", &container); + code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", NULL, &container); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create container\n"); result = TEST_FAIL; @@ -210,7 +212,7 @@ int test_error_detail_with_flag() { // Trigger an error - try to read non-existent item const char *read_json = NULL; - code = cosmos_container_read_item(&ctx, container, "pk1", "nonexistent-item", &read_json); + code = cosmos_container_read_item(&ctx, container, "pk1", "nonexistent-item", NULL, &read_json); if (code == COSMOS_ERROR_CODE_NOT_FOUND) { printf("✓ Got expected NOT_FOUND error code\n"); @@ -235,7 +237,7 @@ int test_error_detail_with_flag() { cleanup: if (database && database_created) { - cosmos_database_delete(&ctx, database); + cosmos_database_delete(&ctx, database, NULL); } if (container) { @@ -268,7 +270,8 @@ int test_error_detail_without_flag() { char database_name[64]; snprintf(database_name, sizeof(database_name), "test-no-dtl-%ld", current_time); - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { printf("Failed to create runtime context\n"); return TEST_FAIL; @@ -285,7 +288,7 @@ int test_error_detail_without_flag() { int database_created = 0; // Create client - cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create client\n"); result = TEST_FAIL; @@ -293,7 +296,7 @@ int test_error_detail_without_flag() { } // Create database - code = cosmos_client_create_database(&ctx, client, database_name, &database); + code = cosmos_client_create_database(&ctx, client, database_name, NULL, &database); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create database\n"); result = TEST_FAIL; @@ -302,7 +305,7 @@ int test_error_detail_without_flag() { database_created = 1; // Create container - code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", &container); + code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", NULL, &container); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create container\n"); result = TEST_FAIL; @@ -311,7 +314,7 @@ int test_error_detail_without_flag() { // Trigger an error - try to read non-existent item const char *read_json = NULL; - code = cosmos_container_read_item(&ctx, container, "pk1", "nonexistent-item", &read_json); + code = cosmos_container_read_item(&ctx, container, "pk1", "nonexistent-item", NULL, &read_json); if (code == COSMOS_ERROR_CODE_NOT_FOUND) { printf("✓ Got expected NOT_FOUND error code\n"); @@ -336,7 +339,7 @@ int test_error_detail_without_flag() { cleanup: if (database && database_created) { - cosmos_database_delete(&ctx, database); + cosmos_database_delete(&ctx, database, NULL); } if (container) { @@ -369,7 +372,8 @@ int test_invalid_utf8_strings() { char database_name[64]; snprintf(database_name, sizeof(database_name), "test-utf8-%ld", current_time); - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { printf("Failed to create runtime context\n"); return TEST_FAIL; @@ -386,7 +390,7 @@ int test_invalid_utf8_strings() { int database_created = 0; // Create client - cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create client\n"); result = TEST_FAIL; @@ -394,7 +398,7 @@ int test_invalid_utf8_strings() { } // Create database - code = cosmos_client_create_database(&ctx, client, database_name, &database); + code = cosmos_client_create_database(&ctx, client, database_name, NULL, &database); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create database\n"); result = TEST_FAIL; @@ -403,7 +407,7 @@ int test_invalid_utf8_strings() { database_created = 1; // Create container - code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", &container); + code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", NULL, &container); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create container\n"); result = TEST_FAIL; @@ -421,7 +425,7 @@ int test_invalid_utf8_strings() { invalid_json[len + 1] = '\0'; strcat(invalid_json, "\"}"); - code = cosmos_container_upsert_item(&ctx, container, "pk1", invalid_json); + code = cosmos_container_upsert_item(&ctx, container, "pk1", invalid_json, NULL); if (code == COSMOS_ERROR_CODE_INVALID_UTF8) { printf("✓ Invalid UTF-8 correctly rejected with INVALID_UTF8 error code\n"); @@ -435,7 +439,7 @@ int test_invalid_utf8_strings() { cleanup: if (database && database_created) { - cosmos_database_delete(&ctx, database); + cosmos_database_delete(&ctx, database, NULL); } if (container) { @@ -468,7 +472,8 @@ int test_empty_string_handling() { char database_name[64]; snprintf(database_name, sizeof(database_name), "test-empty-%ld", current_time); - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { printf("Failed to create runtime context\n"); return TEST_FAIL; @@ -485,7 +490,7 @@ int test_empty_string_handling() { int database_created = 0; // Create client - cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create client\n"); result = TEST_FAIL; @@ -493,7 +498,7 @@ int test_empty_string_handling() { } // Test 6a: Empty database name - code = cosmos_client_create_database(&ctx, client, "", &database); + code = cosmos_client_create_database(&ctx, client, "", NULL, &database); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("✓ Empty database name correctly rejected with error code: %d\n", code); } else { @@ -504,7 +509,7 @@ int test_empty_string_handling() { } // Create valid database for remaining tests - code = cosmos_client_create_database(&ctx, client, database_name, &database); + code = cosmos_client_create_database(&ctx, client, database_name, NULL, &database); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create valid database\n"); result = TEST_FAIL; @@ -513,7 +518,7 @@ int test_empty_string_handling() { database_created = 1; // Test 6b: Empty container name - code = cosmos_database_create_container(&ctx, database, "", "/pk", &container); + code = cosmos_database_create_container(&ctx, database, "", "/pk", NULL, &container); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("✓ Empty container name correctly rejected with error code: %d\n", code); } else { @@ -524,7 +529,7 @@ int test_empty_string_handling() { } // Test 6c: Empty partition key path - code = cosmos_database_create_container(&ctx, database, "test-container", "", &container); + code = cosmos_database_create_container(&ctx, database, "test-container", "", NULL, &container); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("✓ Empty partition key path correctly rejected with error code: %d\n", code); } else { @@ -535,7 +540,7 @@ int test_empty_string_handling() { } // Create valid container for remaining tests - code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", &container); + code = cosmos_database_create_container(&ctx, database, "test-container", "/pk", NULL, &container); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("Failed to create valid container\n"); result = TEST_FAIL; @@ -544,7 +549,7 @@ int test_empty_string_handling() { // Test 6d: Empty item ID in JSON const char *json_with_empty_id = "{\"id\":\"\",\"pk\":\"pk1\",\"value\":\"test\"}"; - code = cosmos_container_upsert_item(&ctx, container, "pk1", json_with_empty_id); + code = cosmos_container_upsert_item(&ctx, container, "pk1", json_with_empty_id, NULL); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("✓ Empty item ID correctly rejected with error code: %d\n", code); } else { @@ -554,7 +559,7 @@ int test_empty_string_handling() { // Test 6e: Empty partition key value const char *json_data = "{\"id\":\"item1\",\"pk\":\"pk1\",\"value\":\"test\"}"; - code = cosmos_container_upsert_item(&ctx, container, "", json_data); + code = cosmos_container_upsert_item(&ctx, container, "", json_data, NULL); if (code != COSMOS_ERROR_CODE_SUCCESS) { printf("✓ Empty partition key value correctly rejected with error code: %d\n", code); } else { @@ -564,7 +569,7 @@ int test_empty_string_handling() { cleanup: if (database && database_created) { - cosmos_database_delete(&ctx, database); + cosmos_database_delete(&ctx, database, NULL); } if (container) { diff --git a/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c index 8a9b76cc9c5..7d18188a4b6 100644 --- a/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c @@ -46,9 +46,10 @@ int main() { printf("Database: %s\n", database_name); printf("Container: test-container\n"); - cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL); + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); if (!runtime) { - printf("Failed to create runtime context\n"); + display_error(&error); return 1; } cosmos_call_context ctx; @@ -64,7 +65,7 @@ int main() { int container_created = 0; // Create Cosmos client - cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, &client); + cosmos_error_code code = cosmos_client_create_with_key(&ctx, endpoint, key, NULL, &client); if (code != COSMOS_ERROR_CODE_SUCCESS) { display_error(&ctx.error); result = 1; @@ -73,7 +74,7 @@ int main() { printf("✓ Created Cosmos client\n"); // Create database - code = cosmos_client_create_database(&ctx, client, database_name, &database); + code = cosmos_client_create_database(&ctx, client, database_name, NULL, &database); if (code != COSMOS_ERROR_CODE_SUCCESS) { display_error(&ctx.error); result = 1; @@ -83,7 +84,7 @@ int main() { printf("✓ Created database: %s\n", database_name); // Create container with partition key - code = cosmos_database_create_container(&ctx, database, "test-container", PARTITION_KEY_PATH, &container); + code = cosmos_database_create_container(&ctx, database, "test-container", PARTITION_KEY_PATH, NULL, &container); if (code != COSMOS_ERROR_CODE_SUCCESS) { display_error(&ctx.error); result = 1; @@ -101,7 +102,7 @@ int main() { printf("Upserting document: %s\n", json_data); // Upsert the item - code = cosmos_container_upsert_item(&ctx, container, PARTITION_KEY_VALUE, json_data); + code = cosmos_container_upsert_item(&ctx, container, PARTITION_KEY_VALUE, json_data, NULL); if (code != COSMOS_ERROR_CODE_SUCCESS) { display_error(&ctx.error); result = 1; @@ -110,7 +111,7 @@ int main() { printf("✓ Upserted item successfully\n"); // Read the item back - code = cosmos_container_read_item(&ctx, container, PARTITION_KEY_VALUE, ITEM_ID, &read_json); + code = cosmos_container_read_item(&ctx, container, PARTITION_KEY_VALUE, ITEM_ID, NULL, &read_json); if (code != COSMOS_ERROR_CODE_SUCCESS) { display_error(&ctx.error); result = 1; @@ -146,7 +147,7 @@ int main() { // Delete database (this will also delete the container) if (database && database_created) { printf("Deleting database: %s\n", database_name); - cosmos_error_code delete_code = cosmos_database_delete(&ctx, database); + cosmos_error_code delete_code = cosmos_database_delete(&ctx, database, NULL); if (delete_code != COSMOS_ERROR_CODE_SUCCESS) { display_error(&ctx.error); } else { diff --git a/sdk/cosmos/azure_data_cosmos_native/include/.gitignore b/sdk/cosmos/azure_data_cosmos_native/include/.gitignore deleted file mode 100644 index b429f3066b0..00000000000 --- a/sdk/cosmos/azure_data_cosmos_native/include/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Ignore everything except this ignore file, this directory contains build artifacts -* -!.gitignore diff --git a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h new file mode 100644 index 00000000000..5823a63cc8e --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h @@ -0,0 +1,547 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This file is auto-generated by cbindgen. Do not edit manually. +// cSpell: disable +// Build identifier: $Id: azure_data_cosmos_native, Version: 0.27.0, Commit: unknown, Branch: unknown, Build ID: unknown, Build Number: unknown, Timestamp: 1763496209$ + + +#include +#include +#include +#include + +// Specifies the version of cosmosclient this header file was generated from. +// This should match the version of libcosmosclient you are referencing. +#define COSMOSCLIENT_H_VERSION "0.27.0" + +enum cosmos_error_code +#ifdef __cplusplus + : int32_t +#endif // __cplusplus + { + COSMOS_ERROR_CODE_SUCCESS = 0, + COSMOS_ERROR_CODE_INVALID_ARGUMENT = 1, + COSMOS_ERROR_CODE_CONNECTION_FAILED = 2, + COSMOS_ERROR_CODE_UNKNOWN_ERROR = 999, + COSMOS_ERROR_CODE_BAD_REQUEST = 400, + COSMOS_ERROR_CODE_UNAUTHORIZED = 401, + COSMOS_ERROR_CODE_FORBIDDEN = 403, + COSMOS_ERROR_CODE_NOT_FOUND = 404, + COSMOS_ERROR_CODE_CONFLICT = 409, + COSMOS_ERROR_CODE_PRECONDITION_FAILED = 412, + COSMOS_ERROR_CODE_REQUEST_TIMEOUT = 408, + COSMOS_ERROR_CODE_TOO_MANY_REQUESTS = 429, + COSMOS_ERROR_CODE_INTERNAL_SERVER_ERROR = 500, + COSMOS_ERROR_CODE_BAD_GATEWAY = 502, + COSMOS_ERROR_CODE_SERVICE_UNAVAILABLE = 503, + COSMOS_ERROR_CODE_AUTHENTICATION_FAILED = 1001, + COSMOS_ERROR_CODE_DATA_CONVERSION = 1002, + COSMOS_ERROR_CODE_PARTITION_KEY_MISMATCH = 2001, + COSMOS_ERROR_CODE_RESOURCE_QUOTA_EXCEEDED = 2002, + COSMOS_ERROR_CODE_REQUEST_RATE_TOO_LARGE = 2003, + COSMOS_ERROR_CODE_ITEM_SIZE_TOO_LARGE = 2004, + COSMOS_ERROR_CODE_PARTITION_KEY_NOT_FOUND = 2005, + COSMOS_ERROR_CODE_INTERNAL_ERROR = 3001, + COSMOS_ERROR_CODE_INVALID_UTF8 = 3002, + COSMOS_ERROR_CODE_INVALID_HANDLE = 3003, + COSMOS_ERROR_CODE_MEMORY_ERROR = 3004, + COSMOS_ERROR_CODE_MARSHALING_ERROR = 3005, + COSMOS_ERROR_CODE_CALL_CONTEXT_MISSING = 3006, + COSMOS_ERROR_CODE_RUNTIME_CONTEXT_MISSING = 3007, + COSMOS_ERROR_CODE_INVALID_C_STRING = 3008, +}; +#ifndef __cplusplus +typedef int32_t cosmos_error_code; +#endif // __cplusplus + +/** + * A client for working with a specific container in a Cosmos DB account. + * + * You can get a `Container` by calling [`DatabaseClient::container_client()`](crate::clients::DatabaseClient::container_client()). + */ +typedef struct cosmos_container_client cosmos_container_client; + +/** + * Client for Azure Cosmos DB. + */ +typedef struct cosmos_client cosmos_client; + +/** + * A client for working with a specific database in a Cosmos DB account. + * + * You can get a `DatabaseClient` by calling [`CosmosClient::database_client()`](crate::CosmosClient::database_client()). + */ +typedef struct cosmos_database_client cosmos_database_client; + +/** + * Provides a RuntimeContext (see [`crate::runtime`]) implementation using the Tokio runtime. + */ +typedef struct cosmos_runtime_context cosmos_runtime_context; + +/** + * External representation of an error across the FFI boundary. + */ +typedef struct cosmos_error { + /** + * The error code representing the type of error. + */ + cosmos_error_code code; + /** + * A static C string message describing the error. This value does not need to be freed. + */ + const char *message; + /** + * An optional detailed C string message providing additional context about the error. + * This is only set if [`include_error_details`](crate::context::CallContext::include_error_details) is true. + * If this pointer is non-null, it must be freed by the caller using [`cosmos_string_free`](crate::string::cosmos_string_free). + */ + const char *detail; +} cosmos_error; + +/** + * Represents the context for a call into the Cosmos DB native SDK. + * + * This structure can be created on the caller side, as long as the caller is able to create a C-compatible struct. + * The `runtime_context` field must be set to a pointer to a `RuntimeContext` created by the + * [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create) function. + * + * The structure can also be created using [`cosmos_call_context_create`](crate::context::cosmos_call_context_create), + * in which case Rust will manage the memory for the structure, and it must be freed using [`cosmos_call_context_free`](crate::context::cosmos_call_context_free). + * + * This structure must remain active and at the memory address specified in the function call for the duration of the call into the SDK. + * If calling an async function, that may mean it must be allocated on the heap to ensure it remains live (depending on the caller's language/runtime). + * + * A single [`CallContext`] may be reused for muliple calls, but cannot be used concurrently from multiple threads. + * When reusing a [`CallContext`] the [`CallContext::error`] field will be overwritten with the error from the most recent call. + * Error details will NOT be freed if the context is reused; the caller is responsible for freeing any error details if needed. + */ +typedef struct cosmos_call_context { + /** + * Pointer to a RuntimeContext created by [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create). + */ + const struct cosmos_runtime_context *runtime_context; + /** + * Indicates whether detailed case-specific error information should be included in error responses. + * + * Normally, a [`CosmosError`] contains only a static error message, which does not need to be freed. + * However, this also means that the error message may not contain detailed information about the specific error that occurred. + * If this field is set to true, the SDK will allocate a detailed error message string for each error that occurs, + * which must be freed by the caller using [`cosmos_string_free`](crate::string::cosmos_string_free) after each error is handled. + */ + bool include_error_details; + /** + * Holds the error information for the last operation performed using this context. + * + * The value of this is ignored on input; it is only set by the SDK to report errors. + * The [`CosmosError::code`] field will always match the returned error code from the function. + * The string associated with the error (if any) will be allocated by the SDK and must be freed + * by the caller using the appropriate function. + */ + struct cosmos_error error; +} cosmos_call_context; + +typedef struct cosmos_call_context_options { + bool include_error_details; +} cosmos_call_context_options; + +typedef struct cosmos_item_options { + +} cosmos_item_options; + +typedef struct cosmos_read_container_options { + +} cosmos_read_container_options; + +typedef struct cosmos_delete_container_options { + +} cosmos_delete_container_options; + +typedef struct cosmos_query_options { + +} cosmos_query_options; + +typedef struct cosmos_client_options { + +} cosmos_client_options; + +typedef struct cosmos_create_database_options { + +} cosmos_create_database_options; + +typedef struct cosmos_read_database_options { + +} cosmos_read_database_options; + +typedef struct cosmos_delete_database_options { + +} cosmos_delete_database_options; + +typedef struct cosmos_create_container_options { + +} cosmos_create_container_options; + +typedef struct cosmos_runtime_options { + +} cosmos_runtime_options; + + + + + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/** + * Returns a constant C string containing the version of the Cosmos Client library. + */ +const char *cosmos_version(void); + +/** + * Installs tracing listeners that output to stdout/stderr based on the `COSMOS_LOG` environment variable. + * + * Just calling this function isn't sufficient to get logging output. You must also set the `COSMOS_LOG` environment variable + * to specify the desired log level and targets. See + * for details on the syntax for this variable. + */ +void cosmos_enable_tracing(void); + +/** + * Releases the memory associated with a C string obtained from Rust. + */ +void cosmos_string_free(const char *str); + +/** + * Creates a new [`CallContext`] and returns a pointer to it. + * This must be freed using [`cosmos_call_context_free`] when no longer needed. + * + * A [`CallContext`] may be reused for multiple calls, but cannot be used concurrently from multiple threads. + * + * # Arguments + * * `runtime` - Pointer to a [`RuntimeContext`] created by [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create). + * * `options` - Pointer to [`CallContextOptions`] for call configuration, may be null. + */ +struct cosmos_call_context *cosmos_call_context_create(const struct cosmos_runtime_context *runtime_context, + const struct cosmos_call_context_options *options); + +/** + * Frees a [`CallContext`] created by [`cosmos_call_context_create`]. + */ +void cosmos_call_context_free(struct cosmos_call_context *ctx); + +/** + * Releases the memory associated with a [`ContainerClient`]. + */ +void cosmos_container_free(struct cosmos_container_client *container); + +/** + * Creates a new item in the specified container. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `container` - Pointer to the `ContainerClient`. + * * `partition_key` - The partition key value as a nul-terminated C string. + * * `json_data` - The item data as a raw JSON nul-terminated C string. + * * `options` - Pointer to [`ItemOptions`] for item creation configuration, may be null. + */ +cosmos_error_code cosmos_container_create_item(struct cosmos_call_context *ctx, + const struct cosmos_container_client *container, + const char *partition_key, + const char *json_data, + const struct cosmos_item_options *options); + +/** + * Upserts an item in the specified container. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `container` - Pointer to the `ContainerClient`. + * * `partition_key` - The partition key value as a nul-terminated C string. + * * `json_data` - The item data as a raw JSON nul-terminated C string. + * * `options` - Pointer to [`ItemOptions`] for item upsert configuration, may be null. + */ +cosmos_error_code cosmos_container_upsert_item(struct cosmos_call_context *ctx, + const struct cosmos_container_client *container, + const char *partition_key, + const char *json_data, + const struct cosmos_item_options *options); + +/** + * Reads an item from the specified container. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `container` - Pointer to the `ContainerClient`. + * * `partition_key` - The partition key value as a nul-terminated C string. + * * `item_id` - The ID of the item to read as a nul-terminated C string. + * * `options` - Pointer to [`ItemOptions`] for item read configuration, may be null. + * * `out_json` - Output parameter that will receive the item data as a raw JSON nul-terminated C string. + */ +cosmos_error_code cosmos_container_read_item(struct cosmos_call_context *ctx, + const struct cosmos_container_client *container, + const char *partition_key, + const char *item_id, + const struct cosmos_item_options *options, + const char **out_json); + +/** + * Replaces an existing item in the specified container. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `container` - Pointer to the `ContainerClient`. + * * `partition_key` - The partition key value as a nul-terminated C string. + * * `item_id` - The ID of the item to replace as a nul-terminated C string. + * * `json_data` - The new item data as a raw JSON nul-terminated C string. + * * `options` - Pointer to [`ItemOptions`] for item replacement configuration, may be null. + */ +cosmos_error_code cosmos_container_replace_item(struct cosmos_call_context *ctx, + const struct cosmos_container_client *container, + const char *partition_key, + const char *item_id, + const char *json_data, + const struct cosmos_item_options *options); + +/** + * Deletes an item from the specified container. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `container` - Pointer to the `ContainerClient`. + * * `partition_key` - The partition key value as a nul-terminated C string. + * * `item_id` - The ID of the item to delete as a nul-terminated C string. + * * `options` - Pointer to [`ItemOptions`] for item deletion configuration, may be null. + */ +cosmos_error_code cosmos_container_delete_item(struct cosmos_call_context *ctx, + const struct cosmos_container_client *container, + const char *partition_key, + const char *item_id, + const struct cosmos_item_options *options); + +/** + * Reads the properties of the specified container. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `container` - Pointer to the `ContainerClient`. + * * `options` - Pointer to [`ReadContainerOptions`] for read container configuration, may be null. + * * `out_json` - Output parameter that will receive the container properties as a raw JSON nul-terminated C string. + */ +cosmos_error_code cosmos_container_read(struct cosmos_call_context *ctx, + const struct cosmos_container_client *container, + const struct cosmos_read_container_options *options, + const char **out_json); + +/** + * Deletes the specified container. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `container` - Pointer to the [`ContainerClient`]. + * * `options` - Pointer to [`DeleteContainerOptions`] for delete container configuration, may be null. + */ +cosmos_error_code cosmos_container_delete(struct cosmos_call_context *ctx, + const struct cosmos_container_client *container, + const struct cosmos_delete_container_options *options); + +/** + * Queries items in the specified container. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `container` - Pointer to the `ContainerClient`. + * * `query` - The query to execute as a nul-terminated C string. + * * `partition_key` - Optional partition key value as a nul-terminated C string. Specify a null pointer for a cross-partition query. + * * `options` - Pointer to [`QueryOptions`] for query configuration, may be null. + * * `out_json` - Output parameter that will receive the query results as a raw JSON nul-terminated C string. + */ +cosmos_error_code cosmos_container_query_items(struct cosmos_call_context *ctx, + const struct cosmos_container_client *container, + const char *query, + const char *partition_key, + const struct cosmos_query_options *options, + const char **out_json); + +/** + * Creates a new CosmosClient and returns a pointer to it via the out parameter. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `endpoint` - The Cosmos DB account endpoint, as a nul-terminated C string. + * * `key` - The Cosmos DB account key, as a nul-terminated C string + * * `options` - Pointer to [`CosmosClientOptions`] for client configuration, may be null. + * * `out_client` - Output parameter that will receive a pointer to the created CosmosClient. + * + * # Returns + * * Returns [`CosmosErrorCode::Success`] on success. + * * Returns [`CosmosErrorCode::InvalidArgument`] if any input pointer is null or if the input strings are invalid. + */ +cosmos_error_code cosmos_client_create_with_key(struct cosmos_call_context *ctx, + const char *endpoint, + const char *key, + const struct cosmos_client_options *options, + struct cosmos_client **out_client); + +/** + * Releases the memory associated with a [`CosmosClient`]. + */ +void cosmos_client_free(struct cosmos_client *client); + +/** + * Gets a [`DatabaseClient`] from the given [`CosmosClient`] for the specified database ID. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `client` - Pointer to the [`CosmosClient`]. + * * `database_id` - The database ID as a nul-terminated C string. + * * `out_database` - Output parameter that will receive a pointer to the created [`DatabaseClient`]. + */ +cosmos_error_code cosmos_client_database_client(struct cosmos_call_context *ctx, + const struct cosmos_client *client, + const char *database_id, + struct cosmos_database_client **out_database); + +/** + * Queries the databases in the Cosmos DB account using the provided SQL query string. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `client` - Pointer to the [`CosmosClient`]. + * * `query` - The SQL query string as a nul-terminated C string. + * * `options` - Pointer to [`QueryOptions`] for query configuration, may be null. + * * `out_json` - Output parameter that will receive a pointer to the resulting JSON string + */ +cosmos_error_code cosmos_client_query_databases(struct cosmos_call_context *ctx, + const struct cosmos_client *client, + const char *query, + const struct cosmos_query_options *options, + const char **out_json); + +/** + * Creates a new database in the Cosmos DB account with the specified database ID, and returns a pointer to the created [`DatabaseClient`]. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `client` - Pointer to the [`CosmosClient`]. + * * `options` - Pointer to [`CreateDatabaseOptions`] for create database configuration, may be null. + * * `database_id` - The database ID as a nul-terminated C string. + */ +cosmos_error_code cosmos_client_create_database(struct cosmos_call_context *ctx, + const struct cosmos_client *client, + const char *database_id, + const struct cosmos_create_database_options *options, + struct cosmos_database_client **out_database); + +/** + * Releases the memory associated with a [`DatabaseClient`]. + */ +void cosmos_database_free(struct cosmos_database_client *database); + +/** + * Retrieves a pointer to a [`ContainerClient`] for the specified container ID within the given database. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `database` - Pointer to the [`DatabaseClient`]. + * * `container_id` - The container ID as a nul-terminated C string. + * * `out_container` - Output parameter that will receive a pointer to the [`ContainerClient`]. + */ +cosmos_error_code cosmos_database_container_client(struct cosmos_call_context *ctx, + const struct cosmos_database_client *database, + const char *container_id, + struct cosmos_container_client **out_container); + +/** + * Reads the properties of the specified database and returns them as a JSON string. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `database` - Pointer to the [`DatabaseClient`]. + * * `options` - Pointer to [`ReadDatabaseOptions`] for read configuration, may be null. + * * `out_json` - Output parameter that will receive a pointer to the JSON string. + */ +cosmos_error_code cosmos_database_read(struct cosmos_call_context *ctx, + const struct cosmos_database_client *database, + const struct cosmos_read_database_options *options, + const char **out_json); + +/** + * Deletes the specified database. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `database` - Pointer to the [`DatabaseClient`]. + * * `options` - Pointer to [`DeleteDatabaseOptions`] for delete configuration, may be null. + */ +cosmos_error_code cosmos_database_delete(struct cosmos_call_context *ctx, + const struct cosmos_database_client *database, + const struct cosmos_delete_database_options *options); + +/** + * Creates a new container within the specified database. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `database` - Pointer to the [`DatabaseClient`]. + * * `container_id` - The container ID as a nul-terminated C string. + * * `partition_key_path` - The partition key path as a nul-terminated C string. + * * `options` - Pointer to [`CreateContainerOptions`] for create container configuration, may be null. + * * `out_container` - Output parameter that will receive a pointer to the newly created [`ContainerClient`]. + */ +cosmos_error_code cosmos_database_create_container(struct cosmos_call_context *ctx, + const struct cosmos_database_client *database, + const char *container_id, + const char *partition_key_path, + const struct cosmos_create_container_options *options, + struct cosmos_container_client **out_container); + +/** + * Queries the containers within the specified database and returns the results as a JSON string. + * + * # Arguments + * * `ctx` - Pointer to a [`CallContext`] to use for this call. + * * `database` - Pointer to the [`DatabaseClient`]. + * * `query` - The query string as a nul-terminated C string. + * * `options` - Pointer to [`QueryOptions`] for query configuration, may be null. + * * `out_json` - Output parameter that will receive a pointer to the JSON string. + */ +cosmos_error_code cosmos_database_query_containers(struct cosmos_call_context *ctx, + const struct cosmos_database_client *database, + const char *query, + const struct cosmos_query_options *options, + const char **out_json); + +/** + * Creates a new [`RuntimeContext`] for Cosmos DB Client API calls. + * + * This must be called before any other Cosmos DB Client API functions are used, + * and the returned pointer must be passed within a `CallContext` structure to those functions. + * + * When the `RuntimeContext` is no longer needed, it should be freed using the + * [`cosmos_runtime_context_free`] function. However, if the program is terminating, + * it is not strictly necessary to free it. + * + * If this function fails, it will return a null pointer, and the `out_error` parameter + * (if not null) will be set to contain the error details. + * + * The error will contain a dynamically-allocated [`CosmosError::detail`] string that must be + * freed by the caller using the [`cosmos_string_free`](crate::string::cosmos_string_free) function. + * + * # Arguments + * + * * `options` - Pointer to [`RuntimeOptions`] for runtime configuration, may be null. + * * `out_error` - Output parameter that will receive error details if the function fails. + */ +struct cosmos_runtime_context *cosmos_runtime_context_create(const struct cosmos_runtime_options *options, + struct cosmos_error *out_error); + +/** + * Destroys a [`RuntimeContext`] created by [`cosmos_runtime_context_create`]. + * This frees the memory associated with the `RuntimeContext`. + */ +void cosmos_runtime_context_free(struct cosmos_runtime_context *ctx); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs index 793cc0732b1..cc9a20c6381 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs @@ -8,13 +8,16 @@ use serde_json::value::RawValue; use crate::context::CallContext; use crate::error::{self, CosmosErrorCode, Error}; +use crate::options::{DeleteContainerOptions, ItemOptions, QueryOptions, ReadContainerOptions}; use crate::string::parse_cstr; use crate::unwrap_required_ptr; /// Releases the memory associated with a [`ContainerClient`]. #[no_mangle] +#[tracing::instrument(level = "debug")] pub extern "C" fn cosmos_container_free(container: *mut ContainerClient) { if !container.is_null() { + tracing::trace!(?container, "freeing container client"); unsafe { drop(Box::from_raw(container)) } } } @@ -26,12 +29,19 @@ pub extern "C" fn cosmos_container_free(container: *mut ContainerClient) { /// * `container` - Pointer to the `ContainerClient`. /// * `partition_key` - The partition key value as a nul-terminated C string. /// * `json_data` - The item data as a raw JSON nul-terminated C string. +/// * `options` - Pointer to [`ItemOptions`] for item creation configuration, may be null. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, container = ?container))] pub extern "C" fn cosmos_container_create_item( ctx: *mut CallContext, container: *const ContainerClient, partition_key: *const c_char, json_data: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const ItemOptions, ) -> CosmosErrorCode { context!(ctx).run_async(async { let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; @@ -53,12 +63,19 @@ pub extern "C" fn cosmos_container_create_item( /// * `container` - Pointer to the `ContainerClient`. /// * `partition_key` - The partition key value as a nul-terminated C string. /// * `json_data` - The item data as a raw JSON nul-terminated C string. +/// * `options` - Pointer to [`ItemOptions`] for item upsert configuration, may be null. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, container = ?container))] pub extern "C" fn cosmos_container_upsert_item( ctx: *mut CallContext, container: *const ContainerClient, partition_key: *const c_char, json_data: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const ItemOptions, ) -> CosmosErrorCode { context!(ctx).run_async(async { let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; @@ -80,13 +97,20 @@ pub extern "C" fn cosmos_container_upsert_item( /// * `container` - Pointer to the `ContainerClient`. /// * `partition_key` - The partition key value as a nul-terminated C string. /// * `item_id` - The ID of the item to read as a nul-terminated C string. +/// * `options` - Pointer to [`ItemOptions`] for item read configuration, may be null. /// * `out_json` - Output parameter that will receive the item data as a raw JSON nul-terminated C string. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, container = ?container))] pub extern "C" fn cosmos_container_read_item( ctx: *mut CallContext, container: *const ContainerClient, partition_key: *const c_char, item_id: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const ItemOptions, out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { @@ -113,13 +137,20 @@ pub extern "C" fn cosmos_container_read_item( /// * `partition_key` - The partition key value as a nul-terminated C string. /// * `item_id` - The ID of the item to replace as a nul-terminated C string. /// * `json_data` - The new item data as a raw JSON nul-terminated C string. +/// * `options` - Pointer to [`ItemOptions`] for item replacement configuration, may be null. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, container = ?container))] pub extern "C" fn cosmos_container_replace_item( ctx: *mut CallContext, container: *const ContainerClient, partition_key: *const c_char, item_id: *const c_char, json_data: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const ItemOptions, ) -> CosmosErrorCode { context!(ctx).run_async(async { let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; @@ -142,12 +173,19 @@ pub extern "C" fn cosmos_container_replace_item( /// * `container` - Pointer to the `ContainerClient`. /// * `partition_key` - The partition key value as a nul-terminated C string. /// * `item_id` - The ID of the item to delete as a nul-terminated C string. +/// * `options` - Pointer to [`ItemOptions`] for item deletion configuration, may be null. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, container = ?container))] pub extern "C" fn cosmos_container_delete_item( ctx: *mut CallContext, container: *const ContainerClient, partition_key: *const c_char, item_id: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const ItemOptions, ) -> CosmosErrorCode { context!(ctx).run_async(async { let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; @@ -166,11 +204,18 @@ pub extern "C" fn cosmos_container_delete_item( /// # Arguments /// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `container` - Pointer to the `ContainerClient`. +/// * `options` - Pointer to [`ReadContainerOptions`] for read container configuration, may be null. /// * `out_json` - Output parameter that will receive the container properties as a raw JSON nul-terminated C string. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, container = ?container))] pub extern "C" fn cosmos_container_read( ctx: *mut CallContext, container: *const ContainerClient, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const ReadContainerOptions, out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { @@ -181,6 +226,30 @@ pub extern "C" fn cosmos_container_read( }) } +/// Deletes the specified container. +/// +/// # Arguments +/// * `ctx` - Pointer to a [`CallContext`] to use for this call. +/// * `container` - Pointer to the [`ContainerClient`]. +/// * `options` - Pointer to [`DeleteContainerOptions`] for delete container configuration, may be null. +#[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, container = ?container))] +pub extern "C" fn cosmos_container_delete( + ctx: *mut CallContext, + container: *const ContainerClient, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const DeleteContainerOptions, +) -> CosmosErrorCode { + context!(ctx).run_async(async { + let container = unwrap_required_ptr(container, error::messages::INVALID_CONTAINER_POINTER)?; + container.delete(None).await?; + Ok(()) + }) +} + /// Queries items in the specified container. /// /// # Arguments @@ -188,13 +257,20 @@ pub extern "C" fn cosmos_container_read( /// * `container` - Pointer to the `ContainerClient`. /// * `query` - The query to execute as a nul-terminated C string. /// * `partition_key` - Optional partition key value as a nul-terminated C string. Specify a null pointer for a cross-partition query. +/// * `options` - Pointer to [`QueryOptions`] for query configuration, may be null. /// * `out_json` - Output parameter that will receive the query results as a raw JSON nul-terminated C string. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, container = ?container))] pub extern "C" fn cosmos_container_query_items( ctx: *mut CallContext, container: *const ContainerClient, query: *const c_char, partition_key: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const QueryOptions, out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs index a9268ddda15..0fa01d5ad2a 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs @@ -2,13 +2,12 @@ use std::ffi::CString; use std::os::raw::c_char; use azure_core::credentials::Secret; -use azure_data_cosmos::clients::DatabaseClient; -use azure_data_cosmos::query::Query; -use azure_data_cosmos::CosmosClient; +use azure_data_cosmos::{clients::DatabaseClient, query::Query, CosmosClient, QueryOptions}; use futures::TryStreamExt; use crate::context::CallContext; use crate::error::{self, CosmosErrorCode, Error}; +use crate::options::{ClientOptions, CreateDatabaseOptions}; use crate::string::parse_cstr; use crate::unwrap_required_ptr; @@ -18,16 +17,23 @@ use crate::unwrap_required_ptr; /// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `endpoint` - The Cosmos DB account endpoint, as a nul-terminated C string. /// * `key` - The Cosmos DB account key, as a nul-terminated C string +/// * `options` - Pointer to [`CosmosClientOptions`] for client configuration, may be null. /// * `out_client` - Output parameter that will receive a pointer to the created CosmosClient. /// /// # Returns /// * Returns [`CosmosErrorCode::Success`] on success. /// * Returns [`CosmosErrorCode::InvalidArgument`] if any input pointer is null or if the input strings are invalid. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx))] pub extern "C" fn cosmos_client_create_with_key( ctx: *mut CallContext, endpoint: *const c_char, key: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const ClientOptions, out_client: *mut *mut CosmosClient, ) -> CosmosErrorCode { context!(ctx).run_sync_with_output(out_client, || { @@ -41,8 +47,10 @@ pub extern "C" fn cosmos_client_create_with_key( /// Releases the memory associated with a [`CosmosClient`]. #[no_mangle] +#[tracing::instrument(level = "debug")] pub extern "C" fn cosmos_client_free(client: *mut CosmosClient) { if !client.is_null() { + tracing::trace!(?client, "freeing cosmos client"); unsafe { drop(Box::from_raw(client)) } } } @@ -55,6 +63,7 @@ pub extern "C" fn cosmos_client_free(client: *mut CosmosClient) { /// * `database_id` - The database ID as a nul-terminated C string. /// * `out_database` - Output parameter that will receive a pointer to the created [`DatabaseClient`]. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, client = ?client))] pub extern "C" fn cosmos_client_database_client( ctx: *mut CallContext, client: *const CosmosClient, @@ -75,12 +84,19 @@ pub extern "C" fn cosmos_client_database_client( /// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `client` - Pointer to the [`CosmosClient`]. /// * `query` - The SQL query string as a nul-terminated C string. +/// * `options` - Pointer to [`QueryOptions`] for query configuration, may be null. /// * `out_json` - Output parameter that will receive a pointer to the resulting JSON string #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, client = ?client))] pub extern "C" fn cosmos_client_query_databases( ctx: *mut CallContext, client: *const CosmosClient, query: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const QueryOptions, out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { @@ -109,12 +125,19 @@ pub extern "C" fn cosmos_client_query_databases( /// # Arguments /// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `client` - Pointer to the [`CosmosClient`]. +/// * `options` - Pointer to [`CreateDatabaseOptions`] for create database configuration, may be null. /// * `database_id` - The database ID as a nul-terminated C string. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, client = ?client))] pub extern "C" fn cosmos_client_create_database( ctx: *mut CallContext, client: *const CosmosClient, database_id: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const CreateDatabaseOptions, out_database: *mut *mut DatabaseClient, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_database, async { diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs index a4fb3c9bebc..c2203491341 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs @@ -8,13 +8,18 @@ use futures::TryStreamExt; use crate::context::CallContext; use crate::error::{self, CosmosErrorCode, Error}; +use crate::options::{ + CreateContainerOptions, DeleteDatabaseOptions, QueryOptions, ReadDatabaseOptions, +}; use crate::string::parse_cstr; use crate::unwrap_required_ptr; /// Releases the memory associated with a [`DatabaseClient`]. #[no_mangle] +#[tracing::instrument(level = "debug")] pub extern "C" fn cosmos_database_free(database: *mut DatabaseClient) { if !database.is_null() { + tracing::trace!(?database, "freeing database client"); unsafe { drop(Box::from_raw(database)) } } } @@ -27,6 +32,7 @@ pub extern "C" fn cosmos_database_free(database: *mut DatabaseClient) { /// * `container_id` - The container ID as a nul-terminated C string. /// * `out_container` - Output parameter that will receive a pointer to the [`ContainerClient`]. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, database = ?database))] pub extern "C" fn cosmos_database_container_client( ctx: *mut CallContext, database: *const DatabaseClient, @@ -46,11 +52,18 @@ pub extern "C" fn cosmos_database_container_client( /// # Arguments /// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `database` - Pointer to the [`DatabaseClient`]. +/// * `options` - Pointer to [`ReadDatabaseOptions`] for read configuration, may be null. /// * `out_json` - Output parameter that will receive a pointer to the JSON string. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, database = ?database))] pub extern "C" fn cosmos_database_read( ctx: *mut CallContext, database: *const DatabaseClient, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const ReadDatabaseOptions, out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { @@ -66,10 +79,17 @@ pub extern "C" fn cosmos_database_read( /// # Arguments /// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `database` - Pointer to the [`DatabaseClient`]. +/// * `options` - Pointer to [`DeleteDatabaseOptions`] for delete configuration, may be null. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, database = ?database))] pub extern "C" fn cosmos_database_delete( ctx: *mut CallContext, database: *const DatabaseClient, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const DeleteDatabaseOptions, ) -> CosmosErrorCode { context!(ctx).run_async(async { let database = unwrap_required_ptr(database, error::messages::INVALID_DATABASE_POINTER)?; @@ -85,13 +105,20 @@ pub extern "C" fn cosmos_database_delete( /// * `database` - Pointer to the [`DatabaseClient`]. /// * `container_id` - The container ID as a nul-terminated C string. /// * `partition_key_path` - The partition key path as a nul-terminated C string. +/// * `options` - Pointer to [`CreateContainerOptions`] for create container configuration, may be null. /// * `out_container` - Output parameter that will receive a pointer to the newly created [`ContainerClient`]. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, database = ?database))] pub extern "C" fn cosmos_database_create_container( ctx: *mut CallContext, database: *const DatabaseClient, container_id: *const c_char, partition_key_path: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const CreateContainerOptions, out_container: *mut *mut ContainerClient, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_container, async { @@ -121,12 +148,19 @@ pub extern "C" fn cosmos_database_create_container( /// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `database` - Pointer to the [`DatabaseClient`]. /// * `query` - The query string as a nul-terminated C string. +/// * `options` - Pointer to [`QueryOptions`] for query configuration, may be null. /// * `out_json` - Output parameter that will receive a pointer to the JSON string. #[no_mangle] +#[tracing::instrument(level = "debug", skip_all, fields(ctx = ?ctx, database = ?database))] pub extern "C" fn cosmos_database_query_containers( ctx: *mut CallContext, database: *const DatabaseClient, query: *const c_char, + #[allow( + unused_variables, + reason = "options parameter is reserved for future use, and prefixing with '_' appears in docs" + )] + options: *const QueryOptions, out_json: *mut *const c_char, ) -> CosmosErrorCode { context!(ctx).run_async_with_output(out_json, async { diff --git a/sdk/cosmos/azure_data_cosmos_native/src/context.rs b/sdk/cosmos/azure_data_cosmos_native/src/context.rs index c744c784b46..fdc2d79071e 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/context.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/context.rs @@ -3,6 +3,12 @@ use crate::{ runtime::RuntimeContext, }; +#[repr(C)] +#[derive(Default)] +pub struct CallContextOptions { + pub include_error_details: bool, +} + /// Represents the context for a call into the Cosmos DB native SDK. /// /// This structure can be created on the caller side, as long as the caller is able to create a C-compatible struct. @@ -45,27 +51,39 @@ pub struct CallContext { /// This must be freed using [`cosmos_call_context_free`] when no longer needed. /// /// A [`CallContext`] may be reused for multiple calls, but cannot be used concurrently from multiple threads. +/// +/// # Arguments +/// * `runtime` - Pointer to a [`RuntimeContext`] created by [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create). +/// * `options` - Pointer to [`CallContextOptions`] for call configuration, may be null. #[no_mangle] pub extern "C" fn cosmos_call_context_create( - runtime_ctx: *const RuntimeContext, - include_error_details: bool, + runtime_context: *const RuntimeContext, + options: *const CallContextOptions, ) -> *mut CallContext { + let options = if options.is_null() { + &CallContextOptions::default() + } else { + unsafe { &*options } + }; let ctx = CallContext { - runtime_context: runtime_ctx, - include_error_details, + runtime_context, + include_error_details: options.include_error_details, error: CosmosError { code: CosmosErrorCode::Success, message: crate::error::messages::OPERATION_SUCCEEDED.as_ptr(), detail: std::ptr::null(), }, }; - Box::into_raw(Box::new(ctx)) + let ptr = Box::into_raw(Box::new(ctx)); + tracing::trace!(?ptr, "created call context"); + ptr } /// Frees a [`CallContext`] created by [`cosmos_call_context_create`]. #[no_mangle] pub extern "C" fn cosmos_call_context_free(ctx: *mut CallContext) { if !ctx.is_null() { + tracing::trace!(?ctx, "freeing call context"); unsafe { drop(Box::from_raw(ctx)) } } } @@ -83,7 +101,10 @@ impl CallContext { /// Runs a synchronous operation with no outputs, capturing any error into the CallContext. pub fn run_sync(&mut self, f: impl FnOnce() -> Result<(), Error>) -> CosmosErrorCode { - match f() { + tracing::trace!("starting sync operation"); + let r = f(); + tracing::trace!("sync operation complete"); + match r { Ok(()) => { self.error = Error::SUCCESS.into_ffi(self.include_error_details); CosmosErrorCode::Success @@ -107,7 +128,10 @@ impl CallContext { return CosmosErrorCode::InvalidArgument; } - match f() { + tracing::trace!("starting sync operation"); + let r = f(); + tracing::trace!("sync operation complete"); + match r { Ok(value) => { unsafe { *out = value.into_raw(); @@ -124,7 +148,9 @@ impl CallContext { &mut self, f: impl std::future::Future>, ) -> CosmosErrorCode { + tracing::trace!("starting async operation"); let r = self.runtime().block_on(f); + tracing::trace!("async operation complete"); match r { Ok(()) => { self.error = Error::SUCCESS.into_ffi(self.include_error_details); @@ -149,7 +175,9 @@ impl CallContext { return CosmosErrorCode::InvalidArgument; } + tracing::trace!("starting async operation"); let r = self.runtime().block_on(f); + tracing::trace!("async operation complete"); match r { Ok(value) => { unsafe { @@ -163,6 +191,7 @@ impl CallContext { } fn set_error_and_return_code(&mut self, err: Error) -> CosmosErrorCode { + tracing::error!(%err, "operation failed, preparing error for FFI"); let err = err.into_ffi(self.include_error_details); let code = err.code; self.error = err; @@ -174,12 +203,19 @@ impl CallContext { macro_rules! context { ($param: expr) => { if $param.is_null() { + tracing::error!("call context pointer is null"); return $crate::error::CosmosErrorCode::CallContextMissing; } else { let ctx = $crate::context::CallContext::from_ptr($param); if ctx.runtime_context.is_null() { + tracing::error!(call_context_pointer = ?$param, "call context has null runtime pointer"); return $crate::error::CosmosErrorCode::RuntimeContextMissing; } else { + tracing::trace!( + runtime_pointer = ?ctx.runtime_context, + call_context_pointer = ?$param, + "restored call context from pointer", + ); ctx } } @@ -197,7 +233,13 @@ impl IntoRaw for Box { type Output = *mut T; fn into_raw(self) -> *mut T { - Box::into_raw(self) + let pointer = Box::into_raw(self); + tracing::trace!( + ?pointer, + type_name = std::any::type_name::(), + "converting Box to raw pointer", + ); + pointer } } @@ -205,6 +247,8 @@ impl IntoRaw for std::ffi::CString { type Output = *const std::ffi::c_char; fn into_raw(self) -> *const std::ffi::c_char { - self.into_raw() + let pointer = self.into_raw(); + tracing::trace!(?pointer, "converting CString to raw pointer",); + pointer } } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/error.rs b/sdk/cosmos/azure_data_cosmos_native/src/error.rs index 387e6ec2574..e6bb162260e 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/error.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/error.rs @@ -5,6 +5,7 @@ use std::ffi::{CStr, CString, NulError}; pub mod messages { use std::ffi::CStr; + pub static INVALID_UTF8: &CStr = c"String is not valid UTF-8"; pub static OPERATION_SUCCEEDED: &CStr = c"Operation completed successfully"; pub static NULL_OUTPUT_POINTER: &CStr = c"Output pointer is null"; pub static INVALID_JSON: &CStr = c"Invalid JSON data"; @@ -51,13 +52,14 @@ pub enum CosmosErrorCode { ItemSizeTooLarge = 2004, PartitionKeyNotFound = 2005, - InvalidUTF8 = 3001, // Invalid UTF-8 in string parameters crossing FFI - InvalidHandle = 3002, // Corrupted/invalid handle passed across FFI - MemoryError = 3003, // Memory allocation/deallocation issues at FFI boundary - MarshalingError = 3004, // Data marshaling/unmarshaling failed at FFI boundary - CallContextMissing = 3005, // CallContext not provided where required - RuntimeContextMissing = 3006, // RuntimeContext not provided where required - InvalidCString = 3007, // Invalid C string (not null-terminated or malformed) + InternalError = 3001, // Internal error within the FFI layer + InvalidUTF8 = 3002, // Invalid UTF-8 in string parameters crossing FFI + InvalidHandle = 3003, // Corrupted/invalid handle passed across FFI + MemoryError = 3004, // Memory allocation/deallocation issues at FFI boundary + MarshalingError = 3005, // Data marshaling/unmarshaling failed at FFI boundary + CallContextMissing = 3006, // CallContext not provided where required + RuntimeContextMissing = 3007, // RuntimeContext not provided where required + InvalidCString = 3008, // Invalid C string (not null-terminated or malformed) } /// Internal structure for representing errors. @@ -65,6 +67,8 @@ pub enum CosmosErrorCode { /// This structure is not exposed across the FFI boundary directly. /// Instead, the [`CallContext`](crate::context::CallContext) receives this error and then marshals it /// to an appropriate representation for the caller. +/// cbindgen:ignore +#[derive(Debug)] pub struct Error { /// The error code representing the type of error. code: CosmosErrorCode, @@ -74,7 +78,7 @@ pub struct Error { /// An optional error detail object that can provide additional context about the error. /// This is held as a boxed trait so that it only allocates the string if the user requested detailed errors. - detail: Option>, + detail: Option>, } impl Error { @@ -98,7 +102,7 @@ impl Error { pub fn with_detail( code: CosmosErrorCode, message: &'static CStr, - detail: impl ToString + 'static, + detail: impl std::error::Error + 'static, ) -> Self { Self { code, @@ -129,6 +133,23 @@ impl Error { } } +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} (code: {:?})", + self.message.to_string_lossy(), + self.code + )?; + if let Some(detail) = &self.detail { + write!(f, ": {}", detail.to_string())?; + } + Ok(()) + } +} + +impl std::error::Error for Error {} + /// External representation of an error across the FFI boundary. #[repr(C)] #[derive(Default)] @@ -262,7 +283,7 @@ impl From for Error { Error::with_detail( CosmosErrorCode::DataConversion, c"JSON serialization/deserialization error", - error.to_string(), + error, ) } } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs index 1800aec2f7d..c86834374ef 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs @@ -12,6 +12,7 @@ pub mod context; pub mod blocking; pub mod clients; pub mod error; +pub mod options; pub mod runtime; pub use clients::*; @@ -69,5 +70,7 @@ pub extern "C" fn cosmos_enable_tracing() { tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_env("COSMOS_LOG")) + .with_thread_ids(true) + .with_thread_names(true) .init(); } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/options/mod.rs b/sdk/cosmos/azure_data_cosmos_native/src/options/mod.rs new file mode 100644 index 00000000000..c21a8749c55 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/options/mod.rs @@ -0,0 +1,44 @@ +#[repr(C)] +pub struct ClientOptions { + // Placeholder for future client options +} + +#[repr(C)] +pub struct QueryOptions { + // Placeholder for future query options +} + +#[repr(C)] +pub struct CreateDatabaseOptions { + // Placeholder for future create database options +} + +#[repr(C)] +pub struct ReadDatabaseOptions { + // Placeholder for future read database options +} + +#[repr(C)] +pub struct DeleteDatabaseOptions { + // Placeholder for future read database options +} + +#[repr(C)] +pub struct CreateContainerOptions { + // Placeholder for future create container options +} + +#[repr(C)] +pub struct ReadContainerOptions { + // Placeholder for future read container options +} + +#[repr(C)] +pub struct DeleteContainerOptions { + // Placeholder for future read container options +} + +#[repr(C)] +pub struct ItemOptions { + // Placeholder for future item options +} diff --git a/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs b/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs index b2766dd0240..e9616360a88 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs @@ -14,6 +14,11 @@ pub use tokio::*; use crate::error::CosmosError; +#[repr(C)] +pub struct RuntimeOptions { + // Reserved for future use. +} + /// Creates a new [`RuntimeContext`] for Cosmos DB Client API calls. /// /// This must be called before any other Cosmos DB Client API functions are used, @@ -28,11 +33,22 @@ use crate::error::CosmosError; /// /// The error will contain a dynamically-allocated [`CosmosError::detail`] string that must be /// freed by the caller using the [`cosmos_string_free`](crate::string::cosmos_string_free) function. +/// +/// # Arguments +/// +/// * `options` - Pointer to [`RuntimeOptions`] for runtime configuration, may be null. +/// * `out_error` - Output parameter that will receive error details if the function fails. #[no_mangle] pub extern "C" fn cosmos_runtime_context_create( + options: *const RuntimeOptions, out_error: *mut CosmosError, ) -> *mut RuntimeContext { - let c = match RuntimeContext::new() { + let options = if options.is_null() { + None + } else { + Some(unsafe { &*options }) + }; + let c = match RuntimeContext::new(options) { Ok(c) => c, Err(e) => { unsafe { @@ -51,6 +67,7 @@ pub extern "C" fn cosmos_runtime_context_create( #[no_mangle] pub extern "C" fn cosmos_runtime_context_free(ctx: *mut RuntimeContext) { if !ctx.is_null() { + tracing::trace!(?ctx, "freeing runtime context"); unsafe { drop(Box::from_raw(ctx)) } } } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs b/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs index f61c8341e04..65dc6794a4c 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs @@ -1,6 +1,9 @@ use tokio::runtime::{Builder, Runtime}; -use crate::error::{CosmosErrorCode, Error}; +use crate::{ + error::{CosmosErrorCode, Error}, + runtime::RuntimeOptions, +}; /// Provides a RuntimeContext (see [`crate::runtime`]) implementation using the Tokio runtime. pub struct RuntimeContext { @@ -8,9 +11,10 @@ pub struct RuntimeContext { } impl RuntimeContext { - pub fn new() -> Result { + pub fn new(_options: Option<&RuntimeOptions>) -> Result { let runtime = Builder::new_multi_thread() .enable_all() + .thread_name("cosmos-sdk-runtime") .build() .map_err(|e| { Error::with_detail( @@ -28,6 +32,12 @@ impl RuntimeContext { where F: std::future::Future, { - self.runtime.block_on(future) + self.runtime.block_on(async { + let _span = tracing::trace_span!("block_on").entered(); + tracing::trace!("entered async runtime"); + let r = future.await; + tracing::trace!("leaving async runtime"); + r + }) } } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/string.rs b/sdk/cosmos/azure_data_cosmos_native/src/string.rs index 02ee26b6e56..8daff81fe08 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/string.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/string.rs @@ -44,10 +44,11 @@ pub fn parse_cstr<'a>(ptr: *const c_char, error_msg: &'static CStr) -> Result<&' /// Releases the memory associated with a C string obtained from Rust. #[no_mangle] -pub extern "C" fn cosmos_string_free(ptr: *const c_char) { - if !ptr.is_null() { +pub extern "C" fn cosmos_string_free(str: *const c_char) { + if !str.is_null() { + tracing::trace!(?str, "freeing string"); unsafe { - drop(CString::from_raw(ptr as *mut c_char)); + drop(CString::from_raw(str as *mut c_char)); } } } From 625f2f325db34e203243a4b2f59aae06b477625e Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 20:41:37 +0000 Subject: [PATCH 10/21] license headers --- sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h | 2 +- sdk/cosmos/azure_data_cosmos_native/src/blocking.rs | 3 +++ .../azure_data_cosmos_native/src/clients/container_client.rs | 3 +++ .../azure_data_cosmos_native/src/clients/cosmos_client.rs | 3 +++ .../azure_data_cosmos_native/src/clients/database_client.rs | 3 +++ sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs | 3 +++ sdk/cosmos/azure_data_cosmos_native/src/context.rs | 3 +++ sdk/cosmos/azure_data_cosmos_native/src/error.rs | 3 +++ sdk/cosmos/azure_data_cosmos_native/src/options/mod.rs | 3 +++ sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs | 3 +++ sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs | 3 +++ sdk/cosmos/azure_data_cosmos_native/src/string.rs | 3 +++ 12 files changed, 34 insertions(+), 1 deletion(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h index 5823a63cc8e..b47f9aa5a2e 100644 --- a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h +++ b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h @@ -3,7 +3,7 @@ // This file is auto-generated by cbindgen. Do not edit manually. // cSpell: disable -// Build identifier: $Id: azure_data_cosmos_native, Version: 0.27.0, Commit: unknown, Branch: unknown, Build ID: unknown, Build Number: unknown, Timestamp: 1763496209$ +// Build identifier: $Id: azure_data_cosmos_native, Version: 0.27.0, Commit: unknown, Branch: unknown, Build ID: unknown, Build Number: unknown, Timestamp: 1763498483$ #include diff --git a/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs b/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs index 7e397bcfe11..4778e146794 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + use std::{future::Future, sync::OnceLock}; use tokio::runtime::Runtime; diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs index cc9a20c6381..73a218638da 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + use std::ffi::CString; use std::os::raw::c_char; diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs index 0fa01d5ad2a..c01aed03727 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + use std::ffi::CString; use std::os::raw::c_char; diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs index c2203491341..37f2dfb9258 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + use std::ffi::CString; use std::os::raw::c_char; diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs index 413b847dfc7..80fc0243706 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + #![allow( clippy::missing_safety_doc, reason = "We're operating on raw pointers received from FFI." diff --git a/sdk/cosmos/azure_data_cosmos_native/src/context.rs b/sdk/cosmos/azure_data_cosmos_native/src/context.rs index fdc2d79071e..7eb7a41c9d6 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/context.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/context.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + use crate::{ error::{CosmosError, CosmosErrorCode, Error}, runtime::RuntimeContext, diff --git a/sdk/cosmos/azure_data_cosmos_native/src/error.rs b/sdk/cosmos/azure_data_cosmos_native/src/error.rs index e6bb162260e..7216e70a127 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/error.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/error.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + use azure_core::error::ErrorKind; use std::ffi::{CStr, CString, NulError}; diff --git a/sdk/cosmos/azure_data_cosmos_native/src/options/mod.rs b/sdk/cosmos/azure_data_cosmos_native/src/options/mod.rs index c21a8749c55..76865d5f9f5 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/options/mod.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/options/mod.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + #[repr(C)] pub struct ClientOptions { // Placeholder for future client options diff --git a/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs b/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs index e9616360a88..7abbd519bb2 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + //! This module provides runtime abstractions and implementations for different async runtimes. //! When compiling the C library, a feature is used to select which runtime implementation to include. //! Currently, only the Tokio runtime is supported. diff --git a/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs b/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs index 65dc6794a4c..cf9519c5f92 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + use tokio::runtime::{Builder, Runtime}; use crate::{ diff --git a/sdk/cosmos/azure_data_cosmos_native/src/string.rs b/sdk/cosmos/azure_data_cosmos_native/src/string.rs index 8daff81fe08..69dc5708279 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/string.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/string.rs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + use std::ffi::{CStr, CString}; use std::os::raw::c_char; From 9ad047cbb920b8ec566e93254c422d1a3d521c5f Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 20:42:23 +0000 Subject: [PATCH 11/21] fix clippy lints --- sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h | 2 +- sdk/cosmos/azure_data_cosmos_native/src/error.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h index b47f9aa5a2e..7cfc5800f18 100644 --- a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h +++ b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h @@ -3,7 +3,7 @@ // This file is auto-generated by cbindgen. Do not edit manually. // cSpell: disable -// Build identifier: $Id: azure_data_cosmos_native, Version: 0.27.0, Commit: unknown, Branch: unknown, Build ID: unknown, Build Number: unknown, Timestamp: 1763498483$ +// Build identifier: $Id: azure_data_cosmos_native, Version: 0.27.0, Commit: unknown, Branch: unknown, Build ID: unknown, Build Number: unknown, Timestamp: 1763498539$ #include diff --git a/sdk/cosmos/azure_data_cosmos_native/src/error.rs b/sdk/cosmos/azure_data_cosmos_native/src/error.rs index 7216e70a127..f59281b0b37 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/error.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/error.rs @@ -145,7 +145,7 @@ impl std::fmt::Display for Error { self.code )?; if let Some(detail) = &self.detail { - write!(f, ": {}", detail.to_string())?; + write!(f, ": {}", detail)?; } Ok(()) } From 2d27aaf495c78ad85df493aa7bb9e3a4439d75df Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 21:57:12 +0000 Subject: [PATCH 12/21] remove old blocking implementation --- .../include/azurecosmos.h | 2 +- .../azure_data_cosmos_native/src/blocking.rs | 57 ------------------- 2 files changed, 1 insertion(+), 58 deletions(-) delete mode 100644 sdk/cosmos/azure_data_cosmos_native/src/blocking.rs diff --git a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h index 7cfc5800f18..f34cc35b643 100644 --- a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h +++ b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h @@ -3,7 +3,7 @@ // This file is auto-generated by cbindgen. Do not edit manually. // cSpell: disable -// Build identifier: $Id: azure_data_cosmos_native, Version: 0.27.0, Commit: unknown, Branch: unknown, Build ID: unknown, Build Number: unknown, Timestamp: 1763498539$ +// Build identifier: $Id: azure_data_cosmos_native, Version: 0.27.0, Commit: unknown, Branch: unknown, Build ID: unknown, Build Number: unknown, Timestamp: 1763503020$ #include diff --git a/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs b/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs deleted file mode 100644 index 4778e146794..00000000000 --- a/sdk/cosmos/azure_data_cosmos_native/src/blocking.rs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -use std::{future::Future, sync::OnceLock}; -use tokio::runtime::Runtime; - -// Centralized runtime - single instance per process (following Azure SDK pattern) -// https://github.com/Azure/azure-sdk-for-cpp/blob/main/sdk/core/azure-core-amqp/src/impl/rust_amqp/rust_amqp/rust_wrapper/src/amqp/connection.rs#L100-L107 -pub static RUNTIME: OnceLock = OnceLock::new(); - -pub fn block_on(future: F) -> F::Output -where - F: Future, -{ - let runtime = RUNTIME.get_or_init(|| { - tracing::trace!("Initializing blocking Tokio runtime"); - Runtime::new().expect("Failed to create Tokio runtime") - }); - runtime.block_on(future) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_blocking_runtime_initialization() { - let result1 = block_on(async { 42 }); - let result2 = block_on(async { 24 }); - - assert_eq!(result1, 42); - assert_eq!(result2, 24); - } - - #[test] - fn test_blocking_async_operation() { - let result = block_on(async { - tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; - "completed" - }); - - assert_eq!(result, "completed"); - } - - #[test] - fn test_runtime_singleton() { - block_on(async { 1 }); - let runtime1 = RUNTIME.get(); - - block_on(async { 2 }); - let runtime2 = RUNTIME.get(); - - assert!(runtime1.is_some()); - assert!(runtime2.is_some()); - assert!(std::ptr::eq(runtime1.unwrap(), runtime2.unwrap())); - } -} From 219dc8ddaea0519440b21d80a8e352253d57f228 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 21:57:35 +0000 Subject: [PATCH 13/21] fix doc comment --- sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h | 4 ++-- sdk/cosmos/azure_data_cosmos_native/src/context.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h index f34cc35b643..11b0b1ee481 100644 --- a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h +++ b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h @@ -3,7 +3,7 @@ // This file is auto-generated by cbindgen. Do not edit manually. // cSpell: disable -// Build identifier: $Id: azure_data_cosmos_native, Version: 0.27.0, Commit: unknown, Branch: unknown, Build ID: unknown, Build Number: unknown, Timestamp: 1763503020$ +// Build identifier: $Id: azure_data_cosmos_native, Version: 0.27.0, Commit: unknown, Branch: unknown, Build ID: unknown, Build Number: unknown, Timestamp: 1763503050$ #include @@ -219,7 +219,7 @@ void cosmos_string_free(const char *str); * A [`CallContext`] may be reused for multiple calls, but cannot be used concurrently from multiple threads. * * # Arguments - * * `runtime` - Pointer to a [`RuntimeContext`] created by [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create). + * * `runtime_context` - Pointer to a [`RuntimeContext`] created by [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create). * * `options` - Pointer to [`CallContextOptions`] for call configuration, may be null. */ struct cosmos_call_context *cosmos_call_context_create(const struct cosmos_runtime_context *runtime_context, diff --git a/sdk/cosmos/azure_data_cosmos_native/src/context.rs b/sdk/cosmos/azure_data_cosmos_native/src/context.rs index 7eb7a41c9d6..ef8afa129bd 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/context.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/context.rs @@ -56,7 +56,7 @@ pub struct CallContext { /// A [`CallContext`] may be reused for multiple calls, but cannot be used concurrently from multiple threads. /// /// # Arguments -/// * `runtime` - Pointer to a [`RuntimeContext`] created by [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create). +/// * `runtime_context` - Pointer to a [`RuntimeContext`] created by [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create). /// * `options` - Pointer to [`CallContextOptions`] for call configuration, may be null. #[no_mangle] pub extern "C" fn cosmos_call_context_create( From dd5c07e9f7a1e820bde1432ba125c7ae4fe818a4 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 21:58:17 +0000 Subject: [PATCH 14/21] stop modifying the build identifier comment every build --- sdk/cosmos/azure_data_cosmos_native/build.rs | 3 ++- sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/build.rs b/sdk/cosmos/azure_data_cosmos_native/build.rs index 5de976f223f..30125ecc4bb 100644 --- a/sdk/cosmos/azure_data_cosmos_native/build.rs +++ b/sdk/cosmos/azure_data_cosmos_native/build.rs @@ -28,7 +28,8 @@ fn main() { // cSpell: disable " .to_string(); - header.push_str(&format!("// Build identifier: {}\n", build_id)); + + // TODO: Append the build identifier to the header during a release build let config = cbindgen::Config { language: cbindgen::Language::C, diff --git a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h index 11b0b1ee481..8ceba2f0ac1 100644 --- a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h +++ b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h @@ -3,7 +3,6 @@ // This file is auto-generated by cbindgen. Do not edit manually. // cSpell: disable -// Build identifier: $Id: azure_data_cosmos_native, Version: 0.27.0, Commit: unknown, Branch: unknown, Build ID: unknown, Build Number: unknown, Timestamp: 1763503050$ #include From ad62ddabf53a1507124d0a1059f17ac752300e2f Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 21:59:09 +0000 Subject: [PATCH 15/21] use 'azurecosmos' as the ID string in the build id --- sdk/cosmos/azure_data_cosmos_native/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/build.rs b/sdk/cosmos/azure_data_cosmos_native/build.rs index 30125ecc4bb..1ae5433debf 100644 --- a/sdk/cosmos/azure_data_cosmos_native/build.rs +++ b/sdk/cosmos/azure_data_cosmos_native/build.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; fn main() { let build_id = format!( "$Id: {}, Version: {}, Commit: {}, Branch: {}, Build ID: {}, Build Number: {}, Timestamp: {}$", - env!("CARGO_PKG_NAME"), + "azurecosmos", env!("CARGO_PKG_VERSION"), option_env!("BUILD_SOURCEVERSION").unwrap_or("unknown"), option_env!("BUILD_SOURCEBRANCH").unwrap_or("unknown"), From 194711896442dc0e330ffe066199882dcc0762a0 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 22:00:44 +0000 Subject: [PATCH 16/21] a few extra doc comments --- sdk/cosmos/azure_data_cosmos_native/src/context.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/src/context.rs b/sdk/cosmos/azure_data_cosmos_native/src/context.rs index ef8afa129bd..d3652c71ddd 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/context.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/context.rs @@ -225,16 +225,18 @@ macro_rules! context { }; } -/// Marker trait that indicates that a type can be converted into a pointer type for FFI output parameters. +/// Trait for converting Rust types into raw pointers for FFI. pub trait IntoRaw { type Output; + /// Consumes the value and returns a raw pointer. fn into_raw(self) -> Self::Output; } impl IntoRaw for Box { type Output = *mut T; + /// Converts a Box into a `*mut T` using [`Box::into_raw`]. fn into_raw(self) -> *mut T { let pointer = Box::into_raw(self); tracing::trace!( @@ -249,6 +251,7 @@ impl IntoRaw for Box { impl IntoRaw for std::ffi::CString { type Output = *const std::ffi::c_char; + /// Converts a CString into a `*const c_char` using [`CString::into_raw`](std::ffi::CString::into_raw). fn into_raw(self) -> *const std::ffi::c_char { let pointer = self.into_raw(); tracing::trace!(?pointer, "converting CString to raw pointer",); From c402fc26139ef3e5b7e22d0208255b26ebda891f Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 22:03:24 +0000 Subject: [PATCH 17/21] restore CosmosError::SUCCESS --- sdk/cosmos/azure_data_cosmos_native/build.rs | 4 +--- sdk/cosmos/azure_data_cosmos_native/src/context.rs | 8 ++++---- sdk/cosmos/azure_data_cosmos_native/src/error.rs | 12 ++++++------ sdk/cosmos/azure_data_cosmos_native/src/lib.rs | 1 - 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/build.rs b/sdk/cosmos/azure_data_cosmos_native/build.rs index 1ae5433debf..ec344be3e35 100644 --- a/sdk/cosmos/azure_data_cosmos_native/build.rs +++ b/sdk/cosmos/azure_data_cosmos_native/build.rs @@ -21,7 +21,7 @@ fn main() { ); println!("cargo:rustc-env=BUILD_IDENTIFIER={}", build_id); - let mut header: String = r"// Copyright (c) Microsoft Corporation. All rights reserved. + let header: String = r"// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // This file is auto-generated by cbindgen. Do not edit manually. @@ -29,8 +29,6 @@ fn main() { " .to_string(); - // TODO: Append the build identifier to the header during a release build - let config = cbindgen::Config { language: cbindgen::Language::C, header: Some(header), diff --git a/sdk/cosmos/azure_data_cosmos_native/src/context.rs b/sdk/cosmos/azure_data_cosmos_native/src/context.rs index d3652c71ddd..5849536f698 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/context.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/context.rs @@ -109,7 +109,7 @@ impl CallContext { tracing::trace!("sync operation complete"); match r { Ok(()) => { - self.error = Error::SUCCESS.into_ffi(self.include_error_details); + self.error = CosmosError::SUCCESS; CosmosErrorCode::Success } Err(err) => self.set_error_and_return_code(err), @@ -139,7 +139,7 @@ impl CallContext { unsafe { *out = value.into_raw(); } - self.error = Error::SUCCESS.into_ffi(self.include_error_details); + self.error = CosmosError::SUCCESS; CosmosErrorCode::Success } Err(err) => self.set_error_and_return_code(err), @@ -156,7 +156,7 @@ impl CallContext { tracing::trace!("async operation complete"); match r { Ok(()) => { - self.error = Error::SUCCESS.into_ffi(self.include_error_details); + self.error = CosmosError::SUCCESS; CosmosErrorCode::Success } Err(err) => self.set_error_and_return_code(err), @@ -186,7 +186,7 @@ impl CallContext { unsafe { *out = value.into_raw(); } - self.error = Error::SUCCESS.into_ffi(self.include_error_details); + self.error = CosmosError::SUCCESS; CosmosErrorCode::Success } Err(err) => self.set_error_and_return_code(err), diff --git a/sdk/cosmos/azure_data_cosmos_native/src/error.rs b/sdk/cosmos/azure_data_cosmos_native/src/error.rs index f59281b0b37..e750a2c21b3 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/error.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/error.rs @@ -170,12 +170,12 @@ pub struct CosmosError { } impl CosmosError { - // /// cbindgen:ignore - // pub static SUCCESS: Self = Self { - // code: CosmosErrorCode::Success, - // message: messages::OPERATION_SUCCEEDED.as_ptr(), - // detail: std::ptr::null(), - // }; + /// cbindgen:ignore + pub const SUCCESS: Self = Self { + code: CosmosErrorCode::Success, + message: messages::OPERATION_SUCCEEDED.as_ptr(), + detail: std::ptr::null(), + }; } pub fn http_status_to_error_code(status_code: u16) -> CosmosErrorCode { diff --git a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs index c86834374ef..b5dec834927 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs @@ -9,7 +9,6 @@ use std::ffi::{c_char, CStr}; pub mod string; #[macro_use] pub mod context; -pub mod blocking; pub mod clients; pub mod error; pub mod options; From e7db578fdd9bd9d0f8f405130bdff9290ecd8709 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 22:09:09 +0000 Subject: [PATCH 18/21] final touch-ups --- sdk/cosmos/azure_data_cosmos_native/src/error.rs | 3 ++- sdk/cosmos/azure_data_cosmos_native/src/lib.rs | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/src/error.rs b/sdk/cosmos/azure_data_cosmos_native/src/error.rs index e750a2c21b3..01edb143cbb 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/error.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/error.rs @@ -9,6 +9,7 @@ pub mod messages { use std::ffi::CStr; pub static INVALID_UTF8: &CStr = c"String is not valid UTF-8"; + pub static STRING_CONTAINS_NUL: &CStr = c"String contains NUL bytes"; pub static OPERATION_SUCCEEDED: &CStr = c"Operation completed successfully"; pub static NULL_OUTPUT_POINTER: &CStr = c"Output pointer is null"; pub static INVALID_JSON: &CStr = c"Invalid JSON data"; @@ -295,7 +296,7 @@ impl From for Error { fn from(_error: NulError) -> Self { Error::new( CosmosErrorCode::InvalidCString, - c"String contains NUL bytes", + messages::STRING_CONTAINS_NUL, ) } } diff --git a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs index b5dec834927..5b4f95398ad 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs @@ -39,6 +39,11 @@ pub fn unwrap_required_ptr<'a, T>( msg, )) } else { + tracing::trace!( + ?ptr, + type_name = std::any::type_name::(), + "unwrapped pointer" + ); Ok(unsafe { &*ptr }) } } From e8a0052a0e9af5f38a253955814fd498721c8173 Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Tue, 18 Nov 2025 23:16:43 +0000 Subject: [PATCH 19/21] use single-threaded runtime on wasm --- sdk/cosmos/azure_data_cosmos_native/Cargo.toml | 8 +++++++- .../azure_data_cosmos_native/src/runtime/tokio.rs | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/Cargo.toml b/sdk/cosmos/azure_data_cosmos_native/Cargo.toml index 0a93afd786b..0953fe6c2e4 100644 --- a/sdk/cosmos/azure_data_cosmos_native/Cargo.toml +++ b/sdk/cosmos/azure_data_cosmos_native/Cargo.toml @@ -15,13 +15,19 @@ crate-type = ["cdylib", "staticlib"] [dependencies] futures.workspace = true -tokio = { workspace = true, optional = true, features = ["rt-multi-thread", "macros"] } serde_json = { workspace = true, features = ["raw_value"] } azure_core.workspace = true azure_data_cosmos = { path = "../azure_data_cosmos", features = [ "key_auth", "preview_query_engine" ] } tracing.workspace = true tracing-subscriber = { workspace = true, optional = true, features = ["fmt", "env-filter"] } +[target.'cfg(target_family = "wasm")'.dependencies] +# The 'rt-multi-thread' feature is not supported in wasm targets +tokio = { workspace = true, optional = true, features = ["rt", "macros"] } + +[target.'cfg(not(target_family = "wasm"))'.dependencies] +tokio = { workspace = true, optional = true, features = ["rt-multi-thread", "macros"] } + [features] default = ["tokio", "reqwest", "reqwest_native_tls", "tracing"] tokio = ["dep:tokio"] diff --git a/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs b/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs index cf9519c5f92..58f090243e9 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs @@ -15,6 +15,19 @@ pub struct RuntimeContext { impl RuntimeContext { pub fn new(_options: Option<&RuntimeOptions>) -> Result { + #[cfg(target_family = "wasm")] + let runtime = Builder::new_current_thread() + .enable_all() + .thread_name("cosmos-sdk-runtime") + .build() + .map_err(|e| { + Error::with_detail( + CosmosErrorCode::UnknownError, + c"Unknown error initializing Cosmos SDK runtime", + e, + ) + })?; + #[cfg(not(target_family = "wasm"))] let runtime = Builder::new_multi_thread() .enable_all() .thread_name("cosmos-sdk-runtime") From 881cb47dd5d91f6f15c99c1dd049d9aea569246e Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Wed, 19 Nov 2025 22:52:13 +0000 Subject: [PATCH 20/21] fix doc comment errors --- sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h | 6 +++--- .../azure_data_cosmos_native/src/clients/cosmos_client.rs | 2 +- sdk/cosmos/azure_data_cosmos_native/src/context.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h index 8ceba2f0ac1..85d9eaf322e 100644 --- a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h +++ b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h @@ -105,8 +105,8 @@ typedef struct cosmos_error { * The `runtime_context` field must be set to a pointer to a `RuntimeContext` created by the * [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create) function. * - * The structure can also be created using [`cosmos_call_context_create`](crate::context::cosmos_call_context_create), - * in which case Rust will manage the memory for the structure, and it must be freed using [`cosmos_call_context_free`](crate::context::cosmos_call_context_free). + * The structure can also be created using [`cosmos_call_context_create`], + * in which case Rust will manage the memory for the structure, and it must be freed using [`cosmos_call_context_free`]. * * This structure must remain active and at the memory address specified in the function call for the duration of the call into the SDK. * If calling an async function, that may mean it must be allocated on the heap to ensure it remains live (depending on the caller's language/runtime). @@ -369,7 +369,7 @@ cosmos_error_code cosmos_container_query_items(struct cosmos_call_context *ctx, * * `ctx` - Pointer to a [`CallContext`] to use for this call. * * `endpoint` - The Cosmos DB account endpoint, as a nul-terminated C string. * * `key` - The Cosmos DB account key, as a nul-terminated C string - * * `options` - Pointer to [`CosmosClientOptions`] for client configuration, may be null. + * * `options` - Pointer to [`ClientOptions`] for client configuration, may be null. * * `out_client` - Output parameter that will receive a pointer to the created CosmosClient. * * # Returns diff --git a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs index c01aed03727..eaa77eca93b 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs @@ -20,7 +20,7 @@ use crate::unwrap_required_ptr; /// * `ctx` - Pointer to a [`CallContext`] to use for this call. /// * `endpoint` - The Cosmos DB account endpoint, as a nul-terminated C string. /// * `key` - The Cosmos DB account key, as a nul-terminated C string -/// * `options` - Pointer to [`CosmosClientOptions`] for client configuration, may be null. +/// * `options` - Pointer to [`ClientOptions`] for client configuration, may be null. /// * `out_client` - Output parameter that will receive a pointer to the created CosmosClient. /// /// # Returns diff --git a/sdk/cosmos/azure_data_cosmos_native/src/context.rs b/sdk/cosmos/azure_data_cosmos_native/src/context.rs index 5849536f698..dd6b585c946 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/context.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/context.rs @@ -18,8 +18,8 @@ pub struct CallContextOptions { /// The `runtime_context` field must be set to a pointer to a `RuntimeContext` created by the /// [`cosmos_runtime_context_create`](crate::runtime::cosmos_runtime_context_create) function. /// -/// The structure can also be created using [`cosmos_call_context_create`](crate::context::cosmos_call_context_create), -/// in which case Rust will manage the memory for the structure, and it must be freed using [`cosmos_call_context_free`](crate::context::cosmos_call_context_free). +/// The structure can also be created using [`cosmos_call_context_create`], +/// in which case Rust will manage the memory for the structure, and it must be freed using [`cosmos_call_context_free`]. /// /// This structure must remain active and at the memory address specified in the function call for the duration of the call into the SDK. /// If calling an async function, that may mean it must be allocated on the heap to ensure it remains live (depending on the caller's language/runtime). @@ -236,7 +236,7 @@ pub trait IntoRaw { impl IntoRaw for Box { type Output = *mut T; - /// Converts a Box into a `*mut T` using [`Box::into_raw`]. + /// Converts a `Box` into a `*mut T` using [`Box::into_raw`]. fn into_raw(self) -> *mut T { let pointer = Box::into_raw(self); tracing::trace!( From 1497762953f41c41e1ff03faf0940df293d6419d Mon Sep 17 00:00:00 2001 From: Ashley Stanton-Nurse Date: Wed, 19 Nov 2025 23:07:22 +0000 Subject: [PATCH 21/21] me spell pretty one day --- sdk/cosmos/.dict.txt | 4 ++++ sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h | 2 +- sdk/cosmos/azure_data_cosmos_native/src/context.rs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/.dict.txt b/sdk/cosmos/.dict.txt index a11c42c68c5..c0e124aee9a 100644 --- a/sdk/cosmos/.dict.txt +++ b/sdk/cosmos/.dict.txt @@ -8,8 +8,12 @@ udfs backoff pluggable cloneable + +# Here at Cosmos DB, we upserts, they're the best ;) upsert upserts +upserted +upserting # Cosmos' docs all use "Autoscale" as a single word, rather than a compound "AutoScale" or "Auto Scale" autoscale diff --git a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h index 85d9eaf322e..754807c5e37 100644 --- a/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h +++ b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h @@ -111,7 +111,7 @@ typedef struct cosmos_error { * This structure must remain active and at the memory address specified in the function call for the duration of the call into the SDK. * If calling an async function, that may mean it must be allocated on the heap to ensure it remains live (depending on the caller's language/runtime). * - * A single [`CallContext`] may be reused for muliple calls, but cannot be used concurrently from multiple threads. + * A single [`CallContext`] may be reused for multiple calls, but cannot be used concurrently from multiple threads. * When reusing a [`CallContext`] the [`CallContext::error`] field will be overwritten with the error from the most recent call. * Error details will NOT be freed if the context is reused; the caller is responsible for freeing any error details if needed. */ diff --git a/sdk/cosmos/azure_data_cosmos_native/src/context.rs b/sdk/cosmos/azure_data_cosmos_native/src/context.rs index dd6b585c946..8b01a99108c 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/context.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/context.rs @@ -24,7 +24,7 @@ pub struct CallContextOptions { /// This structure must remain active and at the memory address specified in the function call for the duration of the call into the SDK. /// If calling an async function, that may mean it must be allocated on the heap to ensure it remains live (depending on the caller's language/runtime). /// -/// A single [`CallContext`] may be reused for muliple calls, but cannot be used concurrently from multiple threads. +/// A single [`CallContext`] may be reused for multiple calls, but cannot be used concurrently from multiple threads. /// When reusing a [`CallContext`] the [`CallContext::error`] field will be overwritten with the error from the most recent call. /// Error details will NOT be freed if the context is reused; the caller is responsible for freeing any error details if needed. #[repr(C)]