Skip to content

Commit 47785df

Browse files
committed
Add Constant tests
1 parent 8ce2a65 commit 47785df

File tree

8 files changed

+297
-6
lines changed

8 files changed

+297
-6
lines changed

Firestore/Source/API/FIRPipelineBridge.mm

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,7 +928,9 @@ - (id)initWithCppResult:(api::PipelineResult)result db:(std::shared_ptr<api::Fir
928928
FSTUserDataWriter *dataWriter =
929929
[[FSTUserDataWriter alloc] initWithFirestore:_db
930930
serverTimestampBehavior:serverTimestampBehavior];
931-
return [dataWriter convertedValue:*data];
931+
NSDictionary<NSString *, id> *dictionary = [dataWriter convertedValue:*data];
932+
NSLog(@"Dictionary contents: %@", dictionary);
933+
return dictionary;
932934
}
933935

934936
- (nullable id)get:(id)field {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
public class ArrayExpression: FunctionExpr, @unchecked Sendable {
16+
var result: [Expr] = []
17+
public init(_ elements: [Sendable]) {
18+
for element in elements {
19+
result.append(Helper.sendableToExpr(element))
20+
}
21+
22+
super.init("array", result)
23+
}
24+
}

Firestore/Swift/Source/SwiftAPI/Pipeline/Expr/Constant.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ public struct Constant: Expr, BridgeWrapper, @unchecked Sendable {
4848
self.init(value as Any)
4949
}
5050

51+
// Initializer for Bytes
52+
public init(_ value: [UInt8]) {
53+
self.init(value as Any)
54+
}
55+
5156
// Initializer for GeoPoint values
5257
public init(_ value: GeoPoint) {
5358
self.init(value as Any)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
public class MapExpression: FunctionExpr, @unchecked Sendable {
16+
var result: [Expr] = []
17+
public init(_ elements: [String: Sendable]) {
18+
for element in elements {
19+
result.append(Constant(element.key))
20+
result.append(Helper.sendableToExpr(element.value))
21+
}
22+
23+
super.init("map", result)
24+
}
25+
}

Firestore/Swift/Source/SwiftAPI/Pipeline/Pipeline.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,13 @@ public struct Pipeline: @unchecked Sendable {
246246
)
247247
}
248248

249+
public func select(_ selections: [Selectable]) -> Pipeline {
250+
return Pipeline(
251+
stages: stages + [Select(selections: selections)],
252+
db: db
253+
)
254+
}
255+
249256
/// Filters documents from previous stages, including only those matching the specified
250257
/// `BooleanExpr`.
251258
///

Firestore/Swift/Source/SwiftAPI/Pipeline/PipelineResult.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public struct PipelineResult: @unchecked Sendable {
4545
public let updateTime: Timestamp?
4646

4747
/// Retrieves all fields in the result as a dictionary.
48-
public let data: [String: Sendable]
48+
public let data: [String: Sendable?]
4949

5050
/// Retrieves the field specified by `fieldPath`.
5151
/// - Parameter fieldPath: The field path (e.g., "foo" or "foo.bar").

Firestore/Swift/Tests/Integration/PipelineTests.swift

Lines changed: 229 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import FirebaseFirestore
1919
import Foundation
2020
import XCTest // For XCTFail, XCTAssertEqual etc.
2121

22-
private let bookDocs: [String: [String: Any]] = [
22+
private let bookDocs: [String: [String: Sendable]] = [
2323
"book1": [
2424
"title": "The Hitchhiker's Guide to the Galaxy",
2525
"author": "Douglas Adams",
@@ -125,6 +125,76 @@ private let bookDocs: [String: [String: Any]] = [
125125
],
126126
]
127127

128+
// A custom function to compare two values of type 'Sendable'
129+
private func areEqual(_ value1: Sendable?, _ value2: Sendable?) -> Bool {
130+
if value1 == nil || value2 == nil {
131+
return (value1 == nil || value1 as! NSObject == NSNull()) &&
132+
(value2 == nil || value2 as! NSObject == NSNull())
133+
}
134+
switch (value1!, value2!) {
135+
case let (v1 as [String: Sendable?], v2 as [String: Sendable?]):
136+
return areDictionariesEqual(v1, v2)
137+
case let (v1 as [Sendable?], v2 as [Sendable?]):
138+
return areArraysEqual(v1, v2)
139+
case let (v1 as Timestamp, v2 as Timestamp):
140+
return v1 == v2
141+
case let (v1 as Date, v2 as Timestamp):
142+
// Firestore converts Dates to Timestamps
143+
return Timestamp(date: v1) == v2
144+
case let (v1 as GeoPoint, v2 as GeoPoint):
145+
return v1.latitude == v2.latitude && v1.longitude == v2.longitude
146+
case let (v1 as DocumentReference, v2 as DocumentReference):
147+
return v1.path == v2.path
148+
case let (v1 as VectorValue, v2 as VectorValue):
149+
return v1.array == v2.array
150+
case let (v1 as Data, v2 as Data):
151+
return v1 == v2
152+
case let (v1 as Int, v2 as Int):
153+
return v1 == v2
154+
case let (v1 as String, v2 as String):
155+
return v1 == v2
156+
case let (v1 as Bool, v2 as Bool):
157+
return v1 == v2
158+
case let (v1 as UInt8, v2 as UInt8):
159+
return v1 == v2
160+
default:
161+
// Fallback for any other types, might need more specific checks
162+
return false
163+
}
164+
}
165+
166+
// A function to compare two dictionaries
167+
private func areDictionariesEqual(_ dict1: [String: Sendable?],
168+
_ dict2: [String: Sendable?]) -> Bool {
169+
guard dict1.count == dict2.count else { return false }
170+
171+
for (key, value1) in dict1 {
172+
print("key1: \(key)")
173+
print("value1: \(String(describing: value1))")
174+
print("value2: \(String(describing: dict2[key]))")
175+
guard let value2 = dict2[key], areEqual(value1, value2) else {
176+
return false
177+
}
178+
}
179+
return true
180+
}
181+
182+
// A function to compare two arrays
183+
private func areArraysEqual(_ array1: [Sendable?], _ array2: [Sendable?]) -> Bool {
184+
guard array1.count == array2.count else { return false }
185+
186+
for (index, value1) in array1.enumerated() {
187+
print("value1: \(String(describing: value1))")
188+
189+
let value2 = array2[index]
190+
print("value2: \(String(describing: value2))")
191+
if !areEqual(value1, value2) {
192+
return false
193+
}
194+
}
195+
return true
196+
}
197+
128198
func expectResults(_ snapshot: PipelineSnapshot,
129199
expectedCount: Int,
130200
file: StaticString = #file,
@@ -161,6 +231,13 @@ func expectResults(_ snapshot: PipelineSnapshot,
161231
)
162232
}
163233

234+
func expectResults(result: PipelineResult,
235+
expected: [String: Sendable],
236+
file: StaticString = #file,
237+
line: UInt = #line) {
238+
XCTAssertTrue(areDictionariesEqual(result.data, expected))
239+
}
240+
164241
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
165242
class PipelineIntegrationTests: FSTIntegrationTestCase {
166243
override func setUp() {
@@ -531,4 +608,155 @@ class PipelineIntegrationTests: FSTIntegrationTestCase {
531608
expectedIDs: [subSubCollDocRef.documentID, collADocRef.documentID, collBDocRef.documentID]
532609
)
533610
}
611+
612+
func testAcceptsAndReturnsAllSupportedDataTypes() async throws {
613+
let db = firestore()
614+
let randomCol = collectionRef() // Ensure a unique collection for the test
615+
616+
// Add a dummy document to the collection.
617+
// A pipeline query with .select against an empty collection might not behave as expected.
618+
try await randomCol.document("dummyDoc").setData(["field": "value"])
619+
620+
let refDate = Date(timeIntervalSince1970: 1_678_886_400)
621+
let refTimestamp = Timestamp(date: refDate)
622+
623+
let constantsFirst: [Selectable] = [
624+
Constant(1).as("number"),
625+
Constant("a string").as("string"),
626+
Constant(true).as("boolean"),
627+
Constant.nil.as("nil"),
628+
Constant(GeoPoint(latitude: 0.1, longitude: 0.2)).as("geoPoint"),
629+
Constant(refTimestamp).as("timestamp"),
630+
Constant(refDate).as("date"), // Firestore will convert this to a Timestamp
631+
Constant([1, 2, 3, 4, 5, 6, 7, 0] as [UInt8]).as("bytes"),
632+
Constant(db.document("foo/bar")).as("documentReference"),
633+
Constant(VectorValue([1, 2, 3])).as("vectorValue"),
634+
Constant([1, 2, 3]).as("arrayValue"), // Treated as an array of numbers
635+
]
636+
637+
let constantsSecond: [Selectable] = [
638+
MapExpression([
639+
"number": 1,
640+
"string": "a string",
641+
"boolean": true,
642+
"nil": Constant.nil,
643+
"geoPoint": GeoPoint(latitude: 0.1, longitude: 0.2),
644+
"timestamp": refTimestamp,
645+
"date": refDate,
646+
"uint8Array": Data([1, 2, 3, 4, 5, 6, 7, 0]),
647+
"documentReference": Constant(db.document("foo/bar")),
648+
"vectorValue": VectorValue([1, 2, 3]),
649+
"map": [
650+
"number": 2,
651+
"string": "b string",
652+
],
653+
"array": [1, "c string"],
654+
]).as("map"),
655+
ArrayExpression([
656+
1000,
657+
"another string",
658+
false,
659+
Constant.nil,
660+
GeoPoint(latitude: 10.1, longitude: 20.2),
661+
Timestamp(date: Date(timeIntervalSince1970: 1_700_000_000)), // Different timestamp
662+
Date(timeIntervalSince1970: 1_700_000_000), // Different date
663+
[11, 22, 33] as [UInt8],
664+
db.document("another/doc"),
665+
VectorValue([7, 8, 9]),
666+
[
667+
"nestedInArrayMapKey": "value",
668+
"anotherNestedKey": refTimestamp,
669+
],
670+
[2000, "deep nested array string"],
671+
]).as("array"),
672+
]
673+
674+
let expectedResultsMap: [String: Sendable?] = [
675+
"number": 1,
676+
"string": "a string",
677+
"boolean": true,
678+
"nil": nil,
679+
"geoPoint": GeoPoint(latitude: 0.1, longitude: 0.2),
680+
"timestamp": refTimestamp,
681+
"date": refTimestamp, // Dates are converted to Timestamps
682+
"bytes": [1, 2, 3, 4, 5, 6, 7, 0] as [UInt8],
683+
"documentReference": db.document("foo/bar"),
684+
"vectorValue": VectorValue([1, 2, 3]),
685+
"arrayValue": [1, 2, 3],
686+
"map": [
687+
"number": 1,
688+
"string": "a string",
689+
"boolean": true,
690+
"nil": nil,
691+
"geoPoint": GeoPoint(latitude: 0.1, longitude: 0.2),
692+
"timestamp": refTimestamp,
693+
"date": refTimestamp,
694+
"uint8Array": Data([1, 2, 3, 4, 5, 6, 7, 0]),
695+
"documentReference": db.document("foo/bar"),
696+
"vectorValue": VectorValue([1, 2, 3]),
697+
"map": [
698+
"number": 2,
699+
"string": "b string",
700+
],
701+
"array": [1, "c string"],
702+
],
703+
"array": [
704+
1000,
705+
"another string",
706+
false,
707+
nil,
708+
GeoPoint(latitude: 10.1, longitude: 20.2),
709+
Timestamp(date: Date(timeIntervalSince1970: 1_700_000_000)),
710+
Timestamp(date: Date(timeIntervalSince1970: 1_700_000_000)), // Dates are converted
711+
[11, 22, 33] as [UInt8],
712+
db.document("another/doc"),
713+
VectorValue([7, 8, 9]),
714+
[
715+
"nestedInArrayMapKey": "value",
716+
"anotherNestedKey": refTimestamp,
717+
],
718+
[2000, "deep nested array string"],
719+
],
720+
]
721+
722+
let pipeline = db.pipeline()
723+
.collection(randomCol.path)
724+
.limit(1)
725+
.select(
726+
constantsFirst + constantsSecond
727+
)
728+
let snapshot = try await pipeline.execute()
729+
730+
expectResults(result: snapshot.results[0], expected: expectedResultsMap)
731+
}
732+
733+
func testAcceptsAndReturnsNil() async throws {
734+
let db = firestore()
735+
let randomCol = collectionRef() // Ensure a unique collection for the test
736+
737+
// Add a dummy document to the collection.
738+
// A pipeline query with .select against an empty collection might not behave as expected.
739+
try await randomCol.document("dummyDoc").setData(["field": "value"])
740+
741+
let refDate = Date(timeIntervalSince1970: 1_678_886_400)
742+
let refTimestamp = Timestamp(date: refDate)
743+
744+
let constantsFirst: [Selectable] = [
745+
Constant.nil.as("nil"),
746+
]
747+
748+
let expectedResultsMap: [String: Sendable?] = [
749+
"nil": nil,
750+
]
751+
752+
let pipeline = db.pipeline()
753+
.collection(randomCol.path)
754+
.limit(1)
755+
.select(
756+
constantsFirst
757+
)
758+
let snapshot = try await pipeline.execute()
759+
760+
expectResults(result: snapshot.results[0], expected: expectedResultsMap)
761+
}
534762
}

Firestore/core/src/api/stages.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,12 +293,12 @@ class Unnest : public Stage {
293293
absl::optional<std::string> index_field_;
294294
};
295295

296-
class AddStage : public Stage {
296+
class RawStage : public Stage {
297297
public:
298-
AddStage(std::string name,
298+
RawStage(std::string name,
299299
std::vector<std::shared_ptr<Expr>> params,
300300
std::unordered_map<std::string, std::shared_ptr<Expr>> options);
301-
~AddStage() override = default;
301+
~RawStage() override = default;
302302
google_firestore_v1_Pipeline_Stage to_proto() const override;
303303

304304
private:

0 commit comments

Comments
 (0)