diff --git a/Microsoft.Azure.Cosmos.Encryption.Custom/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption.Custom/src/EncryptionContainer.cs index b898a77179..470c86b387 100644 --- a/Microsoft.Azure.Cosmos.Encryption.Custom/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption.Custom/src/EncryptionContainer.cs @@ -1028,6 +1028,19 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithAllV onChangesDelegate); } #endif + +#if SDKPROJECTREF + public override Task IsFeedRangePartOfAsync( + Cosmos.FeedRange x, + Cosmos.FeedRange y, + CancellationToken cancellationToken = default) + { + return this.container.IsFeedRangePartOfAsync( + x, + y, + cancellationToken); + } +#endif private async Task ReadManyItemsHelperAsync( IReadOnlyList<(string id, PartitionKey partitionKey)> items, ReadManyRequestOptions readManyRequestOptions = null, diff --git a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs index 64c7dc36a1..ab83f9e04d 100644 --- a/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs +++ b/Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs @@ -762,6 +762,14 @@ public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithAllV { throw new NotImplementedException(); } + + public override Task IsFeedRangePartOfAsync( + Cosmos.FeedRange x, + Cosmos.FeedRange y, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } #endif /// /// This function handles the scenario where a container is deleted(say from different Client) and recreated with same Id but with different client encryption policy. diff --git a/Microsoft.Azure.Cosmos/src/FeedRange/FeedRanges/FeedRangeEpk.cs b/Microsoft.Azure.Cosmos/src/FeedRange/FeedRanges/FeedRangeEpk.cs index 199864b69f..9319dbe830 100644 --- a/Microsoft.Azure.Cosmos/src/FeedRange/FeedRanges/FeedRangeEpk.cs +++ b/Microsoft.Azure.Cosmos/src/FeedRange/FeedRanges/FeedRangeEpk.cs @@ -25,6 +25,11 @@ internal sealed class FeedRangeEpk : FeedRangeInternal public FeedRangeEpk(Documents.Routing.Range range) { + if (!range.IsMinInclusive) + { + throw new ArgumentOutOfRangeException(paramName: nameof(range), message: $"{nameof(range.IsMinInclusive)} must be true."); + } + this.Range = range ?? throw new ArgumentNullException(nameof(range)); } diff --git a/Microsoft.Azure.Cosmos/src/Resource/Container/Container.cs b/Microsoft.Azure.Cosmos/src/Resource/Container/Container.cs index 19c00f5a52..6b3f78f91a 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Container/Container.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Container/Container.cs @@ -1677,9 +1677,9 @@ public abstract ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder( /// An instance of public abstract ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManualCheckpoint( string processorName, - ChangeFeedStreamHandlerWithManualCheckpoint onChangesDelegate); - -#if PREVIEW + ChangeFeedStreamHandlerWithManualCheckpoint onChangesDelegate); + +#if PREVIEW /// /// Deletes all items in the Container with the specified value. /// Starts an asynchronous Cosmos DB background operation which deletes all items in the Container with the specified value. @@ -1781,7 +1781,40 @@ public abstract Task> GetPartitionKeyRangesAsync( /// An instance of public abstract ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes( string processorName, - ChangeFeedHandler> onChangesDelegate); -#endif + ChangeFeedHandler> onChangesDelegate); + + /// + /// Determines whether the given y feed range is a part of the specified x feed range. + /// + /// The feed range representing the x range. + /// The feed range representing the y range. + /// A token to cancel the operation if needed. + /// + /// + /// + /// + /// + /// Returns a boolean indicating whether the y feed range is fully contained within the x feed range. + public virtual Task IsFeedRangePartOfAsync( + Cosmos.FeedRange x, + Cosmos.FeedRange y, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +#endif } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs index 1c1300bdc2..34a4ee0b3a 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.Items.cs @@ -25,7 +25,8 @@ namespace Microsoft.Azure.Cosmos using Microsoft.Azure.Cosmos.Query.Core.Monads; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; using Microsoft.Azure.Cosmos.ReadFeed; - using Microsoft.Azure.Cosmos.ReadFeed.Pagination; + using Microsoft.Azure.Cosmos.ReadFeed.Pagination; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Cosmos.Serializer; using Microsoft.Azure.Cosmos.Tracing; using Microsoft.Azure.Documents; @@ -1252,6 +1253,322 @@ private ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderPrivate( container: this, changeFeedProcessor: changeFeedProcessor, applyBuilderConfiguration: changeFeedProcessor.ApplyBuildConfiguration).WithChangeFeedMode(mode); + } + + /// + /// This method is useful for determining if a smaller, more granular feed range (y) is fully contained within a broader feed range (x), which is a common operation in distributed systems to manage partitioned data. + /// + /// - **x and y Feed Ranges**: Both `x` and `y` are representations of logical partitions or ranges within the Cosmos DB container. + /// - These ranges are typically used for operations such as querying or reading data within a specified range of partition key values. + /// + /// - **Validation and Parsing**: + /// - The method begins by validating that neither `x` nor `y` is null. If either is null, an `ArgumentNullException` is thrown. + /// - It then checks whether each feed range is of type `FeedRangeInternal`. If not, it attempts to parse the JSON representation of the feed range into the internal format (`FeedRangeInternal`). + /// - If the parsing fails, an `ArgumentException` is thrown, indicating that the feed range is of an unknown or unsupported format. + /// + /// - **Partition Key and Routing Map Setup**: + /// - The partition key definition for the container is retrieved asynchronously using `GetPartitionKeyDefinitionAsync`, as it is required to identify the partition structure. + /// - The method also retrieves the container's resource ID (`containerRId`) and the partition key range routing map from the `IRoutingMapProvider`. These are essential for determining the actual partition key ranges that correspond to the feed ranges. + /// + /// - **Effective Ranges**: + /// - The method uses `GetEffectiveRangesAsync` to retrieve the actual ranges of partition keys that each feed range represents. + /// - These effective ranges are returned as lists of `Range`, which represent the partition key boundaries. + /// + /// - **Inclusivity Consistency**: + /// - Before performing the subset comparison, the method checks that the inclusivity of the boundary conditions (`IsMinInclusive` and `IsMaxInclusive`) is consistent across all ranges in both the x and y feed ranges. + /// - This ensures that the comparison between ranges is logically correct and avoids potential mismatches due to differing boundary conditions. + /// + /// - **Subset Check**: + /// - Finally, the method calls `ContainerCore.IsSubset`, which checks if the merged effective range of the y feed range is fully contained within the merged effective range of the x feed range. + /// - Merging the ranges ensures that the comparison accounts for multiple ranges and considers the full span of each feed range. + /// + /// - **Exception Handling**: + /// - Any exceptions related to document client errors are caught, and a `CosmosException` is thrown, wrapping the original `DocumentClientException`. + /// + /// The broader feed range representing the larger, encompassing logical partition. + /// The smaller, more granular feed range that needs to be checked for containment within the broader feed range. + /// An optional cancellation token to cancel the operation before completion. + /// Returns a boolean indicating whether the y feed range is fully contained within the x feed range. + public override async Task IsFeedRangePartOfAsync( + FeedRange x, + FeedRange y, + CancellationToken cancellationToken = default) + { + using (ITrace trace = Tracing.Trace.GetRootTrace("ContainerCore FeedRange IsFeedRangePartOfAsync Async", TraceComponent.Unknown, Tracing.TraceLevel.Info)) + { + if (x == null || y == null) + { + throw new ArgumentNullException(x == null + ? nameof(x) + : nameof(y), $"Argument cannot be null."); + } + + try + { + FeedRangeInternal xFeedRangeInternal = ContainerCore.ConvertToFeedRangeInternal(x, nameof(x)); + FeedRangeInternal yFeedRangeInternal = ContainerCore.ConvertToFeedRangeInternal(y, nameof(y)); + + PartitionKeyDefinition partitionKeyDefinition = await this.GetPartitionKeyDefinitionAsync(cancellationToken); + + string containerRId = await this.GetCachedRIDAsync( + forceRefresh: false, + trace: trace, + cancellationToken: cancellationToken); + + Routing.IRoutingMapProvider routingMapProvider = await this.ClientContext.DocumentClient.GetPartitionKeyRangeCacheAsync(trace); + List> xEffectiveRanges = await xFeedRangeInternal.GetEffectiveRangesAsync( + routingMapProvider: routingMapProvider, + containerRid: containerRId, + partitionKeyDefinition: partitionKeyDefinition, + trace: trace); + List> yEffectiveRanges = await yFeedRangeInternal.GetEffectiveRangesAsync( + routingMapProvider: routingMapProvider, + containerRid: containerRId, + partitionKeyDefinition: partitionKeyDefinition, + trace: trace); + + ContainerCore.EnsureConsistentInclusivity(xEffectiveRanges); + ContainerCore.EnsureConsistentInclusivity(yEffectiveRanges); + + return ContainerCore.IsSubset( + ContainerCore.MergeRanges(xEffectiveRanges), + ContainerCore.MergeRanges(yEffectiveRanges)); + } + catch (DocumentClientException dce) + { + throw CosmosExceptionFactory.Create(dce, trace); + } + } + } + + /// + /// Converts a given feed range to its internal representation (FeedRangeInternal). + /// If the provided feed range is already of type FeedRangeInternal, it returns it directly. + /// Otherwise, it attempts to parse the feed range into a FeedRangeInternal. + /// If parsing fails, an is thrown. + /// + /// The feed range to be converted into an internal representation. + /// The name of the parameter being converted, used for exception messages. + /// The converted FeedRangeInternal object. + /// Thrown when the provided feed range cannot be parsed into a known format. + private static FeedRangeInternal ConvertToFeedRangeInternal(FeedRange feedRange, string paramName) + { + if (feedRange is not FeedRangeInternal feedRangeInternal) + { + if (!FeedRangeInternal.TryParse(feedRange.ToJsonString(), out feedRangeInternal)) + { + throw new ArgumentException( + string.Format("The provided string, '{0}', for '{1}', does not represent any known format.", feedRange.ToJsonString(), paramName)); + } + } + + return feedRangeInternal; + } + + /// + /// Merges a list of feed ranges into a single range by taking the minimum value of the first range and the maximum value of the last range. + /// This function ensures that the resulting range covers the entire span of the input ranges. + /// + /// - The method begins by checking if the list contains only one range: + /// - If there is only one range, it simply returns that range without performing any additional logic. + /// + /// - If the list contains multiple ranges: + /// - It first sorts the ranges based on the minimum value of each range using a custom comparator (`MinComparer`). + /// - It selects the first range (after sorting) to extract the minimum value, ensuring the merged range starts with the lowest value across all ranges. + /// - It selects the last range (after sorting) to extract the maximum value, ensuring the merged range ends with the highest value across all ranges. + /// + /// - The inclusivity of the boundaries (`IsMinInclusive` and `IsMaxInclusive`) is inherited from the first range in the list: + /// - `IsMinInclusive` from the first range determines whether the merged range includes its minimum value. + /// - `IsMaxInclusive` from the last range would generally be expected to influence whether the merged range includes its maximum value, but this method uses `IsMaxInclusive` from the first range for both boundaries. + /// - **Note**: This could result in unexpected behavior if inclusivity should differ for the merged max value. + /// + /// - The merged range spans the minimum value of the first range and the maximum value of the last range, effectively combining multiple ranges into a single, continuous range. + /// + /// The list of feed ranges to merge. Each range contains a minimum and maximum value along with boundary inclusivity flags (`IsMinInclusive`, `IsMaxInclusive`). + /// + /// A new merged range with the minimum value from the first range and the maximum value from the last range. + /// If the list contains a single range, it returns that range directly without modification. + /// + /// + /// Thrown when the list of ranges is empty. + /// + private static Documents.Routing.Range MergeRanges( + List> ranges) + { + if (ranges.Count == 1) + { + return ranges.First(); + } + + ranges.Sort(Documents.Routing.Range.MinComparer.Instance); + + Documents.Routing.Range firstRange = ranges.First(); + Documents.Routing.Range lastRange = ranges.Last(); + + return new Documents.Routing.Range( + min: firstRange.Min, + max: lastRange.Max, + isMinInclusive: firstRange.IsMinInclusive, + isMaxInclusive: firstRange.IsMaxInclusive); + } + + /// + /// Validates whether all ranges in the list have consistent inclusivity for both `IsMinInclusive` and `IsMaxInclusive` boundaries. + /// This ensures that all ranges either have the same inclusivity or exclusivity for their minimum and maximum boundaries. + /// If there are any inconsistencies in the inclusivity/exclusivity of the ranges, it throws an `InvalidOperationException`. + /// + /// The logic works as follows: + /// - The method assumes that the `ranges` list is never null. + /// - It starts by checking the first range in the list to establish a baseline for comparison. + /// - It then iterates over the remaining ranges, comparing their `IsMinInclusive` and `IsMaxInclusive` values with those of the first range. + /// - If any range differs from the first in terms of inclusivity or exclusivity (either for the min or max boundary), the method sets a flag (`areAnyDifferent`) and exits the loop early. + /// - If any differences are found, the method gathers the distinct `IsMinInclusive` and `IsMaxInclusive` values found across all ranges. + /// - It then throws an `InvalidOperationException`, including the distinct values in the exception message to indicate the specific inconsistencies. + /// + /// This method is useful in scenarios where the ranges need to have uniform inclusivity for boundary conditions. + /// + /// The list of ranges to validate. Each range has `IsMinInclusive` and `IsMaxInclusive` values that represent the inclusivity of its boundaries. + /// + /// Thrown when `IsMinInclusive` or `IsMaxInclusive` values are inconsistent across ranges. The exception message includes details of the inconsistencies. + /// + /// + /// > ranges = new List> + /// { + /// new Documents.Routing.Range { IsMinInclusive = true, IsMaxInclusive = false }, + /// new Documents.Routing.Range { IsMinInclusive = true, IsMaxInclusive = true }, + /// new Documents.Routing.Range { IsMinInclusive = true, IsMaxInclusive = false }, + /// new Documents.Routing.Range { IsMinInclusive = false, IsMaxInclusive = false } + /// }; + /// + /// EnsureConsistentInclusivity(ranges); + /// + /// // This will throw an InvalidOperationException because there are different inclusivity values for IsMinInclusive and IsMaxInclusive. + /// ]]> + /// + internal static void EnsureConsistentInclusivity(List> ranges) + { + bool areAnyDifferent = false; + Documents.Routing.Range firstRange = ranges[0]; + + foreach (Documents.Routing.Range range in ranges.Skip(1)) + { + if (range.IsMinInclusive != firstRange.IsMinInclusive || range.IsMaxInclusive != firstRange.IsMaxInclusive) + { + areAnyDifferent = true; + break; + } + } + + if (areAnyDifferent) + { + string result = $"IsMinInclusive found: {string.Join(", ", ranges.Select(range => range.IsMinInclusive).Distinct())}, IsMaxInclusive found: {string.Join(", ", ranges.Select(range => range.IsMaxInclusive).Distinct())}."; + + throw new InvalidOperationException($"Not all 'IsMinInclusive' or 'IsMaxInclusive' values are the same. {result}"); + } + } + + /// + /// Determines whether the specified y range is entirely within the bounds of the x range. + /// This includes checking both the minimum and maximum boundaries of the ranges for inclusion. + /// + /// The method checks whether the `Min` and `Max` boundaries of `y` are within `x`, + /// taking into account whether each boundary is inclusive or exclusive. + /// + /// - For the `Max` boundary: + /// - If the x range's max is exclusive and the y range's max is inclusive, it checks whether the x range contains the y range's max value. + /// - If the x range's max is inclusive and the y range's max is exclusive, this combination is not supported and a is thrown. + /// - For all other cases, it checks if the max values are equal or whether the x range contains the y range's max. + /// - This applies to the following combinations: + /// - (false, true): x max is exclusive, y max is inclusive. + /// - (true, true): Both max values are inclusive. + /// - (false, false): Both max values are exclusive. + /// - (true, false): x max is inclusive, y max is exclusive. + /// - **NotSupportedException Scenario:** This case is not supported because handling a scenario where the x range has an inclusive maximum and the y range has an exclusive maximum requires additional logic that is not implemented. + /// - If encountered, a is thrown with a message explaining that this combination is not supported. + /// + /// - For the `Min` boundary: + /// - It checks whether the x range contains the y range's min value, regardless of inclusivity. + /// + /// The method ensures the y range is considered a subset only if both its min and max values fall within the x range. + /// + /// Summary of combinations for `x.IsMaxInclusive` and `y.IsMaxInclusive`: + /// 1. x.IsMaxInclusive == false, y.IsMaxInclusive == true: + /// - The x range is exclusive at max, but the y range is inclusive. This is supported and will check if the x contains the y's max. + /// 2. x.IsMaxInclusive == false, y.IsMaxInclusive == false: + /// - Both ranges are exclusive at max. This is supported and will check if the x contains the y's max. + /// 3. x.IsMaxInclusive == true, y.IsMaxInclusive == true: + /// - Both ranges are inclusive at max. This is supported and will check if the max values are equal or if the x contains the y's max. + /// 4. x.IsMaxInclusive == true, y.IsMaxInclusive == false: + /// - The x range is inclusive at max, but the y range is exclusive. This combination is not supported and will result in a being thrown. + /// + /// The method returns true only if both the min and max boundaries of the y range are within the x range's boundaries. + /// + /// Additionally, the method performs null checks on the parameters: + /// - If is null, an is thrown. + /// - If is null, an is thrown. + /// + /// + /// Thrown when or is null. + /// + /// + /// Thrown when is inclusive at max and is exclusive at max. + /// This combination is not supported and requires specific handling. + /// + /// + /// + /// x = new Documents.Routing.Range("A", "Z", true, true); + /// Documents.Routing.Range y = new Documents.Routing.Range("B", "Y", true, true); + /// + /// bool isSubset = IsSubset(x, y); + /// isSubset will be true because the y range (B-Y) is fully contained within the x range (A-Z). + /// ]]> + /// + /// + /// Returns true if the y range is a subset of the x range, meaning the y range's + /// minimum and maximum values fall within the bounds of the x range. Returns false otherwise. + /// + internal static bool IsSubset( + Documents.Routing.Range x, + Documents.Routing.Range y) + { + if (x is null) + { + throw new ArgumentNullException(nameof(x)); + } + + if (y is null) + { + throw new ArgumentNullException(nameof(y)); + } + + bool isMaxWithinX = (x.IsMaxInclusive, y.IsMaxInclusive) switch + { + (false, true) => x.Contains(y.Max), // x max is exclusive, y max is inclusive + (true, false) => throw new NotSupportedException("The combination where the x range's maximum is inclusive and the y range's maximum is exclusive is not supported in the current implementation."), + + _ => ContainerCore.IsYMaxWithinX(x, y) // Default for the following combinations: + // (true, true): Both max values are inclusive + // (false, false): Both max values are exclusive + }; + + bool isMinWithinX = x.Contains(y.Min); + + return isMinWithinX && isMaxWithinX; + } + + /// + /// Determines whether the given maximum value of the y range is either equal to or contained within the x range. + /// + /// The x range to compare against, which defines the boundary. + /// The y range to be checked. + /// True if the maximum value of the y range is equal to or contained within the x range; otherwise, false. + private static bool IsYMaxWithinX( + Documents.Routing.Range x, + Documents.Routing.Range y) + { + return x.Max == y.Max || x.Contains(y.Max); } } } diff --git a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerInlineCore.cs b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerInlineCore.cs index 2bf366e577..11a3b866c4 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerInlineCore.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerInlineCore.cs @@ -577,6 +577,9 @@ public override TransactionalBatch CreateTransactionalBatch(PartitionKey partiti public override Task> GetFeedRangesAsync(CancellationToken cancellationToken = default) { + // TODO: The current use of Documents.OperationType.ReadFeed is not a precise fit for this operation. + // A more suitable or generic Documents.OperationType should be created in the future to accurately represent this action. + return this.ClientContext.OperationHelperAsync( operationName: nameof(GetFeedRangesAsync), containerName: this.Id, @@ -674,5 +677,25 @@ public override Task DeleteAllItemsByPartitionKeyStreamAsync( task: (trace) => base.DeleteAllItemsByPartitionKeyStreamAsync(partitionKey, trace, requestOptions, cancellationToken), openTelemetry: new (OpenTelemetryConstants.Operations.DeleteAllItemsByPartitionKey, (response) => new OpenTelemetryResponse(response))); } + + public override Task IsFeedRangePartOfAsync( + FeedRange x, + FeedRange y, + CancellationToken cancellationToken = default) + { + // TODO: The current use of Documents.OperationType.ReadFeed is not a precise fit for this operation. + // A more suitable or generic Documents.OperationType should be created in the future to accurately represent this action. + + return this.ClientContext.OperationHelperAsync( + operationName: nameof(IsFeedRangePartOfAsync), + containerName: this.Id, + databaseName: this.Database.Id, + operationType: Documents.OperationType.ReadFeed, + requestOptions: null, + task: (trace) => base.IsFeedRangePartOfAsync( + x, + y, + cancellationToken: cancellationToken)); + } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerInternal.cs b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerInternal.cs index cedef3f3de..84fb20480c 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerInternal.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerInternal.cs @@ -151,6 +151,11 @@ public abstract Task> GetPartitionKeyRangesAsync( public abstract ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithAllVersionsAndDeletes( string processorName, ChangeFeedHandler> onChangesDelegate); + + public abstract Task IsFeedRangePartOfAsync( + Cosmos.FeedRange x, + Cosmos.FeedRange y, + CancellationToken cancellationToken = default); #endif public abstract class TryExecuteQueryResult diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/IsFeedRangePartOfAsyncTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/IsFeedRangePartOfAsyncTests.cs new file mode 100644 index 0000000000..b867c764e1 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/IsFeedRangePartOfAsyncTests.cs @@ -0,0 +1,1271 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; + + [TestClass] + public class IsFeedRangePartOfAsyncTests + { + private CosmosClient cosmosClient = null; + private Cosmos.Database cosmosDatabase = null; + public TestContext TestContext { get; set; } + + [TestInitialize] + public async Task TestInit() + { + this.cosmosClient = TestCommon.CreateCosmosClient(); + this.cosmosDatabase = await this.cosmosClient.CreateDatabaseIfNotExistsAsync(id: Guid.NewGuid().ToString()); + + await this.TestContext.SetContainerContextsAsync( + cosmosDatabase: this.cosmosDatabase, + createSinglePartitionContainerAsync: IsFeedRangePartOfAsyncTests.CreateSinglePartitionContainerAsync, + createHierarchicalPartitionContainerAsync: IsFeedRangePartOfAsyncTests.CreateHierarchicalPartitionContainerAsync); + } + + [TestCleanup] + public async Task TestCleanup() + { + if (this.cosmosClient == null) + { + return; + } + + if (this.cosmosDatabase != null) + { + await this.cosmosDatabase.DeleteStreamAsync(); + } + + this.cosmosClient.Dispose(); + } + + private async static Task CreateSinglePartitionContainerAsync(Database cosmosDatabase, PartitionKeyDefinitionVersion version) + { + ContainerResponse containerResponse = await cosmosDatabase.CreateContainerIfNotExistsAsync( + new() + { + PartitionKeyDefinitionVersion = version, + Id = Guid.NewGuid().ToString(), + PartitionKeyPaths = new Collection { "/pk" } + }); + + return (ContainerInternal)containerResponse.Container; + } + + private async static Task CreateHierarchicalPartitionContainerAsync(Database cosmosDatabase, PartitionKeyDefinitionVersion version) + { + ContainerResponse containerResponse = await cosmosDatabase.CreateContainerIfNotExistsAsync( + new() + { + PartitionKeyDefinitionVersion = version, + Id = Guid.NewGuid().ToString(), + PartitionKeyPaths = new Collection { "/pk", "/id" } + }); + + return (ContainerInternal)containerResponse.Container; + } + + /// + /// + /// + /// The starting value of the x feed range. + /// The ending value of the x feed range. + /// Indicates whether the y partition key is expected to be part of the x feed range (true if it is, false if it is not). + [TestMethod] + [Owner("philipthomas-MSFT")] + [DataRow("", "FFFFFFFFFFFFFFFF", true, DisplayName = "Full range is subset")] + [DataRow("3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, DisplayName = "Range 3FFFFFFFFFFFFFFF-7FFFFFFFFFFFFFFF is not subset")] + [DataRow("", "FFFFFFFFFFFFFFFF", true, DisplayName = "Full range is subset using V2 hash testContext")] + [DataRow("3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, DisplayName = "Range 3FFFFFFFFFFFFFFF-7FFFFFFFFFFFFFFF is not subset")] + [Description("Validate if the y partition key is part of the x feed range using either V1 or V2 PartitionKeyDefinitionVersion.")] + public async Task GivenFeedRangeYPartitionKeyIsPartOfXFeedRange( + string xMinimum, + string xMaximum, + bool expectedIsFeedRangePartOfAsync) + { + try + { + PartitionKey partitionKey = new("WA"); + FeedRange feedRange = FeedRange.FromPartitionKey(partitionKey); + if (!this.TestContext.TryGetContainerContexts(out List containerContexts)) + { + this.TestContext.WriteLine("ContainerContexts do not exist in TestContext.Properties."); + } + + ConcurrentBag exceptions = new(); + object lockObject = new(); + + IEnumerable tasks = containerContexts + .Where(context => !context.IsHierarchicalPartition) + .Select(async containerContext => + { + this.TestContext.LogTestExecutionForContainer(containerContext); + + bool actualIsFeedRangePartOfAsync = await containerContext.Container.IsFeedRangePartOfAsync( + new FeedRangeEpk(new Documents.Routing.Range(xMinimum, xMaximum, true, false)), + feedRange, + cancellationToken: CancellationToken.None); + + if (actualIsFeedRangePartOfAsync != expectedIsFeedRangePartOfAsync) + { + lock (lockObject) + { + exceptions.Add( + new Exception( + string.Format( + TestContextExtensions.FeedRangeComparisonFailure, + containerContext.Container.Id, + containerContext.Version, + containerContext.IsHierarchicalPartition ? "Hierarchical Partitioning" : "Single Partitioning", + expectedIsFeedRangePartOfAsync, + actualIsFeedRangePartOfAsync))); + } + } + }); + + await Task.WhenAll(tasks); + + this.TestContext.HandleAggregatedExceptions(exceptions); + } + catch (Exception exception) + { + Assert.Fail(exception.Message); + } + } + + /// + /// + /// + /// The starting value of the x feed range. + /// The ending value of the x feed range. + /// A boolean value indicating whether the y hierarchical partition key is expected to be part of the x feed range (true if it is, false if it is not). + [TestMethod] + [Owner("philipthomas-MSFT")] + [DataRow("", "FFFFFFFFFFFFFFFF", true, DisplayName = "Full range")] + [DataRow("3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, DisplayName = "Made-up range 3FFFFFFFFFFFFFFF-7FFFFFFFFFFFFFFF")] + [Description("Validate if the y hierarchical partition key is part of the x feed range.")] + public async Task GivenFeedRangeYHierarchicalPartitionKeyIsPartOfXFeedRange( + string xMinimum, + string xMaximum, + bool expectedIsFeedRangePartOfAsync) + { + try + { + PartitionKey partitionKey = new PartitionKeyBuilder() + .Add("WA") + .Add(Guid.NewGuid().ToString()) + .Build(); + FeedRange feedRange = FeedRange.FromPartitionKey(partitionKey); + if (!this.TestContext.TryGetContainerContexts(out List containerContexts)) + { + this.TestContext.WriteLine("ContainerContexts do not exist in TestContext.Properties."); + } + + ConcurrentBag exceptions = new(); + object lockObject = new(); + + IEnumerable tasks = containerContexts + .Where(context => context.IsHierarchicalPartition) + .Select(async containerContext => + { + this.TestContext.LogTestExecutionForContainer(containerContext); + + bool actualIsFeedRangePartOfAsync = await containerContext.Container.IsFeedRangePartOfAsync( + new FeedRangeEpk(new Documents.Routing.Range(xMinimum, xMaximum, true, false)), + feedRange, + cancellationToken: CancellationToken.None); + + if (actualIsFeedRangePartOfAsync != expectedIsFeedRangePartOfAsync) + { + lock (lockObject) + { + exceptions.Add( + new Exception( + string.Format( + TestContextExtensions.FeedRangeComparisonFailure, + containerContext.Container.Id, + containerContext.Version, + containerContext.IsHierarchicalPartition ? "Hierarchical Partitioning" : "Single Partitioning", + expectedIsFeedRangePartOfAsync, + actualIsFeedRangePartOfAsync))); + } + } + }); + + await Task.WhenAll(tasks); + + this.TestContext.HandleAggregatedExceptions(exceptions); + } + catch (Exception exception) + { + Assert.Fail(exception.Message); + } + } + + /// + /// + /// + [TestMethod] + [Owner("philipthomas-MSFT")] + public async Task GivenFeedRangeThrowsArgumentNullExceptionWhenYFeedRangeIsNull() + { + FeedRange feedRange = default; + + await this.GivenInvalidYFeedRangeExpectsArgumentExceptionIsFeedRangePartOfAsyncTestAsync( + feedRange: feedRange, + expectedMessage: $"Argument cannot be null."); + } + + /// + /// + /// + [TestMethod] + [Owner("philipthomas-MSFT")] + public async Task GivenFeedRangeThrowsArgumentNullExceptionWhenYFeedRangeHasNoJson() + { + FeedRange feedRange = Mock.Of(); + + await this.GivenInvalidYFeedRangeExpectsArgumentExceptionIsFeedRangePartOfAsyncTestAsync( + feedRange: feedRange, + expectedMessage: $"Value cannot be null. (Parameter 'value')"); + } + + /// + /// + /// + [TestMethod] + [Owner("philipthomas-MSFT")] + public async Task GivenFeedRangeThrowsArgumentExceptionWhenYFeedRangeHasInvalidJson() + { + Mock mockFeedRange = new Mock(MockBehavior.Strict); + mockFeedRange.Setup(feedRange => feedRange.ToJsonString()).Returns(""); + FeedRange feedRange = mockFeedRange.Object; + + await this.GivenInvalidYFeedRangeExpectsArgumentExceptionIsFeedRangePartOfAsyncTestAsync( + feedRange: feedRange, + expectedMessage: $"The provided string, '', for 'y', does not represent any known format."); + } + + private async Task GivenInvalidYFeedRangeExpectsArgumentExceptionIsFeedRangePartOfAsyncTestAsync( + FeedRange feedRange, + string expectedMessage) + where TExceeption : Exception + { + try + { + if (!this.TestContext.TryGetContainerContexts(out List containerContexts)) + { + this.TestContext.WriteLine("ContainerContexts do not exist in TestContext.Properties."); + } + + ConcurrentBag exceptions = new(); + object lockObject = new(); + + IEnumerable tasks = containerContexts + .Select(async containerContext => + { + this.TestContext.LogTestExecutionForContainer(containerContext); + + TExceeption exception = await Assert.ThrowsExceptionAsync( + async () => await containerContext.Container.IsFeedRangePartOfAsync( + new FeedRangeEpk(new Documents.Routing.Range("", "FFFFFFFFFFFFFFFF", true, false)), + feedRange, + cancellationToken: CancellationToken.None)); + + if (exception == null) + { + lock (lockObject) + { + exceptions.Add(new Exception("Failed: {testContext}. Expected exception was null.")); + } + } + else if (!exception.Message.Contains(expectedMessage)) + { + lock (lockObject) + { + exceptions.Add( + new Exception( + string.Format( + TestContextExtensions.ExceptionMessageMismatch, + containerContext.Container.Id, + containerContext.Version, + containerContext.IsHierarchicalPartition ? "Hierarchical Partitioning" : "Single Partitioning", + expectedMessage, + exception.Message))); + } + } + }); + + await Task.WhenAll(tasks); + + this.TestContext.HandleAggregatedExceptions(exceptions); + } + catch (Exception exception) + { + Assert.Fail(exception.Message); + } + } + + /// + /// + /// + [TestMethod] + [Owner("philipthomas-MSFT")] + public async Task GivenFeedRangeThrowsArgumentNullExceptionWhenXFeedRangeIsNull() + { + FeedRange feedRange = default; + + await this.GivenInvalidXFeedRangeExpectsArgumentExceptionIsFeedRangePartOfAsyncTestAsync( + feedRange: feedRange, + expectedMessage: $"Argument cannot be null."); + } + + /// + /// + /// + [TestMethod] + [Owner("philipthomas-MSFT")] + public async Task GivenFeedRangeThrowsArgumentNullExceptionWhenXFeedRangeHasNoJson() + { + FeedRange feedRange = Mock.Of(); + + await this.GivenInvalidXFeedRangeExpectsArgumentExceptionIsFeedRangePartOfAsyncTestAsync( + feedRange: feedRange, + expectedMessage: $"Value cannot be null. (Parameter 'value')"); + } + + /// + /// + /// + [TestMethod] + [Owner("philipthomas-MSFT")] + public async Task GivenFeedRangeThrowsArgumentExceptionWhenXFeedRangeHasInvalidJson() + { + Mock mockFeedRange = new Mock(MockBehavior.Strict); + mockFeedRange.Setup(feedRange => feedRange.ToJsonString()).Returns(""); + FeedRange feedRange = mockFeedRange.Object; + + await this.GivenInvalidXFeedRangeExpectsArgumentExceptionIsFeedRangePartOfAsyncTestAsync( + feedRange: feedRange, + expectedMessage: $"The provided string, '', for 'x', does not represent any known format."); + } + + private async Task GivenInvalidXFeedRangeExpectsArgumentExceptionIsFeedRangePartOfAsyncTestAsync( + FeedRange feedRange, + string expectedMessage) + where TException : Exception + { + try + { + if (!this.TestContext.TryGetContainerContexts(out List containerContexts)) + { + this.TestContext.WriteLine("ContainerContexts do not exist in TestContext.Properties."); + } + + ConcurrentBag exceptions = new(); + object lockObject = new(); + + IEnumerable tasks = containerContexts + .Select(async containerContext => + { + this.TestContext.LogTestExecutionForContainer(containerContext); + + TException exception = await Assert.ThrowsExceptionAsync( + async () => await containerContext.Container.IsFeedRangePartOfAsync( + feedRange, + new FeedRangeEpk(new Documents.Routing.Range("", "3FFFFFFFFFFFFFFF", true, false)), + cancellationToken: CancellationToken.None)); + + if (exception == null) + { + lock (lockObject) + { + exceptions.Add(new Exception($"Failed: {containerContext}. Expected exception was null.")); + } + } + else if (!exception.Message.Contains(expectedMessage)) + { + lock (lockObject) + { + exceptions.Add( + new Exception( + string.Format( + TestContextExtensions.ExceptionMessageMismatch, + containerContext.Container.Id, + containerContext.Version, + containerContext.IsHierarchicalPartition ? "Hierarchical Partitioning" : "Single Partitioning", + expectedMessage, + exception.Message))); + } + } + }); + + await Task.WhenAll(tasks); + + this.TestContext.HandleAggregatedExceptions(exceptions); + } + catch (Exception exception) + { + Assert.Fail(exception.Message); + } + } + + /// + /// + /// + /// The starting value of the y feed range. + /// The ending value of the y feed range. + /// Specifies whether the maximum value of the y feed range is inclusive. + /// The starting value of the x feed range. + /// The ending value of the x feed range. + /// Specifies whether the maximum value of the x feed range is inclusive. + [TestMethod] + [Owner("philipthomas-MSFT")] + [DynamicData(nameof(IsFeedRangePartOfAsyncTests.FeedRangeThrowsNotSupportedExceptionWhenYIsMaxExclusiveAndXIsMaxInclusive), DynamicDataSourceType.Method)] + public async Task GivenFeedRangeYPartOfOrNotPartOfXWhenBothIsMaxInclusiveCanBeTrueOrFalseNotSupportedExceptionTestAsync( + string yMinimum, + string yMaximum, + bool yIsMaxInclusive, + string xMinimum, + string xMaximum, + bool xIsMaxInclusive) + { + try + { + if (!this.TestContext.TryGetContainerContexts(out List containerContexts)) + { + this.TestContext.WriteLine("ContainerContexts do not exist in TestContext.Properties."); + } + + ConcurrentBag exceptions = new(); + object lockObject = new(); + + IEnumerable tasks = containerContexts + .Select(async containerContext => + { + this.TestContext.LogTestExecutionForContainer(containerContext); + + NotSupportedException exception = await Assert.ThrowsExceptionAsync( + async () => + await containerContext.Container.IsFeedRangePartOfAsync( + new FeedRangeEpk(new Documents.Routing.Range(xMinimum, xMaximum, true, xIsMaxInclusive)), + new FeedRangeEpk(new Documents.Routing.Range(yMinimum, yMaximum, true, yIsMaxInclusive)), + cancellationToken: CancellationToken.None)); + + if (exception == null) + { + lock (lockObject) + { + exceptions.Add(new Exception($"Failed: {containerContext}. Expected exception was null.")); + } + } + }); + + // Await all tasks to complete + await Task.WhenAll(tasks); + + // Handle the aggregated exceptions using the extension method + this.TestContext.HandleAggregatedExceptions(exceptions); + } + catch (Exception exception) + { + Assert.Fail(exception.Message); + } + } + + /// + /// + /// + /// The starting value of the y feed range. + /// The ending value of the y feed range. + /// Specifies whether the maximum value of the y feed range is inclusive. + /// The starting value of the x feed range. + /// The ending value of the x feed range. + /// Specifies whether the maximum value of the x feed range is inclusive. + /// Indicates whether the y feed range is expected to be a subset of the x feed range. + [TestMethod] + [Owner("philipthomas-MSFT")] + [DynamicData(nameof(IsFeedRangePartOfAsyncTests.FeedRangeYPartOfXWhenBothYAndXIsMaxInclusiveTrue), DynamicDataSourceType.Method)] + [DynamicData(nameof(IsFeedRangePartOfAsyncTests.FeedRangeYNotPartOfXWhenBothYAndXIsMaxInclusiveTrue), DynamicDataSourceType.Method)] + [DynamicData(nameof(IsFeedRangePartOfAsyncTests.FeedRangeYNotPartOfXWhenBothIsMaxInclusiveAreFalse), DynamicDataSourceType.Method)] + [DynamicData(nameof(IsFeedRangePartOfAsyncTests.FeedRangeYNotPartOfXWhenYAndXIsMaxInclusiveAreFalse), DynamicDataSourceType.Method)] + [DynamicData(nameof(IsFeedRangePartOfAsyncTests.FeedRangeYPartOfXWhenYIsMaxInclusiveTrueAndXIsMaxInclusiveFalse), DynamicDataSourceType.Method)] + [DynamicData(nameof(IsFeedRangePartOfAsyncTests.FeedRangeYNotPartOfXWhenYIsMaxInclusiveTrueAndXIsMaxInclusiveFalse), DynamicDataSourceType.Method)] + public async Task GivenFeedRangeYPartOfOrNotPartOfXWhenBothIsMaxInclusiveCanBeTrueOrFalseTestAsync( + string yMinimum, + string yMaximum, + bool yIsMaxInclusive, + string xMinimum, + string xMaximum, + bool xIsMaxInclusive, + bool expectedIsFeedRangePartOfAsync) + { + try + { + if (!this.TestContext.TryGetContainerContexts(out List containerContexts)) + { + this.TestContext.WriteLine("ContainerContexts do not exist in TestContext.Properties."); + } + + ConcurrentBag exceptions = new(); + object lockObject = new(); + + IEnumerable tasks = containerContexts + .Select(async containerContext => + { + this.TestContext.LogTestExecutionForContainer(containerContext); + + bool actualIsFeedRangePartOfAsync = await containerContext.Container.IsFeedRangePartOfAsync( + new FeedRangeEpk(new Documents.Routing.Range(xMinimum, xMaximum, true, xIsMaxInclusive)), + new FeedRangeEpk(new Documents.Routing.Range(yMinimum, yMaximum, true, yIsMaxInclusive)), + cancellationToken: CancellationToken.None); + + if (expectedIsFeedRangePartOfAsync != actualIsFeedRangePartOfAsync) + { + lock (lockObject) + { + exceptions.Add( + new Exception( + string.Format( + TestContextExtensions.FeedRangeComparisonFailure, + containerContext.Container.Id, + containerContext.Version, + containerContext.IsHierarchicalPartition ? "Hierarchical Partitioning" : "Single Partitioning", + expectedIsFeedRangePartOfAsync, + actualIsFeedRangePartOfAsync))); + } + } + }); + + await Task.WhenAll(tasks); + + this.TestContext.HandleAggregatedExceptions(exceptions); + } + catch (Exception exception) + { + Assert.Fail(exception.Message); + } + } + + /// + /// + /// + private static IEnumerable FeedRangeYNotPartOfXWhenBothIsMaxInclusiveAreFalse() + { + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", false, true }; // The y range, starting from a lower bound minimum and ending just before 3FFFFFFFFFFFFFFF, fits entirely within the x range, which starts from a lower bound minimum and ends just before FFFFFFFFFFFFFFFF. + yield return new object[] { "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", false, true }; // The y range, from 3FFFFFFFFFFFFFFF to just before 7FFFFFFFFFFFFFFF, fits entirely within the x range, which starts from a lower bound minimum and ends just before FFFFFFFFFFFFFFFF. + yield return new object[] { "7FFFFFFFFFFFFFFF", "BFFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", false, true }; // The y range, from 7FFFFFFFFFFFFFFF to just before BFFFFFFFFFFFFFFF, fits entirely within the x range, which starts from a lower bound minimum and ends just before FFFFFFFFFFFFFFFF. + yield return new object[] { "BFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", false, true }; // The y range, from BFFFFFFFFFFFFFFF to just before FFFFFFFFFFFFFFFF, does fit within the x range, which starts from a lower bound minimum and ends just before FFFFFFFFFFFFFFFF. + yield return new object[] { "3FFFFFFFFFFFFFFF", "4CCCCCCCCCCCCCCC", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, true }; // The y range, from 3FFFFFFFFFFFFFFF to just before 4CCCCCCCCCCCCCCC, fits entirely within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "4CCCCCCCCCCCCCCC", "5999999999999999", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, true }; // The y range, from 4CCCCCCCCCCCCCCC to just before 5999999999999999, fits entirely within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "5999999999999999", "6666666666666666", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, true }; // The y range, from 5999999999999999 to just before 6666666666666666, fits entirely within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "6666666666666666", "7333333333333333", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, true }; // The y range, from 6666666666666666 to just before 7333333333333333, fits entirely within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "7333333333333333", "7FFFFFFFFFFFFFFF", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, true }; // The y range, from 7333333333333333 to just before 7FFFFFFFFFFFFFFF, does fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "", "3FFFFFFFFFFFFFFF", false, true }; // The y range, starting from a lower bound minimum and ending just before 3FFFFFFFFFFFFFFF, does not fit within the x range, which starts from a lower bound minimum and ends just before 3FFFFFFFFFFFFFFF. + } + + /// + /// + /// + private static IEnumerable FeedRangeYNotPartOfXWhenYAndXIsMaxInclusiveAreFalse() + { + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range ends just before 3FFFFFFFFFFFFFFF, but is not part of the x range from 3FFFFFFFFFFFFFFF to 7FFFFFFFFFFFFFFF. + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "7FFFFFFFFFFFFFFF", "BFFFFFFFFFFFFFFF", false, false }; // The y range ends just before 3FFFFFFFFFFFFFFF, but is not part of the x range from 7FFFFFFFFFFFFFFF to BFFFFFFFFFFFFFFF. + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "BFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFF", false, false }; // The y range ends just before 3FFFFFFFFFFFFFFF, but is not part of the x range from BFFFFFFFFFFFFFFF to FFFFFFFFFFFFFFFF. + yield return new object[] { "", "3333333333333333", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range ends just before 3333333333333333, but is not part of the x range from 3FFFFFFFFFFFFFFF to 7FFFFFFFFFFFFFFF. + yield return new object[] { "3333333333333333", "6666666666666666", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range from 3333333333333333 to just before 6666666666666666 is not part of the x range from 3FFFFFFFFFFFFFFF to 7FFFFFFFFFFFFFFF. + yield return new object[] { "7333333333333333", "FFFFFFFFFFFFFFFF", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range from 7333333333333333 to just before FFFFFFFFFFFFFFFF is not part of the x range from 3FFFFFFFFFFFFFFF to 7FFFFFFFFFFFFFFF. + yield return new object[] { "", "7333333333333333", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range ends just before 7333333333333333, but is not part of the x range from 3FFFFFFFFFFFFFFF to 7FFFFFFFFFFFFFFF. + } + + /// + /// + /// + private static IEnumerable FeedRangeYPartOfXWhenYIsMaxInclusiveTrueAndXIsMaxInclusiveFalse() + { + yield return new object[] { "", "3FFFFFFFFFFFFFFF", true, "", "FFFFFFFFFFFFFFFF", false, true }; // The y range, starting from a lower bound minimum and ending at 3FFFFFFFFFFFFFFF (inclusive), fits within the x range, which starts from a lower bound minimum and ends just before FFFFFFFFFFFFFFFF. + yield return new object[] { "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, "", "FFFFFFFFFFFFFFFF", false, true }; // The y range, from 3FFFFFFFFFFFFFFF to 7FFFFFFFFFFFFFFF (inclusive), fits within the x range, starting from a lower bound minimum and ending just before FFFFFFFFFFFFFFFF. + yield return new object[] { "7FFFFFFFFFFFFFFF", "BFFFFFFFFFFFFFFF", true, "", "FFFFFFFFFFFFFFFF", false, true }; // The y range, from 7FFFFFFFFFFFFFFF to BFFFFFFFFFFFFFFF (inclusive), fits within the x range, starting from a lower bound minimum and ending just before FFFFFFFFFFFFFFFF. + yield return new object[] { "BFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFF", true, "", "FFFFFFFFFFFFFFFF", false, false }; // "The y range, from BFFFFFFFFFFFFFFF to FFFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from a lower bound minimum and ends just before FFFFFFFFFFFFFFFF. + yield return new object[] { "", "3FFFFFFFFFFFFFFF", true, "", "3FFFFFFFFFFFFFFF", false, false }; // The y range, from a lower bound minimum to 3FFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from a lower bound minimum and ends just before 3FFFFFFFFFFFFFFF. + yield return new object[] { "3FFFFFFFFFFFFFFF", "4CCCCCCCCCCCCCCC", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, true }; // The y range, from 3FFFFFFFFFFFFFFF to 4CCCCCCCCCCCCCCC (inclusive), fits within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "4CCCCCCCCCCCCCCC", "5999999999999999", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, true }; // The y range, from 4CCCCCCCCCCCCCCC to 5999999999999999 (inclusive), fits within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "5999999999999999", "6666666666666666", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, true }; // The y range, from 5999999999999999 to 6666666666666666 (inclusive), fits within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "6666666666666666", "7333333333333333", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, true }; // The y range, from 6666666666666666 to 7333333333333333 (inclusive), fits within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "7333333333333333", "7FFFFFFFFFFFFFFF", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range, from 7333333333333333 to 7FFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + } + + /// + /// + /// + private static IEnumerable FeedRangeYNotPartOfXWhenYIsMaxInclusiveTrueAndXIsMaxInclusiveFalse() + { + yield return new object[] { "", "3FFFFFFFFFFFFFFF", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range, starting from a lower bound minimum and ending at 3FFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "", "3FFFFFFFFFFFFFFF", true, "7FFFFFFFFFFFFFFF", "BFFFFFFFFFFFFFFF", false, false }; // The y range, starting from a lower bound minimum and ending at 3FFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from 7FFFFFFFFFFFFFFF and ends just before BFFFFFFFFFFFFFFF. + yield return new object[] { "", "3FFFFFFFFFFFFFFF", true, "BFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFF", false, false }; // The y range, starting from a lower bound minimum and ending at 3FFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from BFFFFFFFFFFFFFFF and ends just before FFFFFFFFFFFFFFFF. + yield return new object[] { "", "3333333333333333", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range, starting from a lower bound minimum and ending at 3333333333333333 (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "3333333333333333", "6666666666666666", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range, from 3333333333333333 to 6666666666666666 (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "7333333333333333", "FFFFFFFFFFFFFFFF", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range, from 7333333333333333 to FFFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "", "7333333333333333", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, false }; // The y range, starting from a lower bound minimum and ending at 7333333333333333 (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends just before 7FFFFFFFFFFFFFFF. + yield return new object[] { "AA", "AA", true, "", "AA", false, false }; // The y range, which starts and ends at AA (inclusive), does not fit within the x range, which starts from a lower bound minimum and ends just before AA (non-inclusive), due to the x's non-inclusive upper boundary. + yield return new object[] { "AA", "AA", true, "AA", "BB", false, true }; // The y range, which starts and ends at AA (inclusive), fits entirely within the x range, which starts at AA and ends just before BB (non-inclusive), due to the y's inclusive boundary at AA. + } + + /// + /// + /// + private static IEnumerable FeedRangeYPartOfXWhenYIsMaxInclusiveFalseAndXIsMaxInclusiveTrue() + { + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", true }; // + yield return new object[] { "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", true }; // + yield return new object[] { "7FFFFFFFFFFFFFFF", "BFFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", true }; // + yield return new object[] { "BFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", true }; // + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "", "3FFFFFFFFFFFFFFF", true }; // + yield return new object[] { "3FFFFFFFFFFFFFFF", "4CCCCCCCCCCCCCCC", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // + yield return new object[] { "4CCCCCCCCCCCCCCC", "5999999999999999", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // + yield return new object[] { "5999999999999999", "6666666666666666", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // + yield return new object[] { "6666666666666666", "7333333333333333", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true}; // + yield return new object[] { "7333333333333333", "7FFFFFFFFFFFFFFF", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // + yield return new object[] { "10", "11", false, "10", "10", true }; // + yield return new object[] { "A", "B", false, "A", "A", true }; // + } + + /// + /// + /// + private static IEnumerable FeedRangeThrowsNotSupportedExceptionWhenYIsMaxExclusiveAndXIsMaxInclusive() + { + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '' to '3FFFFFFFFFFFFFFF' vs '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF') + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "7FFFFFFFFFFFFFFF", "BFFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '' to '3FFFFFFFFFFFFFFF' vs '7FFFFFFFFFFFFFFF' to 'BFFFFFFFFFFFFFFF') + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "BFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '' to '3FFFFFFFFFFFFFFF' vs 'BFFFFFFFFFFFFFFF' to 'FFFFFFFFFFFFFFFF') + yield return new object[] { "", "3333333333333333", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '' to '3333333333333333' vs '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF') + yield return new object[] { "3333333333333333", "6666666666666666", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '3333333333333333' to '6666666666666666' vs '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF') + yield return new object[] { "7333333333333333", "FFFFFFFFFFFFFFFF", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '7333333333333333' to 'FFFFFFFFFFFFFFFF' vs '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF') + yield return new object[] { "", "7333333333333333", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '' to '7333333333333333' vs '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF') + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '' to '3FFFFFFFFFFFFFFF' vs '' to 'FFFFFFFFFFFFFFFF') + yield return new object[] { "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF' vs '' to 'FFFFFFFFFFFFFFFF') + yield return new object[] { "7FFFFFFFFFFFFFFF", "BFFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '7FFFFFFFFFFFFFFF' to 'BFFFFFFFFFFFFFFF' vs '' to 'FFFFFFFFFFFFFFFF') + yield return new object[] { "BFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFF", false, "", "FFFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: 'BFFFFFFFFFFFFFFF' to 'FFFFFFFFFFFFFFFF' vs '' to 'FFFFFFFFFFFFFFFF') + yield return new object[] { "", "3FFFFFFFFFFFFFFF", false, "", "3FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '' to '3FFFFFFFFFFFFFFF' vs '' to '3FFFFFFFFFFFFFFF') + yield return new object[] { "3FFFFFFFFFFFFFFF", "4CCCCCCCCCCCCCCC", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '3FFFFFFFFFFFFFFF' to '4CCCCCCCCCCCCCCC' vs '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF') + yield return new object[] { "4CCCCCCCCCCCCCCC", "5999999999999999", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '4CCCCCCCCCCCCCCC' to '5999999999999999' vs '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF') + yield return new object[] { "5999999999999999", "6666666666666666", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '5999999999999999' to '6666666666666666' vs '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF') + yield return new object[] { "6666666666666666", "7333333333333333", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '6666666666666666' to '7333333333333333' vs '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF') + yield return new object[] { "7333333333333333", "7FFFFFFFFFFFFFFF", false, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '7333333333333333' to '7FFFFFFFFFFFFFFF' vs '3FFFFFFFFFFFFFFF' to '7FFFFFFFFFFFFFFF') + yield return new object[] { "10", "11", false, "10", "10", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: '10' to '11' vs '10' to '10') + yield return new object[] { "A", "B", false, "A", "A", true }; // NotSupportedException thrown for y max exclusive and x max inclusive (Range: 'A' to 'B' vs 'A' to 'A') + } + + /// + /// + /// + private static IEnumerable FeedRangeYPartOfXWhenBothYAndXIsMaxInclusiveTrue() + { + yield return new object[] { "", "3FFFFFFFFFFFFFFF", true, "", "FFFFFFFFFFFFFFFF", true, true }; // The y range, starting from a lower bound minimum and ending at 3FFFFFFFFFFFFFFF (inclusive), fits entirely within the x range, which starts from a lower bound minimum and ends at FFFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, "", "FFFFFFFFFFFFFFFF", true, true }; // The y range, from 3FFFFFFFFFFFFFFF to 7FFFFFFFFFFFFFFF (inclusive), fits entirely within the x range, which starts from a lower bound minimum and ends at FFFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "7FFFFFFFFFFFFFFF", "BFFFFFFFFFFFFFFF", true, "", "FFFFFFFFFFFFFFFF", true, true }; // The y range, from 7FFFFFFFFFFFFFFF to BFFFFFFFFFFFFFFF (inclusive), fits entirely within the x range, which starts from a lower bound minimum and ends at FFFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "BFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFF", true, "", "FFFFFFFFFFFFFFFF", true, true }; // The y range, from BFFFFFFFFFFFFFFF to FFFFFFFFFFFFFFFF (inclusive), fits entirely within the x range, which starts from a lower bound minimum and ends at FFFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "", "3FFFFFFFFFFFFFFF", true, "", "3FFFFFFFFFFFFFFF", true, true }; // The y range, from a lower bound minimum to 3FFFFFFFFFFFFFFF (inclusive), fits entirely within the x range, which starts from a lower bound minimum and ends at 3FFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "3FFFFFFFFFFFFFFF", "4CCCCCCCCCCCCCCC", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, true }; // The y range, from 3FFFFFFFFFFFFFFF to 4CCCCCCCCCCCCCCC (inclusive), fits entirely within the x range, which starts from 3FFFFFFFFFFFFFFF and ends at 7FFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "4CCCCCCCCCCCCCCC", "5999999999999999", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, true }; // The y range, from 4CCCCCCCCCCCCCCC to 5999999999999999 (inclusive), fits entirely within the x range, which starts from 3FFFFFFFFFFFFFFF and ends at 7FFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "5999999999999999", "6666666666666666", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, true }; // The y range, from 5999999999999999 to 6666666666666666 (inclusive), fits entirely within the x range, which starts from 3FFFFFFFFFFFFFFF and ends at 7FFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "6666666666666666", "7333333333333333", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, true }; // The y range, from 6666666666666666 to 7333333333333333 (inclusive), fits entirely within the x range, which starts from 3FFFFFFFFFFFFFFF and ends at 7FFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "7333333333333333", "7FFFFFFFFFFFFFFF", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, true }; // The y range, from 7333333333333333 to 7FFFFFFFFFFFFFFF (inclusive), fits entirely within the x range, which starts from 3FFFFFFFFFFFFFFF and ends at 7FFFFFFFFFFFFFFF (inclusive). + } + + /// + /// + /// + private static IEnumerable FeedRangeYNotPartOfXWhenBothYAndXIsMaxInclusiveTrue() + { + yield return new object[] { "", "3FFFFFFFFFFFFFFF", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, false }; // The y range, starting from a lower bound minimum and ending at 3FFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends at 7FFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "", "3FFFFFFFFFFFFFFF", true, "7FFFFFFFFFFFFFFF", "BFFFFFFFFFFFFFFF", true, false }; // The y range, starting from a lower bound minimum and ending at 3FFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from 7FFFFFFFFFFFFFFF and ends at BFFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "", "3FFFFFFFFFFFFFFF", true, "BFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFF", true, false }; // The y range, starting from a lower bound minimum and ending at 3FFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from BFFFFFFFFFFFFFFF and ends at FFFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "", "3333333333333333", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, false }; // The y range, starting from a lower bound minimum and ending at 3333333333333333 (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends at 7FFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "3333333333333333", "6666666666666666", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, false }; // The y range, from 3333333333333333 to 6666666666666666 (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends at 7FFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "7333333333333333", "FFFFFFFFFFFFFFFF", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, false }; // The y range, from 7333333333333333 to FFFFFFFFFFFFFFFF (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends at 7FFFFFFFFFFFFFFF (inclusive). + yield return new object[] { "", "7333333333333333", true, "3FFFFFFFFFFFFFFF", "7FFFFFFFFFFFFFFF", true, false }; // The y range, starting from a lower bound minimum and ending at 7333333333333333 (inclusive), does not fit within the x range, which starts from 3FFFFFFFFFFFFFFF and ends at 7FFFFFFFFFFFFFFF (inclusive). + } + + /// + /// + /// + /// The version of the PartitionKeyDefinition (V1 or V2) used for the validation. + [TestMethod] + [Owner("philipthomas-MSFT")] + public async Task GivenFeedRangeThrowsArgumentOutOfRangeExceptionWhenYComparedToXWithXIsMinInclusiveFalse() + { + await this.FeedRangeThrowsArgumentOutOfRangeExceptionWhenIsMinInclusiveFalse( + xFeedRange: new Documents.Routing.Range("", "3FFFFFFFFFFFFFFF", false, true), + yFeedRange: new Documents.Routing.Range("", "FFFFFFFFFFFFFFFF", true, false)); + } + + /// + /// + /// + [TestMethod] + [Owner("philipthomas-MSFT")] + public async Task GivenFeedRangeThrowsArgumentOutOfRangeExceptionWhenYComparedToXWithYIsMinInclusiveFalse() + { + await this.FeedRangeThrowsArgumentOutOfRangeExceptionWhenIsMinInclusiveFalse( + xFeedRange: new Documents.Routing.Range("", "3FFFFFFFFFFFFFFF", true, false), + yFeedRange: new Documents.Routing.Range("", "FFFFFFFFFFFFFFFF", false, true)); + } + + private async Task FeedRangeThrowsArgumentOutOfRangeExceptionWhenIsMinInclusiveFalse( + Documents.Routing.Range xFeedRange, + Documents.Routing.Range yFeedRange) + { + try + { + if (!this.TestContext.TryGetContainerContexts(out List containerContexts)) + { + this.TestContext.WriteLine("ContainerContexts do not exist in TestContext.Properties."); + } + + ConcurrentBag exceptions = new(); + object lockObject = new(); + + IEnumerable tasks = containerContexts + .Select(async containerContext => + { + this.TestContext.LogTestExecutionForContainer(containerContext); + + ArgumentOutOfRangeException exception = await Assert.ThrowsExceptionAsync( + async () => await containerContext.Container + .IsFeedRangePartOfAsync( + new FeedRangeEpk(xFeedRange), + new FeedRangeEpk(yFeedRange), + cancellationToken: CancellationToken.None)); + + if (exception == null) + { + lock (lockObject) + { + exceptions.Add(new Exception($"Failed: {containerContext}. Expected exception was null.")); + } + } + else if (!exception.Message.Contains("IsMinInclusive must be true.")) + { + lock (lockObject) + { + exceptions.Add( + new Exception( + string.Format( + TestContextExtensions.IsMinInclusiveExceptionMismatch, + containerContext.Container.Id, + containerContext.Version, + containerContext.IsHierarchicalPartition ? "Hierarchical Partitioning" : "Single Partitioning", + exception.Message))); + } + } + }); + + await Task.WhenAll(tasks); + + this.TestContext.HandleAggregatedExceptions(exceptions); + } + catch (Exception exception) + { + Assert.Fail(exception.Message); + } + } + + /// + /// and max value + /// And the x feed range is inclusive of its min value + /// And the x feed range is inclusive of its max value + /// When a y feed range with min value and max value is compared + /// And the y feed range is inclusive of its min value + /// And the y feed range is inclusive of its max value + /// Then the result should be indicating if the y is a subset of the x + /// ]]> + /// + /// Indicates whether the x range's minimum value is inclusive. + /// Indicates whether the x range's maximum value is inclusive. + /// The minimum value of the x range. + /// The maximum value of the x range. + /// Indicates whether the y range's minimum value is inclusive. + /// Indicates whether the y range's maximum value is inclusive. + /// The minimum value of the y range. + /// The maximum value of the y range. + /// A boolean indicating whether the y feed range is expected to be a subset of the x feed range. True if the y is a subset, false otherwise. + [TestMethod] + [Owner("philipthomas-MSFT")] + [DataRow(true, true, "A", "Z", true, true, "A", "Z", true, DisplayName = "(true, true) Given both x and y ranges (A to Z) are fully inclusive and equal, y is a subset")] + [DataRow(true, true, "A", "A", true, true, "A", "A", true, DisplayName = "(true, true) Given both x and y ranges (A to A) are fully inclusive and equal, and min and max range is the same, y is a subset")] + [DataRow(true, true, "A", "A", true, true, "B", "B", false, DisplayName = "(true, true) Given both x and y ranges are fully inclusive but min and max ranges are not the same (A to A, B to B), y is not a subset")] + [DataRow(true, true, "B", "B", true, true, "A", "A", false, DisplayName = "(true, true) Given x range (B to B) is fully inclusive and y range (A to A) is fully inclusive, y is not a subset")] + [DataRow(true, false, "A", "Z", true, true, "A", "Y", true, DisplayName = "(false, true) Given x range (A to Z) has an exclusive max and y range (A to Y) is fully inclusive, y is a subset")] + [DataRow(true, false, "A", "Y", true, true, "A", "Z", false, DisplayName = "(false, true) Given x range (A to Y) has an exclusive max but y range (A to Z) exceeds the x’s max with an inclusive bound, y is not a subset")] + [DataRow(true, false, "A", "Z", true, true, "A", "Z", false, DisplayName = "(false, true) Given x range (A to Z) has an exclusive max and y range (A to Z) is fully inclusive, y is not a subset")] + [DataRow(true, false, "A", "Y", true, false, "A", "Y", true, DisplayName = "(false, false) Given x range (A to Y) is inclusive at min and exclusive at max, and y range (A to Y) is inclusive at min and exclusive at max, y is a subset")] + [DataRow(true, false, "A", "W", true, false, "A", "Y", false, DisplayName = "(false, false) Given x range (A to W) is inclusive at min and exclusive at max, and y range (A to Y) is inclusive at min and exclusive at max, y is not a subset")] + [DataRow(true, false, "A", "Y", true, false, "A", "W", true, DisplayName = "(false, false) Given x range (A to Y) is inclusive at min and exclusive at max, and y range (A to W) is inclusive at min and exclusive at max, y is a subset")] + public void GivenXRangeWhenYRangeComparedThenValidateIfSubset( + bool xIsMinInclusive, + bool xIsMaxInclusive, + string xMinValue, + string xMaxValue, + bool yIsMinInclusive, + bool yIsMaxInclusive, + string yMinValue, + string yMaxValue, + bool expectedIsSubset) + { + bool actualIsSubset = ContainerCore.IsSubset( + new Documents.Routing.Range(isMinInclusive: xIsMinInclusive, isMaxInclusive: xIsMaxInclusive, min: xMinValue, max: xMaxValue), + new Documents.Routing.Range(isMinInclusive: yIsMinInclusive, isMaxInclusive: yIsMaxInclusive, min: yMinValue, max: yMaxValue)); + + Assert.AreEqual( + expected: expectedIsSubset, + actual: actualIsSubset); + } + + /// + /// + /// + /// Indicates whether the x range's minimum value is inclusive. + /// Indicates whether the x range's maximum value is inclusive. + /// The minimum value of the x range. + /// The maximum value of the x range. + /// Indicates whether the y range's minimum value is inclusive. + /// Indicates whether the y range's maximum value is inclusive. + /// The minimum value of the y range. + /// The maximum value of the y range. + [TestMethod] + [Owner("philipthomas-MSFT")] + [DataRow(true, true, "A", "Y", true, false, "A", "W", DisplayName = "(true, false) Given x range (A to Y) is inclusive at min and max, and y range (A to W) is inclusive at min and exclusive at max, expects NotSupportedException")] + [DataRow(true, true, "A", "Z", true, false, "A", "X", DisplayName = "(true, false) Given x range (A to Z) is inclusive at min and max, and y range (A to X) is inclusive at min and exclusive at max, expects NotSupportedException")] + [DataRow(true, true, "A", "Y", true, false, "A", "Y", DisplayName = "(true, false) Given x range (A to Y) is inclusive at min and max, and y range (A to Y) is inclusive at min and exclusive at max, expects NotSupportedException")] + public void GivenXMaxInclusiveYMaxExclusiveWhenCallingIsSubsetThenExpectNotSupportedExceptionIsThrown( + bool xIsMinInclusive, + bool xIsMaxInclusive, + string xMinValue, + string xMaxValue, + bool yIsMinInclusive, + bool yIsMaxInclusive, + string yMinValue, + string yMaxValue) + { + NotSupportedException exception = Assert.ThrowsException(() => ContainerCore.IsSubset( + new Documents.Routing.Range(min: xMinValue, max: xMaxValue, isMinInclusive: xIsMinInclusive, isMaxInclusive: xIsMaxInclusive), + new Documents.Routing.Range(min: yMinValue, max: yMaxValue, isMinInclusive: yIsMinInclusive, isMaxInclusive: yIsMaxInclusive))); + + Assert.IsNotNull(exception); + } + + /// + /// + /// + [TestMethod] + [Owner("philipthomas-MSFT")] + public void GivenNullXFeedRangeWhenCallingIsSubsetThenArgumentNullExceptionIsThrown() + { + ArgumentNullException exception = Assert.ThrowsException(() => ContainerCore.IsSubset( + null, + new Documents.Routing.Range(min: "A", max: "Z", isMinInclusive: true, isMaxInclusive: true))); + + Assert.IsNotNull(exception); + } + + /// + /// + /// + [TestMethod] + [Owner("philipthomas-MSFT")] + public void GivenNullYFeedRangeWhenCallingIsSubsetThenArgumentNullExceptionIsThrown() + { + ArgumentNullException exception = Assert.ThrowsException(() => ContainerCore.IsSubset( + new Documents.Routing.Range(min: "A", max: "Z", isMinInclusive: true, isMaxInclusive: true), + null)); + + Assert.IsNotNull(exception); + } + + /// + /// Validates if all ranges in the list have consistent inclusivity for both IsMinInclusive and IsMaxInclusive. + /// Throws InvalidOperationException if any inconsistencies are found. + /// + /// + /// + /// + /// + /// Indicates if the test should pass without throwing an exception. + /// IsMinInclusive value for first range. + /// IsMaxInclusive value for first range. + /// IsMinInclusive value for second range. + /// IsMaxInclusive value for second range. + /// IsMinInclusive value for third range. + /// IsMaxInclusive value for third range. + /// The expected exception message.> + [TestMethod] + [Owner("philipthomas-MSFT")] + [DataRow(true, true, false, true, false, true, false, "", DisplayName = "All ranges consistent")] + [DataRow(false, true, false, false, false, true, false, "Not all 'IsMinInclusive' or 'IsMaxInclusive' values are the same. IsMinInclusive found: True, False, IsMaxInclusive found: False.", DisplayName = "Inconsistent MinInclusive")] + [DataRow(false, true, false, true, true, true, false, "Not all 'IsMinInclusive' or 'IsMaxInclusive' values are the same. IsMinInclusive found: True, IsMaxInclusive found: False, True.", DisplayName = "Inconsistent MaxInclusive")] + [DataRow(false, true, false, false, true, true, false, "Not all 'IsMinInclusive' or 'IsMaxInclusive' values are the same. IsMinInclusive found: True, False, IsMaxInclusive found: False, True.", DisplayName = "Inconsistent Min and Max Inclusive")] + [DataRow(true, null, null, null, null, null, null, "", DisplayName = "Empty range list")] + public void GivenListOfFeedRangesEnsureConsistentInclusivityValidatesRangesTest( + bool shouldNotThrow, + bool? isMin1, + bool? isMax1, + bool? isMin2, + bool? isMax2, + bool? isMin3, + bool? isMax3, + string expectedMessage) + { + List> ranges = new List>(); + + if (isMin1.HasValue && isMax1.HasValue) + { + ranges.Add(new Documents.Routing.Range(min: "A", max: "B", isMinInclusive: isMin1.Value, isMaxInclusive: isMax1.Value)); + } + + if (isMin2.HasValue && isMax2.HasValue) + { + ranges.Add(new Documents.Routing.Range(min: "C", max: "D", isMinInclusive: isMin2.Value, isMaxInclusive: isMax2.Value)); + } + + if (isMin3.HasValue && isMax3.HasValue) + { + ranges.Add(new Documents.Routing.Range(min: "E", max: "F", isMinInclusive: isMin3.Value, isMaxInclusive: isMax3.Value)); + } + + InvalidOperationException exception = default; + + if (!shouldNotThrow) + { + exception = Assert.ThrowsException(() => ContainerCore.EnsureConsistentInclusivity(ranges)); + + Assert.IsNotNull(exception); + Assert.AreEqual(expected: expectedMessage, actual: exception.Message); + + return; + } + + Assert.IsNull(exception); + } + } + + internal record struct ContainerContext( + ContainerInternal Container, + PartitionKeyDefinitionVersion Version, + bool IsHierarchicalPartition) + { + public override readonly string ToString() + { + return $"{{\"Container\": \"{this.Container.Id}\", \"Version\": \"{this.Version}\", \"IsHierarchicalPartition\": {this.IsHierarchicalPartition.ToString().ToLower()}}}"; + } + } + + internal static class TestContextExtensions + { + public const string FeedRangeComparisonFailure = "Test failed for container '{0}' using Partition Key Definition Version '{1}' with {2}. Expected the feed range comparison result to be '{3}', but the actual result was '{4}'."; + + public const string ExceptionMessageMismatch = "Test failed for container '{0}' using Partition Key Definition Version '{1}' with {2}. Expected the exception message to contain '{3}', but the actual message was '{4}'."; + + public const string IsMinInclusiveExceptionMismatch = "Test failed for container '{0}' using Partition Key Definition Version '{1}' with {2}. Expected the exception message to contain 'IsMinInclusive must be true.', but the actual message was '{3}'."; + + /// + /// Attempts to retrieve the list of objects stored in the properties. + /// If the property "ContainerContexts" exists and is of type , it is returned via the out parameter. + /// Returns true if successful, false otherwise. + /// + /// The instance from which to attempt retrieving the container contexts. + /// When this method returns, contains the if the retrieval was successful; otherwise, null. + /// true if the retrieval was successful; otherwise, false. + public static bool TryGetContainerContexts(this TestContext testContext, out List containerContexts) + { + if (testContext.Properties["ContainerContexts"] is List contexts) + { + containerContexts = contexts; + return true; + } + else + { + containerContexts = null; + return false; + } + } + + /// + /// Logs exceptions, prints details, and throws an AssertFailedException with the aggregated exceptions. + /// + /// The instance from which to attempt retrieving the container contexts. + /// A collection of exceptions to aggregate and log. + public static void HandleAggregatedExceptions(this TestContext testContext, ConcurrentBag exceptions) + { + // Check if any exceptions were captured + if (exceptions.Any()) + { + // Aggregate the exceptions + AggregateException aggregateException = new AggregateException(exceptions); + + // Log out the details of each inner exception + foreach (Exception innerException in aggregateException.InnerExceptions) + { + testContext.WriteLine($"Exception: {innerException.Message}"); + testContext.WriteLine(innerException.StackTrace); + } + + // Throw an AssertFailedException with the aggregated exceptions + throw new AssertFailedException("One or more assertions failed. See inner exceptions for details.", aggregateException); + } + } + + // + /// Asynchronously sets up the ContainerContexts property in the TestContext by creating containers with specified partition key definitions. + /// + /// The instance from which to attempt retrieving the container contexts. + /// The Cosmos database used for creating the containers. + /// A delegate function that creates a container with a single partition key definition version asynchronously. + /// A delegate function that creates a container with a hierarchical partition key definition version asynchronously. + /// A task representing the asynchronous operation, which sets up the ContainerContexts property in the TestContext. + public static async Task SetContainerContextsAsync( + this TestContext testContext, + Database cosmosDatabase, + Func> createSinglePartitionContainerAsync, + Func> createHierarchicalPartitionContainerAsync) + { + testContext.Properties["ContainerContexts"] = new List() + { + new (await createSinglePartitionContainerAsync(cosmosDatabase, PartitionKeyDefinitionVersion.V1), PartitionKeyDefinitionVersion.V1, false), + new (await createSinglePartitionContainerAsync(cosmosDatabase, PartitionKeyDefinitionVersion.V2), PartitionKeyDefinitionVersion.V2, false), + new (await createHierarchicalPartitionContainerAsync(cosmosDatabase, PartitionKeyDefinitionVersion.V1), PartitionKeyDefinitionVersion.V1, true), + new (await createHierarchicalPartitionContainerAsync(cosmosDatabase, PartitionKeyDefinitionVersion.V2), PartitionKeyDefinitionVersion.V2, true), + }; + } + + /// + /// Logs a message indicating the current container test being executed. + /// + /// The instance from which to attempt retrieving the container contexts. + /// The container context that is being executed. + public static void LogTestExecutionForContainer(this TestContext testContext, ContainerContext containerContext) + { + string partitionType = containerContext.IsHierarchicalPartition ? "Hierarchical Partition" : "Single Partition"; + + testContext.WriteLine($"Executing test for container with ID: '{containerContext.Container.Id}', " + + $"Partition Key Definition Version: '{containerContext.Version}', " + + $"{partitionType}."); + } + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json index 1107f80682..17523aa5c9 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json @@ -323,6 +323,11 @@ "Attributes": [], "MethodInfo": "System.Threading.Tasks.Task`1[Microsoft.Azure.Cosmos.ResponseMessage] DeleteAllItemsByPartitionKeyStreamAsync(Microsoft.Azure.Cosmos.PartitionKey, Microsoft.Azure.Cosmos.RequestOptions, System.Threading.CancellationToken);IsAbstract:True;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, + "System.Threading.Tasks.Task`1[System.Boolean] IsFeedRangePartOfAsync(Microsoft.Azure.Cosmos.FeedRange, Microsoft.Azure.Cosmos.FeedRange, System.Threading.CancellationToken)": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "System.Threading.Tasks.Task`1[System.Boolean] IsFeedRangePartOfAsync(Microsoft.Azure.Cosmos.FeedRange, Microsoft.Azure.Cosmos.FeedRange, System.Threading.CancellationToken);IsAbstract:False;IsStatic:False;IsVirtual:True;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + }, "System.Threading.Tasks.Task`1[System.Collections.Generic.IEnumerable`1[System.String]] GetPartitionKeyRangesAsync(Microsoft.Azure.Cosmos.FeedRange, System.Threading.CancellationToken)": { "Type": "Method", "Attributes": [],