use std::sync::Arc;
use crate::{
driver::dataflow::query_plan::DistinctType,
models::{
effective_partition_key::{normalized_epk_len, EffectivePartitionKey},
CosmosOperation, FeedRange,
},
};
use super::{
intersect_feed_ranges,
query_plan::{QueryInfo, QueryPlan},
DrainedLeaf, PartitionRoutingRefresh, Pipeline, PipelineNode, PipelineNodeState, RangedToken,
Request, RequestTarget, ResolvedRange, SequentialDrain, TopologyProvider, UnorderedMerge,
};
pub(crate) fn build_trivial_pipeline(
operation: Arc<CosmosOperation>,
resume: Option<PipelineNodeState>,
) -> crate::error::Result<Pipeline> {
debug_assert!(
operation.is_trivial(),
"build_trivial_pipeline called with non-trivial operation: {:?} targeting {:?}",
operation.operation_type(),
operation.target(),
);
let target = operation.target();
let initial_continuation = match resume {
None => None,
Some(PipelineNodeState::Request {
server_continuation,
}) => server_continuation,
Some(PipelineNodeState::Drained) => {
return Ok(Pipeline::new(Box::new(DrainedLeaf)));
}
Some(other) => {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_SHAPE_MISMATCH)
.with_message(format!(
"continuation token shape {} does not match a trivial operation",
snapshot_kind(&other)
))
.build());
}
};
let request_target = match target {
None => RequestTarget::NonPartitioned,
Some(f) => {
if let Some(pk) = f.partition_key() {
RequestTarget::LogicalPartitionKey(pk.clone())
} else {
return Err(crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::CLIENT_FEED_RANGE_REQUIRES_FANOUT_PIPELINE,
)
.with_message(
"FeedRange targeting requires a fan-out pipeline; \
use plan_operation for cross-partition queries",
)
.build());
}
}
};
let root = Request::new(operation, request_target, initial_continuation);
Ok(Pipeline::new(Box::new(root)))
}
pub(crate) async fn build_sequential_drain(
query_plan: &QueryPlan,
topology_provider: &mut dyn TopologyProvider,
operation: &Arc<CosmosOperation>,
resume: Option<PipelineNodeState>,
) -> crate::error::Result<Pipeline> {
validate_query_plan(query_plan)?;
let saved_snapshot = match resume {
None => None,
Some(PipelineNodeState::Drained) => {
return Ok(Pipeline::new(Box::new(DrainedLeaf)));
}
Some(PipelineNodeState::SequentialDrain {
left_most_undrained_epk,
active_tokens,
}) => Some(validate_saved_snapshot(
left_most_undrained_epk,
active_tokens,
)?),
Some(other) => {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_SHAPE_MISMATCH)
.with_message(format!(
"continuation token shape {} does not match a cross-partition operation",
snapshot_kind(&other)
))
.build());
}
};
let request_nodes = if let Some(saved) = saved_snapshot.as_ref() {
plan_resume_from_saved_snapshot(query_plan, topology_provider, operation, saved).await?
} else {
plan_fresh(query_plan, topology_provider, operation).await?
};
if request_nodes.is_empty() {
if saved_snapshot.is_some() {
return Ok(Pipeline::new(Box::new(DrainedLeaf)));
}
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::CLIENT_QUERY_PLAN_PRODUCED_EMPTY_RANGES)
.with_message("query plan produced no partition ranges to query")
.build());
}
let root = Box::new(SequentialDrain::new(request_nodes));
Ok(Pipeline::new(root))
}
pub(crate) async fn build_unordered_merge(
feed_range: &FeedRange,
topology_provider: &mut dyn TopologyProvider,
operation: &Arc<CosmosOperation>,
resume: Option<PipelineNodeState>,
) -> crate::error::Result<Pipeline> {
let (saved_tokens, resume_start) = match resume {
None => (None, None),
Some(PipelineNodeState::Drained) => {
return Ok(Pipeline::new(Box::new(DrainedLeaf)));
}
Some(PipelineNodeState::UnorderedMerge {
active_tokens,
start_from,
}) => (
Some(validate_unordered_merge_tokens(active_tokens)?),
start_from,
),
Some(other) => {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_SHAPE_MISMATCH)
.with_message(format!(
"continuation token shape {} does not match a change feed operation",
snapshot_kind(&other)
))
.build());
}
};
let is_resume = saved_tokens.is_some();
let start_marker = if is_resume {
resume_start
} else {
operation.change_feed_start().cloned()
};
let operation: Arc<CosmosOperation> = match (is_resume, &start_marker) {
(true, Some(marker)) => {
Arc::new((**operation).clone().with_change_feed_start(marker.clone()))
}
_ => Arc::clone(operation),
};
let resolved = topology_provider
.resolve_ranges(feed_range, PartitionRoutingRefresh::UseCached)
.await?;
let mut request_nodes: Vec<Box<dyn PipelineNode>> = Vec::new();
for resolved_range in resolved {
let range = intersect_feed_ranges(&resolved_range.range, feed_range)
.expect("topology provider must return ranges that overlap the feed range");
let mut cursor = range.min_inclusive().clone();
let range_max = range.max_exclusive().clone();
if let Some(tokens) = saved_tokens.as_ref() {
for token in tokens {
let Some(slice) = intersect_feed_ranges(&token.range, &range) else {
continue;
};
if &cursor < slice.min_inclusive() {
let gap = FeedRange::new(cursor.clone(), slice.min_inclusive().clone())?;
push_change_feed_leaf(
&mut request_nodes,
&operation,
gap,
&resolved_range,
None,
);
}
cursor = slice.max_exclusive().clone();
push_change_feed_leaf(
&mut request_nodes,
&operation,
slice,
&resolved_range,
Some(token.server_continuation.clone()),
);
}
}
if cursor < range_max {
let tail = FeedRange::new(cursor, range_max)?;
push_change_feed_leaf(&mut request_nodes, &operation, tail, &resolved_range, None);
}
}
if request_nodes.is_empty() {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::CLIENT_QUERY_PLAN_PRODUCED_EMPTY_RANGES)
.with_message("change feed produced no partition ranges to query")
.build());
}
let root = Box::new(UnorderedMerge::new(request_nodes).with_start_marker(start_marker));
Ok(Pipeline::new(root))
}
fn push_change_feed_leaf(
request_nodes: &mut Vec<Box<dyn PipelineNode>>,
operation: &Arc<CosmosOperation>,
leaf_range: FeedRange,
resolved_range: &ResolvedRange,
continuation: Option<String>,
) {
let target = RequestTarget::effective_partition_key_range(
leaf_range,
resolved_range.partition_key_range_id.clone(),
resolved_range.range.clone(),
);
request_nodes.push(Box::new(Request::new(
Arc::clone(operation),
target,
continuation,
)));
}
async fn plan_fresh(
query_plan: &QueryPlan,
topology_provider: &mut dyn TopologyProvider,
operation: &Arc<CosmosOperation>,
) -> crate::error::Result<Vec<Box<dyn PipelineNode>>> {
let mut nodes: Vec<Box<dyn PipelineNode>> = Vec::new();
let scope_range = operation.target();
let normalized_len = operation
.container()
.and_then(|c| normalized_epk_len(c.partition_key_definition()));
for query_range in &query_plan.query_ranges {
let plan_range = query_range_to_feed_range(query_range, normalized_len)?;
let feed_range = match scope_range {
Some(scope) => match intersect_feed_ranges(scope, &plan_range) {
Some(r) => r,
None => continue,
},
None => plan_range,
};
let resolved = topology_provider
.resolve_ranges(&feed_range, PartitionRoutingRefresh::UseCached)
.await?;
for resolved_range in resolved {
let range =
intersect_feed_ranges(&resolved_range.range, &feed_range).ok_or_else(|| {
topology_range_not_overlapping_error(&resolved_range.range, &feed_range)
})?;
let target = RequestTarget::effective_partition_key_range(
range,
resolved_range.partition_key_range_id,
resolved_range.range,
);
nodes.push(Box::new(Request::new(Arc::clone(operation), target, None)));
}
}
Ok(nodes)
}
async fn plan_resume_from_saved_snapshot(
query_plan: &QueryPlan,
topology_provider: &mut dyn TopologyProvider,
operation: &Arc<CosmosOperation>,
saved: &SavedSnapshot,
) -> crate::error::Result<Vec<Box<dyn PipelineNode>>> {
let mut nodes: Vec<Box<dyn PipelineNode>> = Vec::new();
let mut coverage: Vec<Vec<FeedRange>> = vec![Vec::new(); saved.active_tokens.len()];
let scope_range = operation.target();
let normalized_len = operation
.container()
.and_then(|c| normalized_epk_len(c.partition_key_definition()));
for query_range in &query_plan.query_ranges {
let plan_range = query_range_to_feed_range(query_range, normalized_len)?;
let feed_range = match scope_range {
Some(scope) => match intersect_feed_ranges(scope, &plan_range) {
Some(r) => r,
None => continue,
},
None => plan_range,
};
let resolved = topology_provider
.resolve_ranges(&feed_range, PartitionRoutingRefresh::UseCached)
.await?;
for resolved_range in resolved {
let leaf_scope =
intersect_feed_ranges(&resolved_range.range, &feed_range).ok_or_else(|| {
topology_range_not_overlapping_error(&resolved_range.range, &feed_range)
})?;
if leaf_scope.max_exclusive() <= &saved.cursor {
continue;
}
let effective_min = if leaf_scope.min_inclusive() < &saved.cursor {
saved.cursor.clone()
} else {
leaf_scope.min_inclusive().clone()
};
let effective_leaf = FeedRange::new(effective_min, leaf_scope.max_exclusive().clone())?;
let mut cursor_within_leaf = effective_leaf.min_inclusive().clone();
for (idx, entry) in saved.active_tokens.iter().enumerate() {
if entry.range.max_exclusive() <= &cursor_within_leaf {
continue;
}
if entry.range.min_inclusive() >= effective_leaf.max_exclusive() {
break;
}
let overlap_min = if entry.range.min_inclusive() > &cursor_within_leaf {
entry.range.min_inclusive().clone()
} else {
cursor_within_leaf.clone()
};
let overlap_max = if entry.range.max_exclusive() < effective_leaf.max_exclusive() {
entry.range.max_exclusive().clone()
} else {
effective_leaf.max_exclusive().clone()
};
if overlap_min > cursor_within_leaf {
let gap = FeedRange::new(cursor_within_leaf.clone(), overlap_min.clone())?;
let target = RequestTarget::effective_partition_key_range(
gap,
resolved_range.partition_key_range_id.clone(),
resolved_range.range.clone(),
);
nodes.push(Box::new(Request::new(Arc::clone(operation), target, None)));
}
let intersection = FeedRange::new(overlap_min, overlap_max.clone())?;
coverage[idx].push(intersection.clone());
let target = RequestTarget::effective_partition_key_range(
intersection,
resolved_range.partition_key_range_id.clone(),
resolved_range.range.clone(),
);
nodes.push(Box::new(Request::new(
Arc::clone(operation),
target,
Some(entry.server_continuation.clone()),
)));
cursor_within_leaf = overlap_max;
}
if cursor_within_leaf < *effective_leaf.max_exclusive() {
let gap =
FeedRange::new(cursor_within_leaf, effective_leaf.max_exclusive().clone())?;
let target = RequestTarget::effective_partition_key_range(
gap,
resolved_range.partition_key_range_id.clone(),
resolved_range.range.clone(),
);
nodes.push(Box::new(Request::new(Arc::clone(operation), target, None)));
}
}
}
for (idx, entry) in saved.active_tokens.iter().enumerate() {
if !range_fully_covered(&entry.range, &coverage[idx]) {
const MAX_COVERAGE_PIECES_RENDERED: usize = 8;
let coverage_summary = if coverage[idx].is_empty() {
"(no overlapping topology ranges)".to_string()
} else {
let mut sorted = coverage[idx].clone();
sorted.sort_by(|a, b| a.min_inclusive().cmp(b.min_inclusive()));
let total = sorted.len();
let rendered: Vec<String> = sorted
.iter()
.take(MAX_COVERAGE_PIECES_RENDERED)
.map(|r| {
format!(
"[{}, {})",
r.min_inclusive().to_hex(),
r.max_exclusive().to_hex()
)
})
.collect();
if total > MAX_COVERAGE_PIECES_RENDERED {
format!("{} + ... ({} total ranges)", rendered.join(" + "), total)
} else {
rendered.join(" + ")
}
};
return Err(crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_SAVED_RANGE_UNHONORED,
)
.with_message(format!(
"continuation token active range [{}, {}) could not be fully covered \
by the current topology above the cursor (covered: {}); the query \
cannot be safely resumed",
entry.range.min_inclusive().to_hex(),
entry.range.max_exclusive().to_hex(),
coverage_summary,
))
.build());
}
}
Ok(nodes)
}
fn query_range_to_feed_range(
query_range: &super::query_plan::QueryRange,
normalized_len: Option<usize>,
) -> crate::error::Result<FeedRange> {
let min = EffectivePartitionKey::from(query_range.min.as_str());
let max = EffectivePartitionKey::from(query_range.max.as_str());
let max = if query_range.is_max_inclusive && min == max {
match normalized_len {
Some(len) => max.normalized_successor(len),
None => max.successor(),
}
} else {
max
};
FeedRange::new(min, max)
}
fn range_fully_covered(range: &FeedRange, pieces: &[FeedRange]) -> bool {
if pieces.is_empty() {
return false;
}
let mut sorted: Vec<&FeedRange> = pieces.iter().collect();
sorted.sort_by(|a, b| a.min_inclusive().cmp(b.min_inclusive()));
let mut cursor = range.min_inclusive().clone();
for piece in sorted {
debug_assert!(
piece.min_inclusive() >= range.min_inclusive()
&& piece.max_exclusive() <= range.max_exclusive(),
"range_fully_covered piece [{}, {}) is not a subset of range [{}, {})",
piece.min_inclusive().to_hex(),
piece.max_exclusive().to_hex(),
range.min_inclusive().to_hex(),
range.max_exclusive().to_hex(),
);
if piece.min_inclusive() > &cursor {
return false;
}
if piece.max_exclusive() > &cursor {
cursor = piece.max_exclusive().clone();
}
}
&cursor >= range.max_exclusive()
}
#[derive(Debug)]
struct SavedSnapshot {
cursor: EffectivePartitionKey,
active_tokens: Vec<SavedActiveToken>,
}
#[derive(Debug)]
struct SavedActiveToken {
range: FeedRange,
server_continuation: String,
}
fn validate_saved_snapshot(
left_most_undrained_epk: String,
active_tokens: Vec<RangedToken>,
) -> crate::error::Result<SavedSnapshot> {
let cursor = EffectivePartitionKey::from(left_most_undrained_epk);
let mut parsed: Vec<SavedActiveToken> = Vec::with_capacity(active_tokens.len());
for entry in active_tokens {
let min = EffectivePartitionKey::from(entry.min_epk);
let max = EffectivePartitionKey::from(entry.max_epk);
if min > max {
return Err(crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE,
)
.with_message(format!(
"continuation token has invalid active_tokens entry (min `{}` > max `{}`)",
min.to_hex(),
max.to_hex(),
))
.build());
}
if min == max {
return Err(crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE,
)
.with_message(format!(
"continuation token has zero-width active_tokens entry (min == max == `{}`); \
zero-width entries cannot carry remaining work",
min.to_hex(),
))
.build());
}
let range = FeedRange::new(min, max)?;
if let Some(prev) = parsed.last() {
if range.min_inclusive() < prev.range.max_exclusive() {
return Err(crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE,
)
.with_message(format!(
"continuation token active_tokens must be sorted and non-overlapping; \
entry [{}, {}) is out of order or overlaps the previous entry [{}, {})",
range.min_inclusive().to_hex(),
range.max_exclusive().to_hex(),
prev.range.min_inclusive().to_hex(),
prev.range.max_exclusive().to_hex(),
))
.build());
}
}
parsed.push(SavedActiveToken {
range,
server_continuation: entry.server_continuation,
});
}
if let Some(first) = parsed.first() {
if &cursor > first.range.min_inclusive() {
return Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE)
.with_message(format!(
"continuation token cursor `{}` is past the first active_tokens entry [{}, {}); \
cursor must be at or before every active range",
cursor.to_hex(),
first.range.min_inclusive().to_hex(),
first.range.max_exclusive().to_hex(),
))
.build());
}
}
Ok(SavedSnapshot {
cursor,
active_tokens: parsed,
})
}
fn snapshot_kind(state: &PipelineNodeState) -> &'static str {
match state {
PipelineNodeState::Drained => "Drained",
PipelineNodeState::Request { .. } => "Request",
PipelineNodeState::SequentialDrain { .. } => "SequentialDrain",
PipelineNodeState::UnorderedMerge { .. } => "UnorderedMerge",
}
}
fn validate_unordered_merge_tokens(
active_tokens: Vec<RangedToken>,
) -> crate::error::Result<Vec<SavedActiveToken>> {
let mut parsed: Vec<SavedActiveToken> = Vec::with_capacity(active_tokens.len());
for entry in active_tokens {
let min = EffectivePartitionKey::from(entry.min_epk);
let max = EffectivePartitionKey::from(entry.max_epk);
if min >= max {
return Err(crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE,
)
.with_message(format!(
"continuation token has invalid active_tokens entry \
(min `{}` >= max `{}`)",
min.to_hex(),
max.to_hex(),
))
.build());
}
let range = FeedRange::new(min, max)?;
if let Some(prev) = parsed.last() {
if range.min_inclusive() < prev.range.max_exclusive() {
return Err(crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE,
)
.with_message(format!(
"continuation token active_tokens must be sorted and non-overlapping; \
entry [{}, {}) overlaps [{}, {})",
range.min_inclusive().to_hex(),
range.max_exclusive().to_hex(),
prev.range.min_inclusive().to_hex(),
prev.range.max_exclusive().to_hex(),
))
.build());
}
}
parsed.push(SavedActiveToken {
range,
server_continuation: entry.server_continuation,
});
}
Ok(parsed)
}
fn validate_query_plan(plan: &QueryPlan) -> crate::error::Result<()> {
if plan.hybrid_search_query_info.is_some() {
return Err(unsupported_feature("hybrid search queries"));
}
if let Some(info) = &plan.query_info {
validate_query_info(info)?;
}
Ok(())
}
fn validate_query_info(info: &QueryInfo) -> crate::error::Result<()> {
if info.top.is_some() {
return Err(unsupported_feature("TOP clause in cross-partition queries"));
}
if info.limit.is_some() {
return Err(unsupported_feature(
"LIMIT clause in cross-partition queries",
));
}
if !info.order_by.is_empty() {
return Err(unsupported_feature("ORDER BY in cross-partition queries"));
}
if !info.aggregates.is_empty() {
return Err(unsupported_feature("aggregates in cross-partition queries"));
}
if !info.group_by_expressions.is_empty() {
return Err(unsupported_feature("GROUP BY in cross-partition queries"));
}
if info.distinct_type != DistinctType::None {
return Err(unsupported_feature("DISTINCT in cross-partition queries"));
}
Ok(())
}
fn unsupported_feature(feature: &str) -> crate::error::CosmosError {
crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::CLIENT_UNSUPPORTED_QUERY_FEATURE)
.with_message(format!("unsupported query feature: {feature}"))
.build()
}
fn topology_range_not_overlapping_error(
resolved: &FeedRange,
query: &FeedRange,
) -> crate::error::CosmosError {
crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::CLIENT_QUERY_PLAN_RANGE_NOT_COVERED_BY_TOPOLOGY)
.with_message(format!(
"resolved topology range {} does not overlap query plan EPK {}",
render_feed_range_for_error(resolved),
render_feed_range_for_error(query),
))
.build()
}
fn render_feed_range_for_error(range: &FeedRange) -> String {
format!(
"range [{}, {})",
range.min_inclusive().to_hex(),
range.max_exclusive().to_hex(),
)
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
use crate::{
driver::dataflow::{mocks::*, query_plan::QueryRange, RangedToken, ResolvedRange},
models::{
effective_partition_key::EffectivePartitionKey, AccountReference, ContainerProperties,
ContainerReference, DatabaseReference, ItemReference, OperationType, PartitionKey,
PartitionKeyDefinition, ResourceType, SystemProperties,
},
};
fn test_account() -> AccountReference {
AccountReference::with_master_key(
url::Url::parse("https://test.documents.azure.com:443/").unwrap(),
"dGVzdA==",
)
}
fn test_database() -> DatabaseReference {
DatabaseReference::from_name(test_account(), "db".to_owned())
}
fn test_partition_key_definition() -> PartitionKeyDefinition {
serde_json::from_str(r#"{"paths":["/pk"],"version":2}"#).unwrap()
}
fn test_container_props() -> ContainerProperties {
ContainerProperties {
id: Cow::Owned("coll".into()),
partition_key: test_partition_key_definition(),
system_properties: SystemProperties::default(),
}
}
fn test_container() -> ContainerReference {
ContainerReference::new(
test_account(),
"db",
"db_rid",
"coll",
"coll_rid",
&test_container_props(),
)
}
fn cross_partition_query_operation() -> CosmosOperation {
CosmosOperation::query_items(test_container(), Some(FeedRange::full()))
.with_body(br#"{"query":"SELECT * FROM c"}"#.to_vec())
}
#[test]
fn plans_non_partitioned_pipeline_for_database_read() {
let op = CosmosOperation::read_database(test_database());
let pipeline = build_trivial_pipeline(Arc::new(op), None).unwrap();
let request = pipeline.root().downcast_ref::<Request>().unwrap();
assert_eq!(*request.target(), RequestTarget::NonPartitioned);
assert_eq!(request.operation().operation_type(), OperationType::Read);
assert_eq!(request.operation().resource_type(), ResourceType::Database);
}
#[test]
fn plans_logical_partition_pipeline_for_item_read() {
let pk = PartitionKey::from("pk-value");
let item = ItemReference::from_name(&test_container(), pk.clone(), "doc1");
let op = CosmosOperation::read_item(item);
let pipeline = build_trivial_pipeline(Arc::new(op), None).unwrap();
let request = pipeline.root().downcast_ref::<Request>().unwrap();
assert_eq!(
*request.target(),
RequestTarget::LogicalPartitionKey(pk.clone())
);
assert_eq!(request.operation().operation_type(), OperationType::Read);
assert_eq!(request.operation().resource_type(), ResourceType::Document);
}
#[test]
fn plans_logical_partition_pipeline_for_partition_scoped_query() {
let pk = PartitionKey::from("pk-value");
let feed_range = FeedRange::for_partition(pk.clone(), &test_partition_key_definition());
let op = CosmosOperation::query_items(test_container(), Some(feed_range))
.with_body(br#"{"query":"SELECT * FROM c"}"#.to_vec());
assert!(op.is_trivial());
let pipeline = build_trivial_pipeline(Arc::new(op), None).unwrap();
let request = pipeline.root().downcast_ref::<Request>().unwrap();
assert_eq!(*request.target(), RequestTarget::LogicalPartitionKey(pk));
}
#[test]
fn rejects_feed_range_target() {
let op = CosmosOperation::read_all_items_cross_partition(test_container());
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
build_trivial_pipeline(Arc::new(op), None)
}));
match result {
Err(_) if cfg!(debug_assertions) => {}
Err(_) => panic!("did not expect panic for FeedRange target"),
Ok(Err(err)) => {
let rendered = err.to_string();
assert!(
rendered.ends_with(
"FeedRange targeting requires a fan-out pipeline; \
use plan_operation for cross-partition queries"
),
"unexpected: {rendered}"
);
}
_ => panic!("expected error or panic for FeedRange target"),
}
}
fn qr(min: &str, max: &str) -> QueryRange {
QueryRange {
min: min.to_string(),
max: max.to_string(),
is_min_inclusive: true,
is_max_inclusive: false,
}
}
fn rr(min: &str, max: &str, pk_range_id: &str) -> ResolvedRange {
ResolvedRange {
partition_key_range_id: pk_range_id.to_string(),
range: FeedRange::new(
EffectivePartitionKey::from(min),
EffectivePartitionKey::from(max),
)
.unwrap(),
}
}
fn plan_with_ranges(ranges: Vec<QueryRange>) -> QueryPlan {
QueryPlan {
partitioned_query_execution_info_version: 1,
query_info: None,
query_ranges: ranges,
hybrid_search_query_info: None,
}
}
type ExpectedDrainRequestWithPartition<'a> = (&'a str, &'a str, &'a str, &'a str, &'a str);
type ExpectedDrainRequestWithContinuation<'a> =
(&'a str, &'a str, &'a str, &'a str, &'a str, Option<&'a str>);
fn assert_drain_requests(pipeline: Pipeline, expected: &[(&str, &str, &str)]) {
let expected = expected
.iter()
.map(|&(min, max, pk_range_id)| (min, max, pk_range_id, min, max))
.collect::<Vec<_>>();
assert_drain_requests_with_partitions(pipeline, &expected);
}
fn assert_drain_requests_with_partitions(
pipeline: Pipeline,
expected: &[ExpectedDrainRequestWithPartition<'_>],
) {
let drain = pipeline
.into_root()
.downcast::<SequentialDrain>()
.expect("expected SequentialDrain root");
let children = drain.into_children();
assert_eq!(
children.len(),
expected.len(),
"expected {} request nodes, got {}",
expected.len(),
children.len(),
);
for (child, &(min, max, pk_range_id, partition_min, partition_max)) in
children.into_iter().zip(expected)
{
let request = child
.downcast::<Request>()
.expect("expected Request child node");
assert_eq!(
*request.target(),
RequestTarget::effective_partition_key_range(
FeedRange::new(
EffectivePartitionKey::from(min),
EffectivePartitionKey::from(max),
)
.unwrap(),
pk_range_id.to_string(),
FeedRange::new(
EffectivePartitionKey::from(partition_min),
EffectivePartitionKey::from(partition_max),
)
.unwrap(),
),
"mismatch for pk range {pk_range_id}"
);
}
}
fn assert_drain_requests_with_partitions_and_continuation(
pipeline: Pipeline,
expected: &[ExpectedDrainRequestWithContinuation<'_>],
) {
let drain = pipeline
.into_root()
.downcast::<SequentialDrain>()
.expect("expected SequentialDrain root");
let children = drain.into_children();
assert_eq!(
children.len(),
expected.len(),
"expected {} request nodes, got {}",
expected.len(),
children.len(),
);
for (child, &(min, max, pk_range_id, partition_min, partition_max, continuation)) in
children.into_iter().zip(expected)
{
let request = child
.downcast::<Request>()
.expect("expected Request child node");
assert_eq!(
*request.target(),
RequestTarget::effective_partition_key_range(
FeedRange::new(
EffectivePartitionKey::from(min),
EffectivePartitionKey::from(max),
)
.unwrap(),
pk_range_id.to_string(),
FeedRange::new(
EffectivePartitionKey::from(partition_min),
EffectivePartitionKey::from(partition_max),
)
.unwrap(),
),
"mismatch for pk range {pk_range_id}"
);
let expected_state = PipelineNodeState::Request {
server_continuation: continuation.map(ToOwned::to_owned),
};
assert_eq!(request.snapshot_state().unwrap(), expected_state);
}
}
#[tokio::test]
async fn builds_single_node_pipeline_for_one_partition() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pkrange-0")])]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None)
.await
.unwrap();
assert_drain_requests(pipeline, &[("", "FF", "pkrange-0")]);
}
#[tokio::test]
async fn builds_sequential_drain_for_multiple_partitions() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![
rr("", "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,
&[("", "80", "pkrange-left"), ("80", "FF", "pkrange-right")],
);
}
#[tokio::test]
async fn builds_pipeline_for_multiple_query_ranges() {
let plan = plan_with_ranges(vec![qr("", "40"), qr("80", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![
Ok(vec![rr("", "40", "pkrange-A")]),
Ok(vec![rr("80", "FF", "pkrange-C")]),
]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None)
.await
.unwrap();
assert_drain_requests(
pipeline,
&[("", "40", "pkrange-A"), ("80", "FF", "pkrange-C")],
);
}
#[tokio::test]
async fn query_range_spans_multiple_topology_partitions() {
let plan = plan_with_ranges(vec![qr("00", "C0")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![
rr("00", "40", "pkrange-1"),
rr("40", "80", "pkrange-2"),
rr("80", "C0", "pkrange-3"),
])]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None)
.await
.unwrap();
assert_drain_requests(
pipeline,
&[
("00", "40", "pkrange-1"),
("40", "80", "pkrange-2"),
("80", "C0", "pkrange-3"),
],
);
}
#[tokio::test]
async fn multiple_query_ranges_each_spanning_multiple_partitions() {
let plan = plan_with_ranges(vec![qr("", "60"), qr("A0", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![
Ok(vec![
rr("", "30", "pkrange-alpha"),
rr("30", "60", "pkrange-beta"),
]),
Ok(vec![
rr("A0", "D0", "pkrange-gamma"),
rr("D0", "FF", "pkrange-delta"),
]),
]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None)
.await
.unwrap();
assert_drain_requests(
pipeline,
&[
("", "30", "pkrange-alpha"),
("30", "60", "pkrange-beta"),
("A0", "D0", "pkrange-gamma"),
("D0", "FF", "pkrange-delta"),
],
);
}
#[tokio::test]
async fn topology_partition_wider_than_query_range() {
let plan = plan_with_ranges(vec![qr("20", "80")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pkrange-wide")])]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None)
.await
.unwrap();
assert_drain_requests_with_partitions(pipeline, &[("20", "80", "pkrange-wide", "", "FF")]);
}
#[tokio::test]
async fn closed_point_query_range_emits_epk_window() {
let point = QueryRange {
min: "30".to_string(),
max: "30".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
};
let plan = plan_with_ranges(vec![point]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pkrange-0")])]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None)
.await
.unwrap();
let s30 = EffectivePartitionKey::from("30")
.normalized_successor(16)
.to_hex();
assert_drain_requests_with_partitions(
pipeline,
&[("30", s30.as_str(), "pkrange-0", "", "FF")],
);
}
#[tokio::test]
async fn in_predicate_colocated_points_emit_one_window_each() {
let plan = plan_with_ranges(vec![
QueryRange {
min: "30".to_string(),
max: "30".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
},
QueryRange {
min: "50".to_string(),
max: "50".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
},
]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![
Ok(vec![rr("", "FF", "pkrange-0")]),
Ok(vec![rr("", "FF", "pkrange-0")]),
]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None)
.await
.unwrap();
let s30 = EffectivePartitionKey::from("30")
.normalized_successor(16)
.to_hex();
let s50 = EffectivePartitionKey::from("50")
.normalized_successor(16)
.to_hex();
assert_drain_requests_with_partitions(
pipeline,
&[
("30", s30.as_str(), "pkrange-0", "", "FF"),
("50", s50.as_str(), "pkrange-0", "", "FF"),
],
);
}
#[tokio::test]
async fn in_predicate_points_across_partitions_emit_one_window_each() {
let plan = plan_with_ranges(vec![
QueryRange {
min: "20".to_string(),
max: "20".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
},
QueryRange {
min: "C0".to_string(),
max: "C0".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
},
]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![
Ok(vec![rr("", "80", "pkrange-left")]),
Ok(vec![rr("80", "FF", "pkrange-right")]),
]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None)
.await
.unwrap();
let s20 = EffectivePartitionKey::from("20")
.normalized_successor(16)
.to_hex();
let sc0 = EffectivePartitionKey::from("C0")
.normalized_successor(16)
.to_hex();
assert_drain_requests_with_partitions(
pipeline,
&[
("20", s20.as_str(), "pkrange-left", "", "80"),
("C0", sc0.as_str(), "pkrange-right", "80", "FF"),
],
);
}
#[test]
fn query_range_to_feed_range_normalizes_closed_point_to_window() {
let point = QueryRange {
min: "30".to_string(),
max: "30".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
};
let fr = query_range_to_feed_range(&point, None).unwrap();
assert_eq!(fr.min_inclusive().to_hex(), "30");
assert_eq!(fr.max_exclusive().to_hex(), "31");
}
#[test]
fn query_range_to_feed_range_preserves_half_open() {
let fr = query_range_to_feed_range(&qr("20", "80"), None).unwrap();
assert_eq!(fr.min_inclusive().to_hex(), "20");
assert_eq!(fr.max_exclusive().to_hex(), "80");
}
#[test]
fn query_range_to_feed_range_closed_non_point_range_passes_through_when_len_unknown() {
let closed = QueryRange {
min: "20".to_string(),
max: "3AFF".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
};
let fr = query_range_to_feed_range(&closed, None).unwrap();
assert_eq!(fr.min_inclusive().to_hex(), "20");
assert_eq!(fr.max_exclusive().to_hex(), "3AFF");
}
#[test]
fn query_range_to_feed_range_closed_non_point_range_passes_through_even_with_known_len() {
let closed = QueryRange {
min: "20".to_string(),
max: "3A".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
};
let fr = query_range_to_feed_range(&closed, Some(16)).unwrap();
assert_eq!(fr.min_inclusive().to_hex(), "20");
assert_eq!(fr.max_exclusive().to_hex(), "3A");
}
#[test]
fn query_range_to_feed_range_normalizes_point_to_full_width() {
let point = QueryRange {
min: "3A".to_string(),
max: "3A".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
};
let fr = query_range_to_feed_range(&point, Some(16)).unwrap();
assert_eq!(fr.min_inclusive().to_hex(), "3A");
let mut expected = vec![0x3Au8];
expected.resize(16, 0x00);
expected[15] = 0x01;
let expected_hex: String = expected.iter().map(|b| format!("{:02X}", b)).collect();
assert_eq!(fr.max_exclusive().to_hex(), expected_hex);
}
#[tokio::test]
async fn rejects_query_plan_with_top() {
let plan = QueryPlan {
query_info: Some(QueryInfo {
top: Some(10),
..Default::default()
}),
..plan_with_ranges(vec![qr("", "FF")])
};
let op = cross_partition_query_operation();
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("unsupported query feature: TOP clause in cross-partition queries"),
"unexpected: {rendered}"
);
}
#[tokio::test]
async fn rejects_query_plan_with_limit() {
let plan = QueryPlan {
query_info: Some(QueryInfo {
limit: Some(20),
..Default::default()
}),
..plan_with_ranges(vec![qr("", "FF")])
};
let op = cross_partition_query_operation();
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("unsupported query feature: LIMIT clause in cross-partition queries"),
"unexpected: {rendered}"
);
}
#[tokio::test]
async fn rejects_query_plan_with_order_by() {
use super::super::query_plan::SortOrder;
let plan = QueryPlan {
query_info: Some(QueryInfo {
order_by: vec![SortOrder::Ascending],
..Default::default()
}),
..plan_with_ranges(vec![qr("", "FF")])
};
let op = cross_partition_query_operation();
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("unsupported query feature: ORDER BY in cross-partition queries"),
"unexpected: {rendered}"
);
}
#[tokio::test]
async fn rejects_query_plan_with_aggregates() {
let plan = QueryPlan {
query_info: Some(QueryInfo {
aggregates: vec!["Count".to_string()],
..Default::default()
}),
..plan_with_ranges(vec![qr("", "FF")])
};
let op = cross_partition_query_operation();
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("unsupported query feature: aggregates in cross-partition queries"),
"unexpected: {rendered}"
);
}
#[tokio::test]
async fn rejects_query_plan_with_group_by() {
let plan = QueryPlan {
query_info: Some(QueryInfo {
group_by_expressions: vec!["c.category".to_string()],
..Default::default()
}),
..plan_with_ranges(vec![qr("", "FF")])
};
let op = cross_partition_query_operation();
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("unsupported query feature: GROUP BY in cross-partition queries"),
"unexpected: {rendered}"
);
}
#[tokio::test]
async fn rejects_query_plan_with_hybrid_search() {
let plan = QueryPlan {
hybrid_search_query_info: Some(super::super::query_plan::HybridSearchQueryInfo {
global_statistics_query: "SELECT COUNT(1) FROM c".to_string(),
component_query_infos: vec![],
component_weights: vec![],
skip: None,
take: Some(10),
requires_global_statistics: true,
}),
..plan_with_ranges(vec![qr("", "FF")])
};
let op = cross_partition_query_operation();
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("unsupported query feature: hybrid search queries"),
"unexpected: {rendered}"
);
}
#[tokio::test]
async fn accepts_query_plan_with_no_query_info() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pkrange-0")])]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None)
.await
.unwrap();
assert_drain_requests(pipeline, &[("", "FF", "pkrange-0")]);
}
#[tokio::test]
async fn rejects_empty_query_ranges() {
let plan = plan_with_ranges(vec![]);
let op = cross_partition_query_operation();
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")]);
let op = cross_partition_query_operation();
let mut topology =
MockTopologyProvider::new(vec![Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message("topology resolution failed")
.build())]);
let err = build_sequential_drain(&plan, &mut topology, &Arc::new(op), None)
.await
.unwrap_err();
let rendered = err.to_string();
assert!(
rendered.ends_with("topology resolution failed"),
"unexpected: {rendered}"
);
}
fn saved_drain(children: Vec<(&str, &str, PipelineNodeState)>) -> PipelineNodeState {
let mut cursor: Option<String> = None;
let mut active_tokens: Vec<RangedToken> = Vec::new();
for (min, max, state) in children {
match state {
PipelineNodeState::Drained => {
debug_assert!(
cursor.is_none(),
"saved_drain helper does not support drained children after the cursor; \
construct the sparse shape directly for that case",
);
cursor = Some(max.to_owned());
}
PipelineNodeState::Request {
server_continuation,
} => {
if cursor.is_none() {
cursor = Some(min.to_owned());
}
if let Some(token) = server_continuation {
active_tokens.push(RangedToken {
min_epk: min.to_owned(),
max_epk: max.to_owned(),
server_continuation: token,
});
}
}
other => panic!(
"saved_drain helper does not accept nested SequentialDrain states; \
construct the sparse shape directly. Got: {other:?}"
),
}
}
PipelineNodeState::SequentialDrain {
left_most_undrained_epk: cursor.unwrap_or_default(),
active_tokens,
}
}
fn saved_request(server_continuation: Option<&str>) -> PipelineNodeState {
PipelineNodeState::Request {
server_continuation: server_continuation.map(str::to_owned),
}
}
#[tokio::test]
async fn resume_drained_state_yields_drained_pipeline() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pkrange-0")])]);
let pipeline = build_sequential_drain(
&plan,
&mut topology,
&Arc::new(op),
Some(PipelineNodeState::Drained),
)
.await
.unwrap();
assert!(matches!(
pipeline.snapshot_state().unwrap(),
PipelineNodeState::Drained
));
}
#[tokio::test]
async fn resume_skips_topology_below_first_saved_child() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![
rr("", "55", "pk-a"),
rr("55", "AA", "pk-b"),
rr("AA", "FF", "pk-c"),
])]);
let resume = saved_drain(vec![
("55", "AA", saved_request(None)),
("AA", "FF", saved_request(None)),
]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
assert_drain_requests(pipeline, &[("55", "AA", "pk-b"), ("AA", "FF", "pk-c")]);
}
#[tokio::test]
async fn resume_propagates_server_continuation_to_every_surviving_leaf_after_split() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![
rr("", "55", "pk-a"),
rr("55", "70", "pk-b1"),
rr("70", "AA", "pk-b2"),
rr("AA", "FF", "pk-c"),
])]);
let resume = saved_drain(vec![
("55", "AA", saved_request(Some("server-token-xyz"))),
("AA", "FF", saved_request(None)),
]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
assert_drain_requests_with_partitions_and_continuation(
pipeline,
&[
("55", "70", "pk-b1", "55", "70", Some("server-token-xyz")),
("70", "AA", "pk-b2", "70", "AA", Some("server-token-xyz")),
("AA", "FF", "pk-c", "AA", "FF", None),
],
);
}
#[tokio::test]
async fn resume_does_not_leak_continuation_into_siblings_past_saved_scope() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![
rr("", "55", "pk-a"),
rr("55", "AA", "pk-b"),
rr("AA", "FF", "pk-c"),
])]);
let resume = saved_drain(vec![
("55", "AA", saved_request(Some("server-token-xyz"))),
("AA", "FF", saved_request(None)),
]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
assert_drain_requests_with_partitions_and_continuation(
pipeline,
&[
("55", "AA", "pk-b", "55", "AA", Some("server-token-xyz")),
("AA", "FF", "pk-c", "AA", "FF", None),
],
);
}
#[tokio::test]
async fn resume_does_not_leak_continuation_across_query_ranges() {
let plan = plan_with_ranges(vec![qr("", "55"), qr("80", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![
Ok(vec![rr("", "30", "pk-a"), rr("30", "55", "pk-b")]),
Ok(vec![rr("80", "C0", "pk-c"), rr("C0", "FF", "pk-d")]),
]);
let resume = saved_drain(vec![
("30", "55", saved_request(Some("server-token-xyz"))),
("80", "C0", saved_request(None)),
("C0", "FF", saved_request(None)),
]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
assert_drain_requests_with_partitions_and_continuation(
pipeline,
&[
("30", "55", "pk-b", "30", "55", Some("server-token-xyz")),
("80", "C0", "pk-c", "80", "C0", None),
("C0", "FF", "pk-d", "C0", "FF", None),
],
);
}
#[tokio::test]
async fn resume_with_cursor_past_topology_yields_drained_pipeline() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pk-a")])]);
let resume = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: "FF".to_owned(),
active_tokens: vec![],
};
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
assert!(matches!(
pipeline.snapshot_state().unwrap(),
PipelineNodeState::Drained
));
}
#[tokio::test]
async fn resume_on_merged_range_splits_resumed_slice_and_tail() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pk-merged")])]);
let resume = saved_drain(vec![
("55", "AA", saved_request(Some("server-token-xyz"))),
("AA", "FF", saved_request(None)),
]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
assert_drain_requests_with_partitions_and_continuation(
pipeline,
&[
("55", "AA", "pk-merged", "", "FF", Some("server-token-xyz")),
("AA", "FF", "pk-merged", "", "FF", None),
],
);
}
#[tokio::test]
async fn resume_validates_saved_children_sorted_non_overlapping() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pk-a")])]);
let resume = saved_drain(vec![
("55", "AA", saved_request(Some("tok-a"))),
("00", "55", saved_request(Some("tok-b"))),
]);
let err = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap_err();
assert_eq!(
err.status().sub_status(),
Some(crate::error::SubStatusCode::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE),
"expected invalid-children sub-status, got: {err}",
);
}
#[tokio::test]
async fn resume_validates_saved_children_no_overlap() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pk-a")])]);
let resume = saved_drain(vec![
("00", "80", saved_request(Some("tok-a"))),
("55", "FF", saved_request(Some("tok-b"))),
]);
let err = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap_err();
assert_eq!(
err.status().sub_status(),
Some(crate::error::SubStatusCode::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE),
"expected invalid-children sub-status, got: {err}",
);
}
#[tokio::test]
async fn resume_errors_when_non_drained_saved_range_unhonored() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "40", "pk-a")])]);
let resume = saved_drain(vec![("55", "AA", saved_request(Some("server-token-xyz")))]);
let err = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap_err();
assert_eq!(
err.status().sub_status(),
Some(crate::error::SubStatusCode::CLIENT_CONTINUATION_TOKEN_SAVED_RANGE_UNHONORED),
"expected saved-range-unhonored sub-status, got: {err}",
);
}
#[tokio::test]
async fn resume_with_cursor_skips_drained_prefix_and_fresh_starts_uncovered_tail() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![
rr("", "55", "pk-a"),
rr("55", "AA", "pk-b"),
rr("AA", "FF", "pk-c"),
])]);
let resume = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: "55".to_owned(),
active_tokens: vec![RangedToken {
min_epk: "55".to_owned(),
max_epk: "AA".to_owned(),
server_continuation: "server-token-xyz".to_owned(),
}],
};
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
assert_drain_requests_with_partitions_and_continuation(
pipeline,
&[
("55", "AA", "pk-b", "55", "AA", Some("server-token-xyz")),
("AA", "FF", "pk-c", "AA", "FF", None),
],
);
}
#[tokio::test]
async fn resume_in_predicate_drops_point_partition_below_cursor() {
let plan = plan_with_ranges(vec![
QueryRange {
min: "20".to_string(),
max: "20".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
},
QueryRange {
min: "C0".to_string(),
max: "C0".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
},
]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![
Ok(vec![rr("", "80", "pk-left")]),
Ok(vec![rr("80", "FF", "pk-right")]),
]);
let resume = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: "80".to_owned(),
active_tokens: vec![],
};
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
let sc0 = EffectivePartitionKey::from("C0")
.normalized_successor(16)
.to_hex();
assert_drain_requests_with_partitions_and_continuation(
pipeline,
&[("C0", sc0.as_str(), "pk-right", "80", "FF", None)],
);
}
#[tokio::test]
async fn resume_in_predicate_colocated_windows_carry_their_continuations() {
let plan = plan_with_ranges(vec![
QueryRange {
min: "20".to_string(),
max: "20".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
},
QueryRange {
min: "50".to_string(),
max: "50".to_string(),
is_min_inclusive: true,
is_max_inclusive: true,
},
]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![
Ok(vec![rr("", "FF", "pk-0")]),
Ok(vec![rr("", "FF", "pk-0")]),
]);
let s20 = EffectivePartitionKey::from("20")
.normalized_successor(16)
.to_hex();
let s50 = EffectivePartitionKey::from("50")
.normalized_successor(16)
.to_hex();
let resume = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: "".to_owned(),
active_tokens: vec![
RangedToken {
min_epk: "20".to_owned(),
max_epk: s20.clone(),
server_continuation: "tok-a".to_owned(),
},
RangedToken {
min_epk: "50".to_owned(),
max_epk: s50.clone(),
server_continuation: "tok-b".to_owned(),
},
],
};
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
assert_drain_requests_with_partitions_and_continuation(
pipeline,
&[
("20", s20.as_str(), "pk-0", "", "FF", Some("tok-a")),
("50", s50.as_str(), "pk-0", "", "FF", Some("tok-b")),
],
);
}
#[tokio::test]
async fn resume_multiple_saved_children_in_one_resolved_range_no_duplicate_leaves() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pk-merged")])]);
let resume = saved_drain(vec![
("10", "30", saved_request(Some("tok-a"))),
("30", "60", saved_request(Some("tok-b"))),
]);
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
assert_drain_requests_with_partitions_and_continuation(
pipeline,
&[
("10", "30", "pk-merged", "", "FF", Some("tok-a")),
("30", "60", "pk-merged", "", "FF", Some("tok-b")),
("60", "FF", "pk-merged", "", "FF", None),
],
);
}
#[tokio::test]
async fn resume_emits_fresh_leaves_for_topology_gaps_above_cursor() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![
rr("", "20", "pk-a"),
rr("20", "40", "pk-b"),
rr("40", "60", "pk-c"),
rr("60", "80", "pk-d"),
rr("80", "FF", "pk-e"),
])]);
let resume = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: "40".to_owned(),
active_tokens: vec![RangedToken {
min_epk: "40".to_owned(),
max_epk: "60".to_owned(),
server_continuation: "tok".to_owned(),
}],
};
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.unwrap();
assert_drain_requests_with_partitions_and_continuation(
pipeline,
&[
("40", "60", "pk-c", "40", "60", Some("tok")),
("60", "80", "pk-d", "60", "80", None),
("80", "FF", "pk-e", "80", "FF", None),
],
);
}
#[tokio::test]
async fn legacy_top_level_bare_request_shape_fails_to_resume() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pkrange-0")])]);
let legacy = PipelineNodeState::Request {
server_continuation: Some("OLD".to_owned()),
};
let result =
build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(legacy)).await;
let err = result.expect_err("bare top-level Request shape must be rejected on resume");
assert_eq!(
err.status(),
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_SHAPE_MISMATCH,
"expected SHAPE_MISMATCH for top-level bare Request shape; got {err:?}",
);
}
#[tokio::test]
async fn rejects_zero_width_saved_child_entry_with_clear_message() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pkrange-0")])]);
let resume = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: String::new(),
active_tokens: vec![RangedToken {
min_epk: "40".to_owned(),
max_epk: "40".to_owned(),
server_continuation: "tok".to_owned(),
}],
};
let err = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.expect_err("zero-width active_tokens entry must be rejected");
assert_eq!(
err.status(),
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE,
);
let rendered = err.to_string();
assert!(
rendered.contains("zero-width"),
"error message should describe the zero-width entry; got: {rendered}"
);
}
#[tokio::test]
async fn malformed_min_greater_than_max_child_is_rejected_by_validator() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pkrange-0")])]);
let resume = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: String::new(),
active_tokens: vec![RangedToken {
min_epk: "FF".to_owned(),
max_epk: "00".to_owned(),
server_continuation: "tok".to_owned(),
}],
};
let err = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.expect_err("malformed min>max entry must be rejected by the validator");
assert_eq!(
err.status(),
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE,
"malformed min>max entry must trip the EPK-range validator path",
);
}
#[tokio::test]
async fn malformed_min_greater_than_max_appended_to_valid_children_still_rejects() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![rr("", "FF", "pkrange-0")])]);
let resume = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: String::new(),
active_tokens: vec![
RangedToken {
min_epk: String::new(),
max_epk: "80".to_owned(),
server_continuation: "real-token".to_owned(),
},
RangedToken {
min_epk: "FF".to_owned(),
max_epk: "00".to_owned(),
server_continuation: "tok".to_owned(),
},
],
};
let err = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.expect_err("appended malformed min>max entry must still be rejected");
assert_eq!(
err.status(),
crate::error::CosmosStatus::CLIENT_CONTINUATION_TOKEN_INVALID_EPK_RANGE,
);
}
#[tokio::test]
async fn cascading_split_of_front_sibling_propagates_token_to_grand_children() {
let plan = plan_with_ranges(vec![qr("", "FF")]);
let op = cross_partition_query_operation();
let mut topology = MockTopologyProvider::new(vec![Ok(vec![
rr("", "40", "pkrange-front-left"),
rr("40", "80", "pkrange-front-right"),
rr("80", "FF", "pkrange-back"),
])]);
let resume = PipelineNodeState::SequentialDrain {
left_most_undrained_epk: String::new(),
active_tokens: vec![RangedToken {
min_epk: String::new(),
max_epk: "80".to_owned(),
server_continuation: "T1".to_owned(),
}],
};
let pipeline = build_sequential_drain(&plan, &mut topology, &Arc::new(op), Some(resume))
.await
.expect("front-sibling cascading split must plan cleanly");
let snap = pipeline.snapshot_state().unwrap();
let (cursor, active_tokens) = match snap {
PipelineNodeState::SequentialDrain {
left_most_undrained_epk,
active_tokens,
} => (left_most_undrained_epk, active_tokens),
other => panic!("expected SequentialDrain, got {other:?}"),
};
assert_eq!(cursor, "", "cursor must remain at start");
assert_eq!(
active_tokens.len(),
2,
"expected 2 active tokens for the front grand-children, got {active_tokens:?}",
);
for (idx, expected_min, expected_max) in [(0, "", "40"), (1, "40", "80")] {
assert_eq!(
active_tokens[idx].min_epk, expected_min,
"active_tokens[{idx}] min_epk mismatch",
);
assert_eq!(
active_tokens[idx].max_epk, expected_max,
"active_tokens[{idx}] max_epk mismatch",
);
assert_eq!(
active_tokens[idx].server_continuation, "T1",
"front grand-child {idx} must carry T1",
);
}
}
}