Skip to content

Commit

Permalink
feat: Add sum and average aggregates
Browse files Browse the repository at this point in the history
  • Loading branch information
jskeet committed Oct 3, 2023
1 parent d0879a6 commit 9a8bfd5
Show file tree
Hide file tree
Showing 14 changed files with 901 additions and 99 deletions.
@@ -0,0 +1,147 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License").
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Cloud.Firestore.IntegrationTests.Models;
using Google.Cloud.Firestore.V1;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using static Google.Cloud.Firestore.AggregateField;

namespace Google.Cloud.Firestore.IntegrationTests;

[Collection(nameof(FirestoreFixture))]
public class AggregateQueryTest
{
private readonly FirestoreFixture _fixture;

public AggregateQueryTest(FirestoreFixture fixture) => _fixture = fixture;

[Fact]
public async Task Count_WithoutLimit()
{
CollectionReference collection = _fixture.HighScoreCollection;
var snapshot = await collection.Count().GetSnapshotAsync();
Assert.Equal(HighScore.Data.Length, snapshot.Count);
}

[Fact]
public async Task Count_WithLimit()
{
CollectionReference collection = _fixture.HighScoreCollection;
var snapshotWithoutLimit = await collection.Count().GetSnapshotAsync();
var snapshotWithLimit = await collection.Limit(2).Count().GetSnapshotAsync();
Assert.Equal(HighScore.Data.Length, snapshotWithoutLimit.Count);
Assert.Equal(2, snapshotWithLimit.Count);
}

[Fact]
public async Task Sum_ResultTypes()
{
var db = _fixture.FirestoreDb;
var collection = _fixture.CreateUniqueCollection();
var batch = db.StartBatch();
batch.Set(collection.Document("a"), new { x = 1, y = 1L << 63, z = 1.0 });
batch.Set(collection.Document("b"), new { x = 2, y = 1L << 63, z = 2.0 });
await batch.CommitAsync();

var snapshot = await collection
.Aggregate(AggregateField.Sum("x"), AggregateField.Sum("y"), AggregateField.Sum("z"))
.GetSnapshotAsync();

var sumX = snapshot.GetValue<object>("Sum_x");
var sumY = snapshot.GetValue<object>("Sum_y");
var sumZ = snapshot.GetValue<object>("Sum_z");

// These assertions check the value *and* the type together.
Assert.Equal(3L, sumX);
// The result is a double, because 2^64 can't be represented as a signed 64-bit integer.
Assert.Equal((1L << 63) * 2.0, sumY);
Assert.Equal(3.0, sumZ);
}

[Fact]
public async Task Sum()
{
CollectionReference collection = _fixture.StudentCollection;
var snapshot = await collection.Aggregate(AggregateField.Sum("Level", "Sum_Of_Levels"), AggregateField.Sum("MathScore"), AggregateField.Sum("EnglishScore"), AggregateField.Sum("Name")).GetSnapshotAsync();
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()
{
CollectionReference collection = _fixture.StudentCollection;
var snapshot = await collection.Aggregate(Average("MathScore"), Average("Level", "Avg_Of_Level"), Average("EnglishScore"), Average("Name")).GetSnapshotAsync();
Assert.Equal(Student.Data.Average(c => c.MathScore), snapshot.GetValue<double>("Avg_MathScore")); // Double value check
Assert.Equal(Student.Data.Average(c => c.Level), snapshot.GetValue<double>("Avg_Of_Level")); // Alias check
Assert.Equal(double.NaN, snapshot.GetValue<double>("Avg_EnglishScore")); // NaN value check
}

[Fact]
public async Task SumWithLimit()
{
CollectionReference collection = _fixture.HighScoreCollection;
var snapshot = await collection.Limit(2).Aggregate(AggregateField.Sum("Score")).GetSnapshotAsync();
Assert.Equal(HighScore.Data.OrderBy(c => c.Score).Take(2).Sum(c => c.Score), snapshot.GetValue<long>("Sum_Score"));
}

[Fact]
public async Task AvgWithLimit()
{
CollectionReference collection = _fixture.HighScoreCollection;
var snapshot = await collection.Limit(2).Aggregate(Average("Level")).GetSnapshotAsync();
Assert.Equal(HighScore.Data.OrderBy(c => c.Level).Take(2).Average(c => c.Level), snapshot.GetValue<double>("Avg_Level"));
}

[Fact]
public async Task SumWithFilter()
{
CollectionReference collection = _fixture.HighScoreCollection;
var snapshot = await collection.WhereGreaterThan("Score", 100).Aggregate(AggregateField.Sum("Score")).GetSnapshotAsync();
Assert.Equal(HighScore.Data.Where(x => x.Score > 100).Sum(c => c.Score), snapshot.GetValue<long>("Sum_Score"));
}

[Fact]
public async Task AvgWithFilter()
{
CollectionReference collection = _fixture.HighScoreCollection;
var snapshot = await collection.WhereGreaterThan("Level", 20).Aggregate(Average("Level")).GetSnapshotAsync();
Assert.Equal(HighScore.Data.Where(x => x.Level > 20).Average(c => c.Level), snapshot.GetValue<double>("Avg_Level"));
}

[Fact]
public async Task MultipleAggregations()
{
CollectionReference collection = _fixture.StudentCollection;
var snapshot = await collection.Aggregate(AggregateField.Sum("MathScore"), Average("MathScore", "avg_score"), Count()).GetSnapshotAsync();
Assert.Equal(Student.Data.Length, snapshot.GetValue<Value>("Count").IntegerValue);
Assert.Equal(Student.Data.Length, snapshot.Count.Value);
Assert.Equal(Student.Data.Sum(c => c.MathScore), snapshot.GetValue<double>("Sum_MathScore"));
Assert.Equal(Student.Data.Average(c => c.MathScore), snapshot.GetValue<double>("avg_score"));
}

[Fact]
public async Task NestedAggregations()
{
CollectionReference collection = _fixture.StudentCollection;
var snapshot = await collection.Aggregate(AggregateField.Sum("Nested.Math"), Average("Nested.Math", "avg_score"), Count()).GetSnapshotAsync();
Assert.Equal(Student.Data.Length, snapshot.GetValue<Value>("Count").IntegerValue);
Assert.Equal(Student.Data.Length, snapshot.Count.Value);
Assert.Equal(Student.Data.Sum(c => c.Nested.Math), snapshot.GetValue<double>("Sum_Nested.Math"));
Assert.Equal(Student.Data.Average(c => c.Nested.Math), snapshot.GetValue<double>("avg_score"));
}
}
@@ -1,4 +1,4 @@
// Copyright 2017, Google Inc. All rights reserved.
// Copyright 2017, Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -44,6 +44,11 @@ public class FirestoreFixture : CloudProjectFixtureBase, ICollectionFixture<Fire
/// </summary>
public CollectionReference HighScoreCollection { get; }

/// <summary>
/// A collection with <see cref="Student"/> data in. Don't modify in tests!
/// </summary>
public CollectionReference StudentCollection { get; }

/// <summary>
/// A collection with <see cref="ArrayDocument"/> data in. Don't modify in tests!
/// </summary>
Expand Down Expand Up @@ -77,6 +82,7 @@ public FirestoreFixture() : base(ProjectEnvironmentVariable)
FirestoreDb = new FirestoreDbBuilder { ProjectId = ProjectId, EmulatorDetection = EmulatorDetection.EmulatorOrProduction }.Build();
NonQueryCollection = FirestoreDb.Collection(CollectionPrefix + "-non-query");
HighScoreCollection = FirestoreDb.Collection(CollectionPrefix + "-high-scores");
StudentCollection = FirestoreDb.Collection(CollectionPrefix + "-students");
ArrayQueryCollection = FirestoreDb.Collection(CollectionPrefix + "-array-query");
CollectionGroupQueryCollection = FirestoreDb.Collection(CollectionPrefix + "-collection-groups");
Task.Run(PopulateCollections).Wait();
Expand All @@ -86,6 +92,7 @@ private async Task PopulateCollections()
{
await PopulateCollection(HighScoreCollection, HighScore.Data);
await PopulateCollection(ArrayQueryCollection, ArrayDocument.Data);
await PopulateCollection(StudentCollection, Student.Data);
}

private async Task PopulateCollection(CollectionReference collection, IEnumerable<object> documents)
Expand Down
@@ -0,0 +1,108 @@
// Copyright 2023, Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Api.Gax;
using System;

namespace Google.Cloud.Firestore.IntegrationTests.Models;

[FirestoreData]
public class Student : IEquatable<Student>
{
// Note: Keep ordered by name, so we don't need to do that explicitly in tests.
// All names have distinct values.
// "mathScore" has decimal values, "englishScore" has double.NaN value.
public static Student[] Data = new[]
{
new Student("Andy", 10, 87.2, double.NaN),
new Student("Bharati", 20, 98.5, 90),
new Student("Colin", 15, 99, 95),
new Student("Deborah", 25, 88, 100),
new Student("Edward", 30, 100, 80),
new Student("Faisal", 30, 80.4, 85)
};

/// <summary>
/// Needed for deserialization.
/// </summary>
public Student()
{
}

public Student(string name, int level, double mathScore, double englishScore)
{
Name = name;
Level = level;
MathScore = mathScore;
EnglishScore = englishScore;
Nested = new NestedScores { Math = mathScore, English = englishScore };
}

[FirestoreProperty]
public string Name { get; set; }

[FirestoreProperty]
public int Level { get; set; }

[FirestoreProperty]
public double MathScore { get; set; }

[FirestoreProperty]
public double EnglishScore { get; set; }

/// <summary>
/// A nested representation of the scores, used to test aggregation with dotted field
/// paths. The values in here are constructed to be the same as MathScore and EnglishScore.
/// </summary>
[FirestoreProperty]
public NestedScores Nested { get; set; }

public override int GetHashCode() =>
GaxEqualityHelpers.CombineHashCodes(
Name?.GetHashCode() ?? 0,
Level.GetHashCode(),
MathScore.GetHashCode(),
EnglishScore.GetHashCode(),
Nested?.GetHashCode() ?? 0);

public override bool Equals(object obj) => Equals(obj as Student);

public bool Equals(Student other) =>
other is not null &&
Name == other.Name &&
Level == other.Level &&
MathScore == other.MathScore &&
EnglishScore == other.EnglishScore &&
Equals(Nested, other.Nested);
}

[FirestoreData]
public class NestedScores : IEquatable<NestedScores>
{
[FirestoreProperty]
public double English { get; set; }

[FirestoreProperty]
public double Math { get; set; }

public override int GetHashCode() =>
GaxEqualityHelpers.CombineHashCodes(English.GetHashCode(), Math.GetHashCode());

public override bool Equals(object obj) => Equals(obj as NestedScores);

public bool Equals(NestedScores other) =>
other is not null &&
Math == other.Math &&
English == other.English;
}
Expand Up @@ -380,7 +380,7 @@ public async Task WhereIn_DocumentId_WithoutDocRef()
batch.Set(collection.Document("d"), new { X = 4 });
await batch.CommitAsync();

var query = collection.WhereIn(FieldPath.DocumentId, new[] { "a","c" });
var query = collection.WhereIn(FieldPath.DocumentId, new[] { "a", "c" });
var snapshot = await query.GetSnapshotAsync();
Assert.Equal(2, snapshot.Count);
}
Expand Down Expand Up @@ -450,24 +450,6 @@ public async Task WhereArrayContainsAny()
Assert.Equal(new[] { "a", "b", "d", "e" }, ids);
}

[Fact]
public async Task Count_WithoutLimit()
{
CollectionReference collection = _fixture.HighScoreCollection;
var snapshot = await collection.Count().GetSnapshotAsync();
Assert.Equal(HighScore.Data.Length, snapshot.Count);
}

[Fact]
public async Task Count_WithLimit()
{
CollectionReference collection = _fixture.HighScoreCollection;
var snapshotWithoutLimit = await collection.Count().GetSnapshotAsync();
var snapshotWithLimit = await collection.Limit(2).Count().GetSnapshotAsync();
Assert.Equal(HighScore.Data.Length, snapshotWithoutLimit.Count);
Assert.Equal(2, snapshotWithLimit.Count);
}

public static TheoryData<string, object, string[]> ArrayContainsTheoryData = new TheoryData<string, object, string[]>
{
{ "StringArray", "x", new[] { "string-x,y", "mixed" } },
Expand Down
@@ -1,4 +1,4 @@
// Copyright 2017, Google Inc. All rights reserved.
// Copyright 2017, Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -13,11 +13,14 @@
// limitations under the License.

using Google.Cloud.Firestore.IntegrationTests.Models;
using Google.Cloud.Firestore.V1;
using Grpc.Core;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using static Google.Cloud.Firestore.AggregateField;
using static Google.Cloud.Firestore.IntegrationTests.FirestoreAssert;

namespace Google.Cloud.Firestore.IntegrationTests
Expand Down Expand Up @@ -133,7 +136,7 @@ async Task IncrementCounter(Transaction transaction)
}

[Fact]
public async Task TransactionWithCountAggregation()
public async Task TransactionWithCount()
{
var collection = _fixture.HighScoreCollection;
await _fixture.FirestoreDb.RunTransactionAsync(async txn =>
Expand All @@ -143,5 +146,19 @@ public async Task TransactionWithCountAggregation()
Assert.Equal(HighScore.Data.Length, snapshot.Count);
});
}

[Fact]
public async Task TransactionWithAggregation()
{
var collection = _fixture.HighScoreCollection;
await _fixture.FirestoreDb.RunTransactionAsync(async txn =>
{
var aggQuery = collection.Aggregate(Sum("Score","SumOfScores"), Average("Score"), Count());
var snapshot = await txn.GetSnapshotAsync(aggQuery);
Assert.Equal(HighScore.Data.Sum(c => c.Score), snapshot.GetValue<long>("SumOfScores"));
Assert.Equal(HighScore.Data.Average(c => c.Score), snapshot.GetValue<double>("Avg_Score"));
Assert.Equal(HighScore.Data.Length, snapshot.GetValue<long>("Count"));
});
}
}
}

0 comments on commit 9a8bfd5

Please sign in to comment.