diff --git a/Cargo.lock b/Cargo.lock index fe1abf0d8f6..2eb2d0e54ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,8 +335,14 @@ dependencies = [ name = "azure_data_cosmos_native" version = "0.27.0" dependencies = [ + "azure_core", "azure_data_cosmos", "cbindgen", + "futures", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", ] [[package]] diff --git a/sdk/cosmos/.dict.txt b/sdk/cosmos/.dict.txt index 00b93cefe42..c0e124aee9a 100644 --- a/sdk/cosmos/.dict.txt +++ b/sdk/cosmos/.dict.txt @@ -9,9 +9,16 @@ 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 # 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/CMakeLists.txt b/sdk/cosmos/azure_data_cosmos_native/CMakeLists.txt index e1b8b7289b8..5136d218f56 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,11 +19,14 @@ 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 + ./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/Cargo.toml b/sdk/cosmos/azure_data_cosmos_native/Cargo.toml index 504ff194432..0953fe6c2e4 100644 --- a/sdk/cosmos/azure_data_cosmos_native/Cargo.toml +++ b/sdk/cosmos/azure_data_cosmos_native/Cargo.toml @@ -14,7 +14,26 @@ name = "azurecosmos" crate-type = ["cdylib", "staticlib"] [dependencies] -azure_data_cosmos = { path = "../azure_data_cosmos" } +futures.workspace = true +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"] +reqwest = ["azure_core/reqwest"] +reqwest_native_tls = ["azure_core/reqwest_native_tls"] +tracing = ["dep:tracing-subscriber"] [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..ec344be3e35 100644 --- a/sdk/cosmos/azure_data_cosmos_native/build.rs +++ b/sdk/cosmos/azure_data_cosmos_native/build.rs @@ -3,10 +3,12 @@ // 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: {}$", - env!("CARGO_PKG_NAME"), + "azurecosmos", env!("CARGO_PKG_VERSION"), option_env!("BUILD_SOURCEVERSION").unwrap_or("unknown"), option_env!("BUILD_SOURCEBRANCH").unwrap_or("unknown"), @@ -19,26 +21,73 @@ 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. // cSpell: disable " .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()], + + // 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()), + ("CosmosError".into(), "error".into()), + ("CosmosErrorCode".into(), "error_code".into()), + ("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() + }, + ..Default::default() + }; let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - cbindgen::Builder::new() + let Ok(bindings) = 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_cpp_compat(true) - .with_header(header) + .with_config(config) .generate() - .expect("unable to generate bindings") - .write_to_file("include/cosmosclient.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 new file mode 100644 index 00000000000..3d56e095e99 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/context_memory_management.c @@ -0,0 +1,315 @@ +// 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_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); + 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_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); + 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_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); + 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("Error: Missing required environment variables.\n"); + printf("Required: AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_KEY\n"); + return TEST_FAIL; + } + + 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; + } + + 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, NULL, &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("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_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); + 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, NULL, &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, NULL, &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", NULL, &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, NULL); + 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", NULL, &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", NULL, &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, NULL); + } + + 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..36d00fb6ecc --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/error_handling.c @@ -0,0 +1,611 @@ +// 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_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); + 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, NULL, &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, NULL, &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, 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, 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, NULL, &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, NULL, &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_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); + 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, NULL, &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, NULL, &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", NULL, &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", NULL, &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, NULL); + } + + 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_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); + 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, NULL, &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, NULL, &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", NULL, &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", NULL, &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, NULL); + } + + 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_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); + 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, NULL, &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, NULL, &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", NULL, &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, NULL); + + 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, NULL); + } + + 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_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); + 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, NULL, &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, "", NULL, &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, NULL, &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", NULL, &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", "", NULL, &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", NULL, &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, NULL); + 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, NULL); + 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, NULL); + } + + 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 new file mode 100644 index 00000000000..7d18188a4b6 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/c_tests/item_crud.c @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#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" + +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(); + + // Get environment variables (only endpoint and key required) + const char *endpoint = getenv("AZURE_COSMOS_ENDPOINT"); + const char *key = getenv("AZURE_COSMOS_KEY"); + + if (!endpoint || !key) { + printf("Error: Missing required environment variables.\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-item-crud-%ld", current_time); + + printf("Running Cosmos DB item CRUD test...\n"); + printf("Endpoint: %s\n", endpoint); + printf("Database: %s\n", database_name); + printf("Container: test-container\n"); + + cosmos_error error; + cosmos_runtime_context *runtime = cosmos_runtime_context_create(NULL, &error); + if (!runtime) { + display_error(&error); + 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; + 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(&ctx, endpoint, key, NULL, &client); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + display_error(&ctx.error); + result = 1; + goto cleanup; + } + printf("✓ Created Cosmos client\n"); + + // Create database + code = cosmos_client_create_database(&ctx, client, database_name, NULL, &database); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + display_error(&ctx.error); + result = 1; + goto cleanup; + } + database_created = 1; + printf("✓ Created database: %s\n", database_name); + + // Create container with partition key + 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; + goto cleanup; + } + 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]; + 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(&ctx, container, PARTITION_KEY_VALUE, json_data, NULL); + if (code != COSMOS_ERROR_CODE_SUCCESS) { + display_error(&ctx.error); + result = 1; + goto cleanup; + } + printf("✓ Upserted item successfully\n"); + + // Read the item back + 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; + 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: + // 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_code delete_code = cosmos_database_delete(&ctx, database, NULL); + if (delete_code != COSMOS_ERROR_CODE_SUCCESS) { + display_error(&ctx.error); + } else { + printf("✓ Deleted database successfully\n"); + } + } + + if (container) { + cosmos_container_free(container); + } + if (database) { + cosmos_database_free(database); + } + if (client) { + cosmos_client_free(client); + } + + 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/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..754807c5e37 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/include/azurecosmos.h @@ -0,0 +1,546 @@ +// 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 + + +#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`], + * 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). + * + * 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. + */ +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_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, + 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 [`ClientOptions`] 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 new file mode 100644 index 00000000000..73a218638da --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/container_client.rs @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use std::ffi::CString; +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::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)) } + } +} + +/// 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. +#[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)?; + 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. +/// * `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)?; + 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. +/// * `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 { + 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)?; + + // 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. +/// * `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)?; + 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. +/// * `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)?; + 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 + +/// 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. +#[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 { + 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)?) + }) +} + +/// 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 +/// * `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. +#[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 { + 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() { + 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 new file mode 100644 index 00000000000..eaa77eca93b --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/cosmos_client.rs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use std::ffi::CString; +use std::os::raw::c_char; + +use azure_core::credentials::Secret; +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; + +/// 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 [`ClientOptions`] 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, || { + 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)?; + + Ok(Box::new(client)) + }) +} + +/// 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)) } + } +} + +/// 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`]. +#[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, + database_id: *const c_char, + out_database: *mut *mut DatabaseClient, +) -> CosmosErrorCode { + context!(ctx).run_sync_with_output(out_database, || { + 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)) + }) +} + +/// 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 +#[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 { + 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); + 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, 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. +#[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 { + 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?; + 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 new file mode 100644 index 00000000000..37f2dfb9258 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/database_client.rs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use std::ffi::CString; +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::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)) } + } +} + +/// 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`]. +#[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, + container_id: *const c_char, + out_container: *mut *mut ContainerClient, +) -> CosmosErrorCode { + context!(ctx).run_sync_with_output(out_container, || { + 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)) + }) +} + +/// 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. +#[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 { + 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)?) + }) +} + +/// 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. +#[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)?; + 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. +/// * `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 { + 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(); + 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() + }; + + database.create_container(properties, None).await?; + + let container_client = database.container_client(&container_id); + + Ok(Box::new(container_client)) + }) +} + +/// 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. +#[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 { + 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); + 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/clients/mod.rs b/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs new file mode 100644 index 00000000000..80fc0243706 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/clients/mod.rs @@ -0,0 +1,15 @@ +// 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." +)] + +pub mod container_client; +pub mod cosmos_client; +pub mod database_client; + +pub use container_client::*; +pub use cosmos_client::*; +pub use database_client::*; 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..8b01a99108c --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/context.rs @@ -0,0 +1,260 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use crate::{ + error::{CosmosError, CosmosErrorCode, Error}, + 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. +/// 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`], +/// 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). +/// +/// 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)] +#[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. +/// +/// # Arguments +/// * `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( + 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, + include_error_details: options.include_error_details, + error: CosmosError { + code: CosmosErrorCode::Success, + message: crate::error::messages::OPERATION_SUCCEEDED.as_ptr(), + detail: std::ptr::null(), + }, + }; + 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)) } + } +} + +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 { + tracing::trace!("starting sync operation"); + let r = f(); + tracing::trace!("sync operation complete"); + match r { + Ok(()) => { + self.error = CosmosError::SUCCESS; + 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; + } + + tracing::trace!("starting sync operation"); + let r = f(); + tracing::trace!("sync operation complete"); + match r { + Ok(value) => { + unsafe { + *out = value.into_raw(); + } + self.error = CosmosError::SUCCESS; + 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 { + tracing::trace!("starting async operation"); + let r = self.runtime().block_on(f); + tracing::trace!("async operation complete"); + match r { + Ok(()) => { + self.error = CosmosError::SUCCESS; + 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; + } + + tracing::trace!("starting async operation"); + let r = self.runtime().block_on(f); + tracing::trace!("async operation complete"); + match r { + Ok(value) => { + unsafe { + *out = value.into_raw(); + } + self.error = CosmosError::SUCCESS; + CosmosErrorCode::Success + } + Err(err) => self.set_error_and_return_code(err), + } + } + + 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; + code + } +} + +#[macro_export] +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 + } + } + }; +} + +/// 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!( + ?pointer, + type_name = std::any::type_name::(), + "converting Box to raw pointer", + ); + pointer + } +} + +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",); + pointer + } +} 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..01edb143cbb --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/error.rs @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use azure_core::error::ErrorKind; +use std::ffi::{CStr, CString, NulError}; + +/// Collection of static C strings for error messages. +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"; + 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 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)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum CosmosErrorCode { + #[default] + 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, + + 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. +/// +/// 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, + + /// 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 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, + }; + + /// 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, + message, + detail: None, + } + } + + /// 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 std::error::Error + 'static, + ) -> Self { + Self { + code, + message, + detail: Some(Box::new(detail)), + } + } + + 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() + }; + + CosmosError { + code: self.code, + message: self.message.as_ptr(), + detail: detail_ptr, + } + } +} + +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)?; + } + Ok(()) + } +} + +impl std::error::Error for Error {} + +/// 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 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 { + 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, &'static CStr) { + if error_message.contains("PartitionKeyMismatch") + || error_message.contains("partition key mismatch") + { + ( + CosmosErrorCode::PartitionKeyMismatch, + c"Partition key mismatch", + ) + } else if error_message.contains("Resource quota exceeded") + || error_message.contains("Request rate is large") + { + ( + CosmosErrorCode::ResourceQuotaExceeded, + c"Resource quota exceeded", + ) + } else if error_message.contains("429") && error_message.contains("Request rate is large") { + ( + CosmosErrorCode::RequestRateTooLarge, + c"Request rate too large", + ) + } else if error_message.contains("Entity is too large") + || error_message.contains("Request entity too large") + { + (CosmosErrorCode::ItemSizeTooLarge, c"Item size too large") + } else if error_message.contains("Partition key") && error_message.contains("not found") { + ( + CosmosErrorCode::PartitionKeyNotFound, + c"Partition key not found", + ) + } else { + (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) -> Error { + let error_string = azure_error.to_string(); + + if let Some(status_code) = azure_error.http_status() { + let (cosmos_error_code, message) = extract_cosmos_db_error_info(&error_string); + + if cosmos_error_code != CosmosErrorCode::UnknownError { + Error::with_detail(cosmos_error_code, message, azure_error) + } else { + let error_code = http_status_to_error_code(u16::from(status_code)); + Error::with_detail(error_code, c"HTTP error", azure_error) + } + } else { + match azure_error.kind() { + ErrorKind::Credential => Error::with_detail( + CosmosErrorCode::AuthenticationFailed, + c"Authentication failed", + azure_error, + ), + ErrorKind::Io => Error::with_detail( + CosmosErrorCode::ConnectionFailed, + c"Connection failed", + azure_error, + ), + ErrorKind::DataConversion => { + if error_string.contains("Not Found") || error_string.contains("not found") { + Error::with_detail( + CosmosErrorCode::NotFound, + c"Resource not found", + azure_error, + ) + } else { + Error::with_detail( + CosmosErrorCode::DataConversion, + c"Data conversion failed", + azure_error, + ) + } + } + _ => Error::with_detail(CosmosErrorCode::UnknownError, c"Unknown error", azure_error), + } + } +} + +impl From for Error { + fn from(error: azure_core::Error) -> Self { + convert_azure_error_native(error) + } +} + +impl From for Error { + fn from(error: serde_json::Error) -> Self { + Error::with_detail( + CosmosErrorCode::DataConversion, + c"JSON serialization/deserialization error", + error, + ) + } +} + +impl From for Error { + fn from(_error: NulError) -> Self { + Error::new( + CosmosErrorCode::InvalidCString, + 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 6ad18528ce2..5b4f95398ad 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/lib.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/lib.rs @@ -1,19 +1,80 @@ // 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 string; +#[macro_use] +pub mod context; +pub mod clients; +pub mod error; +pub mod options; +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 { + tracing::trace!( + ?ptr, + type_name = std::any::type_name::(), + "unwrapped pointer" + ); + 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 -#[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() } + +/// 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")) + .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..76865d5f9f5 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/options/mod.rs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#[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 new file mode 100644 index 00000000000..7abbd519bb2 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/runtime/mod.rs @@ -0,0 +1,76 @@ +// 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. +//! +//! 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; + +#[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, +/// 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. +#[no_mangle] +pub extern "C" fn cosmos_runtime_context_create( + options: *const RuntimeOptions, + out_error: *mut CosmosError, +) -> *mut RuntimeContext { + let options = if options.is_null() { + None + } else { + Some(unsafe { &*options }) + }; + let c = match RuntimeContext::new(options) { + 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() { + 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 new file mode 100644 index 00000000000..58f090243e9 --- /dev/null +++ b/sdk/cosmos/azure_data_cosmos_native/src/runtime/tokio.rs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +use tokio::runtime::{Builder, Runtime}; + +use crate::{ + error::{CosmosErrorCode, Error}, + runtime::RuntimeOptions, +}; + +/// Provides a RuntimeContext (see [`crate::runtime`]) implementation using the Tokio runtime. +pub struct RuntimeContext { + runtime: Runtime, +} + +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") + .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(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/macros.rs b/sdk/cosmos/azure_data_cosmos_native/src/string.rs similarity index 50% rename from sdk/cosmos/azure_data_cosmos_native/src/macros.rs rename to sdk/cosmos/azure_data_cosmos_native/src/string.rs index a5ba7d43603..69dc5708279 100644 --- a/sdk/cosmos/azure_data_cosmos_native/src/macros.rs +++ b/sdk/cosmos/azure_data_cosmos_native/src/string.rs @@ -1,6 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +use crate::error::{CosmosErrorCode, Error}; + +#[macro_export] macro_rules! c_str { ($s:expr) => { const { @@ -24,3 +30,28 @@ macro_rules! c_str { } }; } + +// 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") +} + +pub fn parse_cstr<'a>(ptr: *const c_char, error_msg: &'static CStr) -> Result<&'a str, Error> { + if ptr.is_null() { + return Err(Error::new(CosmosErrorCode::InvalidArgument, error_msg)); + } + unsafe { CStr::from_ptr(ptr) } + .to_str() + .map_err(|_| Error::new(CosmosErrorCode::InvalidArgument, error_msg)) +} + +/// Releases the memory associated with a C string obtained from Rust. +#[no_mangle] +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(str as *mut c_char)); + } + } +}