Skip to content

Commit de86fdd

Browse files
authored
Cosmos: Cutover transactional batch operation to Driver (#4281)
Migrates `ContainerClient::execute_transactional_batch()` from the legacy `CosmosRequest` + `BatchOptions::apply_headers()` + `container_connection.send()` pipeline to `CosmosOperation` + `driver.execute_operation()`, matching the pattern established in #4147, #4174, and #4128. This was the last item-level operation using the old SDK pipeline for header manipulation, so the batch-specific helpers are removed as dead code. ## Changes **`azure_data_cosmos_driver/src/models/cosmos_operation.rs`** - Added `CosmosOperation::batch()` factory (POST to items feed with partition key, `OperationType::Batch`), following the `create_item` pattern. **`azure_data_cosmos/src/clients/container_client.rs`** - Rewrote `execute_transactional_batch` to build a `CosmosOperation::batch`, apply options, execute through the driver, and bridge the response. - Added `apply_batch_options` helper that takes `&BatchOptions` and wires session token onto the operation. - Removed the `items_link` field from `ContainerClient` — it was only used by the batch method's old `CosmosRequest` path. **`azure_data_cosmos/src/options/mod.rs`** - Removed `BatchOptions::apply_headers()`. - Removed the `apply_content_response_on_write_header()` helper (sole caller was `BatchOptions::apply_headers`). - Removed 4 batch header tests that validated the removed methods. - Cleaned up unused imports. **`azure_data_cosmos/src/constants.rs`** - Removed the `PREFER_MINIMAL` constant, which is no longer referenced in the SDK crate. The driver handles this header internally via its operation pipeline.
1 parent 8f47150 commit de86fdd

6 files changed

Lines changed: 132 additions & 153 deletions

File tree

sdk/cosmos/azure_data_cosmos/src/clients/container_client.rs

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ use serde::{de::DeserializeOwned, Serialize};
3737
#[derive(Clone)]
3838
pub struct ContainerClient {
3939
link: ResourceLink,
40-
items_link: ResourceLink,
4140
container_connection: Arc<ContainerConnection>,
4241
container_ref: ContainerReference,
4342
context: ClientContext,
@@ -53,7 +52,6 @@ impl ContainerClient {
5352
let link = database_link
5453
.feed(ResourceType::Containers)
5554
.item(container_id);
56-
let items_link = link.feed(ResourceType::Documents);
5755

5856
// Eagerly resolve immutable container metadata from the driver.
5957
let container_ref = context
@@ -80,7 +78,6 @@ impl ContainerClient {
8078

8179
Ok(Self {
8280
link,
83-
items_link,
8481
container_connection,
8582
container_ref,
8683
context,
@@ -792,19 +789,22 @@ impl ContainerClient {
792789
options: Option<BatchOptions>,
793790
) -> azure_core::Result<BatchResponse> {
794791
let options = options.unwrap_or_default();
795-
let partition_key = batch.partition_key().clone();
792+
let body = serde_json::to_vec(batch.operations())?;
793+
let driver_pk = batch.partition_key().clone().into_driver_partition_key();
796794

797-
let mut cosmos_request =
798-
CosmosRequest::builder(OperationType::Batch, self.items_link.clone())
799-
.partition_key(partition_key)
800-
.json(batch.operations())
801-
.build()?;
802-
options.apply_headers(&mut cosmos_request.headers);
795+
let operation =
796+
CosmosOperation::batch(self.container_ref.clone(), driver_pk).with_body(body);
797+
let operation = apply_batch_options(operation, &options);
803798

804-
self.container_connection
805-
.send(cosmos_request, Context::default())
806-
.await
807-
.map(BatchResponse::new)
799+
let driver_response = self
800+
.context
801+
.driver
802+
.execute_operation(operation, options.operation)
803+
.await?;
804+
805+
Ok(BatchResponse::new(
806+
crate::driver_bridge::driver_response_to_cosmos_response(driver_response),
807+
))
808808
}
809809

810810
/// Gets the feed ranges for this container.
@@ -1001,6 +1001,17 @@ fn apply_item_options(
10011001
operation
10021002
}
10031003

1004+
/// Applies [`BatchOptions`] fields to a [`CosmosOperation`].
1005+
///
1006+
/// [`BatchOptions`] carries a session token but no precondition (ETag-based
1007+
/// conditions are specified per-operation within the batch itself).
1008+
fn apply_batch_options(mut operation: CosmosOperation, options: &BatchOptions) -> CosmosOperation {
1009+
if let Some(session_token) = &options.session_token {
1010+
operation = operation.with_session_token(session_token.clone());
1011+
}
1012+
operation
1013+
}
1014+
10041015
#[cfg(test)]
10051016
mod tests {
10061017
use super::*;

sdk/cosmos/azure_data_cosmos/src/constants.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@
66

77
//! Constants defining HTTP headers and other values relevant to Azure Cosmos DB APIs.
88
9-
use azure_core::http::{
10-
headers::{HeaderName, HeaderValue},
11-
request::options::ContentType,
12-
StatusCode,
13-
};
9+
use azure_core::http::{headers::HeaderName, request::options::ContentType, StatusCode};
1410

1511
/// Macro to define Cosmos DB header constants and the allowed headers list in one place.
1612
macro_rules! cosmos_headers {
@@ -201,8 +197,6 @@ cosmos_headers! {
201197

202198
pub const QUERY_CONTENT_TYPE: ContentType = ContentType::from_static("application/query+json");
203199

204-
pub(crate) const PREFER_MINIMAL: HeaderValue = HeaderValue::from_static("return=minimal");
205-
206200
pub const ACCOUNT_PROPERTIES_KEY: &str = "account_properties_key";
207201

208202
/// The Cosmos DB-specific 449 Retry With status code.

sdk/cosmos/azure_data_cosmos/src/options/mod.rs

Lines changed: 1 addition & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
use crate::constants;
54
use crate::models::ThroughputProperties;
6-
use azure_core::http::headers::{self, Headers};
5+
use azure_core::http::headers::Headers;
76
use std::fmt;
87
use std::fmt::Display;
98

@@ -19,20 +18,6 @@ pub use azure_data_cosmos_driver::options::{
1918
ThroughputControlGroupOptions,
2019
};
2120

22-
// Temporary: applies the prefer header based on the content_response_on_write option.
23-
// Will be removed when write operations use the internal pipeline directly.
24-
fn apply_content_response_on_write_header(
25-
content_response_on_write: Option<&ContentResponseOnWrite>,
26-
headers: &mut Headers,
27-
) {
28-
match content_response_on_write {
29-
Some(ContentResponseOnWrite::Enabled) => {}
30-
_ => {
31-
headers.insert(headers::PREFER, constants::PREFER_MINIMAL);
32-
}
33-
}
34-
}
35-
3621
/// Options used when creating a [`CosmosClient`](crate::CosmosClient).
3722
///
3823
/// This struct is used internally by [`CosmosClientBuilder`](crate::CosmosClientBuilder).
@@ -256,28 +241,6 @@ impl BatchOptions {
256241
}
257242
}
258243

259-
impl BatchOptions {
260-
// Temporary: applies option values as HTTP headers for the SDK pipeline.
261-
// Will be removed when batch operations use the internal pipeline directly.
262-
pub(crate) fn apply_headers(&self, headers: &mut Headers) {
263-
if let Some(custom_headers) = self.operation.custom_headers() {
264-
for (name, value) in custom_headers {
265-
// Only insert if not already set — SDK/request headers take priority.
266-
if headers.get_optional_str(name).is_none() {
267-
headers.insert(name.clone(), value.clone());
268-
}
269-
}
270-
}
271-
if let Some(session_token) = &self.session_token {
272-
headers.insert(constants::SESSION_TOKEN, session_token.to_string());
273-
}
274-
apply_content_response_on_write_header(
275-
self.operation.content_response_on_write.as_ref(),
276-
headers,
277-
);
278-
}
279-
}
280-
281244
/// Options to be passed to [`DatabaseClient::query_containers()`](crate::clients::DatabaseClient::query_containers()).
282245
#[derive(Clone, Default)]
283246
#[non_exhaustive]
@@ -393,98 +356,4 @@ mod tests {
393356
headers_to_map(headers_expected)
394357
);
395358
}
396-
397-
#[test]
398-
fn batch_options_as_headers() {
399-
let mut custom_headers = HashMap::new();
400-
custom_headers.insert(
401-
HeaderName::from_static("x-custom-header"),
402-
HeaderValue::from_static("custom_value"),
403-
);
404-
405-
let mut operation = OperationOptions::default().with_custom_headers(custom_headers);
406-
operation.content_response_on_write = Some(ContentResponseOnWrite::Enabled);
407-
408-
let batch_options = BatchOptions {
409-
operation,
410-
..Default::default()
411-
}
412-
.with_session_token("BatchSessionToken".to_string());
413-
414-
let mut headers_result = Headers::new();
415-
batch_options.apply_headers(&mut headers_result);
416-
417-
let headers_expected: Vec<(HeaderName, HeaderValue)> = vec![
418-
("x-custom-header".into(), "custom_value".into()),
419-
(constants::SESSION_TOKEN, "BatchSessionToken".into()),
420-
];
421-
422-
assert_eq!(
423-
headers_to_map(headers_result),
424-
headers_to_map(headers_expected)
425-
);
426-
}
427-
428-
#[test]
429-
fn batch_options_custom_headers_should_not_override_sdk_set_headers() {
430-
let mut custom_headers = HashMap::new();
431-
custom_headers.insert(
432-
constants::SESSION_TOKEN,
433-
HeaderValue::from_static("CustomSession"),
434-
);
435-
436-
let operation = OperationOptions::default().with_custom_headers(custom_headers);
437-
438-
let batch_options = BatchOptions {
439-
operation,
440-
..Default::default()
441-
}
442-
.with_session_token("RealSessionToken".to_string());
443-
444-
let mut headers_result = Headers::new();
445-
batch_options.apply_headers(&mut headers_result);
446-
447-
let headers_expected: Vec<(HeaderName, HeaderValue)> = vec![
448-
(constants::SESSION_TOKEN, "RealSessionToken".into()),
449-
(headers::PREFER, constants::PREFER_MINIMAL),
450-
];
451-
452-
assert_eq!(
453-
headers_to_map(headers_result),
454-
headers_to_map(headers_expected)
455-
);
456-
}
457-
458-
#[test]
459-
fn batch_options_default_as_headers() {
460-
let batch_options = BatchOptions::default();
461-
462-
let mut headers_result = Headers::new();
463-
batch_options.apply_headers(&mut headers_result);
464-
let headers_result: Vec<(HeaderName, HeaderValue)> = headers_result.into_iter().collect();
465-
466-
let headers_expected: Vec<(HeaderName, HeaderValue)> =
467-
vec![(headers::PREFER, constants::PREFER_MINIMAL)];
468-
469-
assert_eq!(headers_result, headers_expected);
470-
}
471-
472-
#[test]
473-
fn batch_options_with_content_response_enabled() {
474-
let mut operation = OperationOptions::default();
475-
operation.content_response_on_write = Some(ContentResponseOnWrite::Enabled);
476-
477-
let batch_options = BatchOptions {
478-
operation,
479-
..Default::default()
480-
};
481-
482-
let mut headers_result = Headers::new();
483-
batch_options.apply_headers(&mut headers_result);
484-
let headers_result: Vec<(HeaderName, HeaderValue)> = headers_result.into_iter().collect();
485-
486-
let headers_expected: Vec<(HeaderName, HeaderValue)> = vec![];
487-
488-
assert_eq!(headers_result, headers_expected);
489-
}
490359
}

sdk/cosmos/azure_data_cosmos_driver/src/driver/pipeline/operation_pipeline.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,23 @@ fn build_transport_request(
502502
);
503503
}
504504

505+
// Cosmos DB uses POST for batch (same endpoint as create/upsert);
506+
// the service requires these headers to process the request as a batch.
507+
if operation.operation_type() == OperationType::Batch {
508+
headers.insert(
509+
HeaderName::from_static(request_header_names::IS_BATCH_REQUEST),
510+
HeaderValue::from_static("True"),
511+
);
512+
headers.insert(
513+
HeaderName::from_static(request_header_names::BATCH_ATOMIC),
514+
HeaderValue::from_static("True"),
515+
);
516+
headers.insert(
517+
HeaderName::from_static(request_header_names::BATCH_CONTINUE_ON_ERROR),
518+
HeaderValue::from_static("False"),
519+
);
520+
}
521+
505522
// Add operation type header for fault injection rule matching
506523
#[cfg(feature = "fault_injection")]
507524
{
@@ -1313,6 +1330,79 @@ mod tests {
13131330
);
13141331
}
13151332

1333+
#[test]
1334+
fn build_transport_request_sets_batch_headers() {
1335+
let operation = CosmosOperation::batch(test_container(), PartitionKey::from("pk1"))
1336+
.with_body(b"[]".to_vec());
1337+
1338+
let routing = test_routing();
1339+
let activity_id = ActivityId::from_string("default-activity".to_string());
1340+
let ctx = TransportRequestContext {
1341+
routing: &routing,
1342+
activity_id: &activity_id,
1343+
execution_context: ExecutionContext::Initial,
1344+
deadline: None,
1345+
resolved_session_token: None,
1346+
throughput_control: None,
1347+
};
1348+
let request =
1349+
build_transport_request(&operation, None, &ctx).expect("request should build");
1350+
1351+
assert_eq!(
1352+
request
1353+
.headers
1354+
.get_optional_str(&HeaderName::from_static("x-ms-cosmos-is-batch-request")),
1355+
Some("True"),
1356+
"is-batch-request header should be set"
1357+
);
1358+
assert_eq!(
1359+
request
1360+
.headers
1361+
.get_optional_str(&HeaderName::from_static("x-ms-cosmos-batch-atomic")),
1362+
Some("True"),
1363+
"batch-atomic header should be set"
1364+
);
1365+
assert_eq!(
1366+
request.headers.get_optional_str(&HeaderName::from_static(
1367+
"x-ms-cosmos-batch-continue-on-error"
1368+
)),
1369+
Some("False"),
1370+
"batch-continue-on-error header should be set"
1371+
);
1372+
}
1373+
1374+
#[test]
1375+
fn build_transport_request_omits_batch_headers_for_create() {
1376+
let container = test_container();
1377+
let operation = CosmosOperation::create_item(ItemReference::from_name(
1378+
&container,
1379+
PartitionKey::from("pk1"),
1380+
"doc1",
1381+
))
1382+
.with_body(b"{}".to_vec());
1383+
1384+
let routing = test_routing();
1385+
let activity_id = ActivityId::from_string("default-activity".to_string());
1386+
let ctx = TransportRequestContext {
1387+
routing: &routing,
1388+
activity_id: &activity_id,
1389+
execution_context: ExecutionContext::Initial,
1390+
deadline: None,
1391+
resolved_session_token: None,
1392+
throughput_control: None,
1393+
};
1394+
let request =
1395+
build_transport_request(&operation, None, &ctx).expect("request should build");
1396+
1397+
assert!(
1398+
request
1399+
.headers
1400+
.get_optional_str(&HeaderName::from_static("x-ms-cosmos-is-batch-request"))
1401+
.is_none(),
1402+
"batch headers should not be set for create"
1403+
);
1404+
}
1405+
13161406
#[test]
13171407
fn build_transport_request_sets_priority_level_header() {
13181408
let container = test_container();

sdk/cosmos/azure_data_cosmos_driver/src/models/cosmos_headers.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ pub(crate) mod request_header_names {
2121
pub const IF_NONE_MATCH: &str = "if-none-match";
2222
pub const PREFER: &str = "prefer";
2323
pub const IS_UPSERT: &str = "x-ms-documentdb-is-upsert";
24+
pub const IS_BATCH_REQUEST: &str = "x-ms-cosmos-is-batch-request";
25+
pub const BATCH_ATOMIC: &str = "x-ms-cosmos-batch-atomic";
26+
pub const BATCH_CONTINUE_ON_ERROR: &str = "x-ms-cosmos-batch-continue-on-error";
2427
pub const OFFER_THROUGHPUT: &str = "x-ms-offer-throughput";
2528
pub const OFFER_AUTOPILOT_SETTINGS: &str = "x-ms-cosmos-offer-autopilot-settings";
2629
pub const PRIORITY_LEVEL: &str = "x-ms-cosmos-priority-level";

sdk/cosmos/azure_data_cosmos_driver/src/models/cosmos_operation.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,18 @@ impl CosmosOperation {
481481
Self::new(OperationType::Delete, item).with_partition_key(partition_key)
482482
}
483483

484+
/// Executes a transactional batch of operations against a single partition.
485+
///
486+
/// All operations in the batch target the same `partition_key` and are
487+
/// committed atomically. Use `with_body()` to provide the JSON-encoded
488+
/// array of batch operations.
489+
pub fn batch(container: ContainerReference, partition_key: PartitionKey) -> Self {
490+
let resource_ref: CosmosResourceReference = CosmosResourceReference::from(container)
491+
.with_resource_type(ResourceType::Document)
492+
.into_feed_reference();
493+
Self::new(OperationType::Batch, resource_ref).with_partition_key(partition_key)
494+
}
495+
484496
/// Upserts (creates or replaces) an item (document) in a container.
485497
///
486498
/// The `ItemReference` contains the container, partition key, and item identifier,

0 commit comments

Comments
 (0)