Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public InnerEnumerator CloneWithMaxPageSize()

protected override async Task<TryCatch<OrderByQueryPage>> GetNextPageAsync(ITrace trace, CancellationToken cancellationToken)
{
FeedRangeInternal feedRange = HierarchicalPartitionUtils.LimitFeedRangeToSinglePartition(this.PartitionKey, this.FeedRangeState.FeedRange, this.containerQueryProperties);
FeedRangeInternal feedRange = QueryRangeUtils.LimitHpkFeedRangeToPartition(this.PartitionKey, this.FeedRangeState.FeedRange, this.containerQueryProperties);

TryCatch<QueryPage> monadicQueryPage = await this.queryDataSource
.MonadicQueryAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public static TryCatch<IQueryPipelineStage> MonadicCreate(

if (targetRanges.Count == 0)
{
throw new ArgumentException($"{nameof(targetRanges)} must have some elements");
return TryCatch<IQueryPipelineStage>.FromResult(new EmptyQueryPipelineStage());
}

TryCatch<CrossFeedRangeState<QueryState>> monadicExtractState = MonadicExtractState(continuationToken, targetRanges);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ protected override Task<TryCatch<QueryPage>> GetNextPageAsync(ITrace trace, Canc
throw new ArgumentNullException(nameof(trace));
}

FeedRangeInternal feedRange = HierarchicalPartitionUtils.LimitFeedRangeToSinglePartition(this.partitionKey, this.FeedRangeState.FeedRange, this.containerQueryProperties);
FeedRangeInternal feedRange = QueryRangeUtils.LimitHpkFeedRangeToPartition(this.partitionKey, this.FeedRangeState.FeedRange, this.containerQueryProperties);
return this.queryDataSource.MonadicQueryAsync(
sqlQuerySpec: this.sqlQuerySpec,
feedRangeState: new FeedRangeState<QueryState>(feedRange, this.FeedRangeState.State),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static TryCatch<IQueryPipelineStage> MonadicCreate(

if (targetRanges.Count == 0)
{
throw new ArgumentException($"{nameof(targetRanges)} must not be empty.");
return TryCatch<IQueryPipelineStage>.FromResult(new EmptyQueryPipelineStage());
}

if (queryInfo == null && hybridSearchQueryInfo == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ public abstract Task<ContainerQueryProperties> GetCachedContainerQueryProperties
PartitionKey? partitionKey,
ITrace trace,
CancellationToken cancellationToken);

// ISSUE-TODO-adityasa-2025/12/29 - Reduce Coupling: we should not use PartitionKeyRange as return type for this internal interface.
// PartitionKeyRange contains a lot more information (for e.g. RidPrefix, Throughput related information, LSN, parent range id etc),
// none of which is required by callers of these methods. The only information required is min & max values.
// Furthermore, the range is always min-inclusive and max-exclusive (since original PartitionKeyRange is such).
// Callers ultimately convert the returned PartitionKeyRange into a FeedRangeEpk.
// Applies to other methods below as well.

/// <summary>
/// Returns list of effective partition key ranges for a collection.
Expand Down Expand Up @@ -80,7 +87,7 @@ public abstract Task<PartitionedQueryExecutionInfo> ExecuteQueryPlanRequestAsync
public abstract void ClearSessionTokenCache(string collectionFullName);

public abstract Task<List<Documents.PartitionKeyRange>> GetTargetPartitionKeyRangeByFeedRangeAsync(
string resourceLink,
string resourceLink,
string collectionResourceId,
Documents.PartitionKeyDefinition partitionKeyDefinition,
FeedRangeInternal feedRangeInternal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------

namespace Microsoft.Azure.Cosmos.Query.Core.Pipeline.CrossPartition
namespace Microsoft.Azure.Cosmos.Query.Core
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Azure.Cosmos.Query.Core.QueryClient;
using Microsoft.Azure.Documents.Routing;

internal static class HierarchicalPartitionUtils
internal static class QueryRangeUtils
{
private static readonly bool IsLengthAwareComparisonEnabled = ConfigurationManager.IsLengthAwareRangeComparatorEnabled();

/// <summary>
/// Updates the FeedRange to limit the scope of incoming feedRange to logical partition within a single physical partition.
/// Generally speaking, a subpartitioned container can experience split partition at any level of hierarchical partition key.
Expand All @@ -20,7 +22,7 @@ internal static class HierarchicalPartitionUtils
/// Since such an epk range does not exist at the container level, Service generates a GoneException.
/// This method restrics the range of each enumerator by intersecting it with physical partition range.
/// </summary>
public static FeedRangeInternal LimitFeedRangeToSinglePartition(PartitionKey? partitionKey, FeedRangeInternal feedRange, ContainerQueryProperties containerQueryProperties)
public static FeedRangeInternal LimitHpkFeedRangeToPartition(PartitionKey? partitionKey, FeedRangeInternal feedRange, ContainerQueryProperties containerQueryProperties)
{
// We sadly need to check the partition key, since a user can set a partition key in the request options with a different continuation token.
// In the future the partition filtering and continuation information needs to be a tightly bounded contract (like cross feed range state).
Expand Down Expand Up @@ -108,5 +110,88 @@ public static FeedRangeInternal LimitFeedRangeToSinglePartition(PartitionKey? pa

return feedRange;
}

/// <summary>
/// Limits the partition key ranges to fit within the provided EPK ranges.
/// Computes the overall min and max from the provided ranges, then trims each partition key range to fit within those boundaries.
/// </summary>
/// <param name="partitionKeyRanges">The list of partition key ranges to trim</param>
/// <param name="providedRanges">The EPK ranges to use as boundaries</param>
/// <returns>A list of trimmed partition key ranges that fit within the provided ranges</returns>
public static List<Documents.PartitionKeyRange> LimitPartitionKeyRangesToProvidedRanges(
List<Documents.PartitionKeyRange> partitionKeyRanges,
IReadOnlyList<Documents.Routing.Range<string>> providedRanges)
{
IComparer<Range<string>> minComparer = IsLengthAwareComparisonEnabled
? Documents.Routing.Range<string>.LengthAwareMinComparer.Instance
: Documents.Routing.Range<string>.MinComparer.Instance;

IComparer<Range<string>> maxComparer = IsLengthAwareComparisonEnabled
? Documents.Routing.Range<string>.LengthAwareMaxComparer.Instance
: Documents.Routing.Range<string>.MaxComparer.Instance;

// Compute the overall min and max from providedRanges
string overallMin = providedRanges[0].Min;
string overallMax = providedRanges[0].Max;

foreach (Range<string> providedRange in providedRanges)
{
// ProvidedRanges are user input, which can be generally deserialized from a json representation of FeedRangeInternal.
// FeedRangeInternal allows min/max to be included or excluded.
// However PartitionKeyRange assumes min is inclusive and max is exclusive.
// This is also similar to backend behavior where EPK ranges are always min-inclusive and max-exclusive.
// Therefore, despite the possible customization at FeedRangeInternal level, we only support min-inclusive and max-exclusive ranges.
// Ideally this validation should be done at the public API. Since that is not present, we only assert below.
Debug.Assert(providedRange.IsMinInclusive, "QueryRangeUtils Assert!", "Only min-inclusive ranges are supported!");
Debug.Assert(!providedRange.IsMaxInclusive, "QueryRangeUtils Assert!", "Only max-exclusive ranges are supported!");

if (minComparer.Compare(providedRange, CreateSingleValueRange(overallMin)) < 0)
{
overallMin = providedRange.Min;
}

if (maxComparer.Compare(providedRange, CreateSingleValueRange(overallMax)) > 0)
{
overallMax = providedRange.Max;
}
}

// Trim each range to fit within the overall boundaries
List<Documents.PartitionKeyRange> trimmedRanges = new List<Documents.PartitionKeyRange>(partitionKeyRanges.Count);
foreach (Documents.PartitionKeyRange range in partitionKeyRanges)
{
string trimmedMin = range.MinInclusive;
string trimmedMax = range.MaxExclusive;

// Trim min: use the greater of range.Min and overallMin
if (minComparer.Compare(CreateSingleValueRange(range.MinInclusive), CreateSingleValueRange(overallMin)) < 0)
{
trimmedMin = overallMin;
}

// Trim max: use the lesser of range.Max and overallMax
if (maxComparer.Compare(CreateSingleValueRange(range.MaxExclusive), CreateSingleValueRange(overallMax)) > 0)
{
trimmedMax = overallMax;
}

trimmedRanges.Add(
new Documents.PartitionKeyRange
{
Id = range.Id,
MinInclusive = trimmedMin,
MaxExclusive = trimmedMax,
Parents = range.Parents
});
}

return trimmedRanges;
}

private static Range<string> CreateSingleValueRange(string singleValue) => new Range<string>(
singleValue,
singleValue,
isMinInclusive: true,
isMaxInclusive: true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,16 @@ public override async Task<List<PartitionKeyRange>> GetTargetPartitionKeyRangeBy
using (ITrace childTrace = trace.StartChild("Get Overlapping Feed Ranges", TraceComponent.Routing, Tracing.TraceLevel.Info))
{
IRoutingMapProvider routingMapProvider = await this.GetRoutingMapProviderAsync();
List<Range<string>> ranges = await feedRangeInternal.GetEffectiveRangesAsync(routingMapProvider, collectionResourceId, partitionKeyDefinition, trace);
List<Range<string>> providedRanges = await feedRangeInternal.GetEffectiveRangesAsync(routingMapProvider, collectionResourceId, partitionKeyDefinition, trace);

return await this.GetTargetPartitionKeyRangesAsync(
List<PartitionKeyRange> ranges = await this.GetTargetPartitionKeyRangesAsync(
resourceLink,
collectionResourceId,
ranges,
providedRanges,
forceRefresh,
childTrace);

return QueryRangeUtils.LimitPartitionKeyRangesToProvidedRanges(ranges, providedRanges);
}
}

Expand Down Expand Up @@ -280,8 +282,8 @@ public override async Task<List<PartitionKeyRange>> GetTargetPartitionKeyRangesA
if (ranges == null)
{
throw new NotFoundException($"{DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)}: GetTargetPartitionKeyRanges(collectionResourceId:{collectionResourceId}, providedRanges: {string.Join(",", providedRanges)} failed due to stale cache");
}

}
return ranges;
}
}
Expand Down
Loading
Loading