Skip to content
Open
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
90 changes: 90 additions & 0 deletions CosmosDBShell.Tests/CommandTests/QueryCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,94 @@ public void BuildMetrics_CoversAllServerSideMetricsProperties()
Assert.Contains(propertyToMetric[prop], metricNames);
}
}

[Fact]
public void EvaluatePlan_NoUtilizedIndexes_ReportsFullScan()
{
var evaluation = QueryCommand.EvaluatePlan(
utilizedIndexes: [],
potentialIndexes: [],
indexHitRatio: 0,
retrievedDocumentCount: 1000,
outputDocumentCount: 1);

Assert.True(evaluation.FullScan);
Assert.False(evaluation.IndexSeek);
Assert.Empty(evaluation.UtilizedIndexes);
}

[Fact]
public void EvaluatePlan_WithUtilizedIndexes_ReportsIndexSeek()
{
var evaluation = QueryCommand.EvaluatePlan(
utilizedIndexes: ["/city/?"],
potentialIndexes: [],
indexHitRatio: 1,
retrievedDocumentCount: 1,
outputDocumentCount: 1);

Assert.False(evaluation.FullScan);
Assert.True(evaluation.IndexSeek);
Assert.Equal(1, evaluation.IndexHitRatio);
Assert.Collection(evaluation.UtilizedIndexes, spec => Assert.Equal("/city/?", spec));
}

[Fact]
public void EvaluatePlan_PreservesPotentialIndexRecommendations()
{
var evaluation = QueryCommand.EvaluatePlan(
utilizedIndexes: ["/city/?"],
potentialIndexes: ["/age/?"],
indexHitRatio: 0.5,
retrievedDocumentCount: 200,
outputDocumentCount: 100);

Assert.True(evaluation.IndexSeek);
Assert.Collection(evaluation.PotentialIndexes, spec => Assert.Equal("/age/?", spec));
Assert.Equal(200, evaluation.RetrievedDocumentCount);
Assert.Equal(100, evaluation.OutputDocumentCount);
}

[Fact]
public void ParseIndexPlan_ExtractsSingleAndCompositeIndexSpecs()
{
const string indexMetrics = """
{
"UtilizedIndexes": {
"SingleIndexes": [ { "IndexSpec": "/city/?" } ],
"CompositeIndexes": [ { "IndexSpecs": [ "/age ASC", "/name ASC" ] } ]
},
"PotentialIndexes": {
"SingleIndexes": [ { "IndexSpec": "/status/?" } ],
"CompositeIndexes": []
}
}
""";

var (utilized, potential) = QueryCommand.ParseIndexPlan(indexMetrics);

Assert.Equal(["/city/?", "/age ASC, /name ASC"], utilized);
Assert.Equal(["/status/?"], potential);
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void ParseIndexPlan_NullOrEmpty_ReturnsEmptyLists(string? indexMetrics)
{
var (utilized, potential) = QueryCommand.ParseIndexPlan(indexMetrics);

Assert.Empty(utilized);
Assert.Empty(potential);
}

[Fact]
public void ParseIndexPlan_MalformedJson_ReturnsEmptyLists()
{
var (utilized, potential) = QueryCommand.ParseIndexPlan("{ not valid json");

Assert.Empty(utilized);
Assert.Empty(potential);
}
}
27 changes: 27 additions & 0 deletions CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/PlanEvaluation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------

namespace Azure.Data.Cosmos.Shell.Commands;

using System.Collections.Generic;

/// <summary>
/// Structured evaluation of a query execution plan derived from index metrics and
/// server-side query metrics. Pure data so it can be produced and asserted in tests.
/// </summary>
/// <param name="FullScan">True when no index contributed to the query.</param>
/// <param name="IndexSeek">True when at least one index was utilized.</param>
/// <param name="IndexHitRatio">The index hit ratio in the range [0,1], when available.</param>
/// <param name="RetrievedDocumentCount">Documents loaded by the engine, when available.</param>
/// <param name="OutputDocumentCount">Documents returned by the query, when available.</param>
/// <param name="UtilizedIndexes">Index specifications that contributed to the query.</param>
/// <param name="PotentialIndexes">Index specifications that could improve the query.</param>
internal sealed record PlanEvaluation(
bool FullScan,
bool IndexSeek,
double? IndexHitRatio,
long? RetrievedDocumentCount,
long? OutputDocumentCount,
IReadOnlyList<string> UtilizedIndexes,
IReadOnlyList<string> PotentialIndexes);
Loading
Loading