/* * 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 * * 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. */ import FirebaseFirestore import FirebaseFirestoreSwift import Foundation // TODO(sum/avg) remove `sumAvgIsPublic` from the directive below to enable these tests when sum/avg // is public #if sumAvgIsPublic && swift(>=5.5.2) @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class AggregationIntegrationTests: FSTIntegrationTestCase { func testCount() async throws { let collection = collectionRef() try await collection.addDocument(data: [:]) let snapshot = try await collection.count.getAggregation(source: .server) XCTAssertEqual(snapshot.count, 1) } func testCanRunAggregateQuery() async throws { // TODO(sum/avg) remove the check below when sum and avg are supported in production try XCTSkipIf( !FSTIntegrationTestCase.isRunningAgainstEmulator(), "only tested against emulator" ) let collection = collectionRef() try await collection.addDocument(data: ["author": "authorA", "title": "titleA", "pages": 100, "height": 24.5, "weight": 24.1, "foo": 1, "bar": 2, "baz": 3]) try await collection.addDocument(data: ["author": "authorB", "title": "titleB", "pages": 50, "height": 25.5, "weight": 75.5, "foo": 1, "bar": 2, "baz": 3]) let snapshot = try await collection.aggregate([ AggregateField.count(), AggregateField.sum("pages"), AggregateField.sum("weight"), AggregateField.average("pages"), AggregateField.average("weight"), ]).getAggregation(source: .server) // Count XCTAssertEqual(snapshot.get(AggregateField.count()) as? NSNumber, 2) // Sum XCTAssertEqual(snapshot.get(AggregateField.sum("pages")) as? NSNumber, 150) XCTAssertEqual(snapshot.get(AggregateField.sum("pages")) as? Double, 150) XCTAssertEqual(snapshot.get(AggregateField.sum("pages")) as? Int64, 150) XCTAssertEqual(snapshot.get(AggregateField.sum("weight")) as? NSNumber, 99.6) XCTAssertEqual(snapshot.get(AggregateField.sum("weight")) as? Double, 99.6) // Average XCTAssertEqual(snapshot.get(AggregateField.average("pages")) as? NSNumber, 75.0) XCTAssertEqual(snapshot.get(AggregateField.average("pages")) as? Double, 75.0) XCTAssertEqual(snapshot.get(AggregateField.average("pages")) as? Int64, 75) XCTAssertEqual(snapshot.get(AggregateField.average("weight")) as? NSNumber, 49.8) XCTAssertEqual(snapshot.get(AggregateField.average("weight")) as? Double, 49.8) } func testCannotPerformMoreThanMaxAggregations() async throws { // TODO(sum/avg) remove the check below when sum and avg are supported in production try XCTSkipIf( !FSTIntegrationTestCase.isRunningAgainstEmulator(), "only tested against emulator" ) let collection = collectionRef() try await collection.addDocument(data: ["author": "authorA", "title": "titleA", "pages": 100, "height": 24.5, "weight": 24.1, "foo": 1, "bar": 2, "baz": 3]) // Max is 5, we're attempting 6. I also like to live dangerously. do { let snapshot = try await collection.aggregate([ AggregateField.count(), AggregateField.sum("pages"), AggregateField.sum("weight"), AggregateField.average("pages"), AggregateField.average("weight"), AggregateField.average("foo"), ]).getAggregation(source: .server) XCTFail("Error expected.") } catch let error as NSError { XCTAssertNotNil(error) XCTAssertTrue(error.localizedDescription.contains("maximum number of aggregations")) } } func testPerformsAggregationsWhenNaNExistsForSomeFieldValues() async throws { // TODO(sum/avg) remove the check below when sum and avg are supported in production try XCTSkipIf( !FSTIntegrationTestCase.isRunningAgainstEmulator(), "only tested against emulator" ) let collection = collectionRef() try await collection.addDocument(data: ["author": "authorA", "title": "titleA", "pages": 100, "year": 1980, "rating": 4]) try await collection.addDocument(data: ["author": "authorB", "title": "titleB", "pages": 50, "year": 2020, "rating": Double.nan]) let snapshot = try await collection.aggregate([ AggregateField.sum("pages"), AggregateField.sum("rating"), AggregateField.average("pages"), AggregateField.average("rating"), ]).getAggregation(source: .server) // Sum XCTAssertEqual(snapshot.get(AggregateField.sum("pages")) as? NSNumber, 150) XCTAssertTrue((snapshot.get(AggregateField.sum("rating")) as? Double)?.isNaN ?? false) // Average XCTAssertEqual(snapshot.get(AggregateField.average("pages")) as? NSNumber, 75.0) XCTAssertTrue((snapshot.get(AggregateField.average("rating")) as? Double)?.isNaN ?? false) } func testThrowsAnErrorWhenGettingTheResultOfAnUnrequestedAggregation() async throws { // TODO(sum/avg) remove the check below when sum and avg are supported in production try XCTSkipIf( !FSTIntegrationTestCase.isRunningAgainstEmulator(), "only tested against emulator" ) let collection = collectionRef() try await collection.addDocument(data: [:]) let snapshot = try await collection.aggregate([AggregateField.average("foo")]) .getAggregation(source: .server) XCTAssertTrue(FSTNSExceptionUtil.testForException({ snapshot.count }, reasonContains: "'count()' was not requested in the aggregation query")) XCTAssertTrue(FSTNSExceptionUtil.testForException({ snapshot.get(AggregateField.sum("foo")) }, reasonContains: "'sum(foo)' was not requested in the aggregation query")) XCTAssertTrue(FSTNSExceptionUtil.testForException({ snapshot.get(AggregateField.average("bar")) }, reasonContains: "'avg(bar)' was not requested in the aggregation query")) } func testPerformsAggregationsOnNestedMapValues() async throws { // TODO(sum/avg) remove the check below when sum and avg are supported in production try XCTSkipIf( !FSTIntegrationTestCase.isRunningAgainstEmulator(), "only tested against emulator" ) let collection = collectionRef() try await collection.addDocument(data: ["metadata": [ "pages": 100, "rating": [ "critic": 2, "user": 5, ], ]]) try await collection.addDocument(data: ["metadata": [ "pages": 50, "rating": [ "critic": 4, "user": 4, ], ]]) let snapshot = try await collection.aggregate([ AggregateField.count(), AggregateField.sum("metadata.pages"), AggregateField.sum(FieldPath(["metadata", "rating", "user"])), AggregateField.average("metadata.pages"), AggregateField.average(FieldPath(["metadata", "rating", "critic"])), ]).getAggregation(source: .server) // Count XCTAssertEqual(snapshot.get(AggregateField.count()) as? NSNumber, 2) // Sum XCTAssertEqual( snapshot.get(AggregateField.sum(FieldPath(["metadata", "pages"]))) as? NSNumber, 150 ) XCTAssertEqual(snapshot.get(AggregateField.sum("metadata.pages")) as? NSNumber, 150) XCTAssertEqual(snapshot.get(AggregateField.sum("metadata.rating.user")) as? NSNumber, 9) // Average XCTAssertEqual( snapshot.get(AggregateField.average(FieldPath(["metadata", "pages"]))) as? Double, 75.0 ) XCTAssertEqual( snapshot.get(AggregateField.average("metadata.rating.critic")) as? Double, 3.0 ) } func testSumOverflow() async throws { // TODO(sum/avg) remove the check below when sum and avg are supported in production try XCTSkipIf( !FSTIntegrationTestCase.isRunningAgainstEmulator(), "only tested against emulator" ) let collection = collectionRef() try await collection.addDocument(data: [ "longOverflow": Int64.max, "accumulationOverflow": Int64.max, "positiveInfinity": Double.greatestFiniteMagnitude, "negativeInfinity": -Double.greatestFiniteMagnitude, ]) try await collection.addDocument(data: [ "longOverflow": Int64.max, "accumulationOverflow": 1, "positiveInfinity": Double.greatestFiniteMagnitude, "negativeInfinity": -Double.greatestFiniteMagnitude, ]) try await collection.addDocument(data: [ "longOverflow": Int64.max, "accumulationOverflow": -101, "positiveInfinity": Double.greatestFiniteMagnitude, "negativeInfinity": -Double.greatestFiniteMagnitude, ]) let snapshot = try await collection.aggregate([ AggregateField.sum("longOverflow"), AggregateField.sum("accumulationOverflow"), AggregateField.sum("positiveInfinity"), AggregateField.sum("negativeInfinity"), ]).getAggregation(source: .server) // Sum XCTAssertEqual( snapshot.get(AggregateField.sum("longOverflow")) as? Double, Double(Int64.max) + Double(Int64.max) + Double(Int64.max) ) XCTAssertEqual( snapshot.get(AggregateField.sum("accumulationOverflow")) as? Int64, Int64.max - 100 ) XCTAssertEqual( snapshot.get(AggregateField.sum("positiveInfinity")) as? Double, Double.infinity ) XCTAssertEqual( snapshot.get(AggregateField.sum("negativeInfinity")) as? Double, -Double.infinity ) } func testAverageOverflow() async throws { // TODO(sum/avg) remove the check below when sum and avg are supported in production try XCTSkipIf( !FSTIntegrationTestCase.isRunningAgainstEmulator(), "only tested against emulator" ) let collection = collectionRef() try await collection.addDocument(data: [ "longOverflow": Int64.max, "doubleOverflow": Double.greatestFiniteMagnitude, "negativeInfinity": -Double.greatestFiniteMagnitude, ]) try await collection.addDocument(data: [ "longOverflow": Int64.max, "doubleOverflow": Double.greatestFiniteMagnitude, "negativeInfinity": -Double.greatestFiniteMagnitude, ]) try await collection.addDocument(data: [ "longOverflow": Int64.max, "doubleOverflow": Double.greatestFiniteMagnitude, "negativeInfinity": -Double.greatestFiniteMagnitude, ]) let snapshot = try await collection.aggregate([ AggregateField.average("longOverflow"), AggregateField.average("doubleOverflow"), AggregateField.average("negativeInfinity"), ]).getAggregation(source: .server) // Average XCTAssertEqual( snapshot.get(AggregateField.average("longOverflow")) as? Double, Double(Int64.max) ) XCTAssertEqual( snapshot.get(AggregateField.average("doubleOverflow")) as? Double, Double.infinity ) XCTAssertEqual( snapshot.get(AggregateField.average("negativeInfinity")) as? Double, -Double.infinity ) } func testAverageUnderflow() async throws { // TODO(sum/avg) remove the check below when sum and avg are supported in production try XCTSkipIf( !FSTIntegrationTestCase.isRunningAgainstEmulator(), "only tested against emulator" ) let collection = collectionRef() try await collection.addDocument(data: ["underflowSmall": Double.leastNonzeroMagnitude]) try await collection.addDocument(data: ["underflowSmall": 0]) let snapshot = try await collection.aggregate([AggregateField.average("underflowSmall")]) .getAggregation(source: .server) // Average XCTAssertEqual(snapshot.get(AggregateField.average("underflowSmall")) as? Double, 0.0) } func testPerformsAggregateOverResultSetOfZeroDocuments() async throws { // TODO(sum/avg) remove the check below when sum and avg are supported in production try XCTSkipIf( !FSTIntegrationTestCase.isRunningAgainstEmulator(), "only tested against emulator" ) let collection = collectionRef() try await collection.addDocument(data: ["pages": 100]) try await collection.addDocument(data: ["pages": 50]) let snapshot = try await collection.whereField("pages", isGreaterThan: 200) .aggregate([AggregateField.count(), AggregateField.sum("pages"), AggregateField.average("pages")]).getAggregation(source: .server) // Count XCTAssertEqual(snapshot.get(AggregateField.count()) as? NSNumber, 0) // Sum XCTAssertEqual(snapshot.get(AggregateField.sum("pages")) as? NSNumber, 0) // Average // TODO: (sum/avg) this design is bad, will require and API update XCTAssertEqual(snapshot.get(AggregateField.average("pages")) as? NSNull, NSNull()) } func testPerformsAggregateOverResultSetOfZeroFields() async throws { // TODO(sum/avg) remove the check below when sum and avg are supported in production try XCTSkipIf( !FSTIntegrationTestCase.isRunningAgainstEmulator(), "only tested against emulator" ) let collection = collectionRef() try await collection.addDocument(data: ["pages": 100]) try await collection.addDocument(data: ["pages": 50]) let snapshot = try await collection .aggregate([AggregateField.count(), AggregateField.sum("notInMyDocs"), AggregateField.average("notInMyDocs")]).getAggregation(source: .server) // Count - 0 because aggregation is performed on documents matching the query AND documents // that have all aggregated fields XCTAssertEqual(snapshot.get(AggregateField.count()) as? NSNumber, 0) // Sum XCTAssertEqual(snapshot.get(AggregateField.sum("notInMyDocs")) as? NSNumber, 0) // Average // TODO: (sum/avg) this design is bad, will require and API update XCTAssertEqual(snapshot.get(AggregateField.average("notInMyDocs")) as? NSNull, NSNull()) } } #endif