From 0efa7b81ea47b1193a7fadd67edc269f1d42242f Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:57:06 -0400 Subject: [PATCH 01/12] Restrict cross-partition query fan-out to feed-range scope Cross-partition queries scoped with `FeedScope::range` were fanning out to partitions outside the requested range. `ContainerClient::query_items` correctly stored the requested feed range in the operation target, but the fan-out planner built request leaves from the query plan's ranges (derived from the query text alone) without intersecting them with that target. With topology `[00,80),[80,FF)` and a query plan range of `[00,FF)`, a request scoped to `[00,80)` produced leaves for both partitions. This could return documents outside the requested feed range and caused extra scans / RU usage when callers enumerated `read_feed_ranges()` and queried each range. Add a `clip_to_target` helper that intersects each query-plan range with the operation's explicit-EPK target and skips ranges that fall entirely outside it. Apply it in both the fresh (`plan_fresh`) and resume (`plan_resume_from_saved_snapshot`) leaf builders. Targets that are absent, full, or logical-partition prefixes are passed through unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/cosmos/azure_data_cosmos/CHANGELOG.md | 1 + .../src/driver/dataflow/planner.rs | 128 ++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/sdk/cosmos/azure_data_cosmos/CHANGELOG.md b/sdk/cosmos/azure_data_cosmos/CHANGELOG.md index 1f6baeb657f..abd49add4f4 100644 --- a/sdk/cosmos/azure_data_cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure_data_cosmos/CHANGELOG.md @@ -10,6 +10,7 @@ ### Bugs Fixed +- Fixed cross-partition queries scoped to an explicit feed range (`ContainerClient::query_items` with `FeedScope::range`) fanning out to partitions outside the requested range. The query planner now intersects the query plan's ranges with the requested feed-range target, so only the partitions overlapping the requested range are queried. This prevents returning documents outside the requested feed range and the extra scans / RU usage that occurred when enumerating `read_feed_ranges()` and querying each range. - Fixed the `AZURE_COSMOS_PPCB_*` environment variables (including `AZURE_COSMOS_PPCB_ENABLED` and the `AZURE_COSMOS_PPCB_ENABLED_OVERRIDE` kill switch) being ignored when a `CosmosClient` was built without calling `CosmosClientBuilder::with_partition_failover_options`. The per-partition circuit breaker (PPCB) stayed enabled even with `AZURE_COSMOS_PPCB_ENABLED=false`. The client's driver now resolves these options from the environment when they are not supplied explicitly. ([#4655](https://github.com/Azure/azure-sdk-for-rust/pull/4655)) ### Other Changes diff --git a/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs b/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs index 7cb73a8a82f..10f8bc3efbf 100644 --- a/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs +++ b/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs @@ -210,6 +210,11 @@ async fn plan_fresh( let mut nodes: Vec> = Vec::new(); for query_range in &query_plan.query_ranges { let feed_range = query_range_to_feed_range(query_range)?; + // Restrict the query-plan range to the operation's requested scope. + // Ranges entirely outside the target contribute no request leaves. + let Some(feed_range) = clip_to_target(feed_range, operation) else { + continue; + }; let resolved = topology_provider .resolve_ranges(&feed_range, PartitionRoutingRefresh::UseCached) .await?; @@ -259,6 +264,11 @@ async fn plan_resume_from_saved_snapshot( for query_range in &query_plan.query_ranges { let feed_range = query_range_to_feed_range(query_range)?; + // Restrict the query-plan range to the operation's requested scope, + // mirroring the fresh path. Ranges outside the target are skipped. + let Some(feed_range) = clip_to_target(feed_range, operation) else { + continue; + }; let resolved = topology_provider .resolve_ranges(&feed_range, PartitionRoutingRefresh::UseCached) .await?; @@ -402,6 +412,28 @@ fn query_range_to_feed_range( FeedRange::new(min, max) } +/// Clips a query-plan feed range to the operation's requested scope +/// ([`CosmosOperation::target`]). +/// +/// The backend query plan's ranges are derived from the query text alone and +/// never reflect the caller's requested [`FeedRange`] target, so the planner +/// must intersect them here. Returns `None` when the query range lies entirely +/// outside the target, meaning it contributes no request leaves. +/// +/// When the operation has no target, or targets a logical partition / prefix, +/// the range is returned unchanged: a logical-partition [`FeedRange`] collapses +/// its `[min, max)` bounds to a single effective partition key and therefore +/// cannot be intersected as an EPK range. Prefix-scoped fan-out is handled +/// elsewhere; clipping it here would incorrectly drop every range. +fn clip_to_target(feed_range: FeedRange, operation: &CosmosOperation) -> Option { + match operation.target() { + Some(target) if !target.is_logical_partition() => { + intersect_feed_ranges(&feed_range, target) + } + _ => Some(feed_range), + } +} + /// Returns true if the union of `pieces` covers `range` end-to-end. /// /// Assumes pieces are subsets of `range`. The check sorts pieces by @@ -643,6 +675,18 @@ mod tests { .with_body(br#"{"query":"SELECT * FROM c"}"#.to_vec()) } + /// A cross-partition query scoped to an explicit EPK feed-range target, + /// e.g. `FeedScope::range([min, max))`. + fn query_operation_with_target(min: &str, max: &str) -> CosmosOperation { + let target = FeedRange::new( + EffectivePartitionKey::from(min), + EffectivePartitionKey::from(max), + ) + .unwrap(); + CosmosOperation::query_items(test_container(), Some(target)) + .with_body(br#"{"query":"SELECT * FROM c"}"#.to_vec()) + } + // --- build_trivial_pipeline tests --- #[test] @@ -959,6 +1003,90 @@ mod tests { assert_drain_requests_with_partitions(pipeline, &[("20", "80", "pkrange-wide", "", "FF")]); } + #[tokio::test] + async fn restricts_fanout_to_explicit_target_range() { + // The reported bug: query plan spans the whole space `[, FF)` but the + // caller scoped the query to `[00, 80)` via `FeedScope::range`. Only + // the requested slice must be queried, not the neighbouring `[80, FF)` + // partition. + let plan = plan_with_ranges(vec![qr("", "FF")]); + let op = query_operation_with_target("00", "80"); + let mut topology = + MockTopologyProvider::new(vec![Ok(vec![rr("00", "80", "pkrange-left")])]); + + let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None) + .await + .unwrap(); + assert_drain_requests(pipeline, &[("00", "80", "pkrange-left")]); + } + + #[tokio::test] + async fn drops_query_ranges_outside_target() { + // Two disjoint query-plan ranges; the target only overlaps the first. + // The second range must contribute no request leaves (and its topology + // must never be resolved). + let plan = plan_with_ranges(vec![qr("", "40"), qr("80", "FF")]); + let op = query_operation_with_target("", "40"); + let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "40", "pkrange-A")])]); + + let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None) + .await + .unwrap(); + assert_drain_requests(pipeline, &[("", "40", "pkrange-A")]); + } + + #[tokio::test] + async fn clips_query_range_to_partial_target_overlap() { + // The target `[20, 60)` partially overlaps a single query-plan range + // spanning several partitions. Only partitions within the target, + // clipped to its bounds, may be queried. + let plan = plan_with_ranges(vec![qr("", "FF")]); + let op = query_operation_with_target("20", "60"); + let mut topology = MockTopologyProvider::new(vec![Ok(vec![ + rr("00", "40", "pkrange-1"), + rr("40", "80", "pkrange-2"), + ])]); + + let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None) + .await + .unwrap(); + assert_drain_requests_with_partitions( + pipeline, + &[ + ("20", "40", "pkrange-1", "00", "40"), + ("40", "60", "pkrange-2", "40", "80"), + ], + ); + } + + #[tokio::test] + async fn resume_restricts_fanout_to_target_range() { + // Resuming a target-scoped query must also honour the target: the + // `[80, FF)` partition lies outside `[00, 80)` and must not be queried + // even though the query plan spans `[, FF)`. + let plan = plan_with_ranges(vec![qr("", "FF")]); + let op = query_operation_with_target("00", "80"); + let mut topology = + MockTopologyProvider::new(vec![Ok(vec![rr("00", "80", "pkrange-left")])]); + + let resume = saved_drain(vec![("00", "80", saved_request(Some("server-token-xyz")))]); + + let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume)) + .await + .unwrap(); + assert_drain_requests_with_partitions_and_continuation( + pipeline, + &[( + "00", + "80", + "pkrange-left", + "00", + "80", + Some("server-token-xyz"), + )], + ); + } + #[tokio::test] async fn rejects_query_plan_with_top() { let plan = QueryPlan { From ba795b8d2c0c7e45d796a8fb72129f784a11d12c Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:57:41 -0400 Subject: [PATCH 02/12] Add PR link to changelog entry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/cosmos/azure_data_cosmos/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/cosmos/azure_data_cosmos/CHANGELOG.md b/sdk/cosmos/azure_data_cosmos/CHANGELOG.md index abd49add4f4..aaba634b786 100644 --- a/sdk/cosmos/azure_data_cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure_data_cosmos/CHANGELOG.md @@ -10,7 +10,7 @@ ### Bugs Fixed -- Fixed cross-partition queries scoped to an explicit feed range (`ContainerClient::query_items` with `FeedScope::range`) fanning out to partitions outside the requested range. The query planner now intersects the query plan's ranges with the requested feed-range target, so only the partitions overlapping the requested range are queried. This prevents returning documents outside the requested feed range and the extra scans / RU usage that occurred when enumerating `read_feed_ranges()` and querying each range. +- Fixed cross-partition queries scoped to an explicit feed range (`ContainerClient::query_items` with `FeedScope::range`) fanning out to partitions outside the requested range. The query planner now intersects the query plan's ranges with the requested feed-range target, so only the partitions overlapping the requested range are queried. This prevents returning documents outside the requested feed range and the extra scans / RU usage that occurred when enumerating `read_feed_ranges()` and querying each range. ([#4705](https://github.com/Azure/azure-sdk-for-rust/pull/4705)) - Fixed the `AZURE_COSMOS_PPCB_*` environment variables (including `AZURE_COSMOS_PPCB_ENABLED` and the `AZURE_COSMOS_PPCB_ENABLED_OVERRIDE` kill switch) being ignored when a `CosmosClient` was built without calling `CosmosClientBuilder::with_partition_failover_options`. The per-partition circuit breaker (PPCB) stayed enabled even with `AZURE_COSMOS_PPCB_ENABLED=false`. The client's driver now resolves these options from the environment when they are not supplied explicitly. ([#4655](https://github.com/Azure/azure-sdk-for-rust/pull/4655)) ### Other Changes From 6c81640ca90ef269c92830232c75738853e8b999 Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:05:32 -0400 Subject: [PATCH 03/12] Make feed-range fan-out tests observe the clip The two headline target-scoping tests passed even with the clip reverted: `MockTopologyProvider` ignores its range argument and replays a fixed queue, so the feed range actually handed to `resolve_ranges` was never observed. Both tests were vacuous. Add a range-aware `PhysicalTopologyProvider` that resolves against a fixed physical layout, returning only partitions overlapping the requested range (mirroring real routing). Rewrite the fresh and resume target tests to use it with a two-partition topology so an unclipped planner would resolve and emit the out-of-scope partition and fail. Also add two edge-case tests: a query-plan range that only touches the target's exclusive upper bound (dropped), and a logical-partition (prefix) target that must not be clipped (pass-through guard). All five range-scoped tests now fail when the clip is neutralized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/driver/dataflow/mocks.rs | 39 +++++++++++ .../src/driver/dataflow/planner.rs | 67 +++++++++++++++++-- 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/mocks.rs b/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/mocks.rs index 618366b0db5..93ad29ac74c 100644 --- a/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/mocks.rs +++ b/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/mocks.rs @@ -190,6 +190,45 @@ impl TopologyProvider for MockTopologyProvider { } } +/// A range-aware topology provider backed by a fixed physical partition layout. +/// +/// Unlike [`MockTopologyProvider`] (which ignores the requested range and +/// replays a queue), this resolves *against* the requested range: +/// `resolve_ranges(range)` returns exactly the configured physical partitions +/// whose EPK span overlaps `range`, mirroring real routing. This lets fan-out +/// tests observe when the planner resolves partitions outside the requested +/// scope — a plain replay mock cannot, because it returns the same partitions +/// regardless of the range it is asked about. +pub(crate) struct PhysicalTopologyProvider { + partitions: Vec, +} + +impl PhysicalTopologyProvider { + pub fn new(partitions: Vec) -> Self { + Self { partitions } + } +} + +impl TopologyProvider for PhysicalTopologyProvider { + fn resolve_ranges<'a>( + &'a mut self, + range: &'a FeedRange, + _refresh: PartitionRoutingRefresh, + ) -> BoxFuture<'a, crate::error::Result>> { + let resolved: Vec = self + .partitions + .iter() + .filter(|p| { + // Half-open overlap: [a, b) intersects [c, d) iff a < d && c < b. + p.range.min_inclusive() < range.max_exclusive() + && range.min_inclusive() < p.range.max_exclusive() + }) + .cloned() + .collect(); + Box::pin(async move { Ok(resolved) }) + } +} + // ── Test helpers ──────────────────────────────────────────────────────────── /// Extracts the `CosmosResponse` from a `PageResult::Page`, panicking otherwise. diff --git a/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs b/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs index 10f8bc3efbf..7445765539a 100644 --- a/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs +++ b/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs @@ -1008,11 +1008,15 @@ mod tests { // The reported bug: query plan spans the whole space `[, FF)` but the // caller scoped the query to `[00, 80)` via `FeedScope::range`. Only // the requested slice must be queried, not the neighbouring `[80, FF)` - // partition. + // partition. The physical topology actually contains both partitions, + // so a planner that ignores the target would resolve and emit a leaf + // for `[80, FF)` as well. let plan = plan_with_ranges(vec![qr("", "FF")]); let op = query_operation_with_target("00", "80"); - let mut topology = - MockTopologyProvider::new(vec![Ok(vec![rr("00", "80", "pkrange-left")])]); + let mut topology = PhysicalTopologyProvider::new(vec![ + rr("00", "80", "pkrange-left"), + rr("80", "FF", "pkrange-right"), + ]); let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None) .await @@ -1059,15 +1063,66 @@ mod tests { ); } + #[tokio::test] + async fn drops_query_range_touching_target_boundary() { + // A query-plan range that only *touches* the target's exclusive upper + // bound (target `[, 40)`, range `[40, FF)`) shares no EPKs with the + // target and must be dropped, not queried. Exercises the exact + // boundary case where `intersect_feed_ranges` collapses to empty. + let plan = plan_with_ranges(vec![qr("", "40"), qr("40", "FF")]); + let op = query_operation_with_target("", "40"); + let mut topology = PhysicalTopologyProvider::new(vec![ + rr("", "40", "pkrange-A"), + rr("40", "FF", "pkrange-B"), + ]); + + let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None) + .await + .unwrap(); + assert_drain_requests(pipeline, &[("", "40", "pkrange-A")]); + } + + #[tokio::test] + async fn logical_partition_target_is_not_clipped() { + // A logical-partition (prefix) target must NOT be clipped: its + // `[min, max)` bounds collapse to a single EPK, so intersecting the + // query-plan ranges against it would drop everything. The guard in + // `clip_to_target` leaves such ranges untouched, letting the query + // fan out across the resolved topology. This also documents that + // prefix-scoped over-scan is handled elsewhere (see follow-up). + let plan = plan_with_ranges(vec![qr("", "FF")]); + let target = + FeedRange::for_partition(PartitionKey::from("pk1"), &test_partition_key_definition()); + assert!(target.is_logical_partition()); + let op = CosmosOperation::query_items(test_container(), Some(target)) + .with_body(br#"{"query":"SELECT * FROM c"}"#.to_vec()); + let mut topology = PhysicalTopologyProvider::new(vec![ + rr("00", "80", "pkrange-left"), + rr("80", "FF", "pkrange-right"), + ]); + + let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None) + .await + .unwrap(); + assert_drain_requests( + pipeline, + &[("00", "80", "pkrange-left"), ("80", "FF", "pkrange-right")], + ); + } + #[tokio::test] async fn resume_restricts_fanout_to_target_range() { // Resuming a target-scoped query must also honour the target: the // `[80, FF)` partition lies outside `[00, 80)` and must not be queried - // even though the query plan spans `[, FF)`. + // even though the query plan spans `[, FF)` and that partition exists + // in the physical topology. An unclipped resume would emit a second, + // fresh-start leaf for `[80, FF)`. let plan = plan_with_ranges(vec![qr("", "FF")]); let op = query_operation_with_target("00", "80"); - let mut topology = - MockTopologyProvider::new(vec![Ok(vec![rr("00", "80", "pkrange-left")])]); + let mut topology = PhysicalTopologyProvider::new(vec![ + rr("00", "80", "pkrange-left"), + rr("80", "FF", "pkrange-right"), + ]); let resume = saved_drain(vec![("00", "80", saved_request(Some("server-token-xyz")))]); From ad9e832cfd1602d21889d7c4d0a2393ed6f77c1e Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:06:37 -0400 Subject: [PATCH 04/12] Mirror feed-range fan-out fix in driver CHANGELOG The fix lives in the driver crate (planner.rs) but was only recorded in the public azure_data_cosmos CHANGELOG. Add the matching Bugs Fixed entry to the driver's own release notes, consistent with #4655 which appears in both. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md b/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md index 7c342ffd199..68de8b9c02f 100644 --- a/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md +++ b/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md @@ -10,6 +10,7 @@ ### Bugs Fixed +- Fixed cross-partition query fan-out ignoring an operation's explicit feed-range target. When a query was scoped to an explicit EPK range (`CosmosOperation::query_items` with a `FeedRange` target, surfaced as `FeedScope::range`), the query planner built request leaves from the query plan's ranges — which are derived from the query text alone — without intersecting them against the requested target. Both fan-out paths (`plan_fresh` and the resume path) now clip each query-plan range to the operation's target, so only partitions overlapping the requested range are queried. Absent, full, and logical-partition/prefix targets are unaffected. ([#4705](https://github.com/Azure/azure-sdk-for-rust/pull/4705)) - Fixed `AZURE_COSMOS_PPCB_*` environment variables (including the `AZURE_COSMOS_PPCB_ENABLED` master switch and the `AZURE_COSMOS_PPCB_ENABLED_OVERRIDE` kill switch) being silently ignored when a caller built `DriverOptions` without calling `DriverOptionsBuilder::with_partition_failover_options`. The environment is read only by `PartitionFailoverOptionsBuilder::build`, but the omitted-options path used a bare `PartitionFailoverOptions::default()` (which hard-codes PPCB enabled and reads no environment), so PPCB stayed on even with `AZURE_COSMOS_PPCB_ENABLED=false`. `DriverOptionsBuilder::build` now resolves the partition-failover options from the environment when the caller does not supply them (falling back to defaults, fail-soft, if an environment value is out of bounds). An explicitly supplied `PartitionFailoverOptions` continues to take precedence. ([#4655](https://github.com/Azure/azure-sdk-for-rust/pull/4655)) ### Other Changes From 96390fde63daed0b16022ef48745e257e242e361 Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:09:19 -0400 Subject: [PATCH 05/12] Pin disjoint-target error behavior with a test The fresh fan-out path returns a hard error when a query's target shares no EPKs with any query-plan range (every range clips away, leaving zero request leaves). Add a test that pins this behavior and, via NoopTopologyProvider, asserts no out-of-scope partition is resolved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/driver/dataflow/planner.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs b/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs index 7445765539a..12cffefa7c3 100644 --- a/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs +++ b/sdk/cosmos/azure_data_cosmos_driver/src/driver/dataflow/planner.rs @@ -1308,6 +1308,27 @@ mod tests { ); } + #[tokio::test] + async fn rejects_target_disjoint_from_query_ranges() { + // A target that shares no EPKs with any query-plan range clips every + // range away, leaving zero request leaves. On the fresh path this is + // reported as the same hard error as an empty query plan — the clip + // does not silently swallow the query. `NoopTopologyProvider` asserts + // no partition is ever resolved (the clip skips before resolution). + let plan = plan_with_ranges(vec![qr("80", "FF")]); + let op = query_operation_with_target("00", "40"); + let mut topology = NoopTopologyProvider; + + let err = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None) + .await + .unwrap_err(); + let rendered = err.to_string(); + assert!( + rendered.ends_with("query plan produced no partition ranges to query"), + "unexpected: {rendered}" + ); + } + #[tokio::test] async fn propagates_topology_resolution_error() { let plan = plan_with_ranges(vec![qr("", "FF")]); From 19bb6d408c5a3fce43fe168e02311acc33660e34 Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:32:50 -0400 Subject: [PATCH 06/12] Add e2e regression test for feed-range-scoped queries Fabian asked for an emulator-level regression test alongside the unit tests. Add feed_range_scoped_query_honors_range: it provisions a multi-partition container, enumerates read_feed_ranges(), and queries each range in isolation (the reported customer scenario). It asserts the union across ranges equals the full set exactly once -- before the fan-out clip fix each range returned the whole container, so ids would appear once per range. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/emulator_tests/cosmos_query.rs | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs b/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs index ceda74a5bf5..c32ab4a4793 100644 --- a/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs +++ b/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs @@ -8,9 +8,9 @@ use std::error::Error; use azure_data_cosmos::feed::ContinuationToken; use azure_data_cosmos::{ - clients::DatabaseClient, + clients::{ContainerClient, DatabaseClient}, feed::FeedScope, - models::CosmosStatus, + models::{CosmosStatus, ThroughputProperties}, options::{MaxItemCountHint, QueryOptions}, Query, }; @@ -119,6 +119,22 @@ struct ItemProjection { merge_order: usize, } +/// Drains `select value c.id from c` for the given scope and returns the ids. +async fn collect_ids_for_scope( + container_client: &ContainerClient, + scope: FeedScope, +) -> Result, Box> { + let mut pages = container_client + .query_items::("select value c.id from c", scope, None) + .await? + .into_pages(); + let mut ids = Vec::new(); + while let Some(page) = pages.next().await { + ids.extend(page?.into_items()); + } + Ok(ids) +} + #[tokio::test] #[cfg_attr( not(any(test_category = "emulator", test_category = "emulator_vnext")), @@ -480,6 +496,73 @@ pub async fn cross_partition_query_pagination() -> Result<(), Box> { .await } +#[tokio::test] +#[cfg_attr( + not(any(test_category = "emulator", test_category = "emulator_vnext")), + ignore = "requires test_category 'emulator' or 'emulator_vnext'" +)] +#[cfg_attr( + test_category = "emulator_vnext", + ignore = "skipped on vnext emulator: behavioral divergence" +)] +pub async fn feed_range_scoped_query_honors_range() -> Result<(), Box> { + TestClient::run_with_unique_db( + async |_, db_client| { + // 10 logical partitions × 2 items. Provision 11000 RU/s so the + // service creates at least 2 physical partitions — a scoped query + // then has a neighbouring range it must NOT touch. + let items = test_data::generate_mock_items(10, 2); + let throughput = ThroughputProperties::manual(11000); + let container_client = + test_data::create_container_with_items(db_client, items.clone(), Some(throughput)) + .await?; + + let ranges = container_client.read_feed_ranges(None).await?; + assert!( + ranges.len() >= 2, + "expected at least 2 physical partitions with 11000 RU/s, got {}", + ranges.len() + ); + + // Baseline: the full container returns every seeded item. + let mut all_ids = + collect_ids_for_scope(&container_client, FeedScope::full_container()).await?; + all_ids.sort(); + assert_eq!( + all_ids.len(), + items.len(), + "full-container query should return every seeded item" + ); + + // Enumerate each feed range and query it in isolation — exactly the + // customer scenario (`read_feed_ranges()` then query each range). + // Before the fan-out clip fix, a `FeedScope::range` query was planned + // as a full cross-partition fan-out, so every range returned the + // ENTIRE container and the union below contained each id + // `ranges.len()` times. With the fix each range is clipped to its + // own slice, so every id appears exactly once across all ranges. + let mut union_ids = Vec::new(); + for range in &ranges { + let ids = collect_ids_for_scope(&container_client, FeedScope::range(range.clone())) + .await?; + union_ids.extend(ids); + } + union_ids.sort(); + + assert_eq!( + union_ids, all_ids, + "each feed range must return only its own items: the union across \ + ranges should equal the full set exactly once, with no over-scan \ + or duplication" + ); + + Ok(()) + }, + Some(TestOptions::for_emulator()), + ) + .await +} + #[tokio::test] #[cfg_attr( not(test_category = "emulator"), From 1d74c9d6758e943c9a7108e3737c836fb64619bb Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:49:20 -0400 Subject: [PATCH 07/12] Add feed-range test words to Cosmos cspell ignore list The Build Analyze pipeline flagged 'neighbouring' and 'unclipped' from comments added in the feed-range fan-out tests. Add them to the Cosmos-scoped sdk/cosmos/.cspell.json rather than the repo-wide config. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/cosmos/.cspell.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/cosmos/.cspell.json b/sdk/cosmos/.cspell.json index 49e3796ab21..7d6d9e4edfb 100644 --- a/sdk/cosmos/.cspell.json +++ b/sdk/cosmos/.cspell.json @@ -182,6 +182,7 @@ "mysproc", "myudf", "napi", + "neighbouring", "newtypes", "nocoll", "normalises", @@ -289,6 +290,7 @@ "uksouth", "ukwest", "unavail", + "unclipped", "uncontended", "undrained", "unfaulted", From b817f11b1de8b26077da112dff4b0e8a7b210de1 Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:20:18 -0400 Subject: [PATCH 08/12] Reframe #4705 changelog entries as regression tests The equivalent planner fix shipped independently with the Gateway 2.0 work (#4319), which landed on main first. Reframe both CHANGELOG bullets to credit #4319 for the code fix and describe #4705 as adding the regression tests (unit + emulator) for the reported feed-range scoping scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/cosmos/azure_data_cosmos/CHANGELOG.md | 2 +- sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos/CHANGELOG.md b/sdk/cosmos/azure_data_cosmos/CHANGELOG.md index 7e8d9d8688f..623c447ca15 100644 --- a/sdk/cosmos/azure_data_cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure_data_cosmos/CHANGELOG.md @@ -15,7 +15,7 @@ ### Bugs Fixed -- Fixed cross-partition queries scoped to an explicit feed range (`ContainerClient::query_items` with `FeedScope::range`) fanning out to partitions outside the requested range. The query planner now intersects the query plan's ranges with the requested feed-range target, so only the partitions overlapping the requested range are queried. This prevents returning documents outside the requested feed range and the extra scans / RU usage that occurred when enumerating `read_feed_ranges()` and querying each range. ([#4705](https://github.com/Azure/azure-sdk-for-rust/pull/4705)) +- Fixed cross-partition queries scoped to an explicit feed range (`ContainerClient::query_items` with `FeedScope::range`) fanning out to partitions outside the requested range. The query planner now intersects the query plan's ranges with the requested feed-range target, so only the partitions overlapping the requested range are queried. This prevents returning documents outside the requested feed range and the extra scans / RU usage that occurred when enumerating `read_feed_ranges()` and querying each range. The planner-side fix shipped with the Gateway 2.0 work ([#4319](https://github.com/Azure/azure-sdk-for-rust/pull/4319)); this change adds regression tests (planner unit tests and an emulator test) covering the reported scenario. ([#4705](https://github.com/Azure/azure-sdk-for-rust/pull/4705)) - Fixed the `AZURE_COSMOS_PPCB_*` environment variables (including `AZURE_COSMOS_PPCB_ENABLED` and the `AZURE_COSMOS_PPCB_ENABLED_OVERRIDE` kill switch) being ignored when a `CosmosClient` was built without calling `CosmosClientBuilder::with_partition_failover_options`. The per-partition circuit breaker (PPCB) stayed enabled even with `AZURE_COSMOS_PPCB_ENABLED=false`. The client's driver now resolves these options from the environment when they are not supplied explicitly. ([#4655](https://github.com/Azure/azure-sdk-for-rust/pull/4655)) ### Other Changes diff --git a/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md b/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md index 416a2040e6e..bbf83366acf 100644 --- a/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md +++ b/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md @@ -15,7 +15,7 @@ ### Bugs Fixed -- Fixed cross-partition query fan-out ignoring an operation's explicit feed-range target. When a query was scoped to an explicit EPK range (`CosmosOperation::query_items` with a `FeedRange` target, surfaced as `FeedScope::range`), the query planner built request leaves from the query plan's ranges — which are derived from the query text alone — without intersecting them against the requested target. Both fan-out paths (`plan_fresh` and the resume path) now clip each query-plan range to the operation's target, so only partitions overlapping the requested range are queried. Operations with no target (full-container scope) are unaffected. ([#4705](https://github.com/Azure/azure-sdk-for-rust/pull/4705)) +- Fixed cross-partition query fan-out ignoring an operation's explicit feed-range target. When a query was scoped to an explicit EPK range (`CosmosOperation::query_items` with a `FeedRange` target, surfaced as `FeedScope::range`), the query planner built request leaves from the query plan's ranges — derived from the query text alone — without intersecting them against the requested target. Both fan-out paths (`plan_fresh` and the resume path) now intersect each query-plan range with the operation's target, so only partitions overlapping the requested range are queried; operations with no target (full-container scope) are unaffected. This planner intersection shipped with the Gateway 2.0 work ([#4319](https://github.com/Azure/azure-sdk-for-rust/pull/4319)); this change adds regression tests covering the reported feed-range scenario. ([#4705](https://github.com/Azure/azure-sdk-for-rust/pull/4705)) - Fixed session-token parsing rejecting the `-1` region-progress sentinel that multi-region accounts emit for regions with no local progress. Region LSNs in the version vector are now parsed as signed values, with `-1` (and any negative value) modeled as "no progress" instead of failing the whole token as malformed. This applies to all Session-consistency traffic (not just distributed transactions) and prevents otherwise-valid multi-region session tokens from being discarded. ([#4702](https://github.com/Azure/azure-sdk-for-rust/pull/4702)) - Fixed `AZURE_COSMOS_PPCB_*` environment variables (including the `AZURE_COSMOS_PPCB_ENABLED` master switch and the `AZURE_COSMOS_PPCB_ENABLED_OVERRIDE` kill switch) being silently ignored when a caller built `DriverOptions` without calling `DriverOptionsBuilder::with_partition_failover_options`. The environment is read only by `PartitionFailoverOptionsBuilder::build`, but the omitted-options path used a bare `PartitionFailoverOptions::default()` (which hard-codes PPCB enabled and reads no environment), so PPCB stayed on even with `AZURE_COSMOS_PPCB_ENABLED=false`. `DriverOptionsBuilder::build` now resolves the partition-failover options from the environment when the caller does not supply them (falling back to defaults, fail-soft, if an environment value is out of bounds). An explicitly supplied `PartitionFailoverOptions` continues to take precedence. ([#4655](https://github.com/Azure/azure-sdk-for-rust/pull/4655)) From 5c9b57749db562eb1b41836a79bfab66f51109bb Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:54:44 -0400 Subject: [PATCH 09/12] Attribute feed-range bugfix changelog to #4319 This PR now only adds regression tests, so it needs no changelog entry of its own. Re-point the Bugs Fixed bullet in both crates to #4319 (Tomas' Gateway 2.0 PR), which delivered the planner fix but shipped without a changelog entry for this bug. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/cosmos/azure_data_cosmos/CHANGELOG.md | 2 +- sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos/CHANGELOG.md b/sdk/cosmos/azure_data_cosmos/CHANGELOG.md index 623c447ca15..4c8b12c1415 100644 --- a/sdk/cosmos/azure_data_cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure_data_cosmos/CHANGELOG.md @@ -15,7 +15,7 @@ ### Bugs Fixed -- Fixed cross-partition queries scoped to an explicit feed range (`ContainerClient::query_items` with `FeedScope::range`) fanning out to partitions outside the requested range. The query planner now intersects the query plan's ranges with the requested feed-range target, so only the partitions overlapping the requested range are queried. This prevents returning documents outside the requested feed range and the extra scans / RU usage that occurred when enumerating `read_feed_ranges()` and querying each range. The planner-side fix shipped with the Gateway 2.0 work ([#4319](https://github.com/Azure/azure-sdk-for-rust/pull/4319)); this change adds regression tests (planner unit tests and an emulator test) covering the reported scenario. ([#4705](https://github.com/Azure/azure-sdk-for-rust/pull/4705)) +- Fixed cross-partition queries scoped to an explicit feed range (`ContainerClient::query_items` with `FeedScope::range`) fanning out to partitions outside the requested range. The query planner now intersects the query plan's ranges with the requested feed-range target, so only the partitions overlapping the requested range are queried. This prevents returning documents outside the requested feed range and the extra scans / RU usage that occurred when enumerating `read_feed_ranges()` and querying each range. ([#4319](https://github.com/Azure/azure-sdk-for-rust/pull/4319)) - Fixed the `AZURE_COSMOS_PPCB_*` environment variables (including `AZURE_COSMOS_PPCB_ENABLED` and the `AZURE_COSMOS_PPCB_ENABLED_OVERRIDE` kill switch) being ignored when a `CosmosClient` was built without calling `CosmosClientBuilder::with_partition_failover_options`. The per-partition circuit breaker (PPCB) stayed enabled even with `AZURE_COSMOS_PPCB_ENABLED=false`. The client's driver now resolves these options from the environment when they are not supplied explicitly. ([#4655](https://github.com/Azure/azure-sdk-for-rust/pull/4655)) ### Other Changes diff --git a/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md b/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md index bbf83366acf..ed313f57ac9 100644 --- a/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md +++ b/sdk/cosmos/azure_data_cosmos_driver/CHANGELOG.md @@ -15,7 +15,7 @@ ### Bugs Fixed -- Fixed cross-partition query fan-out ignoring an operation's explicit feed-range target. When a query was scoped to an explicit EPK range (`CosmosOperation::query_items` with a `FeedRange` target, surfaced as `FeedScope::range`), the query planner built request leaves from the query plan's ranges — derived from the query text alone — without intersecting them against the requested target. Both fan-out paths (`plan_fresh` and the resume path) now intersect each query-plan range with the operation's target, so only partitions overlapping the requested range are queried; operations with no target (full-container scope) are unaffected. This planner intersection shipped with the Gateway 2.0 work ([#4319](https://github.com/Azure/azure-sdk-for-rust/pull/4319)); this change adds regression tests covering the reported feed-range scenario. ([#4705](https://github.com/Azure/azure-sdk-for-rust/pull/4705)) +- Fixed cross-partition query fan-out ignoring an operation's explicit feed-range target. When a query was scoped to an explicit EPK range (`CosmosOperation::query_items` with a `FeedRange` target, surfaced as `FeedScope::range`), the query planner built request leaves from the query plan's ranges — derived from the query text alone — without intersecting them against the requested target. Both fan-out paths (`plan_fresh` and the resume path) now intersect each query-plan range with the operation's target, so only partitions overlapping the requested range are queried; operations with no target (full-container scope) are unaffected. ([#4319](https://github.com/Azure/azure-sdk-for-rust/pull/4319)) - Fixed session-token parsing rejecting the `-1` region-progress sentinel that multi-region accounts emit for regions with no local progress. Region LSNs in the version vector are now parsed as signed values, with `-1` (and any negative value) modeled as "no progress" instead of failing the whole token as malformed. This applies to all Session-consistency traffic (not just distributed transactions) and prevents otherwise-valid multi-region session tokens from being discarded. ([#4702](https://github.com/Azure/azure-sdk-for-rust/pull/4702)) - Fixed `AZURE_COSMOS_PPCB_*` environment variables (including the `AZURE_COSMOS_PPCB_ENABLED` master switch and the `AZURE_COSMOS_PPCB_ENABLED_OVERRIDE` kill switch) being silently ignored when a caller built `DriverOptions` without calling `DriverOptionsBuilder::with_partition_failover_options`. The environment is read only by `PartitionFailoverOptionsBuilder::build`, but the omitted-options path used a bare `PartitionFailoverOptions::default()` (which hard-codes PPCB enabled and reads no environment), so PPCB stayed on even with `AZURE_COSMOS_PPCB_ENABLED=false`. `DriverOptionsBuilder::build` now resolves the partition-failover options from the environment when the caller does not supply them (falling back to defaults, fail-soft, if an environment value is out of bounds). An explicitly supplied `PartitionFailoverOptions` continues to take precedence. ([#4655](https://github.com/Azure/azure-sdk-for-rust/pull/4655)) From 0d29d2e1dd0a5fed803a5be0ced88fdf6b9444b8 Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:40:19 -0400 Subject: [PATCH 10/12] Assert per-range results in feed-range e2e test Address review feedback: capture each feed range's result set separately instead of only their union. A union-only check would pass even if one range returned every item and another returned nothing. The test now asserts each range is non-empty, the ranges are pairwise disjoint, and together they cover the full container exactly once. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/emulator_tests/cosmos_query.rs | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs b/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs index c32ab4a4793..982bef68f99 100644 --- a/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs +++ b/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs @@ -536,24 +536,49 @@ pub async fn feed_range_scoped_query_honors_range() -> Result<(), Box // Enumerate each feed range and query it in isolation — exactly the // customer scenario (`read_feed_ranges()` then query each range). - // Before the fan-out clip fix, a `FeedScope::range` query was planned - // as a full cross-partition fan-out, so every range returned the - // ENTIRE container and the union below contained each id - // `ranges.len()` times. With the fix each range is clipped to its - // own slice, so every id appears exactly once across all ranges. - let mut union_ids = Vec::new(); + // Capture each range's result set separately so we can assert on the + // per-range slices, not just their union: a union-only check would + // still pass if one range returned every item and another returned + // nothing, which is exactly the over-scan bug this test guards. + let mut per_range_ids: Vec> = Vec::new(); for range in &ranges { - let ids = collect_ids_for_scope(&container_client, FeedScope::range(range.clone())) - .await?; - union_ids.extend(ids); + let mut ids = + collect_ids_for_scope(&container_client, FeedScope::range(range.clone())) + .await?; + ids.sort(); + per_range_ids.push(ids); + } + + // Every range must return a non-empty slice. Each physical partition + // owns a share of the 10 logical partitions, so a correctly-scoped + // query can never come back empty; an empty range would mean the + // scope was dropped and the items were served by some other range. + for (i, ids) in per_range_ids.iter().enumerate() { + assert!( + !ids.is_empty(), + "feed range {i} returned no items; a scoped query must return \ + that range's own slice, not defer to another range" + ); } - union_ids.sort(); + // The ranges must be disjoint: before the fan-out clip fix every + // range fanned out across the whole container, so ids were + // duplicated across ranges. Flatten and check for duplicates. + let mut flattened: Vec = per_range_ids.iter().flatten().cloned().collect(); + flattened.sort(); + let mut deduped = flattened.clone(); + deduped.dedup(); + assert_eq!( + flattened, deduped, + "feed ranges must be disjoint: no id may be returned by more than \ + one range" + ); + + // And together the ranges must cover exactly the full container — + // no over-scan and nothing missing. assert_eq!( - union_ids, all_ids, - "each feed range must return only its own items: the union across \ - ranges should equal the full set exactly once, with no over-scan \ - or duplication" + flattened, all_ids, + "the union across ranges must equal the full container exactly once" ); Ok(()) From 140feda4231515e8294d3df02571c6e3f7eb0443 Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:51:33 -0400 Subject: [PATCH 11/12] Assert full per-range grouping in feed-range e2e test Replace the three separate property checks (non-empty, disjoint, spanning) with a single assert_eq! comparing the actual per-range query results to an independently computed expected grouping. The expected grouping maps each seeded item to its owning physical range via the SDK's partition-key -> EPK range math, so the assertion covers all three properties at once and stays correct regardless of how the emulator splits the container. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/emulator_tests/cosmos_query.rs | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs b/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs index 982bef68f99..ac1c19dbaae 100644 --- a/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs +++ b/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs @@ -9,7 +9,7 @@ use std::error::Error; use azure_data_cosmos::feed::ContinuationToken; use azure_data_cosmos::{ clients::{ContainerClient, DatabaseClient}, - feed::FeedScope, + feed::{FeedRange, FeedScope}, models::{CosmosStatus, ThroughputProperties}, options::{MaxItemCountHint, QueryOptions}, Query, @@ -536,10 +536,6 @@ pub async fn feed_range_scoped_query_honors_range() -> Result<(), Box // Enumerate each feed range and query it in isolation — exactly the // customer scenario (`read_feed_ranges()` then query each range). - // Capture each range's result set separately so we can assert on the - // per-range slices, not just their union: a union-only check would - // still pass if one range returned every item and another returned - // nothing, which is exactly the over-scan bug this test guards. let mut per_range_ids: Vec> = Vec::new(); for range in &ranges { let mut ids = @@ -549,36 +545,38 @@ pub async fn feed_range_scoped_query_honors_range() -> Result<(), Box per_range_ids.push(ids); } - // Every range must return a non-empty slice. Each physical partition - // owns a share of the 10 logical partitions, so a correctly-scoped - // query can never come back empty; an empty range would mean the - // scope was dropped and the items were served by some other range. - for (i, ids) in per_range_ids.iter().enumerate() { - assert!( - !ids.is_empty(), - "feed range {i} returned no items; a scoped query must return \ - that range's own slice, not defer to another range" - ); + // Independently compute the expected grouping: map each seeded item to + // the physical range that owns its partition key, using the SDK's + // partition-key → EPK-range math as an oracle (which does not depend on + // the query fan-out path under test). Asserting the whole grouping in a + // single comparison covers non-emptiness, disjointness, and full + // coverage at once, and stays correct regardless of how the emulator + // chose to split the container. Before the fan-out clip fix each range + // returned the ENTIRE container, so `per_range_ids` held every id in + // every range and would not match this partition. + let pk_definition = container_client + .read(None) + .await? + .into_model()? + .partition_key; + let mut expected_ids: Vec> = vec![Vec::new(); ranges.len()]; + for item in &items { + let logical = + FeedRange::for_partition(item.partition_key.clone().into(), &pk_definition); + let owner = ranges + .iter() + .position(|range| logical.is_subset_of(range)) + .expect("every partition key must fall within exactly one feed range"); + expected_ids[owner].push(item.id.clone()); + } + for ids in &mut expected_ids { + ids.sort(); } - // The ranges must be disjoint: before the fan-out clip fix every - // range fanned out across the whole container, so ids were - // duplicated across ranges. Flatten and check for duplicates. - let mut flattened: Vec = per_range_ids.iter().flatten().cloned().collect(); - flattened.sort(); - let mut deduped = flattened.clone(); - deduped.dedup(); - assert_eq!( - flattened, deduped, - "feed ranges must be disjoint: no id may be returned by more than \ - one range" - ); - - // And together the ranges must cover exactly the full container — - // no over-scan and nothing missing. assert_eq!( - flattened, all_ids, - "the union across ranges must equal the full container exactly once" + expected_ids, per_range_ids, + "each feed range must return exactly the items whose partition key \ + maps into it — no over-scan, no missing items, no empty ranges" ); Ok(()) From ab07e94f3578cd0f44df16b28f7fdc54e724e5f1 Mon Sep 17 00:00:00 2001 From: Simon Moreno <30335873+simorenoh@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:26:30 -0400 Subject: [PATCH 12/12] Hardcode per-range grouping in feed-range e2e test Replace the computed grouping oracle with a single assert_eq! against a hardcoded per-range id grouping, per review feedback. The values were captured from a real Cosmos DB account and verified identical on the emulator (both split at EPK 0x1FFF...FF), confirming the grouping is algorithm-driven and environment-independent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/emulator_tests/cosmos_query.rs | 88 ++++++++----------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs b/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs index ac1c19dbaae..04ce1db3024 100644 --- a/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs +++ b/sdk/cosmos/azure_data_cosmos/tests/emulator_tests/cosmos_query.rs @@ -9,7 +9,7 @@ use std::error::Error; use azure_data_cosmos::feed::ContinuationToken; use azure_data_cosmos::{ clients::{ContainerClient, DatabaseClient}, - feed::{FeedRange, FeedScope}, + feed::FeedScope, models::{CosmosStatus, ThroughputProperties}, options::{MaxItemCountHint, QueryOptions}, Query, @@ -509,8 +509,8 @@ pub async fn feed_range_scoped_query_honors_range() -> Result<(), Box TestClient::run_with_unique_db( async |_, db_client| { // 10 logical partitions × 2 items. Provision 11000 RU/s so the - // service creates at least 2 physical partitions — a scoped query - // then has a neighbouring range it must NOT touch. + // container splits into 2 physical partitions — a scoped query then + // has a neighbouring range it must NOT touch. let items = test_data::generate_mock_items(10, 2); let throughput = ThroughputProperties::manual(11000); let container_client = @@ -518,63 +518,51 @@ pub async fn feed_range_scoped_query_honors_range() -> Result<(), Box .await?; let ranges = container_client.read_feed_ranges(None).await?; - assert!( - ranges.len() >= 2, - "expected at least 2 physical partitions with 11000 RU/s, got {}", - ranges.len() - ); - - // Baseline: the full container returns every seeded item. - let mut all_ids = - collect_ids_for_scope(&container_client, FeedScope::full_container()).await?; - all_ids.sort(); assert_eq!( - all_ids.len(), - items.len(), - "full-container query should return every seeded item" + ranges.len(), + 2, + "expected exactly 2 physical partitions with 11000 RU/s, got {}", + ranges.len() ); - // Enumerate each feed range and query it in isolation — exactly the - // customer scenario (`read_feed_ranges()` then query each range). - let mut per_range_ids: Vec> = Vec::new(); + // Query each feed range in isolation — exactly the customer scenario + // (`read_feed_ranges()` then query each range) — and collect the ids + // each range returns. Pair each result set with its range so we can + // order the outer list by the range's lower bound, independent of the + // order `read_feed_ranges` happens to return. + let mut per_range: Vec<(String, Vec)> = Vec::new(); for range in &ranges { let mut ids = collect_ids_for_scope(&container_client, FeedScope::range(range.clone())) .await?; - ids.sort(); - per_range_ids.push(ids); - } - - // Independently compute the expected grouping: map each seeded item to - // the physical range that owns its partition key, using the SDK's - // partition-key → EPK-range math as an oracle (which does not depend on - // the query fan-out path under test). Asserting the whole grouping in a - // single comparison covers non-emptiness, disjointness, and full - // coverage at once, and stays correct regardless of how the emulator - // chose to split the container. Before the fan-out clip fix each range - // returned the ENTIRE container, so `per_range_ids` held every id in - // every range and would not match this partition. - let pk_definition = container_client - .read(None) - .await? - .into_model()? - .partition_key; - let mut expected_ids: Vec> = vec![Vec::new(); ranges.len()]; - for item in &items { - let logical = - FeedRange::for_partition(item.partition_key.clone().into(), &pk_definition); - let owner = ranges - .iter() - .position(|range| logical.is_subset_of(range)) - .expect("every partition key must fall within exactly one feed range"); - expected_ids[owner].push(item.id.clone()); - } - for ids in &mut expected_ids { - ids.sort(); + ids.sort_by_key(|id| id.parse::().expect("mock ids are numeric")); + per_range.push((range.min_inclusive().to_hex(), ids)); } + per_range.sort_by(|a, b| a.0.cmp(&b.0)); + let per_range_ids: Vec> = per_range + .iter() + .map(|(_, ids)| ids.iter().map(String::as_str).collect()) + .collect(); + + // The container deterministically splits into two physical ranges at + // EPK `0x1FFF…FF` (`[00, 1FFF…FF)` and `[1FFF…FF, FF)`), so each of the + // 10 partition keys maps to exactly one range and a correctly-scoped + // query returns only that range's ids. These groupings were captured + // from a real Cosmos DB account and are identical on the emulator — + // the effective-partition-key hash and the split boundary are + // algorithm-driven, not environment-specific. Before the fan-out clip + // fix each `FeedScope::range` query fanned out across the whole + // container, so every range returned all 20 ids instead of its slice. + let expected: Vec> = vec![ + vec![ + "0", "1", "20", "21", "30", "31", "40", "41", "50", "51", "70", "71", "90", + "91", + ], + vec!["10", "11", "60", "61", "80", "81"], + ]; assert_eq!( - expected_ids, per_range_ids, + expected, per_range_ids, "each feed range must return exactly the items whose partition key \ maps into it — no over-scan, no missing items, no empty ranges" );