@@ -19,7 +19,7 @@ import FirebaseFirestore
1919import Foundation
2020import 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+
128198func 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 , * )
165242class PipelineIntegrationTests : FSTIntegrationTestCase {
166243 override func setUp( ) {
@@ -489,4 +566,155 @@ class PipelineIntegrationTests: FSTIntegrationTestCase {
489566 expectedIDs: [ subSubCollDocRef. documentID, collADocRef. documentID, collBDocRef. documentID]
490567 )
491568 }
569+
570+ func testAcceptsAndReturnsAllSupportedDataTypes( ) async throws {
571+ let db = firestore ( )
572+ let randomCol = collectionRef ( ) // Ensure a unique collection for the test
573+
574+ // Add a dummy document to the collection.
575+ // A pipeline query with .select against an empty collection might not behave as expected.
576+ try await randomCol. document ( " dummyDoc " ) . setData ( [ " field " : " value " ] )
577+
578+ let refDate = Date ( timeIntervalSince1970: 1_678_886_400 )
579+ let refTimestamp = Timestamp ( date: refDate)
580+
581+ let constantsFirst : [ Selectable ] = [
582+ Constant ( 1 ) . as ( " number " ) ,
583+ Constant ( " a string " ) . as ( " string " ) ,
584+ Constant ( true ) . as ( " boolean " ) ,
585+ Constant . nil. as ( " nil " ) ,
586+ Constant ( GeoPoint ( latitude: 0.1 , longitude: 0.2 ) ) . as ( " geoPoint " ) ,
587+ Constant ( refTimestamp) . as ( " timestamp " ) ,
588+ Constant ( refDate) . as ( " date " ) , // Firestore will convert this to a Timestamp
589+ Constant ( [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 0 ] as [ UInt8 ] ) . as ( " bytes " ) ,
590+ Constant ( db. document ( " foo/bar " ) ) . as ( " documentReference " ) ,
591+ Constant ( VectorValue ( [ 1 , 2 , 3 ] ) ) . as ( " vectorValue " ) ,
592+ Constant ( [ 1 , 2 , 3 ] ) . as ( " arrayValue " ) , // Treated as an array of numbers
593+ ]
594+
595+ let constantsSecond : [ Selectable ] = [
596+ MapExpression ( [
597+ " number " : 1 ,
598+ " string " : " a string " ,
599+ " boolean " : true ,
600+ " nil " : Constant . nil,
601+ " geoPoint " : GeoPoint ( latitude: 0.1 , longitude: 0.2 ) ,
602+ " timestamp " : refTimestamp,
603+ " date " : refDate,
604+ " uint8Array " : Data ( [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 0 ] ) ,
605+ " documentReference " : Constant ( db. document ( " foo/bar " ) ) ,
606+ " vectorValue " : VectorValue ( [ 1 , 2 , 3 ] ) ,
607+ " map " : [
608+ " number " : 2 ,
609+ " string " : " b string " ,
610+ ] ,
611+ " array " : [ 1 , " c string " ] ,
612+ ] ) . as ( " map " ) ,
613+ ArrayExpression ( [
614+ 1000 ,
615+ " another string " ,
616+ false ,
617+ Constant . nil,
618+ GeoPoint ( latitude: 10.1 , longitude: 20.2 ) ,
619+ Timestamp ( date: Date ( timeIntervalSince1970: 1_700_000_000 ) ) , // Different timestamp
620+ Date ( timeIntervalSince1970: 1_700_000_000 ) , // Different date
621+ [ 11 , 22 , 33 ] as [ UInt8 ] ,
622+ db. document ( " another/doc " ) ,
623+ VectorValue ( [ 7 , 8 , 9 ] ) ,
624+ [
625+ " nestedInArrayMapKey " : " value " ,
626+ " anotherNestedKey " : refTimestamp,
627+ ] ,
628+ [ 2000 , " deep nested array string " ] ,
629+ ] ) . as ( " array " ) ,
630+ ]
631+
632+ let expectedResultsMap : [ String : Sendable ? ] = [
633+ " number " : 1 ,
634+ " string " : " a string " ,
635+ " boolean " : true ,
636+ " nil " : nil ,
637+ " geoPoint " : GeoPoint ( latitude: 0.1 , longitude: 0.2 ) ,
638+ " timestamp " : refTimestamp,
639+ " date " : refTimestamp, // Dates are converted to Timestamps
640+ " bytes " : [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 0 ] as [ UInt8 ] ,
641+ " documentReference " : db. document ( " foo/bar " ) ,
642+ " vectorValue " : VectorValue ( [ 1 , 2 , 3 ] ) ,
643+ " arrayValue " : [ 1 , 2 , 3 ] ,
644+ " map " : [
645+ " number " : 1 ,
646+ " string " : " a string " ,
647+ " boolean " : true ,
648+ " nil " : nil ,
649+ " geoPoint " : GeoPoint ( latitude: 0.1 , longitude: 0.2 ) ,
650+ " timestamp " : refTimestamp,
651+ " date " : refTimestamp,
652+ " uint8Array " : Data ( [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 0 ] ) ,
653+ " documentReference " : db. document ( " foo/bar " ) ,
654+ " vectorValue " : VectorValue ( [ 1 , 2 , 3 ] ) ,
655+ " map " : [
656+ " number " : 2 ,
657+ " string " : " b string " ,
658+ ] ,
659+ " array " : [ 1 , " c string " ] ,
660+ ] ,
661+ " array " : [
662+ 1000 ,
663+ " another string " ,
664+ false ,
665+ nil ,
666+ GeoPoint ( latitude: 10.1 , longitude: 20.2 ) ,
667+ Timestamp ( date: Date ( timeIntervalSince1970: 1_700_000_000 ) ) ,
668+ Timestamp ( date: Date ( timeIntervalSince1970: 1_700_000_000 ) ) , // Dates are converted
669+ [ 11 , 22 , 33 ] as [ UInt8 ] ,
670+ db. document ( " another/doc " ) ,
671+ VectorValue ( [ 7 , 8 , 9 ] ) ,
672+ [
673+ " nestedInArrayMapKey " : " value " ,
674+ " anotherNestedKey " : refTimestamp,
675+ ] ,
676+ [ 2000 , " deep nested array string " ] ,
677+ ] ,
678+ ]
679+
680+ let pipeline = db. pipeline ( )
681+ . collection ( randomCol. path)
682+ . limit ( 1 )
683+ . select (
684+ constantsFirst + constantsSecond
685+ )
686+ let snapshot = try await pipeline. execute ( )
687+
688+ expectResults ( result: snapshot. results [ 0 ] , expected: expectedResultsMap)
689+ }
690+
691+ func testAcceptsAndReturnsNil( ) async throws {
692+ let db = firestore ( )
693+ let randomCol = collectionRef ( ) // Ensure a unique collection for the test
694+
695+ // Add a dummy document to the collection.
696+ // A pipeline query with .select against an empty collection might not behave as expected.
697+ try await randomCol. document ( " dummyDoc " ) . setData ( [ " field " : " value " ] )
698+
699+ let refDate = Date ( timeIntervalSince1970: 1_678_886_400 )
700+ let refTimestamp = Timestamp ( date: refDate)
701+
702+ let constantsFirst : [ Selectable ] = [
703+ Constant . nil. as ( " nil " ) ,
704+ ]
705+
706+ let expectedResultsMap : [ String : Sendable ? ] = [
707+ " nil " : nil ,
708+ ]
709+
710+ let pipeline = db. pipeline ( )
711+ . collection ( randomCol. path)
712+ . limit ( 1 )
713+ . select (
714+ constantsFirst
715+ )
716+ let snapshot = try await pipeline. execute ( )
717+
718+ expectResults ( result: snapshot. results [ 0 ] , expected: expectedResultsMap)
719+ }
492720}
0 commit comments