From 3ad4878b6dd39015a8963d9e34ad4a957f4eb9d4 Mon Sep 17 00:00:00 2001 From: Nalu Tripician <27316859+NaluTripician@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:35:07 -0700 Subject: [PATCH] Subpartitioning: Fixes bug for queries on subpartitioned containers (#3934) * initial fix, needs testing on prod * test fix * clean up pr * query rework * refactors previous changes * requested changes and bug fixes * nits * requested changes * bug fixes * start of test * added test * nit: changed name of EffectivePartitionKeyRanges to EffectiveRangesForPartitionKey * Address code comments * Address code comments * saving work * addresses code comments * nit, spacing * PartitionKeyHash fixes * Fixes bugs in tests * Removed bad method, added additional test coverage * Removed EffectivePartitionKeyString use * test fix * requested changes * Requested changes * fixed test * Test fix * Added comment --------- Co-authored-by: SrinikhilReddy --- .../CosmosQueryExecutionContextFactory.cs | 94 +++++----- .../QueryClient/ContainerQueryProperties.cs | 11 +- .../Core/QueryClient/CosmosQueryClient.cs | 9 +- .../Query/v3Query/CosmosQueryClientCore.cs | 35 ++-- .../src/Routing/PartitionKeyHash.cs | 30 +++- .../src/Routing/PartitionKeyHashRange.cs | 2 +- .../src/Routing/PartitionKeyHashRanges.cs | 14 +- .../CosmosItemTests.cs | 4 +- .../CosmosQueryClientCoreTest.cs | 2 +- .../PartitionKeyHashBaselineTest.Lists.xml | 32 ++++ .../Microsoft.Azure.Cosmos.Tests.csproj | 3 + .../Pagination/InMemoryContainer.cs | 164 ++++++++++++++---- ...misticDirectExecutionQueryBaselineTests.cs | 23 ++- .../Query/SplitPartitionQueryTests.cs | 77 ++++++++ .../Routing/PartitionKeyHashBaselineTest.cs | 63 +++++++ .../PartitionKeyHashRangeSplitterAndMerger.cs | 49 +++++- 16 files changed, 471 insertions(+), 141 deletions(-) create mode 100644 Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/BaselineTest/TestBaseline/PartitionKeyHashBaselineTest.Lists.xml create mode 100644 Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/SplitPartitionQueryTests.cs rename Microsoft.Azure.Cosmos/{src => tests/Microsoft.Azure.Cosmos.Tests}/Routing/PartitionKeyHashRangeSplitterAndMerger.cs (73%) diff --git a/Microsoft.Azure.Cosmos/src/Query/Core/Pipeline/CosmosQueryExecutionContextFactory.cs b/Microsoft.Azure.Cosmos/src/Query/Core/Pipeline/CosmosQueryExecutionContextFactory.cs index 47aa1c4490..a526669bfa 100644 --- a/Microsoft.Azure.Cosmos/src/Query/Core/Pipeline/CosmosQueryExecutionContextFactory.cs +++ b/Microsoft.Azure.Cosmos/src/Query/Core/Pipeline/CosmosQueryExecutionContextFactory.cs @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos.Query.Core.ExecutionContext using System.Linq; using System.Threading; using System.Threading.Tasks; + using global::Azure; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Cosmos.CosmosElements; using Microsoft.Azure.Cosmos.Pagination; @@ -27,6 +28,7 @@ namespace Microsoft.Azure.Cosmos.Query.Core.ExecutionContext using Microsoft.Azure.Cosmos.SqlObjects; using Microsoft.Azure.Cosmos.SqlObjects.Visitors; using Microsoft.Azure.Cosmos.Tracing; + using Microsoft.Azure.Documents.Routing; internal static class CosmosQueryExecutionContextFactory { @@ -211,10 +213,10 @@ private static async Task> TryCreateCoreContextAsy // Only thing that matters is that we target the correct range. Documents.PartitionKeyDefinition partitionKeyDefinition = GetPartitionKeyDefinition(inputParameters, containerQueryProperties); - List targetRanges = await cosmosQueryContext.QueryClient.GetTargetPartitionKeyRangesByEpkStringAsync( + List targetRanges = await cosmosQueryContext.QueryClient.GetTargetPartitionKeyRangesAsync( cosmosQueryContext.ResourceLink, containerQueryProperties.ResourceId, - containerQueryProperties.EffectivePartitionKeyString, + containerQueryProperties.EffectiveRangesForPartitionKey, forceRefresh: false, createQueryPipelineTrace); @@ -635,67 +637,54 @@ private static async Task GetPartitionedQueryExec ITrace trace) { List targetRanges; - if (containerQueryProperties.EffectivePartitionKeyString != null) + if (containerQueryProperties.EffectiveRangesForPartitionKey != null) { - targetRanges = await queryClient.GetTargetPartitionKeyRangesByEpkStringAsync( + targetRanges = await queryClient.GetTargetPartitionKeyRangesAsync( resourceLink, containerQueryProperties.ResourceId, - containerQueryProperties.EffectivePartitionKeyString, + containerQueryProperties.EffectiveRangesForPartitionKey, forceRefresh: false, trace); } else if (TryGetEpkProperty(properties, out string effectivePartitionKeyString)) { - targetRanges = await queryClient.GetTargetPartitionKeyRangesByEpkStringAsync( + //Note that here we have no way to consume the EPK string as there is no way to convert + //the string to the partition key type to evaulate the number of components which needs to be done for the + //multihahs methods/classes. This is particually important for queries with prefix partition key. + //the EPK sting header is only for internal use but this needs to be fixed in the future. + List> effectiveRanges = new List> + { Range.GetPointRange(effectivePartitionKeyString) }; + + targetRanges = await queryClient.GetTargetPartitionKeyRangesAsync( resourceLink, containerQueryProperties.ResourceId, - effectivePartitionKeyString, + effectiveRanges, forceRefresh: false, trace); } else if (feedRangeInternal != null) { targetRanges = await queryClient.GetTargetPartitionKeyRangeByFeedRangeAsync( - resourceLink, - containerQueryProperties.ResourceId, - containerQueryProperties.PartitionKeyDefinition, - feedRangeInternal, - forceRefresh: false, - trace); + resourceLink, + containerQueryProperties.ResourceId, + containerQueryProperties.PartitionKeyDefinition, + feedRangeInternal, + forceRefresh: false, + trace); } else { - targetRanges = await queryClient.GetTargetPartitionKeyRangesAsync( - resourceLink, - containerQueryProperties.ResourceId, - partitionedQueryExecutionInfo.QueryRanges, - forceRefresh: false, - trace); + targetRanges = await queryClient.GetTargetPartitionKeyRangesAsync( + resourceLink, + containerQueryProperties.ResourceId, + partitionedQueryExecutionInfo.QueryRanges, + forceRefresh: false, + trace); } return targetRanges; } - private static void SetTestInjectionPipelineType(InputParameters inputParameters, string pipelineType) - { - TestInjections.ResponseStats responseStats = inputParameters?.TestInjections?.Stats; - if (responseStats != null) - { - if (pipelineType == OptimisticDirectExecution) - { - responseStats.PipelineType = TestInjections.PipelineType.OptimisticDirectExecution; - } - else if (pipelineType == Specialized) - { - responseStats.PipelineType = TestInjections.PipelineType.Specialized; - } - else - { - responseStats.PipelineType = TestInjections.PipelineType.Passthrough; - } - } - } - private static bool TryGetEpkProperty( IReadOnlyDictionary properties, out string effectivePartitionKeyString) @@ -718,6 +707,26 @@ private static bool TryGetEpkProperty( return false; } + private static void SetTestInjectionPipelineType(InputParameters inputParameters, string pipelineType) + { + TestInjections.ResponseStats responseStats = inputParameters?.TestInjections?.Stats; + if (responseStats != null) + { + if (pipelineType == OptimisticDirectExecution) + { + responseStats.PipelineType = TestInjections.PipelineType.OptimisticDirectExecution; + } + else if (pipelineType == Specialized) + { + responseStats.PipelineType = TestInjections.PipelineType.Specialized; + } + else + { + responseStats.PipelineType = TestInjections.PipelineType.Passthrough; + } + } + } + private static Documents.PartitionKeyDefinition GetPartitionKeyDefinition(InputParameters inputParameters, ContainerQueryProperties containerQueryProperties) { //todo:elasticcollections this may rely on information from collection cache which is outdated @@ -771,14 +780,13 @@ private static Documents.PartitionKeyDefinition GetPartitionKeyDefinition(InputP else { Documents.PartitionKeyDefinition partitionKeyDefinition = GetPartitionKeyDefinition(inputParameters, containerQueryProperties); - if (inputParameters.PartitionKey != null) + if (inputParameters.PartitionKey.HasValue) { Debug.Assert(partitionKeyDefinition != null, "CosmosQueryExecutionContextFactory Assert!", "PartitionKeyDefinition cannot be null if partitionKey is defined"); - - targetRanges = await cosmosQueryContext.QueryClient.GetTargetPartitionKeyRangesByEpkStringAsync( + targetRanges = await cosmosQueryContext.QueryClient.GetTargetPartitionKeyRangesAsync( cosmosQueryContext.ResourceLink, containerQueryProperties.ResourceId, - containerQueryProperties.EffectivePartitionKeyString, + containerQueryProperties.EffectiveRangesForPartitionKey, forceRefresh: false, trace); } diff --git a/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/ContainerQueryProperties.cs b/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/ContainerQueryProperties.cs index 0baaceb9c6..279b7585b9 100644 --- a/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/ContainerQueryProperties.cs +++ b/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/ContainerQueryProperties.cs @@ -4,24 +4,29 @@ namespace Microsoft.Azure.Cosmos.Query.Core.QueryClient { + using System.Collections.Generic; using Microsoft.Azure.Documents; + using Microsoft.Azure.Documents.Routing; internal readonly struct ContainerQueryProperties { public ContainerQueryProperties( string resourceId, - string effectivePartitionKeyString, + IReadOnlyList> effectivePartitionKeyRanges, PartitionKeyDefinition partitionKeyDefinition, Cosmos.GeospatialType geospatialType) { this.ResourceId = resourceId; - this.EffectivePartitionKeyString = effectivePartitionKeyString; + this.EffectiveRangesForPartitionKey = effectivePartitionKeyRanges; this.PartitionKeyDefinition = partitionKeyDefinition; this.GeospatialType = geospatialType; } public string ResourceId { get; } - public string EffectivePartitionKeyString { get; } + + //A PartitionKey has one range when it is a full PartitionKey value. + //It can span many it is a prefix PartitionKey for a sub-partitioned container. + public IReadOnlyList> EffectiveRangesForPartitionKey { get; } public PartitionKeyDefinition PartitionKeyDefinition { get; } public Cosmos.GeospatialType GeospatialType { get; } } diff --git a/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/CosmosQueryClient.cs b/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/CosmosQueryClient.cs index 3fa4cb90e7..d9dc0ac976 100644 --- a/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/CosmosQueryClient.cs +++ b/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/CosmosQueryClient.cs @@ -76,13 +76,6 @@ public abstract Task ExecuteQueryPlanRequestAsync public abstract void ClearSessionTokenCache(string collectionFullName); - public abstract Task> GetTargetPartitionKeyRangesByEpkStringAsync( - string resourceLink, - string collectionResourceId, - string effectivePartitionKeyString, - bool forceRefresh, - ITrace trace); - public abstract Task> GetTargetPartitionKeyRangeByFeedRangeAsync( string resourceLink, string collectionResourceId, @@ -94,7 +87,7 @@ public abstract Task ExecuteQueryPlanRequestAsync public abstract Task> GetTargetPartitionKeyRangesAsync( string resourceLink, string collectionResourceId, - List> providedRanges, + IReadOnlyList> providedRanges, bool forceRefresh, ITrace trace); diff --git a/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs b/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs index cce1a5268a..a392632649 100644 --- a/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs +++ b/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs @@ -63,17 +63,26 @@ public override async Task GetCachedContainerQueryProp trace, cancellationToken); - string effectivePartitionKeyString = null; + List> effectivePartitionKeyRange = null; if (partitionKey != null) { // Dis-ambiguate the NonePK if used PartitionKeyInternal partitionKeyInternal = partitionKey.Value.IsNone ? containerProperties.GetNoneValue() : partitionKey.Value.InternalKey; - effectivePartitionKeyString = partitionKeyInternal.GetEffectivePartitionKeyString(containerProperties.PartitionKey); + effectivePartitionKeyRange = new List> + { + PartitionKeyInternal.GetEffectivePartitionKeyRange( + containerProperties.PartitionKey, + new Range( + min: partitionKeyInternal, + max: partitionKeyInternal, + isMinInclusive: true, + isMaxInclusive: true)) + }; } return new ContainerQueryProperties( containerProperties.ResourceId, - effectivePartitionKeyString, + effectivePartitionKeyRange, containerProperties.PartitionKey, containerProperties.GeospatialConfig.GeospatialType); } @@ -200,24 +209,6 @@ public override async Task ExecuteQueryPlanReques return partitionedQueryExecutionInfo; } - public override Task> GetTargetPartitionKeyRangesByEpkStringAsync( - string resourceLink, - string collectionResourceId, - string effectivePartitionKeyString, - bool forceRefresh, - ITrace trace) - { - return this.GetTargetPartitionKeyRangesAsync( - resourceLink, - collectionResourceId, - new List> - { - Range.GetPointRange(effectivePartitionKeyString) - }, - forceRefresh, - trace); - } - public override async Task> GetTargetPartitionKeyRangeByFeedRangeAsync( string resourceLink, string collectionResourceId, @@ -243,7 +234,7 @@ public override async Task> GetTargetPartitionKeyRangeBy public override async Task> GetTargetPartitionKeyRangesAsync( string resourceLink, string collectionResourceId, - List> providedRanges, + IReadOnlyList> providedRanges, bool forceRefresh, ITrace trace) { diff --git a/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHash.cs b/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHash.cs index 42ef970915..0d7189abb2 100644 --- a/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHash.cs +++ b/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHash.cs @@ -5,7 +5,7 @@ namespace Microsoft.Azure.Cosmos.Routing { using System; - using System.Runtime.CompilerServices; + using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using Microsoft.Azure.Documents.Routing; @@ -35,12 +35,34 @@ namespace Microsoft.Azure.Cosmos.Routing /// internal readonly struct PartitionKeyHash : IComparable, IEquatable { + private readonly IReadOnlyList values; + public PartitionKeyHash(UInt128 value) + : this(new UInt128[] { value }) { - this.Value = value; } - public UInt128 Value { get; } + public PartitionKeyHash(UInt128[] values) + { + StringBuilder stringBuilder = new StringBuilder(); + foreach (UInt128 value in values) + { + if (stringBuilder.Length > 0) + { + stringBuilder.Append('-'); + } + stringBuilder.Append(value.ToString()); + } + + this.Value = stringBuilder.ToString(); + this.values = values; + } + + public readonly static PartitionKeyHash None = new PartitionKeyHash(0); + + public string Value { get; } + + internal readonly IReadOnlyList HashValues => this.values; public int CompareTo(PartitionKeyHash other) { @@ -66,7 +88,7 @@ public override bool Equals(object obj) public override int GetHashCode() => this.Value.GetHashCode(); - public override string ToString() => this.Value.ToString(); + public override string ToString() => this.Value; public static bool TryParse(string value, out PartitionKeyHash parsedValue) { diff --git a/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRange.cs b/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRange.cs index 21e257b49a..7e8265e470 100644 --- a/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRange.cs +++ b/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRange.cs @@ -181,4 +181,4 @@ public override string ToString() return stringBuilder.ToString(); } } -} +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRanges.cs b/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRanges.cs index a927fcba87..f15556a23d 100644 --- a/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRanges.cs +++ b/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRanges.cs @@ -132,9 +132,9 @@ public static CreateOutcome TryCreate( { if (partitionKeyHashRange.StartInclusive.HasValue) { - if (partitionKeyHashRange.StartInclusive.Value.Value < minStart) + if (partitionKeyHashRange.StartInclusive.Value.HashValues[0] < minStart) { - minStart = partitionKeyHashRange.StartInclusive.Value.Value; + minStart = partitionKeyHashRange.StartInclusive.Value.HashValues[0]; } } else @@ -144,9 +144,9 @@ public static CreateOutcome TryCreate( if (partitionKeyHashRange.EndExclusive.HasValue) { - if (partitionKeyHashRange.EndExclusive.Value.Value > maxEnd) + if (partitionKeyHashRange.EndExclusive.Value.HashValues[0] > maxEnd) { - maxEnd = partitionKeyHashRange.EndExclusive.Value.Value; + maxEnd = partitionKeyHashRange.EndExclusive.Value.HashValues[0]; } } else @@ -154,8 +154,8 @@ public static CreateOutcome TryCreate( maxEnd = UInt128.MaxValue; } - UInt128 width = partitionKeyHashRange.EndExclusive.GetValueOrDefault(new PartitionKeyHash(UInt128.MaxValue)).Value - - partitionKeyHashRange.StartInclusive.GetValueOrDefault(new PartitionKeyHash(UInt128.MinValue)).Value; + UInt128 width = partitionKeyHashRange.EndExclusive.GetValueOrDefault(new PartitionKeyHash(UInt128.MaxValue)).HashValues[0] + - partitionKeyHashRange.StartInclusive.GetValueOrDefault(new PartitionKeyHash(UInt128.MinValue)).HashValues[0]; sumOfWidth += width; if (sumOfWidth < width) { @@ -223,4 +223,4 @@ public enum CreateOutcome Success, } } -} +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs index b237c68c2a..3ce82c4607 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs @@ -1536,7 +1536,6 @@ public async Task EpkPointReadTest() epk = new PartitionKey("test") .InternalKey .GetEffectivePartitionKeyString(this.containerSettings.PartitionKey); - properties = new Dictionary() { { WFConstants.BackendHeaders.EffectivePartitionKeyString, epk }, @@ -1586,9 +1585,11 @@ public async Task ItemEpkQuerySingleKeyRangeValidation() // If this fails the RUs of the container needs to be increased to ensure at least 2 partitions. Assert.IsTrue(ranges.Count > 1, " RUs of the container needs to be increased to ensure at least 2 partitions."); + ContainerQueryProperties containerQueryProperties = new ContainerQueryProperties( containerResponse.Resource.ResourceId, null, + //new List> { new Documents.Routing.Range("AA", "AA", true, true) }, containerResponse.Resource.PartitionKey, containerResponse.Resource.GeospatialConfig.GeospatialType); @@ -1606,6 +1607,7 @@ public async Task ItemEpkQuerySingleKeyRangeValidation() trace: NoOpTrace.Singleton); Assert.IsTrue(partitionKeyRanges.Count == 1, "Only 1 partition key range should be selected since the EPK option is set."); + } finally { diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosQueryClientCoreTest.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosQueryClientCoreTest.cs index 21a8b4ad59..3f325eef9a 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosQueryClientCoreTest.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosQueryClientCoreTest.cs @@ -51,7 +51,7 @@ public async Task TryGetOverlappingRangesAsyncTest() Assert.IsNotNull(containerProperties); Assert.IsNotNull(containerProperties.ResourceId); - Assert.IsNotNull(containerProperties.EffectivePartitionKeyString); + Assert.IsNotNull(containerProperties.EffectiveRangesForPartitionKey); IReadOnlyList pkRange = await this.queryClientCore.TryGetOverlappingRangesAsync( collectionResourceId: containerProperties.ResourceId, diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/BaselineTest/TestBaseline/PartitionKeyHashBaselineTest.Lists.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/BaselineTest/TestBaseline/PartitionKeyHashBaselineTest.Lists.xml new file mode 100644 index 0000000000..3f7faa9f26 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/BaselineTest/TestBaseline/PartitionKeyHashBaselineTest.Lists.xml @@ -0,0 +1,32 @@ + + + + 1 Path List + ["/path1"] + + + 00-00-00-00-00-00-00-00-00-00-00-00-0A-A1-CC-05 + B6-3D-B6-C4-3A-4F-1B-01-4C-63-B0-C2-E6-28-E8-12 + + + + + 2 Path List + ["/path1","/path2"] + + + 00-00-00-00-00-00-00-00-00-00-00-00-0A-A1-CC-05-00-00-00-00-00-00-00-00-00-00-00-00-C9-1E-F0-78 + B6-3D-B6-C4-3A-4F-1B-01-4C-63-B0-C2-E6-28-E8-12-A6-0C-6C-BE-5A-2D-38-6E-5D-AE-1A-AC-94-21-6B-6C + + + + + 3 Path List + ["/path1","/path2","/path3"] + + + 00-00-00-00-00-00-00-00-00-00-00-00-0A-A1-CC-05-00-00-00-00-00-00-00-00-00-00-00-00-C9-1E-F0-78-00-00-00-00-00-00-00-00-00-00-00-00-9A-B4-68-CD + B6-3D-B6-C4-3A-4F-1B-01-4C-63-B0-C2-E6-28-E8-12-A6-0C-6C-BE-5A-2D-38-6E-5D-AE-1A-AC-94-21-6B-6C-88-A6-18-5D-2D-D5-1C-96-D0-47-75-B7-2E-FA-BE-08 + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Microsoft.Azure.Cosmos.Tests.csproj b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Microsoft.Azure.Cosmos.Tests.csproj index 4d25f7e5a7..5b429468d8 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Microsoft.Azure.Cosmos.Tests.csproj +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Microsoft.Azure.Cosmos.Tests.csproj @@ -115,6 +115,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Pagination/InMemoryContainer.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Pagination/InMemoryContainer.cs index f2201d3cc4..5c6e8961e7 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Pagination/InMemoryContainer.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Pagination/InMemoryContainer.cs @@ -31,6 +31,7 @@ namespace Microsoft.Azure.Cosmos.Tests.Pagination using Microsoft.Azure.Cosmos.Tracing; using Microsoft.Azure.Documents; using ResourceIdentifier = Cosmos.Pagination.ResourceIdentifier; + using UInt128 = UInt128; // Collection useful for mocking requests and repartitioning (splits / merge). internal class InMemoryContainer : IMonadicDocumentContainer @@ -280,8 +281,8 @@ static Task> CreateNotFoundException(CosmosElement partitionKey requestCharge: 42))); } - PartitionKeyHash partitionKeyHash = GetHashFromPartitionKey( - partitionKey, + PartitionKeyHash partitionKeyHash = GetHashFromPartitionKeys( + new List { partitionKey }, this.partitionKeyDefinition); if (!this.partitionedRecords.TryGetValue(partitionKeyHash, out Records records)) @@ -797,9 +798,20 @@ public Task MonadicSplitAsync( } // Split the range space - PartitionKeyHashRanges partitionKeyHashRanges = PartitionKeyHashRangeSplitterAndMerger.SplitRange( - parentRange, - rangeCount: 2); + PartitionKeyHashRanges partitionKeyHashRanges; + if (this.partitionKeyDefinition.Kind == PartitionKind.MultiHash && + this.partitionKeyDefinition.Paths.Count > 1) + { + //For MultiHash, to help with testing we will split using the median partition key among documents. + PartitionKeyHash midPoint = this.ComputeMedianSplitPointAmongDocumentsInPKRange(parentRange); + partitionKeyHashRanges = PartitionKeyHashRangeSplitterAndMerger.SplitRange(parentRange, midPoint); + } + else + { + partitionKeyHashRanges = PartitionKeyHashRangeSplitterAndMerger.SplitRange( + parentRange, + rangeCount: 2); + } // Update the partition routing map int maxPartitionKeyRangeId = this.partitionKeyRangeIdToHashRange.Keys.Max(); @@ -1084,16 +1096,16 @@ private static PartitionKeyHash GetHashFromPayload( CosmosObject payload, PartitionKeyDefinition partitionKeyDefinition) { - CosmosElement partitionKey = GetPartitionKeyFromPayload(payload, partitionKeyDefinition); - return GetHashFromPartitionKey(partitionKey, partitionKeyDefinition); + IList partitionKey = GetPartitionKeysFromPayload(payload, partitionKeyDefinition); + return GetHashFromPartitionKeys(partitionKey, partitionKeyDefinition); } private static PartitionKeyHash GetHashFromObjectModel( Cosmos.PartitionKey payload, PartitionKeyDefinition partitionKeyDefinition) { - CosmosElement partitionKey = GetPartitionKeyFromObjectModel(payload); - return GetHashFromPartitionKey(partitionKey, partitionKeyDefinition); + IList partitionKeys = GetPartitionKeysFromObjectModel(payload); + return GetHashFromPartitionKeys(partitionKeys, partitionKeyDefinition); } private static CosmosElement GetPartitionKeyFromPayload(CosmosObject payload, PartitionKeyDefinition partitionKeyDefinition) @@ -1131,23 +1143,56 @@ private static CosmosElement GetPartitionKeyFromPayload(CosmosObject payload, Pa return partitionKey; } - private static CosmosElement GetPartitionKeyFromObjectModel(Cosmos.PartitionKey payload) + private static IList GetPartitionKeysFromPayload(CosmosObject payload, PartitionKeyDefinition partitionKeyDefinition) { - CosmosArray partitionKeyPayload = CosmosArray.Parse(payload.ToJsonString()); - if (partitionKeyPayload.Count != 1) + // Restrict the partition key definition for now to keep things simple + if (partitionKeyDefinition.Kind != PartitionKind.MultiHash && partitionKeyDefinition.Kind != PartitionKind.Hash) { - throw new ArgumentOutOfRangeException("Can only support a single partition key path."); + throw new ArgumentOutOfRangeException("Can only support Hash/MultiHash partitioning"); + } + + if (partitionKeyDefinition.Version != Documents.PartitionKeyDefinitionVersion.V2) + { + throw new ArgumentOutOfRangeException("Can only support hash v2"); + } + + IList cosmosElements = new List(); + foreach (string partitionKeyPath in partitionKeyDefinition.Paths) + { + IEnumerable tokens = partitionKeyPath.Split("/").Skip(1); + CosmosElement partitionKey = payload; + foreach (string token in tokens) + { + if (partitionKey != default) + { + if (!payload.TryGetValue(token, out partitionKey)) + { + partitionKey = default; + } + } + } + cosmosElements.Add(partitionKey); } + return cosmosElements; + } - return partitionKeyPayload[0]; + private static IList GetPartitionKeysFromObjectModel(Cosmos.PartitionKey payload) + { + CosmosArray partitionKeyPayload = CosmosArray.Parse(payload.ToJsonString()); + List cosmosElemementPayload = new List(); + foreach (CosmosElement element in partitionKeyPayload) + { + cosmosElemementPayload.Add(element); + } + return cosmosElemementPayload; } - private static PartitionKeyHash GetHashFromPartitionKey(CosmosElement partitionKey, PartitionKeyDefinition partitionKeyDefinition) + private static PartitionKeyHash GetHashFromPartitionKeys(IList partitionKeys, PartitionKeyDefinition partitionKeyDefinition) { // Restrict the partition key definition for now to keep things simple - if (partitionKeyDefinition.Kind != PartitionKind.Hash) + if (partitionKeyDefinition.Kind != PartitionKind.MultiHash && partitionKeyDefinition.Kind != PartitionKind.Hash) { - throw new ArgumentOutOfRangeException("Can only support hash partitioning"); + throw new ArgumentOutOfRangeException("Can only support Hash/MultiHash partitioning"); } if (partitionKeyDefinition.Version != Documents.PartitionKeyDefinitionVersion.V2) @@ -1155,21 +1200,41 @@ private static PartitionKeyHash GetHashFromPartitionKey(CosmosElement partitionK throw new ArgumentOutOfRangeException("Can only support hash v2"); } - if (partitionKeyDefinition.Paths.Count != 1) + IList partitionKeyHashValues = new List(); + + foreach (CosmosElement partitionKey in partitionKeys) { - throw new ArgumentOutOfRangeException("Can only support a single partition key path."); + if (partitionKey is CosmosArray cosmosArray) + { + foreach (CosmosElement element in cosmosArray) + { + PartitionKeyHash elementHash = element switch + { + null => PartitionKeyHash.V2.HashUndefined(), + CosmosString stringPartitionKey => PartitionKeyHash.V2.Hash(stringPartitionKey.Value), + CosmosNumber numberPartitionKey => PartitionKeyHash.V2.Hash(Number64.ToDouble(numberPartitionKey.Value)), + CosmosBoolean cosmosBoolean => PartitionKeyHash.V2.Hash(cosmosBoolean.Value), + CosmosNull _ => PartitionKeyHash.V2.HashNull(), + _ => throw new ArgumentOutOfRangeException(), + }; + partitionKeyHashValues.Add(elementHash.HashValues[0]); + } + continue; + } + + PartitionKeyHash partitionKeyHash = partitionKey switch + { + null => PartitionKeyHash.V2.HashUndefined(), + CosmosString stringPartitionKey => PartitionKeyHash.V2.Hash(stringPartitionKey.Value), + CosmosNumber numberPartitionKey => PartitionKeyHash.V2.Hash(Number64.ToDouble(numberPartitionKey.Value)), + CosmosBoolean cosmosBoolean => PartitionKeyHash.V2.Hash(cosmosBoolean.Value), + CosmosNull _ => PartitionKeyHash.V2.HashNull(), + _ => throw new ArgumentOutOfRangeException(), + }; + partitionKeyHashValues.Add(partitionKeyHash.HashValues[0]); } - PartitionKeyHash partitionKeyHash = partitionKey switch - { - null => PartitionKeyHash.V2.HashUndefined(), - CosmosString stringPartitionKey => PartitionKeyHash.V2.Hash(stringPartitionKey.Value), - CosmosNumber numberPartitionKey => PartitionKeyHash.V2.Hash(Number64.ToDouble(numberPartitionKey.Value)), - CosmosBoolean cosmosBoolean => PartitionKeyHash.V2.Hash(cosmosBoolean.Value), - CosmosNull _ => PartitionKeyHash.V2.HashNull(), - _ => throw new ArgumentOutOfRangeException(), - }; - return partitionKeyHash; + return new PartitionKeyHash(partitionKeyHashValues.ToArray()); } private static CosmosObject ConvertRecordToCosmosElement(Record record) @@ -1196,9 +1261,16 @@ private static bool IsRecordWithinFeedRange( { if (feedRange is FeedRangePartitionKey feedRangePartitionKey) { - CosmosElement partitionKey = GetPartitionKeyFromObjectModel(feedRangePartitionKey.PartitionKey); - CosmosElement partitionKeyFromRecord = GetPartitionKeyFromPayload(record.Payload, partitionKeyDefinition); - return partitionKey.Equals(partitionKeyFromRecord); + IList partitionKey = GetPartitionKeysFromObjectModel(feedRangePartitionKey.PartitionKey); + IList partitionKeyFromRecord = GetPartitionKeysFromPayload(record.Payload, partitionKeyDefinition); + if (partitionKeyDefinition.Kind == PartitionKind.MultiHash) + { + PartitionKeyHash partitionKeyHash = GetHashFromPartitionKeys(partitionKey, partitionKeyDefinition); + PartitionKeyHash partitionKeyFromRecordHash = GetHashFromPartitionKeys(partitionKeyFromRecord, partitionKeyDefinition); + + return partitionKeyHash.Equals(partitionKeyFromRecordHash) || partitionKeyFromRecordHash.Value.StartsWith(partitionKeyHash.Value); + } + return partitionKey.SequenceEqual(partitionKeyFromRecord); } else if (feedRange is FeedRangeEpk feedRangeEpk) { @@ -1301,6 +1373,32 @@ private static FeedRangeEpk HashRangeToFeedRangeEpk(PartitionKeyHashRange hashRa isMaxInclusive: false)); } + private PartitionKeyHash ComputeMedianSplitPointAmongDocumentsInPKRange(PartitionKeyHashRange hashRange) + { + if (!this.partitionedRecords.TryGetValue(hashRange, out Records parentRecords)) + { + throw new InvalidOperationException("failed to find the range."); + } + + List partitionKeyHashes = new List(); + foreach (Record record in parentRecords) + { + PartitionKeyHash partitionKeyHash = GetHashFromPayload(record.Payload, this.partitionKeyDefinition); + partitionKeyHashes.Add(partitionKeyHash); + } + + partitionKeyHashes.Sort(); + PartitionKeyHash medianPkHash = partitionKeyHashes[partitionKeyHashes.Count / 2]; + + // For MultiHash Collection, split at top level to ensure documents for top level key exist across partitions + // after split + if (medianPkHash.HashValues.Count > 1) + { + return new PartitionKeyHash(medianPkHash.HashValues[0]); + } + + return medianPkHash; + } public Task> MonadicGetResourceIdentifierAsync(ITrace trace, CancellationToken cancellationToken) { return Task.FromResult(TryCatch.FromResult("AYIMAMmFOw8YAAAAAAAAAA==")); @@ -1516,4 +1614,4 @@ public SqlScalarExpression Visit(CosmosUndefined cosmosUndefined) } } } -} +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/OptimisticDirectExecutionQueryBaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/OptimisticDirectExecutionQueryBaselineTests.cs index 81c765adc4..9c18e8b6a4 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/OptimisticDirectExecutionQueryBaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/OptimisticDirectExecutionQueryBaselineTests.cs @@ -959,9 +959,16 @@ public override Task ForceRefreshCollectionCacheAsync(string collectionLink, Can public override Task GetCachedContainerQueryPropertiesAsync(string containerLink, Cosmos.PartitionKey? partitionKey, ITrace trace, CancellationToken cancellationToken) { - return Task.FromResult(new ContainerQueryProperties( + return Task.FromResult(new ContainerQueryProperties( "test", - WFConstants.BackendHeaders.EffectivePartitionKeyString, + new List> + { + new Range( + PartitionKeyInternal.MinimumInclusiveEffectivePartitionKey, + PartitionKeyInternal.MaximumExclusiveEffectivePartitionKey, + true, + true) + }, new PartitionKeyDefinition(), Cosmos.GeospatialType.Geometry)); } @@ -971,17 +978,7 @@ public override Task> GetTargetPartitionKeyRangeByFeedRa throw new NotImplementedException(); } - public override Task> GetTargetPartitionKeyRangesAsync(string resourceLink, string collectionResourceId, List> providedRanges, bool forceRefresh, ITrace trace) - { - return Task.FromResult(new List{new PartitionKeyRange() - { - MinInclusive = PartitionKeyInternal.MinimumInclusiveEffectivePartitionKey, - MaxExclusive = PartitionKeyInternal.MaximumExclusiveEffectivePartitionKey - } - }); - } - - public override Task> GetTargetPartitionKeyRangesByEpkStringAsync(string resourceLink, string collectionResourceId, string effectivePartitionKeyString, bool forceRefresh, ITrace trace) + public override Task> GetTargetPartitionKeyRangesAsync(string resourceLink, string collectionResourceId, IReadOnlyList> providedRanges, bool forceRefresh, ITrace trace) { return Task.FromResult(new List{new PartitionKeyRange() { diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/SplitPartitionQueryTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/SplitPartitionQueryTests.cs new file mode 100644 index 0000000000..4dd4ed1bd8 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/SplitPartitionQueryTests.cs @@ -0,0 +1,77 @@ +namespace Microsoft.Azure.Cosmos.Tests.Query +{ + using System; + using System.Threading.Tasks; + using Microsoft.Azure.Cosmos.Pagination; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.Azure.Documents; + using Microsoft.Azure.Cosmos.Tests.Pagination; + using Microsoft.Azure.Cosmos.CosmosElements; + using Microsoft.Azure.Cosmos.Query.Core.Monads; + using Microsoft.Azure.Cosmos.Tracing; + using Microsoft.Azure.Cosmos.Query.Core.Pipeline.Pagination; + + [TestClass] + public class SplitPartitionQueryTests + { + [TestMethod] + public async Task PrefixPartitionKeyQueryOnSplitParitionTest() + { + int numItems = 500; + IDocumentContainer documentContainer = await CreateSplitDocumentContainerAsync(numItems); + + string query = "SELECT * FROM c"; + for(int i = 0; i < 5; i++) + { + Cosmos.PartitionKey partitionKey = new PartitionKeyBuilder() + .Add(i.ToString()) + .Build(); + + QueryPage queryPage = await documentContainer.QueryAsync( + sqlQuerySpec: new Cosmos.Query.Core.SqlQuerySpec(query), + feedRangeState: new FeedRangeState(new FeedRangePartitionKey(partitionKey), state: null), + queryPaginationOptions: new QueryPaginationOptions(pageSizeHint: int.MaxValue), + trace: NoOpTrace.Singleton, + cancellationToken: default); + + Assert.AreEqual(numItems / 5, queryPage.Documents.Count); + } + } + + private static async Task CreateSplitDocumentContainerAsync(int numItems) + { + PartitionKeyDefinition partitionKeyDefinition = new PartitionKeyDefinition() + { + Paths = new System.Collections.ObjectModel.Collection() + { + "/id", + "/value1", + "/value2" + }, + Kind = PartitionKind.MultiHash, + Version = PartitionKeyDefinitionVersion.V2, + }; + + IMonadicDocumentContainer monadicDocumentContainer = new InMemoryContainer(partitionKeyDefinition); + DocumentContainer documentContainer = new DocumentContainer(monadicDocumentContainer); + + for (int i = 0; i < numItems; i++) + { + // Insert an item + CosmosObject item = CosmosObject.Parse($"{{\"id\" : \"{i%5}\", \"value1\" : \"{Guid.NewGuid()}\", \"value2\" : \"{i}\" }}"); + while (true) + { + TryCatch monadicCreateRecord = await documentContainer.MonadicCreateItemAsync(item, cancellationToken: default); + if (monadicCreateRecord.Succeeded) + { + break; + } + } + } + + await documentContainer.SplitAsync(FeedRangeEpk.FullRange, cancellationToken: default); + + return documentContainer; + } + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Routing/PartitionKeyHashBaselineTest.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Routing/PartitionKeyHashBaselineTest.cs index 4fb7b5b53f..f60d0849ca 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Routing/PartitionKeyHashBaselineTest.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Routing/PartitionKeyHashBaselineTest.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos.Tests.Routing { using System; using System.Collections.Generic; + using System.Linq; using System.Numerics; using System.Xml; using Microsoft.Azure.Cosmos.CosmosElements; @@ -126,6 +127,37 @@ public void Numbers() this.ExecuteTestSuite(inputs); } + [TestMethod] + public void Lists() + { + List inputs = new List() + { + new Input( + description: "1 Path List", + partitionKeyValue: CosmosArray.Create(new List() + { + CosmosString.Create("/path1") + })), + new Input( + description: "2 Path List", + partitionKeyValue: CosmosArray.Create(new List() + { + CosmosString.Create("/path1"), + CosmosString.Create("/path2") + })), + new Input( + description: "3 Path List", + partitionKeyValue: CosmosArray.Create(new List() + { + CosmosString.Create("/path1"), + CosmosString.Create("/path2"), + CosmosString.Create("/path3") + })), + }; + + this.ExecuteTestSuite(inputs); + } + public override Output ExecuteTest(Input input) { CosmosElement value = input.PartitionKeyValue; @@ -159,7 +191,38 @@ public override Output ExecuteTest(Input input) partitionKeyHashV1 = PartitionKeyHash.V1.Hash(Number64.ToDouble(cosmosNumber.Value)); partitionKeyHashV2 = PartitionKeyHash.V2.Hash(Number64.ToDouble(cosmosNumber.Value)); break; + case CosmosArray cosmosArray: + IList partitionKeyHashValuesV1 = new List(); + IList partitionKeyHashValuesV2 = new List(); + foreach (CosmosElement element in cosmosArray) + { + PartitionKeyHash elementHashV1 = element switch + { + null => PartitionKeyHash.V2.HashUndefined(), + CosmosString stringPartitionKey => PartitionKeyHash.V1.Hash(stringPartitionKey.Value), + CosmosNumber numberPartitionKey => PartitionKeyHash.V1.Hash(Number64.ToDouble(numberPartitionKey.Value)), + CosmosBoolean cosmosBoolean => PartitionKeyHash.V1.Hash(cosmosBoolean.Value), + CosmosNull _ => PartitionKeyHash.V1.HashNull(), + _ => throw new ArgumentOutOfRangeException(), + }; + partitionKeyHashValuesV1.Add(elementHashV1.HashValues[0]); + + PartitionKeyHash elementHashV2 = element switch + { + null => PartitionKeyHash.V2.HashUndefined(), + CosmosString stringPartitionKey => PartitionKeyHash.V2.Hash(stringPartitionKey.Value), + CosmosNumber numberPartitionKey => PartitionKeyHash.V2.Hash(Number64.ToDouble(numberPartitionKey.Value)), + CosmosBoolean cosmosBoolean => PartitionKeyHash.V2.Hash(cosmosBoolean.Value), + CosmosNull _ => PartitionKeyHash.V2.HashNull(), + _ => throw new ArgumentOutOfRangeException(), + }; + partitionKeyHashValuesV2.Add(elementHashV2.HashValues[0]); + } + + partitionKeyHashV1 = new PartitionKeyHash(partitionKeyHashValuesV1.ToArray()); + partitionKeyHashV2 = new PartitionKeyHash(partitionKeyHashValuesV2.ToArray()); + break; default: throw new ArgumentOutOfRangeException($"Unknown {nameof(CosmosElement)} type: {value.GetType()}."); } diff --git a/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRangeSplitterAndMerger.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Routing/PartitionKeyHashRangeSplitterAndMerger.cs similarity index 73% rename from Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRangeSplitterAndMerger.cs rename to Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Routing/PartitionKeyHashRangeSplitterAndMerger.cs index 54b72ae3a2..af005b1515 100644 --- a/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyHashRangeSplitterAndMerger.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Routing/PartitionKeyHashRangeSplitterAndMerger.cs @@ -39,8 +39,8 @@ public static SplitOutcome TrySplitRange(PartitionKeyHashRange partitionKeyHashR return SplitOutcome.NumRangesNeedsToBeGreaterThanZero; } - UInt128 actualEnd = partitionKeyHashRange.EndExclusive.HasValue ? partitionKeyHashRange.EndExclusive.Value.Value : UInt128.MaxValue; - UInt128 actualStart = partitionKeyHashRange.StartInclusive.HasValue ? partitionKeyHashRange.StartInclusive.Value.Value : UInt128.MinValue; + UInt128 actualEnd = partitionKeyHashRange.EndExclusive.HasValue ? partitionKeyHashRange.EndExclusive.Value.HashValues[0] : UInt128.MaxValue; + UInt128 actualStart = partitionKeyHashRange.StartInclusive.HasValue ? partitionKeyHashRange.StartInclusive.Value.HashValues[0] : UInt128.MinValue; UInt128 rangeLength = actualEnd - actualStart; if (rangeLength < rangeCount) { @@ -67,7 +67,7 @@ public static SplitOutcome TrySplitRange(PartitionKeyHashRange partitionKeyHashR for (int i = 1; i < rangeCount - 1; i++) { PartitionKeyHash start = new PartitionKeyHash(actualStart + (childRangeLength * i)); - PartitionKeyHash end = new PartitionKeyHash(start.Value + childRangeLength); + PartitionKeyHash end = new PartitionKeyHash(start.HashValues[0] + childRangeLength); childRanges.Add(new PartitionKeyHashRange(start, end)); } @@ -82,6 +82,39 @@ public static SplitOutcome TrySplitRange(PartitionKeyHashRange partitionKeyHashR return SplitOutcome.Success; } + public static PartitionKeyHashRanges SplitRange(PartitionKeyHashRange partitionKeyHashRange, PartitionKeyHash explicitSplitPoint) + { + SplitOutcome splitOutcome = PartitionKeyHashRangeSplitterAndMerger.TrySplitRange( + partitionKeyHashRange, + explicitSplitPoint, + out PartitionKeyHashRanges splitRanges); + + return splitOutcome switch + { + SplitOutcome.Success => splitRanges, + _ => throw new RangeSplitException($"Splitting range failed because {splitOutcome}"), + }; + } + + public static SplitOutcome TrySplitRange(PartitionKeyHashRange partitionKeyHashRange, PartitionKeyHash explicitSplitPoint, out PartitionKeyHashRanges splitRanges) + { + if (explicitSplitPoint < (partitionKeyHashRange.StartInclusive != null ? partitionKeyHashRange.StartInclusive : PartitionKeyHash.None) || + explicitSplitPoint > partitionKeyHashRange.EndExclusive) + { + splitRanges = default; + return SplitOutcome.RangeNotWideEnough; + } + + IList childRanges = new List + { + new PartitionKeyHashRange(partitionKeyHashRange.StartInclusive, explicitSplitPoint), + new PartitionKeyHashRange(explicitSplitPoint, partitionKeyHashRange.EndExclusive) + }; + + splitRanges = PartitionKeyHashRanges.Create(childRanges); + return SplitOutcome.Success; + } + public static PartitionKeyHashRange MergeRanges(PartitionKeyHashRanges partitionedSortedEffectiveRanges) { if (partitionedSortedEffectiveRanges == null) @@ -111,7 +144,13 @@ private sealed class V2 : PartitionKeyHashRangeSplitterAndMerger public override PartitionKeyHashRange FullRange => PartitionKeyHashRangeSplitterAndMerger.V2.fullRange; } - + private sealed class RangeSplitException : Exception + { + public RangeSplitException(string message) + : base(message) + { + } + } public enum SplitOutcome { Success, @@ -119,4 +158,4 @@ public enum SplitOutcome RangeNotWideEnough, } } -} +} \ No newline at end of file