Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype of Firestore query explain functionality #14079

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,32 @@ public async Task Sum()
Assert.Equal(double.NaN, snapshot.GetValue<double>("Sum_EnglishScore")); // NaN value check
}

[Fact]
public async Task Sum_Explain()
{
CollectionReference collection = _fixture.StudentCollection;
var plan = await collection
.Aggregate(AggregateField.Sum("Level", "Sum_Of_Levels"), AggregateField.Sum("MathScore"), AggregateField.Sum("EnglishScore"), AggregateField.Sum("Name"))
.ExplainAsync();
Assert.NotNull(plan);
}

[Fact]
public async Task Sum_ExplainAnalyze()
{
CollectionReference collection = _fixture.StudentCollection;
var queryProfileInfo = await collection.Aggregate(AggregateField.Sum("Level", "Sum_Of_Levels"), AggregateField.Sum("MathScore"), AggregateField.Sum("EnglishScore"), AggregateField.Sum("Name"))
.ExplainAnalyzeAsync();
var plan = queryProfileInfo.Plan;
var stats = queryProfileInfo.Stats;
Assert.NotNull(plan);
Assert.NotNull(stats);
var snapshot = queryProfileInfo.Snapshot;
Assert.Equal(Student.Data.Sum(c => c.Level), snapshot.GetValue<long>("Sum_Of_Levels")); // Long value, Alias check
Assert.Equal(Student.Data.Sum(c => c.MathScore), snapshot.GetValue<double>("Sum_MathScore")); // Double value check
Assert.Equal(double.NaN, snapshot.GetValue<double>("Sum_EnglishScore")); // NaN value check
}

[Fact]
public async Task Avg()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Google.Cloud.Firestore.IntegrationTests.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
Expand Down Expand Up @@ -169,6 +170,30 @@ public async Task StartAfter()
Assert.Equal(HighScore.Data.Where(x => x.Level > 20).OrderBy(x => x.Level), items);
}

[Fact]
public async Task StartAfter_Explain()
{
var query = _fixture.HighScoreCollection.OrderBy("Level").StartAfter(20);
var plan = await query.ExplainAsync();
Assert.NotNull(plan);
}

[Fact]
public async Task StartAfter_ExplainAnalyze()
{
var query = _fixture.HighScoreCollection.OrderBy("Level").StartAfter(20);
var queryProfileInfo = await query.ExplainAnalyzeAsync();
var snapshot = queryProfileInfo.Snapshot;
var plan = queryProfileInfo.Plan;
var stats = queryProfileInfo.Stats;
Assert.NotNull(plan);
Assert.NotNull(stats);
Assert.Equal(snapshot.Documents.Count, stats.ResultsReturned);
Assert.NotEqual(0, stats.ReadOperations);
var items = snapshot.Documents.Select(doc => doc.ConvertTo<HighScore>()).ToList();
Assert.Equal(HighScore.Data.Where(x => x.Level > 20).OrderBy(x => x.Level), items);
}

[Fact]
public async Task EndAt()
{
Expand Down
98 changes: 52 additions & 46 deletions apis/Google.Cloud.Firestore/Google.Cloud.Firestore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,65 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32516.85
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore", "Google.Cloud.Firestore\Google.Cloud.Firestore.csproj", "{23BB3FFF-DC4C-F978-04B5-C5E146893E68}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.Firestore", "Google.Cloud.Firestore\Google.Cloud.Firestore.csproj", "{23BB3FFF-DC4C-F978-04B5-C5E146893E68}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore.Benchmarks", "Google.Cloud.Firestore.Benchmarks\Google.Cloud.Firestore.Benchmarks.csproj", "{3B576042-1EF8-EA8B-2554-C5FF11CB3F48}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.Firestore.Benchmarks", "Google.Cloud.Firestore.Benchmarks\Google.Cloud.Firestore.Benchmarks.csproj", "{3B576042-1EF8-EA8B-2554-C5FF11CB3F48}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore.CleanTestData", "Google.Cloud.Firestore.CleanTestData\Google.Cloud.Firestore.CleanTestData.csproj", "{1A7F1C0F-72C8-8983-9906-1EA379969C43}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.Firestore.CleanTestData", "Google.Cloud.Firestore.CleanTestData\Google.Cloud.Firestore.CleanTestData.csproj", "{1A7F1C0F-72C8-8983-9906-1EA379969C43}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore.IntegrationTests", "Google.Cloud.Firestore.IntegrationTests\Google.Cloud.Firestore.IntegrationTests.csproj", "{ECA6C703-9C4B-5DB6-610C-37217893669E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.Firestore.IntegrationTests", "Google.Cloud.Firestore.IntegrationTests\Google.Cloud.Firestore.IntegrationTests.csproj", "{ECA6C703-9C4B-5DB6-610C-37217893669E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore.Snippets", "Google.Cloud.Firestore.Snippets\Google.Cloud.Firestore.Snippets.csproj", "{5AB44017-319A-FA5E-4BB7-AEE4663EA6F8}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.Firestore.Snippets", "Google.Cloud.Firestore.Snippets\Google.Cloud.Firestore.Snippets.csproj", "{5AB44017-319A-FA5E-4BB7-AEE4663EA6F8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore.Tests", "Google.Cloud.Firestore.Tests\Google.Cloud.Firestore.Tests.csproj", "{3023B3E0-2A3A-0DE8-37B9-FC5DEFC655A7}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.Firestore.Tests", "Google.Cloud.Firestore.Tests\Google.Cloud.Firestore.Tests.csproj", "{3023B3E0-2A3A-0DE8-37B9-FC5DEFC655A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.ClientTesting", "..\..\tools\Google.Cloud.ClientTesting\Google.Cloud.ClientTesting.csproj", "{29974B0C-A7B0-8CA8-AE32-99F622C89044}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.ClientTesting", "..\..\tools\Google.Cloud.ClientTesting\Google.Cloud.ClientTesting.csproj", "{29974B0C-A7B0-8CA8-AE32-99F622C89044}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Google.Cloud.Firestore.V1", "..\Google.Cloud.Firestore.V1\Google.Cloud.Firestore.V1\Google.Cloud.Firestore.V1.csproj", "{73C26D2D-CC8B-10B3-B6AB-5B25E6859268}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Google.Cloud.Firestore.V1", "..\Google.Cloud.Firestore.V1\Google.Cloud.Firestore.V1\Google.Cloud.Firestore.V1.csproj", "{73C26D2D-CC8B-10B3-B6AB-5B25E6859268}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{23BB3FFF-DC4C-F978-04B5-C5E146893E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{23BB3FFF-DC4C-F978-04B5-C5E146893E68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23BB3FFF-DC4C-F978-04B5-C5E146893E68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23BB3FFF-DC4C-F978-04B5-C5E146893E68}.Release|Any CPU.Build.0 = Release|Any CPU
{3B576042-1EF8-EA8B-2554-C5FF11CB3F48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B576042-1EF8-EA8B-2554-C5FF11CB3F48}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B576042-1EF8-EA8B-2554-C5FF11CB3F48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B576042-1EF8-EA8B-2554-C5FF11CB3F48}.Release|Any CPU.Build.0 = Release|Any CPU
{1A7F1C0F-72C8-8983-9906-1EA379969C43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A7F1C0F-72C8-8983-9906-1EA379969C43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A7F1C0F-72C8-8983-9906-1EA379969C43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A7F1C0F-72C8-8983-9906-1EA379969C43}.Release|Any CPU.Build.0 = Release|Any CPU
{ECA6C703-9C4B-5DB6-610C-37217893669E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ECA6C703-9C4B-5DB6-610C-37217893669E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ECA6C703-9C4B-5DB6-610C-37217893669E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ECA6C703-9C4B-5DB6-610C-37217893669E}.Release|Any CPU.Build.0 = Release|Any CPU
{5AB44017-319A-FA5E-4BB7-AEE4663EA6F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AB44017-319A-FA5E-4BB7-AEE4663EA6F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AB44017-319A-FA5E-4BB7-AEE4663EA6F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5AB44017-319A-FA5E-4BB7-AEE4663EA6F8}.Release|Any CPU.Build.0 = Release|Any CPU
{3023B3E0-2A3A-0DE8-37B9-FC5DEFC655A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3023B3E0-2A3A-0DE8-37B9-FC5DEFC655A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3023B3E0-2A3A-0DE8-37B9-FC5DEFC655A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3023B3E0-2A3A-0DE8-37B9-FC5DEFC655A7}.Release|Any CPU.Build.0 = Release|Any CPU
{29974B0C-A7B0-8CA8-AE32-99F622C89044}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29974B0C-A7B0-8CA8-AE32-99F622C89044}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29974B0C-A7B0-8CA8-AE32-99F622C89044}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29974B0C-A7B0-8CA8-AE32-99F622C89044}.Release|Any CPU.Build.0 = Release|Any CPU
{73C26D2D-CC8B-10B3-B6AB-5B25E6859268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{73C26D2D-CC8B-10B3-B6AB-5B25E6859268}.Debug|Any CPU.Build.0 = Debug|Any CPU
{73C26D2D-CC8B-10B3-B6AB-5B25E6859268}.Release|Any CPU.ActiveCfg = Release|Any CPU
{73C26D2D-CC8B-10B3-B6AB-5B25E6859268}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{23BB3FFF-DC4C-F978-04B5-C5E146893E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{23BB3FFF-DC4C-F978-04B5-C5E146893E68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23BB3FFF-DC4C-F978-04B5-C5E146893E68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23BB3FFF-DC4C-F978-04B5-C5E146893E68}.Release|Any CPU.Build.0 = Release|Any CPU
{3B576042-1EF8-EA8B-2554-C5FF11CB3F48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B576042-1EF8-EA8B-2554-C5FF11CB3F48}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B576042-1EF8-EA8B-2554-C5FF11CB3F48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B576042-1EF8-EA8B-2554-C5FF11CB3F48}.Release|Any CPU.Build.0 = Release|Any CPU
{1A7F1C0F-72C8-8983-9906-1EA379969C43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A7F1C0F-72C8-8983-9906-1EA379969C43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A7F1C0F-72C8-8983-9906-1EA379969C43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A7F1C0F-72C8-8983-9906-1EA379969C43}.Release|Any CPU.Build.0 = Release|Any CPU
{ECA6C703-9C4B-5DB6-610C-37217893669E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ECA6C703-9C4B-5DB6-610C-37217893669E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ECA6C703-9C4B-5DB6-610C-37217893669E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ECA6C703-9C4B-5DB6-610C-37217893669E}.Release|Any CPU.Build.0 = Release|Any CPU
{5AB44017-319A-FA5E-4BB7-AEE4663EA6F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AB44017-319A-FA5E-4BB7-AEE4663EA6F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AB44017-319A-FA5E-4BB7-AEE4663EA6F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5AB44017-319A-FA5E-4BB7-AEE4663EA6F8}.Release|Any CPU.Build.0 = Release|Any CPU
{3023B3E0-2A3A-0DE8-37B9-FC5DEFC655A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3023B3E0-2A3A-0DE8-37B9-FC5DEFC655A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3023B3E0-2A3A-0DE8-37B9-FC5DEFC655A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3023B3E0-2A3A-0DE8-37B9-FC5DEFC655A7}.Release|Any CPU.Build.0 = Release|Any CPU
{29974B0C-A7B0-8CA8-AE32-99F622C89044}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29974B0C-A7B0-8CA8-AE32-99F622C89044}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29974B0C-A7B0-8CA8-AE32-99F622C89044}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29974B0C-A7B0-8CA8-AE32-99F622C89044}.Release|Any CPU.Build.0 = Release|Any CPU
{73C26D2D-CC8B-10B3-B6AB-5B25E6859268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{73C26D2D-CC8B-10B3-B6AB-5B25E6859268}.Debug|Any CPU.Build.0 = Debug|Any CPU
{73C26D2D-CC8B-10B3-B6AB-5B25E6859268}.Release|Any CPU.ActiveCfg = Release|Any CPU
{73C26D2D-CC8B-10B3-B6AB-5B25E6859268}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {46F695E8-04B1-4C09-9C59-1101A63C5BE0}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Api;
using Google.Api.Gax;
using Google.Api.Gax.Grpc;
using Google.Cloud.Firestore.V1;
Expand Down Expand Up @@ -58,10 +59,35 @@ internal AggregateQuery(Query query, IReadOnlyList<AggregateField> aggregateFiel
public Task<AggregateQuerySnapshot> GetSnapshotAsync(CancellationToken cancellationToken = default) =>
GetSnapshotAsync(null, cancellationToken);

/// <summary>
///
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<PlanSummary> ExplainAsync(CancellationToken cancellationToken = default)
{
var profileSnapshot = await GetProfileSnapshotAsync(transactionId: null, explainOptions: new ExplainOptions { Analyze = false }, cancellationToken).ConfigureAwait(false);
return profileSnapshot.Plan;
}

/// <summary>
///
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<QueryProfileInfo<AggregateQuerySnapshot>> ExplainAnalyzeAsync(CancellationToken cancellationToken = default) =>
GetProfileSnapshotAsync(transactionId: null, explainOptions: new ExplainOptions { Analyze = true }, cancellationToken);

internal async Task<AggregateQuerySnapshot> GetSnapshotAsync(ByteString transactionId, CancellationToken cancellationToken)
{
var profileSnapshot = await GetProfileSnapshotAsync(transactionId, explainOptions: null, cancellationToken).ConfigureAwait(false);
return profileSnapshot.Snapshot;
}

internal async Task<QueryProfileInfo<AggregateQuerySnapshot>> GetProfileSnapshotAsync(ByteString transactionId, ExplainOptions explainOptions, CancellationToken cancellationToken)
{
var query = ToStructuredAggregationQuery();
IAsyncEnumerable<RunAggregationQueryResponse> responseStream = GetAggregationQueryResponseStreamAsync(transactionId, cancellationToken);
IAsyncEnumerable<RunAggregationQueryResponse> responseStream = GetAggregationQueryResponseStreamAsync(transactionId, explainOptions, cancellationToken);
Timestamp? readTime = null;

// This is a map from the user-specified alias to the resulting value.
Expand All @@ -76,14 +102,18 @@ internal async Task<AggregateQuerySnapshot> GetSnapshotAsync(ByteString transact
var aggregate = _aggregateFields[i];
queryAliasToUserAlias[aggregate.GetAliasForIndex(i)] = aggregate.Alias;
}

ExplainMetrics metrics = null;
await responseStream.ForEachAsync(ProcessResponse, cancellationToken).ConfigureAwait(false);
GaxPreconditions.CheckState(readTime != null, "The stream returned from RunAggregationQuery did not provide a read timestamp.");
return new AggregateQuerySnapshot(this, readTime.Value, data);
GaxPreconditions.CheckState(readTime is not null || explainOptions?.Analyze == false, "The stream returned from RunRunAggregationQueryQuery did not provide a read timestamp.");
GaxPreconditions.CheckState(explainOptions is null || metrics is not null, "The stream returned from RunAggregationQuery did not provide metrics.");

// We rely on AggregateQuerySnapshot.ReadTime not being accessed when we're just doing an explain operation.
var snapshot = new AggregateQuerySnapshot(this, readTime, data);
return new QueryProfileInfo<AggregateQuerySnapshot>(snapshot, metrics);

void ProcessResponse(RunAggregationQueryResponse response)
{
if (response.Result.AggregateFields is { } aggregateFields)
if (response.Result?.AggregateFields is { } aggregateFields)
{
foreach (var pair in aggregateFields)
{
Expand All @@ -94,19 +124,21 @@ void ProcessResponse(RunAggregationQueryResponse response)
}
}
readTime ??= Timestamp.FromProtoOrNull(response.ReadTime);
metrics = response.ExplainMetrics;
}
}

// Note: this *could* just return FirestoreClient.RunAggregationQueryStream, as it's only called
// from GetSnapshotAsync which could ensure it disposes of the response. However, it's simplest
// to keep this implementation in common with Query.StreamResponsesAsync, which effectively
// needs to use an iterator block so we can return an IAsyncEnumerable from Query.StreamAsync.
private async IAsyncEnumerable<RunAggregationQueryResponse> GetAggregationQueryResponseStreamAsync(ByteString transactionId, [EnumeratorCancellation] CancellationToken cancellationToken)
private async IAsyncEnumerable<RunAggregationQueryResponse> GetAggregationQueryResponseStreamAsync(ByteString transactionId, ExplainOptions explainOptions, [EnumeratorCancellation] CancellationToken cancellationToken)
{
RunAggregationQueryRequest request = new RunAggregationQueryRequest
{
Parent = _query.ParentPath,
StructuredAggregationQuery = ToStructuredAggregationQuery()
StructuredAggregationQuery = ToStructuredAggregationQuery(),
ExplainOptions = explainOptions
};
if (transactionId != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public sealed class AggregateQuerySnapshot : IEquatable<AggregateQuerySnapshot>,
/// The data within the snapshot. This is a MapField as that provides equality and hashing out of the box.
/// </summary>
private readonly MapField<string, Value> _data;
private readonly Timestamp? _readTime;

/// <summary>
/// The query producing this snapshot.
Expand All @@ -37,7 +38,7 @@ public sealed class AggregateQuerySnapshot : IEquatable<AggregateQuerySnapshot>,
/// <summary>
/// The time at which the snapshot was read.
/// </summary>
public Timestamp ReadTime { get; }
public Timestamp ReadTime => _readTime ?? throw new InvalidOperationException("No read time available");

/// <summary>
/// Number of documents that matches the query. May be null when count aggregation is not applied on the Query.
Expand All @@ -47,10 +48,10 @@ public sealed class AggregateQuerySnapshot : IEquatable<AggregateQuerySnapshot>,
value.ValueTypeCase == Value.ValueTypeOneofCase.IntegerValue
? value.IntegerValue : null;

internal AggregateQuerySnapshot(AggregateQuery query, Timestamp readTime, MapField<string, Value> data)
internal AggregateQuerySnapshot(AggregateQuery query, Timestamp? readTime, MapField<string, Value> data)
{
Query = query;
ReadTime = readTime;
_readTime = readTime;
_data = data;
}

Expand Down
Loading
Loading