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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
490 changes: 490 additions & 0 deletions .github/agents/msdata-direct-sync-agent.agent.md

Large diffs are not rendered by default.

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
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
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ sealed class SqlFunctionCallScalarExpression : SqlScalarExpression
{ Names.IsNumber, Identifiers.IsNumber },
{ Names.IsObject, Identifiers.IsObject },
{ Names.IsPrimitive, Identifiers.IsPrimitive },
{ Names.IsString, Identifiers.IsString },
{ Names.IsString, Identifiers.IsString },
{ Names.LastSubstringAfter, Identifiers.LastSubstringAfter },
{ Names.LastSubstringBefore, Identifiers.LastSubstringBefore },
{ Names.Left, Identifiers.Left },
{ Names.Length, Identifiers.Length },
{ Names.Like, Identifiers.Like },
Expand Down Expand Up @@ -143,7 +145,9 @@ sealed class SqlFunctionCallScalarExpression : SqlScalarExpression
{ Names.StringToNull, Identifiers.StringToNull },
{ Names.StringToNumber, Identifiers.StringToNumber },
{ Names.StringToObject, Identifiers.StringToObject },
{ Names.Substring, Identifiers.Substring },
{ Names.Substring, Identifiers.Substring },
{ Names.SubstringAfter, Identifiers.SubstringAfter },
{ Names.SubstringBefore, Identifiers.SubstringBefore },
{ Names.Sum, Identifiers.Sum },
{ Names.Tan, Identifiers.Tan },
{ Names.TicksToDateTime, Identifiers.TicksToDateTime },
Expand Down Expand Up @@ -350,7 +354,9 @@ public static class Names
public const string IsObject = "IS_OBJECT";
public const string IsPrimitive = "IS_PRIMITIVE";
public const string IsString = "IS_STRING";
public const string LastIndexOf = "LastIndexOf";
public const string LastIndexOf = "LastIndexOf";
public const string LastSubstringAfter = "LastSubstringAfter";
public const string LastSubstringBefore = "LastSubstringBefore";
public const string Left = "LEFT";
public const string Length = "LENGTH";
public const string Like = "LIKE";
Expand Down Expand Up @@ -398,7 +404,9 @@ public static class Names
public const string StringToNull = "StringToNull";
public const string StringToNumber = "StringToNumber";
public const string StringToObject = "StringToObject";
public const string Substring = "SUBSTRING";
public const string Substring = "SUBSTRING";
public const string SubstringAfter = "SubstringAfter";
public const string SubstringBefore = "SubstringBefore";
public const string Sum = "SUM";
public const string Tan = "TAN";
public const string TicksToDateTime = "TicksToDateTime";
Expand Down Expand Up @@ -528,7 +536,9 @@ public static class Identifiers
public static readonly SqlIdentifier IsObject = SqlIdentifier.Create(Names.IsObject);
public static readonly SqlIdentifier IsPrimitive = SqlIdentifier.Create(Names.IsPrimitive);
public static readonly SqlIdentifier IsString = SqlIdentifier.Create(Names.IsString);
public static readonly SqlIdentifier LastIndexOf = SqlIdentifier.Create(Names.LastIndexOf);
public static readonly SqlIdentifier LastIndexOf = SqlIdentifier.Create(Names.LastIndexOf);
public static readonly SqlIdentifier LastSubstringAfter = SqlIdentifier.Create(Names.LastSubstringAfter);
public static readonly SqlIdentifier LastSubstringBefore = SqlIdentifier.Create(Names.LastSubstringBefore);
public static readonly SqlIdentifier Left = SqlIdentifier.Create(Names.Left);
public static readonly SqlIdentifier Length = SqlIdentifier.Create(Names.Length);
public static readonly SqlIdentifier Like = SqlIdentifier.Create(Names.Like);
Expand Down Expand Up @@ -576,7 +586,9 @@ public static class Identifiers
public static readonly SqlIdentifier StringToNull = SqlIdentifier.Create(Names.StringToNull);
public static readonly SqlIdentifier StringToNumber = SqlIdentifier.Create(Names.StringToNumber);
public static readonly SqlIdentifier StringToObject = SqlIdentifier.Create(Names.StringToObject);
public static readonly SqlIdentifier Substring = SqlIdentifier.Create(Names.Substring);
public static readonly SqlIdentifier Substring = SqlIdentifier.Create(Names.Substring);
public static readonly SqlIdentifier SubstringAfter = SqlIdentifier.Create(Names.SubstringAfter);
public static readonly SqlIdentifier SubstringBefore = SqlIdentifier.Create(Names.SubstringBefore);
public static readonly SqlIdentifier Sum = SqlIdentifier.Create(Names.Sum);
public static readonly SqlIdentifier Tan = SqlIdentifier.Create(Names.Tan);
public static readonly SqlIdentifier TicksToDateTime = SqlIdentifier.Create(Names.TicksToDateTime);
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;
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 =
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(
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