Skip to content

Commit ab3ca55

Browse files
simorenohCopilot
andauthored
Cosmos: Cut over query methods to the driver (#4174)
Migrates query_items, query_databases, and query_containers from the gateway HTTP pipeline to the Cosmos driver pipeline, aligning query operations with the item CRUD operations that were already cut over. - QueryExecutor now takes an Arc<CosmosDriver> and an operation factory closure instead of Arc<GatewayPipeline> and a ResourceLink, executing queries via driver.execute_operation(). - Query-specific headers (content type, continuation, custom headers) are passed through DriverOperationOptions instead of being set directly on the HTTP request. - QueryOptions::apply_headers is now #[cfg(test)]-only since production code no longer uses it. - query_items_cross_partition simplified to delegate to query_items with PartitionKey::EMPTY. - Adds emulator tests for single-partition and cross-partition query pagination. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9ec660d commit ab3ca55

11 files changed

Lines changed: 277 additions & 137 deletions

File tree

sdk/cosmos/azure_data_cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
### Other Changes
2323

2424
- Database and container CRUD operations (`create_database`, `read`, `create_container`, `delete`) now route through the Cosmos driver pipeline. Throughput provisioning uses typed request headers via the driver. ([#4147](https://github.com/Azure/azure-sdk-for-rust/pull/4147))
25+
- Query operations (`query_items`, `query_databases`, `query_containers`) now route through the Cosmos driver pipeline, gaining driver-level transport, routing, and retry capabilities. ([#4174](https://github.com/Azure/azure-sdk-for-rust/pull/4174))
2526

2627
## 0.32.0 (2026-04-09)
2728

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

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ use crate::cosmos_request::CosmosRequest;
2121
use crate::handler::container_connection::ContainerConnection;
2222
use crate::operation_context::OperationType;
2323
use crate::routing::partition_key_range_cache::PartitionKeyRangeCache;
24-
use azure_core::http::headers::AsHeaders;
2524
use azure_core::http::Context;
2625
use azure_data_cosmos_driver::models::{
2726
effective_partition_key::EffectivePartitionKey as DriverEpk, ContainerReference,
@@ -698,23 +697,20 @@ impl ContainerClient {
698697
options: Option<QueryOptions>,
699698
) -> azure_core::Result<FeedItemIterator<T>> {
700699
let options = options.unwrap_or_default();
701-
let partition_key = partition_key.into();
700+
let partition_key: PartitionKey = partition_key.into();
702701
let query = query.into();
703702

704-
let mut headers = azure_core::http::headers::Headers::new();
705-
706-
// Convert PartitionKey and query options into headers.
707-
for (name, value) in partition_key.as_headers()? {
708-
headers.insert(name, value);
709-
}
710-
options.apply_headers(&mut headers);
703+
let driver_pk = partition_key.into_driver_partition_key();
704+
let container_ref = self.container_ref.clone();
705+
let factory =
706+
move || CosmosOperation::query_items(container_ref.clone(), driver_pk.clone());
711707

712708
crate::query::executor::QueryExecutor::new(
713-
self.context.pipeline.clone(),
714-
self.items_link.clone(),
715-
Context::default(),
709+
self.context.driver.clone(),
710+
factory,
716711
query,
717-
headers,
712+
options.operation,
713+
options.session_token,
718714
)
719715
.into_stream()
720716
}

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
use crate::{
55
clients::{ClientContext, DatabaseClient},
66
models::{DatabaseProperties, ResourceResponse},
7-
resource_context::ResourceLink,
87
CreateDatabaseOptions, FeedItemIterator, Query, QueryDatabasesOptions,
98
};
10-
use azure_core::http::{Context, Url};
9+
use azure_core::http::Url;
1110
use azure_data_cosmos_driver::models::CosmosOperation;
1211
use azure_data_cosmos_driver::options::OperationOptions;
1312
use serde::Serialize;
@@ -62,7 +61,6 @@ pub use super::cosmos_client_builder::CosmosClientBuilder;
6261
/// ```
6362
#[derive(Debug, Clone)]
6463
pub struct CosmosClient {
65-
pub(crate) databases_link: ResourceLink,
6664
pub(crate) context: ClientContext,
6765
}
6866

@@ -131,12 +129,15 @@ impl CosmosClient {
131129
query: impl Into<Query>,
132130
_options: Option<QueryDatabasesOptions>,
133131
) -> azure_core::Result<FeedItemIterator<DatabaseProperties>> {
132+
let account = self.context.driver.account().clone();
133+
let factory = move || CosmosOperation::query_databases(account.clone());
134+
134135
crate::query::executor::QueryExecutor::new(
135-
self.context.pipeline.clone(),
136-
self.databases_link.clone(),
137-
Context::default(),
136+
self.context.driver.clone(),
137+
factory,
138138
query.into(),
139-
azure_core::http::headers::Headers::new(),
139+
Default::default(),
140+
None,
140141
)
141142
.into_stream()
142143
}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use crate::{
77
clients::ClientContext,
88
options::ThroughputControlGroupOptions,
99
pipeline::{AuthorizationPolicy, CosmosHeadersPolicy, GatewayPipeline},
10-
resource_context::{ResourceLink, ResourceType},
1110
CosmosAccountReference, CosmosClient, CosmosClientOptions, CosmosCredential, RoutingStrategy,
1211
};
1312

@@ -449,7 +448,6 @@ impl CosmosClientBuilder {
449448
.await?;
450449

451450
Ok(CosmosClient {
452-
databases_link: ResourceLink::root(ResourceType::Databases),
453451
context: ClientContext {
454452
pipeline,
455453
driver,

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use crate::{
99
CreateContainerOptions, DeleteDatabaseOptions, FeedItemIterator, Query, QueryContainersOptions,
1010
ThroughputOptions,
1111
};
12-
use azure_core::http::Context;
1312
use azure_data_cosmos_driver::models::{CosmosOperation, DatabaseReference};
1413
use azure_data_cosmos_driver::options::OperationOptions;
1514

@@ -20,7 +19,6 @@ use super::ThroughputPoller;
2019
/// You can get a `DatabaseClient` by calling [`CosmosClient::database_client()`](crate::CosmosClient::database_client()).
2120
pub struct DatabaseClient {
2221
link: ResourceLink,
23-
containers_link: ResourceLink,
2422
database_id: String,
2523
context: ClientContext,
2624
database_ref: DatabaseReference,
@@ -30,13 +28,11 @@ impl DatabaseClient {
3028
pub(crate) fn new(context: ClientContext, database_id: &str) -> Self {
3129
let database_id = database_id.to_string();
3230
let link = ResourceLink::root(ResourceType::Databases).item(&database_id);
33-
let containers_link = link.feed(ResourceType::Containers);
3431
let database_ref =
3532
DatabaseReference::from_name(context.driver.account().clone(), database_id.clone());
3633

3734
Self {
3835
link,
39-
containers_link,
4036
database_id,
4137
context,
4238
database_ref,
@@ -128,12 +124,18 @@ impl DatabaseClient {
128124
query: impl Into<Query>,
129125
options: Option<QueryContainersOptions>,
130126
) -> azure_core::Result<FeedItemIterator<ContainerProperties>> {
127+
let db_ref = DatabaseReference::from_name(
128+
self.context.driver.account().clone(),
129+
self.database_id.clone(),
130+
);
131+
let factory = move || CosmosOperation::query_containers(db_ref.clone());
132+
131133
crate::query::executor::QueryExecutor::new(
132-
self.context.pipeline.clone(),
133-
self.containers_link.clone(),
134-
Context::default(),
134+
self.context.driver.clone(),
135+
factory,
135136
query.into(),
136-
azure_core::http::headers::Headers::new(),
137+
Default::default(),
138+
None,
137139
)
138140
.into_stream()
139141
}

sdk/cosmos/azure_data_cosmos/src/driver_bridge.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ use crate::{
2424
/// Converts a driver [`DriverResponse`] into the SDK's typed [`CosmosResponse<T>`].
2525
///
2626
/// This reconstructs an `azure_core::Response<T>` from the driver's raw bytes,
27-
/// status code, and headers, then wraps it in the SDK's response type using
28-
/// the pre-parsed headers from the driver to avoid a redundant parse.
27+
/// status code, and headers, then wraps it in the SDK's response type.
28+
///
29+
/// The driver's pre-parsed [`CosmosResponseHeaders`] are passed directly to
30+
/// avoid double-parsing. Some headers (e.g., `index_metrics`) are base64-decoded
31+
/// by the driver; re-parsing from raw headers would fail on already-decoded values.
2932
pub(crate) fn driver_response_to_cosmos_response<T>(
3033
driver_response: DriverResponse,
3134
) -> CosmosResponse<T> {
@@ -266,4 +269,60 @@ mod tests {
266269
assert_eq!(headers.get_optional_str(&SUB_STATUS), None);
267270
assert_eq!(headers.get_optional_str(&OFFER_REPLACE_PENDING), None);
268271
}
272+
273+
/// Regression test: index_metrics (base64-decoded by the driver) must survive
274+
/// the driver→SDK bridge without double-decoding.
275+
///
276+
/// Exercises `CosmosResponse::from_driver_response` which accepts pre-parsed
277+
/// headers, ensuring that already-decoded index_metrics are preserved rather
278+
/// than being base64-decoded a second time (which would silently return None).
279+
#[test]
280+
fn driver_response_preserves_index_metrics() {
281+
use crate::feed::{FeedBody, QueryFeedPage};
282+
use crate::models::CosmosResponse;
283+
284+
let mut cosmos_headers = CosmosResponseHeaders::new();
285+
cosmos_headers.index_metrics = Some(r#"{"UtilizedSingleIndexes":[]}"#.to_string());
286+
cosmos_headers.query_metrics =
287+
Some("totalExecutionTimeInMs=1.23;queryCompileTimeInMs=0.01".to_string());
288+
289+
// Build a minimal raw response with synthesized headers (as the bridge does).
290+
let raw_headers = driver_response_headers_to_headers(&cosmos_headers);
291+
let raw_response = azure_core::http::RawResponse::from_bytes(
292+
StatusCode::Ok,
293+
raw_headers,
294+
Bytes::from_static(br#"{"Documents":[]}"#),
295+
);
296+
let typed_response: azure_core::http::response::Response<FeedBody<serde_json::Value>> =
297+
raw_response.into();
298+
299+
// This is the code path used by driver_response_to_cosmos_response:
300+
// pre-parsed headers are passed directly, skipping re-parsing.
301+
let cosmos_response = CosmosResponse::from_driver_response(typed_response, cosmos_headers);
302+
303+
assert_eq!(
304+
cosmos_response.cosmos_headers().index_metrics.as_deref(),
305+
Some(r#"{"UtilizedSingleIndexes":[]}"#),
306+
"index_metrics should survive the driver bridge without double base64-decoding"
307+
);
308+
assert_eq!(
309+
cosmos_response.cosmos_headers().query_metrics.as_deref(),
310+
Some("totalExecutionTimeInMs=1.23;queryCompileTimeInMs=0.01"),
311+
);
312+
313+
let rt = tokio::runtime::Runtime::new().unwrap();
314+
let page = rt
315+
.block_on(QueryFeedPage::<serde_json::Value>::from_response(
316+
cosmos_response,
317+
))
318+
.unwrap();
319+
assert_eq!(
320+
page.index_metrics(),
321+
Some(r#"{"UtilizedSingleIndexes":[]}"#)
322+
);
323+
assert_eq!(
324+
page.query_metrics(),
325+
Some("totalExecutionTimeInMs=1.23;queryCompileTimeInMs=0.01")
326+
);
327+
}
269328
}

sdk/cosmos/azure_data_cosmos/src/models/cosmos_response.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ impl<T> CosmosResponse<T> {
5050
}
5151
}
5252

53-
/// Creates a `CosmosResponse` from a typed response and pre-parsed headers.
53+
/// Creates a `CosmosResponse` from a typed response and pre-parsed driver headers.
5454
///
55-
/// Used by the driver bridge to avoid re-parsing headers that were already
56-
/// parsed by the driver pipeline.
55+
/// Used by the driver bridge to avoid double-parsing response headers.
56+
/// The driver already decodes headers (e.g., base64 for index metrics),
57+
/// so re-parsing from raw headers would fail on values that are no longer
58+
/// in their wire format.
5759
pub(crate) fn from_driver_response(
5860
response: Response<T>,
5961
cosmos_headers: CosmosResponseHeaders,

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

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -359,24 +359,6 @@ impl QueryOptions {
359359
}
360360
}
361361

362-
impl QueryOptions {
363-
// Temporary: applies option values as HTTP headers for the SDK pipeline.
364-
// Will be removed when query operations use the internal pipeline directly.
365-
pub(crate) fn apply_headers(&self, headers: &mut Headers) {
366-
if let Some(custom_headers) = self.operation.custom_headers() {
367-
for (name, value) in custom_headers {
368-
// Only insert if not already set — SDK/request headers take priority.
369-
if headers.get_optional_str(name).is_none() {
370-
headers.insert(name.clone(), value.clone());
371-
}
372-
}
373-
}
374-
if let Some(session_token) = &self.session_token {
375-
headers.insert(constants::SESSION_TOKEN, session_token.to_string());
376-
}
377-
}
378-
}
379-
380362
/// Options to be passed to [`ContainerClient::read()`](crate::clients::ContainerClient::read()).
381363
#[derive(Clone, Default)]
382364
#[non_exhaustive]
@@ -515,36 +497,6 @@ mod tests {
515497
);
516498
}
517499

518-
#[test]
519-
fn query_options_as_headers() {
520-
let mut custom_headers = HashMap::new();
521-
custom_headers.insert(
522-
HeaderName::from_static("x-custom-header"),
523-
HeaderValue::from_static("custom_value"),
524-
);
525-
526-
let operation = OperationOptions::default().with_custom_headers(custom_headers);
527-
528-
let query_options = QueryOptions {
529-
operation,
530-
..Default::default()
531-
}
532-
.with_session_token("QuerySessionToken".to_string());
533-
534-
let mut headers_result = Headers::new();
535-
query_options.apply_headers(&mut headers_result);
536-
537-
let headers_expected: Vec<(HeaderName, HeaderValue)> = vec![
538-
("x-custom-header".into(), "custom_value".into()),
539-
(constants::SESSION_TOKEN, "QuerySessionToken".into()),
540-
];
541-
542-
assert_eq!(
543-
headers_to_map(headers_result),
544-
headers_to_map(headers_expected)
545-
);
546-
}
547-
548500
#[test]
549501
fn item_write_options_default_as_headers() {
550502
let options = ItemWriteOptions::default();

0 commit comments

Comments
 (0)