Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
96413b7
Add QueryPlan support
aavasthy Feb 13, 2026
3ae9d60
Merge with master.
aavasthy Feb 13, 2026
25cb4bb
Update tests for QueryPlan thinclient
aavasthy Feb 17, 2026
1221556
Merge branch 'master' into users/aavasthy/thinclientqueryplan
aavasthy Feb 17, 2026
80b9eeb
Merge branch 'master' into users/aavasthy/thinclientqueryplan
aavasthy Feb 18, 2026
563bc5e
Merge branch 'master' into users/aavasthy/thinclientqueryplan
aavasthy Feb 18, 2026
4ee8687
Fix tests for thinclient
aavasthy Feb 19, 2026
152523f
Merge with master
aavasthy Feb 19, 2026
2bd0264
Fix tests for thinclient
aavasthy Feb 19, 2026
75c92c7
Merge branch 'master' into users/aavasthy/thinclientqueryplan
aavasthy Feb 25, 2026
c0b3c0e
Update tests and logic for query by passing
aavasthy Feb 27, 2026
e248c42
Merge with Master
aavasthy Feb 27, 2026
c1574cb
Merge branch 'master' into users/aavasthy/thinclientqueryplan
aavasthy Mar 3, 2026
fe47693
Add a separate class for thinclient parsing response
aavasthy Mar 5, 2026
97045ac
Merge with master.
aavasthy Mar 5, 2026
d83421f
Revert changes in PartitionedQueryExecutionInfo
aavasthy Mar 5, 2026
f223861
Update flag check
aavasthy Mar 6, 2026
230343a
Merge branch 'master' into users/aavasthy/thinclientqueryplan
aavasthy Mar 6, 2026
f107e1c
Test code cleanup
aavasthy Mar 8, 2026
9062eec
Update query plan serialization repsonse logic
aavasthy Mar 13, 2026
d4083f3
Merge branch 'master' into users/aavasthy/thinclientqueryplan
aavasthy Mar 13, 2026
a1e8e07
Merge branch 'master' into users/aavasthy/thinclientqueryplan
aavasthy Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions Microsoft.Azure.Cosmos/src/GatewayStoreModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ await GatewayStoreModel.ApplySessionTokenAsync(
request.RequestContext.RegionName = regionName;
}

bool isPPAFEnabled = this.IsPartitionLevelFailoverEnabled();
// This is applicable for both per partition automatic failover and per partition circuit breaker.
if ((isPPAFEnabled || this.isThinClientEnabled)
&& !ReplicatedResourceClient.IsMasterResource(request.ResourceType)
bool isPPAFEnabled = this.IsPartitionLevelFailoverEnabled();
// This is applicable for both per partition automatic failover and per partition circuit breaker.
if ((isPPAFEnabled || this.isThinClientEnabled)
&& !ReplicatedResourceClient.IsMasterResource(request.ResourceType)
&& (request.ResourceType.IsPartitioned() || request.ResourceType == ResourceType.StoredProcedure))
{
(bool isSuccess, PartitionKeyRange partitionKeyRange) = await TryResolvePartitionKeyRangeAsync(
Expand Down Expand Up @@ -563,7 +563,8 @@ internal static bool IsOperationSupportedByThinClient(DocumentServiceRequest req
|| request.OperationType == OperationType.Upsert
|| request.OperationType == OperationType.Replace
|| request.OperationType == OperationType.Delete
|| request.OperationType == OperationType.Query))
|| request.OperationType == OperationType.Query
|| request.OperationType == OperationType.QueryPlan))
{
return true;
}
Expand Down
19 changes: 16 additions & 3 deletions Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ public override async Task<TryCatch<QueryPage>> ExecuteItemQueryAsync(
resourceType,
message,
trace);
}

}
public override async Task<PartitionedQueryExecutionInfo> ExecuteQueryPlanRequestAsync(
string resourceUri,
ResourceType resourceType,
Expand Down Expand Up @@ -209,7 +209,20 @@ public override async Task<PartitionedQueryExecutionInfo> ExecuteQueryPlanReques
{
// Syntax exception are argument exceptions and thrown to the user.
message.EnsureSuccessStatusCode();
partitionedQueryExecutionInfo = this.clientContext.SerializerCore.FromStream<PartitionedQueryExecutionInfo>(message.Content);

if (this.documentClient.isThinClientEnabled)
{
ContainerProperties containerProperties = await this.clientContext.GetCachedContainerPropertiesAsync(
resourceUri, trace, cancellationToken);

partitionedQueryExecutionInfo = ThinClientQueryPlanHelper.DeserializeQueryPlanResponse(
message.Content,
containerProperties.PartitionKey);
}
else
{
partitionedQueryExecutionInfo = this.clientContext.SerializerCore.FromStream<PartitionedQueryExecutionInfo>(message.Content);
}
}

return partitionedQueryExecutionInfo;
Expand Down
162 changes: 162 additions & 0 deletions Microsoft.Azure.Cosmos/src/ThinClientQueryPlanHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------

namespace Microsoft.Azure.Cosmos.Query.Core.QueryPlan
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Newtonsoft.Json;
Comment thread
kirankumarkolli marked this conversation as resolved.
Comment thread
kirankumarkolli marked this conversation as resolved.
using PartitionKeyDefinition = Documents.PartitionKeyDefinition;
using PartitionKeyInternal = Documents.Routing.PartitionKeyInternal;

/// <summary>
/// Handles conversion of thin client query plan responses where query ranges
/// are returned in PartitionKeyInternal format instead of EPK hex strings.
/// Mirrors the conversion logic in <see cref="QueryPartitionProvider.ConvertPartitionedQueryExecutionInfo"/>.
/// </summary>
/// <remarks>
/// Uses System.Text.Json for primary parsing and structural validation.
/// Newtonsoft.Json is used only for deserializing QueryInfo, HybridSearchQueryInfo,
/// and Range&lt;PartitionKeyInternal&gt; because these types and their deep type hierarchies
/// (including the external Direct package types) use Newtonsoft [JsonProperty] attributes
/// and [JsonObject(MemberSerialization.OptIn)] semantics that have no System.Text.Json equivalent.
/// </remarks>
internal static class ThinClientQueryPlanHelper
{
private static readonly Newtonsoft.Json.JsonSerializerSettings NewtonsoftSettings =
Comment thread
kirankumarkolli marked this conversation as resolved.
new Newtonsoft.Json.JsonSerializerSettings
{
DateParseHandling = Newtonsoft.Json.DateParseHandling.None,
MaxDepth = 64,
};

/// <summary>
/// Deserializes a thin client query plan response stream into a
/// <see cref="PartitionedQueryExecutionInfo"/> with EPK string ranges.
/// The response contains query ranges in PartitionKeyInternal format
/// which are converted to EPK hex strings and sorted.
/// </summary>
/// <param name="stream">The response stream containing the raw query plan JSON.</param>
/// <param name="partitionKeyDefinition">The partition key definition for the container.</param>
/// <returns><see cref="PartitionedQueryExecutionInfo"/> with sorted EPK string ranges.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> or <paramref name="partitionKeyDefinition"/> is null.</exception>
/// <exception cref="FormatException">Thrown when the response JSON is malformed or missing required properties.</exception>
public static PartitionedQueryExecutionInfo DeserializeQueryPlanResponse(
Comment thread
aavasthy marked this conversation as resolved.
Stream stream,
PartitionKeyDefinition partitionKeyDefinition)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}

if (partitionKeyDefinition == null)
{
throw new ArgumentNullException(nameof(partitionKeyDefinition));
}

using JsonDocument doc = JsonDocument.Parse(stream);
JsonElement root = doc.RootElement;

if (root.ValueKind != JsonValueKind.Object)
{
throw new FormatException(
$"Thin client query plan response must be a JSON object, but was {root.ValueKind}.");
}

// Validate and extract queryRanges (required)
if (!root.TryGetProperty("queryRanges", out JsonElement queryRangesElement))
{
throw new FormatException(
"Thin client query plan response is missing the required 'queryRanges' property.");
}

if (queryRangesElement.ValueKind != JsonValueKind.Array)
{
throw new FormatException(
$"Expected 'queryRanges' to be a JSON array, but was {queryRangesElement.ValueKind}.");
}

if (queryRangesElement.GetArrayLength() == 0)
{
throw new FormatException(
"Thin client query plan response 'queryRanges' array must not be empty.");
}

// Deserialize QueryInfo using Newtonsoft because QueryInfo uses
// [JsonObject(MemberSerialization.OptIn)] and Newtonsoft-only [JsonProperty] attributes.
QueryInfo queryInfo = null;
if (root.TryGetProperty("queryInfo", out JsonElement queryInfoElement)
&& queryInfoElement.ValueKind != JsonValueKind.Null)
{
queryInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<QueryInfo>(
queryInfoElement.GetRawText(),
ThinClientQueryPlanHelper.NewtonsoftSettings);
}

// Deserialize HybridSearchQueryInfo using Newtonsoft (same constraint as QueryInfo).
HybridSearchQueryInfo hybridSearchQueryInfo = null;
if (root.TryGetProperty("hybridSearchQueryInfo", out JsonElement hybridElement)
&& hybridElement.ValueKind != JsonValueKind.Null)
{
hybridSearchQueryInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<HybridSearchQueryInfo>(
hybridElement.GetRawText(),
ThinClientQueryPlanHelper.NewtonsoftSettings);
}

// Parse and convert query ranges to EPK string ranges.
// Range<PartitionKeyInternal> requires Newtonsoft because PartitionKeyInternal
// is from the external Direct package with Newtonsoft-based serialization.
List<Documents.Routing.Range<string>> effectiveRanges =
new List<Documents.Routing.Range<string>>(queryRangesElement.GetArrayLength());

foreach (JsonElement rangeElement in queryRangesElement.EnumerateArray())
{
if (rangeElement.ValueKind != JsonValueKind.Object)
{
throw new FormatException(
$"Each query range must be a JSON object, but was {rangeElement.ValueKind}.");
}

if (!rangeElement.TryGetProperty("min", out _))
{
throw new FormatException(
"Query range is missing the required 'min' property.");
}

if (!rangeElement.TryGetProperty("max", out _))
{
throw new FormatException(
"Query range is missing the required 'max' property.");
}

Documents.Routing.Range<PartitionKeyInternal> internalRange =
Newtonsoft.Json.JsonConvert.DeserializeObject<Documents.Routing.Range<PartitionKeyInternal>>(
rangeElement.GetRawText(),
ThinClientQueryPlanHelper.NewtonsoftSettings);

if (internalRange == null)
{
throw new FormatException(
"Failed to deserialize query range from thin client response.");
}

effectiveRanges.Add(PartitionKeyInternal.GetEffectivePartitionKeyRange(
partitionKeyDefinition,
internalRange));
}

effectiveRanges.Sort(Documents.Routing.Range<string>.MinComparer.Instance);

return new PartitionedQueryExecutionInfo()
{
QueryInfo = queryInfo,
QueryRanges = effectiveRanges,
HybridSearchQueryInfo = hybridSearchQueryInfo,
};
}
}
}
Loading
Loading