Compare commits
23 Commits
GRDB-4.0
...
dev/includ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e8ddeb691 | ||
|
|
e2c7b5cf73 | ||
|
|
d6075fb310 | ||
|
|
21b299b6fa | ||
|
|
8d38ecf4da | ||
|
|
5d78ea6b84 | ||
|
|
cf12b6415c | ||
|
|
e30b28347d | ||
|
|
b982bfa4ea | ||
|
|
e5af115c08 | ||
|
|
ca35d35b64 | ||
|
|
c3f72f1531 | ||
|
|
507b7e77ee | ||
|
|
e96e347583 | ||
|
|
81cfca7480 | ||
|
|
d70c2e9c11 | ||
|
|
b0fb840143 | ||
|
|
0c6193fdcf | ||
|
|
3392c5bbd0 | ||
|
|
495b714c05 | ||
|
|
58ebc1c49c | ||
|
|
ede4a5fd1e | ||
|
|
12a9e84ce3 |
@ -47,6 +47,12 @@
|
||||
560D92481C672C4B00F4F92B /* PersistableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560D92441C672C4B00F4F92B /* PersistableRecord.swift */; };
|
||||
560D924B1C672C4B00F4F92B /* TableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560D92461C672C4B00F4F92B /* TableRecord.swift */; };
|
||||
560D924C1C672C4B00F4F92B /* TableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560D92461C672C4B00F4F92B /* TableRecord.swift */; };
|
||||
560EBA3D2245F2DA000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA3C2245F2DA000E3FBC /* Row+QueryInterfaceRequest.swift */; };
|
||||
560EBA442245F2E4000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA3C2245F2DA000E3FBC /* Row+QueryInterfaceRequest.swift */; };
|
||||
560EBA452245F2E4000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA3C2245F2DA000E3FBC /* Row+QueryInterfaceRequest.swift */; };
|
||||
560EBA472245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA462245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift */; };
|
||||
560EBA482245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA462245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift */; };
|
||||
560EBA492245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA462245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift */; };
|
||||
5613ED3521A95A5C00DC7A68 /* ValueObservation+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED3421A95A5C00DC7A68 /* ValueObservation+Map.swift */; };
|
||||
5613ED3621A95A5C00DC7A68 /* ValueObservation+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED3421A95A5C00DC7A68 /* ValueObservation+Map.swift */; };
|
||||
5613ED3721A95A5C00DC7A68 /* ValueObservation+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED3421A95A5C00DC7A68 /* ValueObservation+Map.swift */; };
|
||||
@ -209,6 +215,8 @@
|
||||
5644DE6D20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE6C20F8C32E001FFDDE /* DatabaseValueConversion.swift */; };
|
||||
5644DE6E20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE6C20F8C32E001FFDDE /* DatabaseValueConversion.swift */; };
|
||||
5644DE6F20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE6C20F8C32E001FFDDE /* DatabaseValueConversion.swift */; };
|
||||
5646AFDB22440EE00019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5646AFD422440EE00019B5D9 /* AssociationHasManyRowScopeTests.swift */; };
|
||||
5646AFDC22440EE00019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5646AFD422440EE00019B5D9 /* AssociationHasManyRowScopeTests.swift */; };
|
||||
564A50C81BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */; };
|
||||
564CE43121AA901800652B19 /* ValueObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE43021AA901800652B19 /* ValueObserver.swift */; };
|
||||
564CE43221AA901800652B19 /* ValueObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE43021AA901800652B19 /* ValueObserver.swift */; };
|
||||
@ -629,9 +637,9 @@
|
||||
56CEB5011EAA2F4D00BFAF62 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB4F91EAA2F4D00BFAF62 /* FTS4.swift */; };
|
||||
56CEB5041EAA2F4D00BFAF62 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB4F91EAA2F4D00BFAF62 /* FTS4.swift */; };
|
||||
56CEB5071EAA2F4D00BFAF62 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB4F91EAA2F4D00BFAF62 /* FTS4.swift */; };
|
||||
56CEB5111EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */; };
|
||||
56CEB5141EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */; };
|
||||
56CEB5171EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */; };
|
||||
56CEB5111EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */; };
|
||||
56CEB5141EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */; };
|
||||
56CEB5171EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */; };
|
||||
56CEB5191EAA328900BFAF62 /* FTS5+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */; };
|
||||
56CEB51C1EAA328900BFAF62 /* FTS5+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */; };
|
||||
56CEB51F1EAA328900BFAF62 /* FTS5+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */; };
|
||||
@ -935,6 +943,8 @@
|
||||
560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertible.swift; sourceTree = "<group>"; };
|
||||
560D92441C672C4B00F4F92B /* PersistableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistableRecord.swift; sourceTree = "<group>"; };
|
||||
560D92461C672C4B00F4F92B /* TableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRecord.swift; sourceTree = "<group>"; };
|
||||
560EBA3C2245F2DA000E3FBC /* Row+QueryInterfaceRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Row+QueryInterfaceRequest.swift"; sourceTree = "<group>"; };
|
||||
560EBA462245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchableRecord+QueryInterface.swift"; sourceTree = "<group>"; };
|
||||
560FC5101CAEEDF10014AA8E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
5613ED3421A95A5C00DC7A68 /* ValueObservation+Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Map.swift"; sourceTree = "<group>"; };
|
||||
5613ED3E21A95A8B00DC7A68 /* ValueObservation+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Combine.swift"; sourceTree = "<group>"; };
|
||||
@ -996,6 +1006,7 @@
|
||||
56439B501F4CA1DC0066043F /* GRDBOSXPerformanceComparisonTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBOSXPerformanceComparisonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAfterNextTransactionCommitTests.swift; sourceTree = "<group>"; };
|
||||
5644DE6C20F8C32E001FFDDE /* DatabaseValueConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversion.swift; sourceTree = "<group>"; };
|
||||
5646AFD422440EE00019B5D9 /* AssociationHasManyRowScopeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasManyRowScopeTests.swift; sourceTree = "<group>"; };
|
||||
564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseCollationTests.swift; sourceTree = "<group>"; };
|
||||
564CE43021AA901800652B19 /* ValueObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueObserver.swift; sourceTree = "<group>"; };
|
||||
564CE4D521B2DEB500652B19 /* ValueObservation+CompactMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+CompactMap.swift"; sourceTree = "<group>"; };
|
||||
@ -1222,7 +1233,7 @@
|
||||
56CEB4F01EAA2EFA00BFAF62 /* FetchableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableRecord.swift; sourceTree = "<group>"; };
|
||||
56CEB4F81EAA2F4D00BFAF62 /* FTS3.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS3.swift; sourceTree = "<group>"; };
|
||||
56CEB4F91EAA2F4D00BFAF62 /* FTS4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4.swift; sourceTree = "<group>"; };
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FTS3+QueryInterfaceRequest.swift"; sourceTree = "<group>"; };
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FTS3+QueryInterface.swift"; sourceTree = "<group>"; };
|
||||
56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FTS5+QueryInterface.swift"; sourceTree = "<group>"; };
|
||||
56CEB5401EAA359A00BFAF62 /* Column.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Column.swift; sourceTree = "<group>"; };
|
||||
56CEB5411EAA359A00BFAF62 /* SQLExpressible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLExpressible.swift; sourceTree = "<group>"; };
|
||||
@ -1546,11 +1557,13 @@
|
||||
5653EAF720944BC900F46237 /* Association */,
|
||||
56CEB5401EAA359A00BFAF62 /* Column.swift */,
|
||||
56D91AA82205F2F000770D8D /* DatabasePromise.swift */,
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */,
|
||||
560EBA462245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift */,
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */,
|
||||
56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */,
|
||||
56300B6F1C53F592005A543B /* QueryInterfaceRequest.swift */,
|
||||
5653EB242094A14400F46237 /* QueryInterfaceRequest+Association.swift */,
|
||||
5616AAF0207CD45E00AC3664 /* RequestProtocols.swift */,
|
||||
560EBA3C2245F2DA000E3FBC /* Row+QueryInterfaceRequest.swift */,
|
||||
5605F1891C6B1A8700235C62 /* SQLCollatedExpression.swift */,
|
||||
566475991D97D8A000FF74B8 /* SQLCollection.swift */,
|
||||
56CEB5411EAA359A00BFAF62 /* SQLExpressible.swift */,
|
||||
@ -1615,6 +1628,7 @@
|
||||
5653EAC920944B4D00F46237 /* AssociationBelongsToSQLTests.swift */,
|
||||
5653EAC620944B4C00F46237 /* AssociationChainRowScopesTests.swift */,
|
||||
5653EACF20944B4E00F46237 /* AssociationChainSQLTests.swift */,
|
||||
5646AFD422440EE00019B5D9 /* AssociationHasManyRowScopeTests.swift */,
|
||||
56959632222D056D002CB7C9 /* AssociationHasManySQLTests.swift */,
|
||||
56959615222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift */,
|
||||
5653EACA20944B4D00F46237 /* AssociationHasOneSQLDerivationTests.swift */,
|
||||
@ -2318,6 +2332,7 @@
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
@ -2523,7 +2538,7 @@
|
||||
565490C11D5AE236005622CB /* FetchRequest.swift in Sources */,
|
||||
5613ED4621A95B2C00DC7A68 /* ValueReducer.swift in Sources */,
|
||||
5698AD1C1DAAD17F0056AF8C /* FTS5Tokenizer.swift in Sources */,
|
||||
56CEB5171EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */,
|
||||
56CEB5171EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */,
|
||||
56D91AB22205F8AC00770D8D /* SQLSelectQuery.swift in Sources */,
|
||||
56B964B71DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */,
|
||||
5613ED5221A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */,
|
||||
@ -2546,6 +2561,7 @@
|
||||
565490D81D5AE252005622CB /* DatabaseMigrator.swift in Sources */,
|
||||
56CEB5601EAA359A00BFAF62 /* SQLOrdering.swift in Sources */,
|
||||
565490C21D5AE236005622CB /* Row.swift in Sources */,
|
||||
560EBA452245F2E4000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */,
|
||||
565490C71D5AE236005622CB /* StatementColumnConvertible.swift in Sources */,
|
||||
565490D91D5AE252005622CB /* Migration.swift in Sources */,
|
||||
56CEB5001EAA2F4D00BFAF62 /* FTS3.swift in Sources */,
|
||||
@ -2570,6 +2586,7 @@
|
||||
56CEB5591EAA359A00BFAF62 /* SQLExpression.swift in Sources */,
|
||||
5674A6FF1F307F600095F066 /* FetchableRecord+Decodable.swift in Sources */,
|
||||
56E9FAD9221053DD00C703A8 /* SQLLiteral.swift in Sources */,
|
||||
560EBA492245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */,
|
||||
56B964A31DA51B4C0002DA19 /* FTS5.swift in Sources */,
|
||||
56D91AAB2205F2F100770D8D /* DatabasePromise.swift in Sources */,
|
||||
566475A01D97D8A000FF74B8 /* SQLCollection.swift in Sources */,
|
||||
@ -2677,7 +2694,7 @@
|
||||
566B91361FA4D3810012D5B0 /* TransactionObserver.swift in Sources */,
|
||||
564CE43221AA901800652B19 /* ValueObserver.swift in Sources */,
|
||||
5605F1721C672E4000235C62 /* DatabaseValueConvertible+RawRepresentable.swift in Sources */,
|
||||
56CEB5141EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */,
|
||||
56CEB5141EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */,
|
||||
566475CF1D981D5E00FF74B8 /* SQLFunctions.swift in Sources */,
|
||||
5659F49B1EA8D989004A4992 /* Pool.swift in Sources */,
|
||||
5653EB2220944C7C00F46237 /* HasOneAssociation.swift in Sources */,
|
||||
@ -2710,6 +2727,7 @@
|
||||
5657AB121D10899D006283EF /* URL.swift in Sources */,
|
||||
5616AAF2207CD45E00AC3664 /* RequestProtocols.swift in Sources */,
|
||||
5659F48B1EA8D94E004A4992 /* Utils.swift in Sources */,
|
||||
560EBA442245F2E4000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */,
|
||||
56A2387C1B9C75030082EB20 /* Configuration.swift in Sources */,
|
||||
56DAA2DE1DE9C827006E10C8 /* Cursor.swift in Sources */,
|
||||
560A37A51C8F625000949E71 /* DatabasePool.swift in Sources */,
|
||||
@ -2763,6 +2781,7 @@
|
||||
560D92431C672C3E00F4F92B /* StatementColumnConvertible.swift in Sources */,
|
||||
5613ED3621A95A5C00DC7A68 /* ValueObservation+Map.swift in Sources */,
|
||||
566475A51D9810A400FF74B8 /* SQLSelectable+QueryInterface.swift in Sources */,
|
||||
560EBA482245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */,
|
||||
56E9FAD8221053DD00C703A8 /* SQLLiteral.swift in Sources */,
|
||||
5653EB0720944C7C00F46237 /* ForeignKeyRequest.swift in Sources */,
|
||||
5657AABC1D107001006283EF /* NSData.swift in Sources */,
|
||||
@ -2855,6 +2874,7 @@
|
||||
564FCE5F20F7E11B00202B90 /* DatabaseValueConversionErrorTests.swift in Sources */,
|
||||
5653EADF20944B4F00F46237 /* AssociationHasOneSQLDerivationTests.swift in Sources */,
|
||||
5695961D222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
|
||||
5646AFDC22440EE00019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */,
|
||||
56F3E74D1E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */,
|
||||
56EB0AB31BCD787300A3DC55 /* DataMemoryTests.swift in Sources */,
|
||||
56B6EF57208CB4E3002F0ACB /* ColumnExpressionTests.swift in Sources */,
|
||||
@ -3044,6 +3064,7 @@
|
||||
56D4968D1D81316E008276D7 /* DatabaseFunctionTests.swift in Sources */,
|
||||
564FCE5E20F7E11B00202B90 /* DatabaseValueConversionErrorTests.swift in Sources */,
|
||||
5695961C222C456C002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
|
||||
5646AFDB22440EE00019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */,
|
||||
5653EADE20944B4F00F46237 /* AssociationHasOneSQLDerivationTests.swift in Sources */,
|
||||
56D496661D813086008276D7 /* QueryInterfaceRequestTests.swift in Sources */,
|
||||
56F3E7491E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */,
|
||||
@ -3175,7 +3196,7 @@
|
||||
566B91231FA4CF810012D5B0 /* Database+Schema.swift in Sources */,
|
||||
5613ED3F21A95A8B00DC7A68 /* ValueObservation+Combine.swift in Sources */,
|
||||
563B06AB217EF0CC00B38F35 /* ValueObservation.swift in Sources */,
|
||||
56CEB5111EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */,
|
||||
56CEB5111EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */,
|
||||
5659F4901EA8D964004A4992 /* ReadWriteBox.swift in Sources */,
|
||||
566A841A2041146100E50BFD /* DatabaseSnapshot.swift in Sources */,
|
||||
569EF0E2200D2D8400A9FA45 /* DatabaseRegion.swift in Sources */,
|
||||
@ -3216,6 +3237,7 @@
|
||||
560A37A71C8FF6E500949E71 /* SerializedDatabase.swift in Sources */,
|
||||
5653EB252094A14400F46237 /* QueryInterfaceRequest+Association.swift in Sources */,
|
||||
5605F1691C672E4000235C62 /* NSString.swift in Sources */,
|
||||
560EBA3D2245F2DA000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */,
|
||||
560D92401C672C3E00F4F92B /* DatabaseValueConvertible.swift in Sources */,
|
||||
564CE4D621B2DEB600652B19 /* ValueObservation+CompactMap.swift in Sources */,
|
||||
56A8C2301D1914540096E9D4 /* UUID.swift in Sources */,
|
||||
@ -3269,6 +3291,7 @@
|
||||
5698AD211DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */,
|
||||
56A238831B9C75030082EB20 /* DatabaseQueue.swift in Sources */,
|
||||
5605F1671C672E4000235C62 /* NSNumber.swift in Sources */,
|
||||
560EBA472245F5B7000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */,
|
||||
56E9FADA221053DD00C703A8 /* SQLLiteral.swift in Sources */,
|
||||
C96C0F2B2084A442006B2981 /* SQLiteDateParser.swift in Sources */,
|
||||
56A238871B9C75030082EB20 /* Row.swift in Sources */,
|
||||
|
||||
@ -183,6 +183,7 @@ extension DatabaseValue {
|
||||
/// storages instead:
|
||||
///
|
||||
/// 1.databaseValue.storage == 1.0.databaseValue.storage // false
|
||||
@inlinable
|
||||
public static func == (lhs: DatabaseValue, rhs: DatabaseValue) -> Bool {
|
||||
switch (lhs.storage, rhs.storage) {
|
||||
case (.null, .null):
|
||||
@ -208,11 +209,13 @@ extension DatabaseValue {
|
||||
// DatabaseValueConvertible
|
||||
extension DatabaseValue {
|
||||
/// Returns self
|
||||
@inlinable
|
||||
public var databaseValue: DatabaseValue {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Returns the database value
|
||||
@inlinable
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> DatabaseValue? {
|
||||
return dbValue
|
||||
}
|
||||
@ -277,12 +280,6 @@ extension DatabaseValue {
|
||||
public func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return self
|
||||
}
|
||||
|
||||
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
|
||||
/// :nodoc:
|
||||
public func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// CustomStringConvertible
|
||||
|
||||
@ -29,7 +29,7 @@ public final class Row : Equatable, Hashable, RandomAccessCollection, Expressibl
|
||||
|
||||
/// The number of columns in the row.
|
||||
public let count: Int
|
||||
|
||||
|
||||
// MARK: - Building rows
|
||||
|
||||
/// Creates an empty row.
|
||||
@ -504,7 +504,7 @@ extension Row {
|
||||
|
||||
/// Returns the record encoded in the given scope.
|
||||
///
|
||||
/// A fatal error is raised in the row was not fetched with a row adapter
|
||||
/// A fatal error is raised if the row was not fetched with a row adapter
|
||||
/// that defines this scope.
|
||||
///
|
||||
/// See https://github.com/groue/GRDB.swift/blob/master/README.md#joined-queries-support
|
||||
@ -531,6 +531,21 @@ extension Row {
|
||||
}
|
||||
return Record(row: scopedRow)
|
||||
}
|
||||
|
||||
/// Returns the prefetched records encoded for the given key.
|
||||
///
|
||||
/// A fatal error is raised if the row was not fetched with those
|
||||
/// prefetched rows.
|
||||
///
|
||||
/// See https://github.com/groue/GRDB.swift/blob/master/Documentation/AssociationsBasics.md
|
||||
/// for more information.
|
||||
public subscript<Record: FetchableRecord>(_ key: String) -> [Record] {
|
||||
guard let rows = prefetchedRowsTree[key] else {
|
||||
// Programmer error
|
||||
fatalError("no prefetched rows for key: \(key)")
|
||||
}
|
||||
return rows.map(Record.init(row:))
|
||||
}
|
||||
}
|
||||
|
||||
extension Row {
|
||||
@ -584,6 +599,14 @@ extension Row {
|
||||
return ScopesTreeView(scopes: scopes)
|
||||
}
|
||||
|
||||
public var prefetchedRows: [String: [Row]] {
|
||||
return impl.prefetchedRows
|
||||
}
|
||||
|
||||
public var prefetchedRowsTree: PrefetchedRowsTreeView {
|
||||
return PrefetchedRowsTreeView(row: self, scopes: scopes)
|
||||
}
|
||||
|
||||
/// Returns a copy of the row, without any scopes.
|
||||
///
|
||||
/// This property can turn out useful when you want to test the content of
|
||||
@ -605,6 +628,10 @@ extension Row {
|
||||
public var unadapted: Row {
|
||||
return impl.unadaptedRow(self)
|
||||
}
|
||||
|
||||
func withPrefetchedRows(_ rows: [Row], forKey key: String) -> Row {
|
||||
return impl.row(self, withPrefetchedRows: rows, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// A cursor of database rows. For example:
|
||||
@ -936,7 +963,7 @@ extension FetchRequest where RowDecoder: Row {
|
||||
return try Row.fetchOne(db, self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ExpressibleByDictionaryLiteral
|
||||
extension Row {
|
||||
|
||||
@ -1048,7 +1075,7 @@ extension Row {
|
||||
}
|
||||
|
||||
private func debugDescription(level: Int) -> String {
|
||||
if level == 0 && self == self.unadapted {
|
||||
if level == 0 && self == self.unadapted && self.prefetchedRows.isEmpty {
|
||||
return description
|
||||
}
|
||||
let prefix = repeatElement(" ", count: level + 1).joined(separator: "")
|
||||
@ -1065,7 +1092,9 @@ extension Row {
|
||||
for (name, scopedRow) in scopes.sorted(by: { $0.name < $1.name }) {
|
||||
str += "\n" + prefix + "- " + name + ": " + scopedRow.debugDescription(level: level + 1)
|
||||
}
|
||||
|
||||
for (name, rows) in prefetchedRows.sorted(by: { $0.key < $1.key }) {
|
||||
str += "\n" + prefix + "- " + name + ": \(rows.count) row(s)"
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
@ -1234,6 +1263,41 @@ extension Row {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Row.PrefetchedRowsTreeView
|
||||
|
||||
extension Row {
|
||||
|
||||
/// A view on the prefetched rows tree.
|
||||
public struct PrefetchedRowsTreeView {
|
||||
let row: Row
|
||||
let scopes: ScopesView
|
||||
|
||||
public var keys: Set<String> {
|
||||
var keys = Set<String>()
|
||||
keys.formUnion(row.prefetchedRows.keys)
|
||||
for (_, row) in row.scopes {
|
||||
keys.formUnion(row.prefetchedRowsTree.keys)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
public subscript(_ key: String) -> [Row]? {
|
||||
if let rows = row.prefetchedRows[key] {
|
||||
return rows
|
||||
}
|
||||
var fifo = Array(scopes)
|
||||
while !fifo.isEmpty {
|
||||
let scope = fifo.removeFirst()
|
||||
if let rows = scope.row.prefetchedRows[key] {
|
||||
return rows
|
||||
}
|
||||
fifo.append(contentsOf: scope.row.scopes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RowImpl
|
||||
|
||||
// The protocol for Row underlying implementation
|
||||
@ -1241,6 +1305,7 @@ protocol RowImpl {
|
||||
var count: Int { get }
|
||||
var isFetched: Bool { get }
|
||||
var scopes: Row.ScopesView { get }
|
||||
var prefetchedRows: [String: [Row]] { get }
|
||||
func columnName(atUncheckedIndex index: Int) -> String
|
||||
func hasNull(atUncheckedIndex index:Int) -> Bool
|
||||
func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue
|
||||
@ -1255,6 +1320,7 @@ protocol RowImpl {
|
||||
func unscopedRow(_ row: Row) -> Row
|
||||
func unadaptedRow(_ row: Row) -> Row
|
||||
func copiedRow(_ row: Row) -> Row
|
||||
func row(_ row: Row, withPrefetchedRows rows: [Row], forKey key: String) -> Row
|
||||
}
|
||||
|
||||
extension RowImpl {
|
||||
@ -1278,6 +1344,11 @@ extension RowImpl {
|
||||
return Row.ScopesView()
|
||||
}
|
||||
|
||||
var prefetchedRows: [String: [Row]] {
|
||||
// unless customized, assume no prefetched rows (see AdaptedRowImpl for customization)
|
||||
return [:]
|
||||
}
|
||||
|
||||
func hasNull(atUncheckedIndex index:Int) -> Bool {
|
||||
// unless customized, use slow check (see StatementRowImpl and AdaptedRowImpl for customization)
|
||||
return databaseValue(atUncheckedIndex: index).isNull
|
||||
@ -1309,6 +1380,73 @@ extension RowImpl {
|
||||
from: databaseValue(atUncheckedIndex: index),
|
||||
conversionContext: ValueConversionContext(Row(impl: self)).atColumn(index))
|
||||
}
|
||||
|
||||
func row(_ row: Row, withPrefetchedRows rows: [Row], forKey key: String) -> Row {
|
||||
return Row(impl: PrefetchedRowsImpl(base: row, prefetchedRows: [key: rows]))
|
||||
}
|
||||
}
|
||||
|
||||
private struct PrefetchedRowsImpl: RowImpl {
|
||||
var base: Row
|
||||
var prefetchedRows: [String : [Row]]
|
||||
|
||||
var count: Int {
|
||||
return base.count
|
||||
}
|
||||
|
||||
var isFetched: Bool {
|
||||
return base.isFetched
|
||||
}
|
||||
|
||||
var scopes: Row.ScopesView {
|
||||
return base.scopes
|
||||
}
|
||||
|
||||
func columnName(atUncheckedIndex index: Int) -> String {
|
||||
return base.impl.columnName(atUncheckedIndex: index)
|
||||
}
|
||||
|
||||
func hasNull(atUncheckedIndex index:Int) -> Bool {
|
||||
return base.impl.hasNull(atUncheckedIndex: index)
|
||||
}
|
||||
|
||||
func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue {
|
||||
return base.impl.databaseValue(atUncheckedIndex: index)
|
||||
}
|
||||
|
||||
func fastDecode<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ type: Value.Type, atUncheckedIndex index: Int) -> Value {
|
||||
return base.impl.fastDecode(type, atUncheckedIndex: index)
|
||||
}
|
||||
|
||||
func fastDecodeIfPresent<Value: DatabaseValueConvertible & StatementColumnConvertible>(_ type: Value.Type, atUncheckedIndex index: Int) -> Value? {
|
||||
return base.impl.fastDecodeIfPresent(type, atUncheckedIndex: index)
|
||||
}
|
||||
|
||||
func dataNoCopy(atUncheckedIndex index:Int) -> Data? {
|
||||
return base.impl.dataNoCopy(atUncheckedIndex: index)
|
||||
}
|
||||
|
||||
func index(ofColumn name: String) -> Int? {
|
||||
return base.impl.index(ofColumn: name)
|
||||
}
|
||||
|
||||
func unscopedRow(_ row: Row) -> Row {
|
||||
return base.unscoped
|
||||
}
|
||||
|
||||
func unadaptedRow(_ row: Row) -> Row {
|
||||
return base.unadapted
|
||||
}
|
||||
|
||||
func copiedRow(_ row: Row) -> Row {
|
||||
return row
|
||||
}
|
||||
|
||||
func row(_ row: Row, withPrefetchedRows rows: [Row], forKey key: String) -> Row {
|
||||
var impl = self
|
||||
impl.prefetchedRows[key] = rows
|
||||
return Row(impl: impl)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: merge with StatementCopyRowImpl eventually?
|
||||
|
||||
@ -242,32 +242,47 @@ extension Association {
|
||||
}
|
||||
|
||||
extension Association {
|
||||
// TODO
|
||||
public func including<A: AssociationToMany>(all association: A) -> Self where A.OriginRowDecoder == RowDecoder {
|
||||
return mapRelation {
|
||||
association.sqlAssociation.relation(from: $0, kind: .all)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an association that includes another one. The columns of the
|
||||
/// associated record are selected. The returned association does not
|
||||
/// require that the associated database table contains a matching row.
|
||||
public func including<A: Association>(optional association: A) -> Self where A.OriginRowDecoder == RowDecoder {
|
||||
return mapRelation { association.sqlAssociation.relation(from: $0, required: false) }
|
||||
return mapRelation {
|
||||
association.sqlAssociation.relation(from: $0, kind: .optional)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an association that includes another one. The columns of the
|
||||
/// associated record are selected. The returned association requires
|
||||
/// that the associated database table contains a matching row.
|
||||
public func including<A: Association>(required association: A) -> Self where A.OriginRowDecoder == RowDecoder {
|
||||
return mapRelation { association.sqlAssociation.relation(from: $0, required: true) }
|
||||
return mapRelation {
|
||||
association.sqlAssociation.relation(from: $0, kind: .required)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an association that joins another one. The columns of the
|
||||
/// associated record are not selected. The returned association does not
|
||||
/// require that the associated database table contains a matching row.
|
||||
public func joining<A: Association>(optional association: A) -> Self where A.OriginRowDecoder == RowDecoder {
|
||||
return mapRelation { association.select([]).sqlAssociation.relation(from: $0, required: false) }
|
||||
return mapRelation {
|
||||
association.select([]).sqlAssociation.relation(from: $0, kind: .optional)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an association that joins another one. The columns of the
|
||||
/// associated record are not selected. The returned association requires
|
||||
/// that the associated database table contains a matching row.
|
||||
public func joining<A: Association>(required association: A) -> Self where A.OriginRowDecoder == RowDecoder {
|
||||
return mapRelation { association.select([]).sqlAssociation.relation(from: $0, required: true) }
|
||||
return mapRelation {
|
||||
association.select([]).sqlAssociation.relation(from: $0, kind: .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,7 +417,7 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
// SQLAssociation is a non-empty array of association elements
|
||||
private struct Element {
|
||||
var key: String
|
||||
var condition: SQLJoin.Condition
|
||||
var condition: SQLJoinCondition
|
||||
var relation: SQLRelation
|
||||
}
|
||||
private var head: Element
|
||||
@ -415,7 +430,7 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
self.tail = tail
|
||||
}
|
||||
|
||||
init(key: String, condition: SQLJoin.Condition, relation: SQLRelation) {
|
||||
init(key: String, condition: SQLJoinCondition, relation: SQLRelation) {
|
||||
head = Element(key: key, condition: condition, relation: relation)
|
||||
tail = []
|
||||
}
|
||||
@ -440,9 +455,9 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
}
|
||||
|
||||
/// Support for joining methods joining(optional:), etc.
|
||||
func relation(from origin: SQLRelation, required: Bool) -> SQLRelation {
|
||||
func relation(from origin: SQLRelation, kind: SQLJoin.Kind) -> SQLRelation {
|
||||
let headJoin = SQLJoin(
|
||||
isRequired: required,
|
||||
kind: kind,
|
||||
condition: head.condition,
|
||||
relation: head.relation)
|
||||
|
||||
@ -466,7 +481,13 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
let reducedHead = Element(key: next.key, condition: next.condition, relation: nextRelation)
|
||||
let reducedTail = Array(tail.dropFirst())
|
||||
let reducedAssociation = SQLAssociation(head: reducedHead, tail: reducedTail)
|
||||
return reducedAssociation.relation(from: origin, required: required)
|
||||
return reducedAssociation.relation(from: origin, kind: kind)
|
||||
}
|
||||
|
||||
struct Pivot {
|
||||
var relation: SQLRelation
|
||||
var condition: SQLJoinCondition
|
||||
var alias: TableAlias
|
||||
}
|
||||
|
||||
/// Support for (TableRecord & EncodableRecord).request(for:).
|
||||
@ -480,7 +501,7 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
///
|
||||
/// // SELECT head.* FROM head WHERE head.originId = 123
|
||||
/// origin.request(for: association)
|
||||
func relation(to originTable: String, container originContainer: @escaping (Database) throws -> PersistenceContainer) -> SQLRelation {
|
||||
func pivot(from originTable: String, rows: @escaping (Database) throws -> [Row]) -> Pivot {
|
||||
// Build a "pivot" relation whose filter is the pivot condition
|
||||
// injected with values contained in originContainer.
|
||||
let pivotCondition = pivot.condition
|
||||
@ -490,11 +511,13 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
.filter { db in
|
||||
let originAlias = TableAlias(tableName: originTable)
|
||||
|
||||
// Build a join condition: `association.originId = origin.id`
|
||||
let joinExpression = try pivotCondition.sqlExpression(db, leftAlias: originAlias, rightAlias: pivotAlias)
|
||||
// Build a join expression: `association.originId = origin.id`
|
||||
let joinExpression = try pivotCondition.joinExpression(db, leftAlias: originAlias, rightAlias: pivotAlias)
|
||||
|
||||
// Replace `origin.id` with 123
|
||||
return try joinExpression.resolvedExpression(inContext: [originAlias: originContainer(db)])
|
||||
// Resolve to `association.originId = 123` or `association.originId IN (1, 2, 3)`
|
||||
let rows = try rows(db)
|
||||
precondition(!rows.isEmpty)
|
||||
return joinExpression.resolved(with: rows, for: originAlias)
|
||||
}
|
||||
|
||||
// We use elements backward: join conditions have to be reversed.
|
||||
@ -504,12 +527,13 @@ public /* TODO: internal */ struct SQLAssociation {
|
||||
|
||||
// Empty tail?
|
||||
guard var reversedHead = reversedElements.first else {
|
||||
return pivotRelation
|
||||
return Pivot(relation: pivotRelation, condition: pivotCondition, alias: pivotAlias)
|
||||
}
|
||||
|
||||
reversedHead.relation = pivotRelation.select([])
|
||||
let reversedTail = Array(reversedElements.dropFirst())
|
||||
let reversedAssociation = SQLAssociation(head: reversedHead, tail: reversedTail)
|
||||
return reversedAssociation.relation(from: head.relation, required: true)
|
||||
let relation = reversedAssociation.relation(from: head.relation, kind: .required)
|
||||
return Pivot(relation: relation, condition: pivotCondition, alias: pivotAlias)
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +146,7 @@ extension TableRecord {
|
||||
destinationTable: Destination.databaseTableName,
|
||||
foreignKey: foreignKey)
|
||||
|
||||
let condition = SQLJoin.Condition(
|
||||
let condition = SQLJoinCondition(
|
||||
foreignKeyRequest: foreignKeyRequest,
|
||||
originIsLeft: true)
|
||||
|
||||
|
||||
@ -146,7 +146,7 @@ extension TableRecord {
|
||||
destinationTable: databaseTableName,
|
||||
foreignKey: foreignKey)
|
||||
|
||||
let condition = SQLJoin.Condition(
|
||||
let condition = SQLJoinCondition(
|
||||
foreignKeyRequest: foreignKeyRequest,
|
||||
originIsLeft: false)
|
||||
|
||||
|
||||
@ -148,7 +148,7 @@ extension TableRecord {
|
||||
destinationTable: databaseTableName,
|
||||
foreignKey: foreignKey)
|
||||
|
||||
let condition = SQLJoin.Condition(
|
||||
let condition = SQLJoinCondition(
|
||||
foreignKeyRequest: foreignKeyRequest,
|
||||
originIsLeft: false)
|
||||
|
||||
|
||||
@ -36,12 +36,6 @@ extension ColumnExpression {
|
||||
public func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return QualifiedColumn(name, alias: alias)
|
||||
}
|
||||
|
||||
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
|
||||
/// :nodoc:
|
||||
public func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/// A column in a database table.
|
||||
@ -85,16 +79,6 @@ struct QualifiedColumn: ColumnExpression {
|
||||
// Never requalify
|
||||
return self
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
guard
|
||||
let container = context[alias],
|
||||
let value = container.value(forCaseInsensitiveColumn: name) else
|
||||
{
|
||||
return self
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/// Support for column enums:
|
||||
|
||||
72
GRDB/QueryInterface/FetchableRecord+QueryInterface.swift
Normal file
72
GRDB/QueryInterface/FetchableRecord+QueryInterface.swift
Normal file
@ -0,0 +1,72 @@
|
||||
extension FetchableRecord {
|
||||
|
||||
// MARK: Fetching From QueryInterfaceRequest
|
||||
|
||||
/// Returns an array of records fetched from a query interface request.
|
||||
///
|
||||
/// let request = try Player.all()
|
||||
/// let players = try Player.fetchAll(db, request) // [Player]
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - sql: a FetchRequest.
|
||||
/// - returns: An array of records.
|
||||
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
|
||||
@inlinable
|
||||
public static func fetchAll<T>(_ db: Database, _ request: QueryInterfaceRequest<T>) throws -> [Self] {
|
||||
if request.query.needsPrefetch {
|
||||
return try Row.fetchAllWithPrefetchedRows(db, request).map(Self.init(row:))
|
||||
} else {
|
||||
let (statement, adapter) = try request.prepare(db)
|
||||
return try fetchAll(statement, adapter: adapter)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a single record fetched from a query interface request.
|
||||
///
|
||||
/// let request = try Player.filter(key: 1)
|
||||
/// let player = try Player.fetchOne(db, request) // Player?
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - sql: a FetchRequest.
|
||||
/// - returns: An optional record.
|
||||
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
|
||||
@inlinable
|
||||
public static func fetchOne<T>(_ db: Database, _ request: QueryInterfaceRequest<T>) throws -> Self? {
|
||||
if request.query.needsPrefetch {
|
||||
return try Row.fetchOneWithPrefetchedRows(db, request).map(Self.init(row:))
|
||||
} else {
|
||||
let (statement, adapter) = try request.prepare(db)
|
||||
return try fetchOne(statement, adapter: adapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension QueryInterfaceRequest where RowDecoder: FetchableRecord {
|
||||
/// An array of fetched records.
|
||||
///
|
||||
/// let request: ... // Some TypedRequest that fetches Player
|
||||
/// let players = try request.fetchAll(db) // [Player]
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: An array of records.
|
||||
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
|
||||
@inlinable
|
||||
public func fetchAll(_ db: Database) throws -> [RowDecoder] {
|
||||
return try RowDecoder.fetchAll(db, self)
|
||||
}
|
||||
|
||||
/// The first fetched record.
|
||||
///
|
||||
/// let request: ... // Some TypedRequest that fetches Player
|
||||
/// let player = try request.fetchOne(db) // Player?
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: An optional record.
|
||||
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
|
||||
@inlinable
|
||||
public func fetchOne(_ db: Database) throws -> RowDecoder? {
|
||||
return try RowDecoder.fetchOne(db, self)
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,22 @@
|
||||
extension QueryInterfaceRequest where RowDecoder: TableRecord {
|
||||
// MARK: - Associations
|
||||
|
||||
// TODO
|
||||
public func including<A: AssociationToMany>(all association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
|
||||
return mapQuery {
|
||||
$0.mapRelation {
|
||||
association.sqlAssociation.relation(from: $0, kind: .all)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a request that includes an association. The columns of the
|
||||
/// associated record are selected. The returned association does not
|
||||
/// require that the associated database table contains a matching row.
|
||||
public func including<A: Association>(optional association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
|
||||
return mapQuery {
|
||||
$0.mapRelation {
|
||||
association.sqlAssociation.relation(from: $0, required: false)
|
||||
association.sqlAssociation.relation(from: $0, kind: .optional)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,7 +27,7 @@ extension QueryInterfaceRequest where RowDecoder: TableRecord {
|
||||
public func including<A: Association>(required association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
|
||||
return mapQuery {
|
||||
$0.mapRelation {
|
||||
association.sqlAssociation.relation(from: $0, required: true)
|
||||
association.sqlAssociation.relation(from: $0, kind: .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +38,7 @@ extension QueryInterfaceRequest where RowDecoder: TableRecord {
|
||||
public func joining<A: Association>(optional association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
|
||||
return mapQuery {
|
||||
$0.mapRelation {
|
||||
association.select([]).sqlAssociation.relation(from: $0, required: false)
|
||||
association.select([]).sqlAssociation.relation(from: $0, kind: .optional)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -40,7 +49,7 @@ extension QueryInterfaceRequest where RowDecoder: TableRecord {
|
||||
public func joining<A: Association>(required association: A) -> QueryInterfaceRequest where A.OriginRowDecoder == RowDecoder {
|
||||
return mapQuery {
|
||||
$0.mapRelation {
|
||||
association.select([]).sqlAssociation.relation(from: $0, required: true)
|
||||
association.select([]).sqlAssociation.relation(from: $0, kind: .required)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -107,10 +116,12 @@ extension TableRecord where Self: EncodableRecord {
|
||||
/// let team: Team = ...
|
||||
/// let players = try team.players.fetchAll(db) // [Player]
|
||||
public func request<A: Association>(for association: A) -> QueryInterfaceRequest<A.RowDecoder> where A.OriginRowDecoder == Self {
|
||||
let relation = association.sqlAssociation.relation(
|
||||
to: type(of: self).databaseTableName,
|
||||
container: { try PersistenceContainer($0, self) })
|
||||
return QueryInterfaceRequest<A.RowDecoder>(query: SQLSelectQuery(relation: relation))
|
||||
let originTable = type(of: self).databaseTableName
|
||||
let pivot = association.sqlAssociation.pivot(from: originTable, rows: { db in
|
||||
try [Row(PersistenceContainer(db, self))]
|
||||
})
|
||||
let query = SQLSelectQuery(relation: pivot.relation)
|
||||
return QueryInterfaceRequest<A.RowDecoder>(query: query)
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,6 +129,11 @@ extension TableRecord {
|
||||
|
||||
// MARK: - Associations
|
||||
|
||||
// TODO
|
||||
public static func including<A: AssociationToMany>(all association: A) -> QueryInterfaceRequest<Self> where A.OriginRowDecoder == Self {
|
||||
return all().including(all: association)
|
||||
}
|
||||
|
||||
/// Creates a request that includes an association. The columns of the
|
||||
/// associated record are selected. The returned association does not
|
||||
/// require that the associated database table contains a matching row.
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
///
|
||||
/// See https://github.com/groue/GRDB.swift#the-query-interface
|
||||
public struct QueryInterfaceRequest<T> {
|
||||
var query: SQLSelectQuery
|
||||
@usableFromInline var query: SQLSelectQuery
|
||||
}
|
||||
|
||||
extension QueryInterfaceRequest : FetchRequest {
|
||||
@ -58,6 +58,7 @@ extension QueryInterfaceRequest : FetchRequest {
|
||||
/// - parameter db: A database connection.
|
||||
/// :nodoc:
|
||||
public func databaseRegion(_ db: Database) throws -> DatabaseRegion {
|
||||
// TODO: include region of indirect joins
|
||||
return try SQLSelectQueryGenerator(query).databaseRegion(db)
|
||||
}
|
||||
}
|
||||
|
||||
176
GRDB/QueryInterface/Row+QueryInterfaceRequest.swift
Normal file
176
GRDB/QueryInterface/Row+QueryInterfaceRequest.swift
Normal file
@ -0,0 +1,176 @@
|
||||
extension Row {
|
||||
|
||||
// MARK: - Fetching From QueryInterfaceRequest
|
||||
|
||||
/// Returns an array of rows fetched from a fetch request.
|
||||
///
|
||||
/// let request = Player.all()
|
||||
/// let rows = try Row.fetchAll(db, request)
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - request: A FetchRequest.
|
||||
/// - returns: An array of rows.
|
||||
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
|
||||
@inlinable
|
||||
public static func fetchAll<T>(_ db: Database, _ request: QueryInterfaceRequest<T>) throws -> [Row] {
|
||||
if request.query.needsPrefetch {
|
||||
return try fetchAllWithPrefetchedRows(db, request)
|
||||
}
|
||||
let (statement, adapter) = try request.prepare(db)
|
||||
return try fetchAll(statement, adapter: adapter)
|
||||
}
|
||||
|
||||
/// Returns a single row fetched from a fetch request.
|
||||
///
|
||||
/// let request = Player.filter(key: 1)
|
||||
/// let row = try Row.fetchOne(db, request)
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - request: A FetchRequest.
|
||||
/// - returns: An optional row.
|
||||
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
|
||||
@inlinable
|
||||
public static func fetchOne<T>(_ db: Database, _ request: QueryInterfaceRequest<T>) throws -> Row? {
|
||||
if request.query.needsPrefetch {
|
||||
return try fetchOneWithPrefetchedRows(db, request)
|
||||
}
|
||||
let (statement, adapter) = try request.prepare(db)
|
||||
return try fetchOne(statement, adapter: adapter)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
static func fetchAllWithPrefetchedRows<T>(_ db: Database, _ request: QueryInterfaceRequest<T>) throws -> [Row] {
|
||||
// Fetch base rows
|
||||
// TODO: avoid fatal errors "column ... is not selected" by making sure
|
||||
// columns used by joins are fetched (and hidden by a row adapter if added)
|
||||
let (statement, adapter) = try request.prepare(db)
|
||||
let rows = try fetchAll(statement, adapter: adapter)
|
||||
return try rowsWithPrefetchedRows(db, rows, with: request.query)
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
static func fetchOneWithPrefetchedRows<T>(_ db: Database, _ request: QueryInterfaceRequest<T>) throws -> Row? {
|
||||
// Fetch base rows
|
||||
// TODO: avoid fatal errors "column ... is not selected" by making sure
|
||||
// columns used by joins are fetched (and hidden by a row adapter if added)
|
||||
let (statement, adapter) = try request.prepare(db)
|
||||
guard let row = try fetchOne(statement, adapter: adapter) else {
|
||||
return nil
|
||||
}
|
||||
return try rowsWithPrefetchedRows(db, [row], with: request.query)[0]
|
||||
}
|
||||
|
||||
private static func rowsWithPrefetchedRows(_ db: Database, _ rows: [Row], with query: SQLSelectQuery) throws -> [Row] {
|
||||
if rows.isEmpty {
|
||||
return []
|
||||
}
|
||||
|
||||
var rows = rows
|
||||
for (key, join) in query.relation.prefetchedJoins {
|
||||
// Build query for prefetched rows
|
||||
let association = makeAssociation(key: key, join: join)
|
||||
let pivot = association.pivot(from: query.sourceTableName, rows: { _ in rows })
|
||||
let query = SQLSelectQuery(relation: pivot.relation)
|
||||
|
||||
// Annotate with pivot columns, so that we can group and match with
|
||||
// base rows, below.
|
||||
//
|
||||
// Those pivot column are a required addition in the case of "through"
|
||||
// associations:
|
||||
//
|
||||
// // SELECT country.*, passport.citizenId AS grdb_citizenId
|
||||
// // FROM country
|
||||
// // JOIN passport ON passport.countryCode = country.code
|
||||
// // AND passport.citizenId IN (1, 2, 3)
|
||||
// Citizen.including(all: Citizen.countries)
|
||||
let pivotColumns = try pivot.condition.columns(db)
|
||||
let pivotSelection = pivotColumns.right.map { pivot.alias[Column($0)].aliased("grdb_\($0)") }
|
||||
let request = QueryInterfaceRequest<Row>(query: query).annotated(with: pivotSelection)
|
||||
|
||||
// Fetch, Group, Match
|
||||
let prefetchedRows = try request.fetchAll(db)
|
||||
let groupedRows = group(prefetchedRows, on: pivotColumns.right.map { "grdb_\($0)" })
|
||||
rows = rowsWithPrefetchedRows(rows, with: groupedRows, on: pivotColumns.left, forKey: association.key)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
private static func makeAssociation(key: String, join: SQLJoin) -> SQLAssociation {
|
||||
var relation = join.relation
|
||||
let prefetchedJoins = relation.prefetchedJoins
|
||||
relation.joins = relation.directJoins
|
||||
let association = SQLAssociation(key: key, condition: join.condition, relation: relation)
|
||||
guard let (prefetchedKey, prefetchedJoin) = prefetchedJoins.first else {
|
||||
return association
|
||||
}
|
||||
guard prefetchedJoins.count == 1 else {
|
||||
// GRDB bug, or not implemented?
|
||||
// Try:
|
||||
//
|
||||
// Citizen
|
||||
// .including(all: passports) // hasMany passports
|
||||
// .including(all: countries) // hasMany countries through passports
|
||||
fatalError("Can't build an association with multiple prefetched rows")
|
||||
}
|
||||
return makeAssociation(key: prefetchedKey, join: prefetchedJoin).appending(association)
|
||||
}
|
||||
|
||||
private static func group(_ rows: [Row], on columns: [String]) -> [[DatabaseValue]: [Row]] {
|
||||
guard let firstRow = rows.first else {
|
||||
return [:]
|
||||
}
|
||||
let indexes: [Int] = columns.compactMap { firstRow.index(ofColumn: $0) }
|
||||
assert(indexes.count == columns.count)
|
||||
return Dictionary(
|
||||
grouping: rows,
|
||||
by: { row in indexes.map { row.impl.databaseValue(atUncheckedIndex: $0) } })
|
||||
}
|
||||
|
||||
private static func rowsWithPrefetchedRows(_ rows: [Row], with groupedRows: [[DatabaseValue]: [Row]], on columns: [String], forKey key: String) -> [Row] {
|
||||
guard let firstRow = rows.first else {
|
||||
return rows
|
||||
}
|
||||
let indexes: [Int] = columns.compactMap { firstRow.index(ofColumn: $0) }
|
||||
guard indexes.count == columns.count else {
|
||||
fatalError("Column \(columns.joined(separator: ", ")) is not selected")
|
||||
}
|
||||
return rows.map { row in
|
||||
let rowValue = indexes.map { row.impl.databaseValue(atUncheckedIndex: $0) }
|
||||
let prefetchedRows = groupedRows[rowValue] ?? []
|
||||
return row.withPrefetchedRows(prefetchedRows, forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension QueryInterfaceRequest where RowDecoder: Row {
|
||||
|
||||
// MARK: Fetching Rows
|
||||
|
||||
/// An array of fetched rows.
|
||||
///
|
||||
/// let request: ... // Some TypedRequest that fetches Row
|
||||
/// let rows = try request.fetchAll(db)
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: An array of fetched rows.
|
||||
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
|
||||
@inlinable
|
||||
public func fetchAll(_ db: Database) throws -> [Row] {
|
||||
return try Row.fetchAll(db, self)
|
||||
}
|
||||
|
||||
/// The first fetched row.
|
||||
///
|
||||
/// let request: ... // Some TypedRequest that fetches Row
|
||||
/// let row = try request.fetchOne(db)
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: A,n optional rows.
|
||||
/// - throws: A DatabaseError is thrown whenever an SQLite error occurs.
|
||||
@inlinable
|
||||
public func fetchOne(_ db: Database) throws -> Row? {
|
||||
return try Row.fetchOne(db, self)
|
||||
}
|
||||
}
|
||||
@ -89,12 +89,6 @@ public struct SQLExpressionLiteral : SQLExpression {
|
||||
public func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return self
|
||||
}
|
||||
|
||||
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
|
||||
/// :nodoc:
|
||||
public func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQLExpressionUnary
|
||||
@ -166,12 +160,6 @@ public struct SQLExpressionUnary : SQLExpression {
|
||||
public func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return SQLExpressionUnary(op, expression.qualifiedExpression(with: alias))
|
||||
}
|
||||
|
||||
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
|
||||
/// :nodoc:
|
||||
public func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionUnary(op, expression.resolvedExpression(inContext: context))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQLExpressionBinary
|
||||
@ -275,12 +263,6 @@ public struct SQLExpressionBinary : SQLExpression {
|
||||
public func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return SQLExpressionBinary(op, lhs.qualifiedExpression(with: alias), rhs.qualifiedExpression(with: alias))
|
||||
}
|
||||
|
||||
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
|
||||
/// :nodoc:
|
||||
public func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionBinary(op, lhs.resolvedExpression(inContext: context), rhs.resolvedExpression(inContext: context))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQLExpressionAnd
|
||||
@ -308,10 +290,6 @@ struct SQLExpressionAnd : SQLExpression {
|
||||
return SQLExpressionAnd(expressions.map { $0.qualifiedExpression(with: alias) })
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionAnd(expressions.map { $0.resolvedExpression(inContext: context) })
|
||||
}
|
||||
|
||||
func matchedRowIds(rowIdName: String?) -> Set<Int64>? {
|
||||
let matchedRowIds = expressions.compactMap {
|
||||
$0.matchedRowIds(rowIdName: rowIdName)
|
||||
@ -348,10 +326,6 @@ struct SQLExpressionOr : SQLExpression {
|
||||
return SQLExpressionOr(expressions.map { $0.qualifiedExpression(with: alias) })
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionOr(expressions.map { $0.resolvedExpression(inContext: context) })
|
||||
}
|
||||
|
||||
func matchedRowIds(rowIdName: String?) -> Set<Int64>? {
|
||||
if expressions.isEmpty {
|
||||
return []
|
||||
@ -414,10 +388,6 @@ struct SQLExpressionEqual: SQLExpression {
|
||||
return SQLExpressionEqual(op, lhs.qualifiedExpression(with: alias), rhs.qualifiedExpression(with: alias))
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionEqual(op, lhs.resolvedExpression(inContext: context), rhs.resolvedExpression(inContext: context))
|
||||
}
|
||||
|
||||
func matchedRowIds(rowIdName: String?) -> Set<Int64>? {
|
||||
// FIXME: this implementation ignores column aliases
|
||||
switch op {
|
||||
@ -486,10 +456,6 @@ struct SQLExpressionContains : SQLExpression {
|
||||
return SQLExpressionContains(expression.qualifiedExpression(with: alias), collection, negated: isNegated)
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionContains(expression.resolvedExpression(inContext: context), collection, negated: isNegated)
|
||||
}
|
||||
|
||||
func matchedRowIds(rowIdName: String?) -> Set<Int64>? {
|
||||
// FIXME: this implementation ignores column aliases
|
||||
// Look for `id IN (1, 2, 3)`
|
||||
@ -560,14 +526,6 @@ struct SQLExpressionBetween : SQLExpression {
|
||||
upperBound.qualifiedExpression(with: alias),
|
||||
negated: isNegated)
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionBetween(
|
||||
expression.resolvedExpression(inContext: context),
|
||||
lowerBound.resolvedExpression(inContext: context),
|
||||
upperBound.resolvedExpression(inContext: context),
|
||||
negated: isNegated)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQLExpressionFunction
|
||||
@ -646,12 +604,6 @@ public struct SQLExpressionFunction : SQLExpression {
|
||||
public func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return SQLExpressionFunction(functionName, arguments: arguments.map { $0.qualifiedExpression(with: alias) })
|
||||
}
|
||||
|
||||
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
|
||||
/// :nodoc:
|
||||
public func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionFunction(functionName, arguments: arguments.map { $0.resolvedExpression(inContext: context) })
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQLExpressionCount
|
||||
@ -675,10 +627,6 @@ struct SQLExpressionCount : SQLExpression {
|
||||
func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return SQLExpressionCount(counted.qualifiedSelectable(with: alias))
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQLExpressionCountDistinct
|
||||
@ -701,10 +649,6 @@ struct SQLExpressionCountDistinct : SQLExpression {
|
||||
func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return SQLExpressionCountDistinct(counted.qualifiedExpression(with: alias))
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQLExpressionIsEmpty
|
||||
@ -737,10 +681,6 @@ struct SQLExpressionIsEmpty : SQLExpression {
|
||||
func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return SQLExpressionIsEmpty(countExpression.qualifiedExpression(with: alias), isEmpty: isEmpty)
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionIsEmpty(countExpression.resolvedExpression(inContext: context), isEmpty: isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TableMatchExpression
|
||||
@ -758,12 +698,6 @@ struct TableMatchExpression: SQLExpression {
|
||||
alias: self.alias,
|
||||
pattern: pattern.qualifiedExpression(with: alias))
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return TableMatchExpression(
|
||||
alias: alias,
|
||||
pattern: pattern.resolvedExpression(inContext: context))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQLExpressionCollate
|
||||
@ -793,8 +727,4 @@ struct SQLExpressionCollate : SQLExpression {
|
||||
func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return SQLExpressionCollate(expression.qualifiedExpression(with: alias), collationName: collationName)
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionCollate(expression.resolvedExpression(inContext: context), collationName: collationName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,9 +48,6 @@ public protocol SQLExpression : SQLSpecificExpressible, SQLSelectable, SQLOrderi
|
||||
|
||||
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
|
||||
func qualifiedExpression(with alias: TableAlias) -> SQLExpression
|
||||
|
||||
/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features)
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression
|
||||
}
|
||||
|
||||
extension SQLExpression {
|
||||
@ -147,8 +144,4 @@ struct SQLExpressionNot : SQLExpression {
|
||||
func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
return SQLExpressionNot(expression.qualifiedExpression(with: alias))
|
||||
}
|
||||
|
||||
func resolvedExpression(inContext context: [TableAlias: PersistenceContainer]) -> SQLExpression {
|
||||
return SQLExpressionNot(expression.resolvedExpression(inContext: context))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
/// A "relation" as defined by the [relational terminology](https://en.wikipedia.org/wiki/Relational_database#Terminology):
|
||||
///
|
||||
/// > A set of tuples sharing the same attributes; a set of columns and rows.
|
||||
///
|
||||
/// SELECT ... FROM ... JOIN ... WHERE ... ORDER BY ...
|
||||
/// | | | | |
|
||||
/// | | | | • ordering
|
||||
/// | | | • filterPromise
|
||||
/// | | • joins
|
||||
/// | • source
|
||||
/// • selection
|
||||
struct SQLRelation {
|
||||
var source: SQLSource
|
||||
var selection: [SQLSelectable]
|
||||
@ -8,6 +16,25 @@ struct SQLRelation {
|
||||
var ordering: SQLRelation.Ordering
|
||||
var joins: OrderedDictionary<String, SQLJoin>
|
||||
|
||||
/// The "direct" joins that are loaded in a single SQL query with a JOIN or
|
||||
/// LEFT JOIN operator.
|
||||
var directJoins: OrderedDictionary<String, SQLJoin> {
|
||||
return joins.compactMapValues { join in
|
||||
(join.kind == .all) ? nil : join
|
||||
}
|
||||
}
|
||||
|
||||
/// The "prefetched" joins that are loaded in a distinct SQL query.
|
||||
var prefetchedJoins: OrderedDictionary<String, SQLJoin> {
|
||||
return joins.compactMapValues { join in
|
||||
(join.kind == .all) ? join : nil
|
||||
}
|
||||
}
|
||||
|
||||
var needsPrefetch: Bool {
|
||||
return joins.values.contains { $0.needsPrefetch }
|
||||
}
|
||||
|
||||
init(
|
||||
source: SQLSource,
|
||||
selection: [SQLSelectable] = [],
|
||||
@ -93,6 +120,15 @@ enum SQLSource {
|
||||
case table(tableName: String, alias: TableAlias?)
|
||||
indirect case query(SQLSelectQuery)
|
||||
|
||||
var tableName: String {
|
||||
switch self {
|
||||
case let .table(tableName: tableName, alias: _):
|
||||
return tableName
|
||||
case let .query(query):
|
||||
return query.sourceTableName
|
||||
}
|
||||
}
|
||||
|
||||
func qualified(with alias: TableAlias) -> SQLSource {
|
||||
switch self {
|
||||
case .table(let tableName, let sourceAlias):
|
||||
@ -198,90 +234,177 @@ extension SQLRelation {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQLJoinCondition
|
||||
|
||||
/// The condition that links two joined tables.
|
||||
///
|
||||
/// Currently, we only support one kind of join condition: foreign keys.
|
||||
///
|
||||
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
|
||||
/// <- the join condition -->
|
||||
///
|
||||
/// When we eventually add support for new ways to join tables, SQLJoinCondition
|
||||
/// is the type we'll need to update.
|
||||
///
|
||||
/// SQLJoinCondition equality allows merging of associations:
|
||||
///
|
||||
/// // request1 and request2 are equivalent
|
||||
/// let request1 = Book
|
||||
/// .including(required: Book.author)
|
||||
/// let request2 = Book
|
||||
/// .including(required: Book.author)
|
||||
/// .including(required: Book.author)
|
||||
///
|
||||
/// // request3 and request4 are equivalent
|
||||
/// let request3 = Book
|
||||
/// .including(required: Book.author.filter(condition1 && condition2))
|
||||
/// let request4 = Book
|
||||
/// .joining(required: Book.author.filter(condition1))
|
||||
/// .including(optional: Book.author.filter(condition2))
|
||||
struct SQLJoinCondition: Equatable {
|
||||
/// Definition of a foreign key
|
||||
var foreignKeyRequest: ForeignKeyRequest
|
||||
|
||||
/// True if the table at the origin of the foreign key is on the left of
|
||||
/// the sql JOIN operator.
|
||||
///
|
||||
/// Let's consider the `book.authorId -> author.id` foreign key.
|
||||
/// Its origin table is `book`.
|
||||
///
|
||||
/// The origin table `book` is on the left of the JOIN operator for
|
||||
/// the BelongsTo association:
|
||||
///
|
||||
/// -- Book.including(required: Book.author)
|
||||
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
|
||||
///
|
||||
/// The origin table `book`is on the right of the JOIN operator for
|
||||
/// the HasMany and HasOne associations:
|
||||
///
|
||||
/// -- Author.including(required: Author.books)
|
||||
/// SELECT ... FROM author JOIN book ON author.id = book.authorId
|
||||
var originIsLeft: Bool
|
||||
|
||||
var reversed: SQLJoinCondition {
|
||||
return SQLJoinCondition(foreignKeyRequest: foreignKeyRequest, originIsLeft: !originIsLeft)
|
||||
}
|
||||
|
||||
/// Returns an SQL expression for the join condition.
|
||||
///
|
||||
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
|
||||
/// <- the SQL expression -->
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter leftAlias: A TableAlias for the table on the left of the
|
||||
/// JOIN operator.
|
||||
/// - parameter rightAlias: A TableAlias for the table on the right of the
|
||||
/// JOIN operator.
|
||||
/// - Returns: An SQL expression.
|
||||
func joinExpression(_ db: Database, leftAlias: TableAlias, rightAlias: TableAlias) throws -> SQLJoinExpression {
|
||||
let foreignKeyMapping = try foreignKeyRequest.fetchMapping(db)
|
||||
let columnMapping: [(left: Column, right: Column)]
|
||||
if originIsLeft {
|
||||
columnMapping = foreignKeyMapping.map { (left: Column($0.origin), right: Column($0.destination)) }
|
||||
} else {
|
||||
columnMapping = foreignKeyMapping.map { (left: Column($0.destination), right: Column($0.origin)) }
|
||||
}
|
||||
return SQLJoinExpression(leftAlias: leftAlias, rightAlias: rightAlias, mapping: columnMapping)
|
||||
}
|
||||
|
||||
func columns(_ db: Database) throws -> (left: [String], right: [String]) {
|
||||
let foreignKeyMapping = try foreignKeyRequest.fetchMapping(db)
|
||||
if originIsLeft {
|
||||
return (
|
||||
left: foreignKeyMapping.map { $0.origin },
|
||||
right: foreignKeyMapping.map { $0.destination })
|
||||
} else {
|
||||
return (
|
||||
left: foreignKeyMapping.map { $0.destination },
|
||||
right: foreignKeyMapping.map { $0.origin })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SQLJoinExpression: SQLExpression {
|
||||
var leftAlias: TableAlias
|
||||
var rightAlias: TableAlias
|
||||
var mapping: [(left: Column, right: Column)]
|
||||
|
||||
func expressionSQL(_ context: inout SQLGenerationContext) -> String {
|
||||
return mapping
|
||||
.map { $0.right.qualifiedExpression(with: rightAlias) == $0.left.qualifiedExpression(with: leftAlias) }
|
||||
.joined(operator: .and)
|
||||
.expressionSQL(&context)
|
||||
}
|
||||
|
||||
func qualifiedExpression(with alias: TableAlias) -> SQLExpression {
|
||||
// self is already qualified
|
||||
return self
|
||||
}
|
||||
|
||||
func resolved(with rows: [Row], for alias: TableAlias) -> SQLExpression {
|
||||
let valueMappings: [(column: QualifiedColumn, values: [DatabaseValue])]
|
||||
if alias == leftAlias {
|
||||
valueMappings = mapping.map { columns in
|
||||
(column: QualifiedColumn(columns.right.name, alias: rightAlias),
|
||||
values: rows.map { $0[columns.left] })
|
||||
}
|
||||
} else if alias == rightAlias {
|
||||
valueMappings = mapping.map { columns in
|
||||
(column: QualifiedColumn(columns.left.name, alias: leftAlias),
|
||||
values: rows.map { $0[columns.right] })
|
||||
}
|
||||
} else {
|
||||
// Likely a GRDB bug
|
||||
fatalError("Can't resolve SQLJoinExpression with unknown alias")
|
||||
}
|
||||
|
||||
guard let firstValueMapping = valueMappings.first else {
|
||||
fatalError("Empty mapping")
|
||||
}
|
||||
|
||||
if valueMappings.count == 1 {
|
||||
guard let value = firstValueMapping.values.first else {
|
||||
fatalError("No value")
|
||||
}
|
||||
if firstValueMapping.values.count == 1 {
|
||||
// table.a = 1
|
||||
return (firstValueMapping.column == value)
|
||||
} else {
|
||||
// table.a IN (1, 2, 3)
|
||||
return firstValueMapping.values.contains(firstValueMapping.column)
|
||||
}
|
||||
} else {
|
||||
assert(Set(valueMappings.map { $0.values.count }).count == 1, "inconsistent values count")
|
||||
if firstValueMapping.values.count == 1 {
|
||||
// (table.a = 1) AND (table.b = 2)
|
||||
return valueMappings.map { $0.column == $0.values[0] }
|
||||
.joined(operator: .and)
|
||||
|
||||
} else {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SQLJoin
|
||||
|
||||
struct SQLJoin {
|
||||
/// The condition that links two joined tables.
|
||||
///
|
||||
/// Currently, we only support one kind of join condition: foreign keys.
|
||||
///
|
||||
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
|
||||
/// <- the join condition -->
|
||||
///
|
||||
/// When we eventually add support for new ways to join tables, Condition
|
||||
/// is the type we'll need to update.
|
||||
///
|
||||
/// Condition equality allows merging of associations:
|
||||
///
|
||||
/// // request1 and request2 are equivalent
|
||||
/// let request1 = Book
|
||||
/// .including(required: Book.author)
|
||||
/// let request2 = Book
|
||||
/// .including(required: Book.author)
|
||||
/// .including(required: Book.author)
|
||||
///
|
||||
/// // request3 and request4 are equivalent
|
||||
/// let request3 = Book
|
||||
/// .including(required: Book.author.filter(condition1 && condition2))
|
||||
/// let request4 = Book
|
||||
/// .joining(required: Book.author.filter(condition1))
|
||||
/// .including(optional: Book.author.filter(condition2))
|
||||
struct Condition: Equatable {
|
||||
/// Definition of a foreign key
|
||||
var foreignKeyRequest: ForeignKeyRequest
|
||||
|
||||
/// True if the table at the origin of the foreign key is on the left of
|
||||
/// the sql JOIN operator.
|
||||
///
|
||||
/// Let's consider the `book.authorId -> author.id` foreign key.
|
||||
/// Its origin table is `book`.
|
||||
///
|
||||
/// The origin table `book` is on the left of the JOIN operator for
|
||||
/// the BelongsTo association:
|
||||
///
|
||||
/// -- Book.including(required: Book.author)
|
||||
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
|
||||
///
|
||||
/// The origin table `book`is on the right of the JOIN operator for
|
||||
/// the HasMany and HasOne associations:
|
||||
///
|
||||
/// -- Author.including(required: Author.books)
|
||||
/// SELECT ... FROM author JOIN book ON author.id = book.authorId
|
||||
var originIsLeft: Bool
|
||||
|
||||
var reversed: Condition {
|
||||
return Condition(foreignKeyRequest: foreignKeyRequest, originIsLeft: !originIsLeft)
|
||||
}
|
||||
|
||||
/// Returns an SQL expression for the join condition.
|
||||
///
|
||||
/// SELECT ... FROM book JOIN author ON author.id = book.authorId
|
||||
/// <- the SQL expression -->
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter leftAlias: A TableAlias for the table on the left of the
|
||||
/// JOIN operator.
|
||||
/// - parameter rightAlias: A TableAlias for the table on the right of the
|
||||
/// JOIN operator.
|
||||
/// - Returns: An SQL expression.
|
||||
func sqlExpression(_ db: Database, leftAlias: TableAlias, rightAlias: TableAlias) throws -> SQLExpression {
|
||||
let foreignKeyMapping = try foreignKeyRequest.fetchMapping(db)
|
||||
let columnMapping: [(left: Column, right: Column)]
|
||||
if originIsLeft {
|
||||
columnMapping = foreignKeyMapping.map { (left: Column($0.origin), right: Column($0.destination)) }
|
||||
} else {
|
||||
columnMapping = foreignKeyMapping.map { (left: Column($0.destination), right: Column($0.origin)) }
|
||||
}
|
||||
|
||||
return columnMapping
|
||||
.map { $0.right.qualifiedExpression(with: rightAlias) == $0.left.qualifiedExpression(with: leftAlias) }
|
||||
.joined(operator: .and)
|
||||
}
|
||||
enum Kind {
|
||||
case optional
|
||||
case required
|
||||
case all
|
||||
}
|
||||
|
||||
var isRequired: Bool
|
||||
var condition: Condition
|
||||
var kind: Kind
|
||||
var condition: SQLJoinCondition
|
||||
var relation: SQLRelation
|
||||
|
||||
var needsPrefetch: Bool {
|
||||
if case .all = kind {
|
||||
return true
|
||||
}
|
||||
return relation.needsPrefetch
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Merging
|
||||
@ -391,9 +514,34 @@ extension SQLJoin {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let mergedKind = kind.merged(with: other.kind) else {
|
||||
// can't merge
|
||||
return nil
|
||||
}
|
||||
|
||||
return SQLJoin(
|
||||
isRequired: isRequired || other.isRequired,
|
||||
kind: mergedKind,
|
||||
condition: condition,
|
||||
relation: mergedRelation)
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLJoin.Kind {
|
||||
func merged(with other: SQLJoin.Kind) -> SQLJoin.Kind? {
|
||||
switch (self, other) {
|
||||
case (.all, .all):
|
||||
return .all
|
||||
case (.all, _), (_, .all):
|
||||
// Likely a programmer error:
|
||||
//
|
||||
// Author
|
||||
// .including(all: Author.books)
|
||||
// .including(optional: Author.books)
|
||||
return nil
|
||||
case (.required, _), (_, .required):
|
||||
return .required
|
||||
case (.optional, _), (_, .optional):
|
||||
return .optional
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/// SQLSelectQuery is a representation of an SQL SELECT query.
|
||||
///
|
||||
/// See SQLSelectQueryGenerator for actual SQL generation.
|
||||
@usableFromInline
|
||||
struct SQLSelectQuery {
|
||||
var relation: SQLRelation
|
||||
var isDistinct: Bool
|
||||
@ -8,6 +9,14 @@ struct SQLSelectQuery {
|
||||
var havingExpression: SQLExpression?
|
||||
var limit: SQLLimit?
|
||||
|
||||
@usableFromInline var needsPrefetch: Bool {
|
||||
return relation.needsPrefetch
|
||||
}
|
||||
|
||||
var sourceTableName: String {
|
||||
return relation.source.tableName
|
||||
}
|
||||
|
||||
init(
|
||||
relation: SQLRelation,
|
||||
isDistinct: Bool = false,
|
||||
@ -24,6 +33,7 @@ struct SQLSelectQuery {
|
||||
}
|
||||
|
||||
extension SQLSelectQuery: SelectionRequest, FilteredRequest, OrderedRequest {
|
||||
@usableFromInline
|
||||
func select(_ selection: [SQLSelectable]) -> SQLSelectQuery {
|
||||
return mapRelation { $0.select(selection) }
|
||||
}
|
||||
@ -38,6 +48,7 @@ extension SQLSelectQuery: SelectionRequest, FilteredRequest, OrderedRequest {
|
||||
return query
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func filter(_ predicate: @escaping (Database) throws -> SQLExpressible) -> SQLSelectQuery {
|
||||
return mapRelation { $0.filter(predicate) }
|
||||
}
|
||||
@ -58,10 +69,12 @@ extension SQLSelectQuery: SelectionRequest, FilteredRequest, OrderedRequest {
|
||||
return query
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func order(_ orderings: @escaping (Database) throws -> [SQLOrderingTerm]) -> SQLSelectQuery {
|
||||
return mapRelation { $0.order(orderings) }
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func reversed() -> SQLSelectQuery {
|
||||
return mapRelation { $0.reversed() }
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ struct SQLSelectQueryGenerator {
|
||||
|
||||
sql += try " FROM " + relation.source.sql(db, &context)
|
||||
|
||||
for (_, join) in relation.joins {
|
||||
for (_, join) in relation.directJoins {
|
||||
sql += try " " + join.sql(db, &context, leftAlias: relation.alias, isRequiredAllowed: true)
|
||||
}
|
||||
|
||||
@ -132,7 +132,7 @@ struct SQLSelectQueryGenerator {
|
||||
fatalError("Can't delete query with HAVING clause")
|
||||
}
|
||||
|
||||
guard relation.joins.isEmpty else {
|
||||
guard relation.directJoins.isEmpty else {
|
||||
// Programmer error
|
||||
fatalError("Can't delete query with JOIN clause")
|
||||
}
|
||||
@ -215,17 +215,27 @@ struct SQLSelectQueryGenerator {
|
||||
}
|
||||
|
||||
/// A "qualified" relation, where all tables are identified with a table alias.
|
||||
///
|
||||
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
|
||||
/// | | | | | |
|
||||
/// | | | | | • ordering
|
||||
/// | | | | • filterPromise
|
||||
/// | | | • directJoins
|
||||
/// | | • alias
|
||||
/// | • source
|
||||
/// • fullSelection
|
||||
private struct SQLQualifiedRelation {
|
||||
/// The alias for the relation
|
||||
///
|
||||
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
|
||||
/// ^ alias
|
||||
/// |
|
||||
/// • alias
|
||||
let alias: TableAlias
|
||||
|
||||
/// All aliases, including aliases of joined relations
|
||||
var allAliases: [TableAlias] {
|
||||
var aliases = [alias]
|
||||
for join in joins.values {
|
||||
for join in directJoins.values {
|
||||
aliases.append(contentsOf: join.relation.allAliases)
|
||||
}
|
||||
aliases.append(contentsOf: source.allAliases)
|
||||
@ -235,7 +245,8 @@ private struct SQLQualifiedRelation {
|
||||
/// The source
|
||||
///
|
||||
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
|
||||
/// ^ source
|
||||
/// |
|
||||
/// • source
|
||||
let source: SQLQualifiedSource
|
||||
|
||||
/// The selection, not including selection of joined relations
|
||||
@ -244,9 +255,10 @@ private struct SQLQualifiedRelation {
|
||||
/// The full selection, including selection of joined relations
|
||||
///
|
||||
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
|
||||
/// ^ fullSelection
|
||||
/// |
|
||||
/// • fullSelection
|
||||
var selection: [SQLSelectable] {
|
||||
return joins.reduce(into: ownSelection) {
|
||||
return directJoins.reduce(into: ownSelection) {
|
||||
$0.append(contentsOf: $1.value.relation.selection)
|
||||
}
|
||||
}
|
||||
@ -254,7 +266,8 @@ private struct SQLQualifiedRelation {
|
||||
/// The filtering clause
|
||||
///
|
||||
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
|
||||
/// ^ filterPromise
|
||||
/// |
|
||||
/// • filter promise
|
||||
let filterPromise: DatabasePromise<SQLExpression?>
|
||||
|
||||
/// The ordering, not including ordering of joined relations
|
||||
@ -263,18 +276,20 @@ private struct SQLQualifiedRelation {
|
||||
/// The full ordering, including orderings of joined relations
|
||||
///
|
||||
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
|
||||
/// ^ ordering
|
||||
/// |
|
||||
/// • ordering
|
||||
var ordering: SQLRelation.Ordering {
|
||||
return joins.reduce(ownOrdering) {
|
||||
return directJoins.reduce(ownOrdering) {
|
||||
$0.appending($1.value.relation.ordering)
|
||||
}
|
||||
}
|
||||
|
||||
/// The joins
|
||||
/// The direct joins, loaded with a JOIN or LEFT JOIN operator
|
||||
///
|
||||
/// SELECT ... FROM ... AS ... JOIN ... WHERE ... ORDER BY ...
|
||||
/// ^ joins
|
||||
let joins: OrderedDictionary<String, SQLQualifiedJoin>
|
||||
/// |
|
||||
/// • directJoins
|
||||
let directJoins: OrderedDictionary<String, SQLQualifiedJoin>
|
||||
|
||||
init(_ relation: SQLRelation) {
|
||||
// Qualify the source, so that it be disambiguated with an SQL alias
|
||||
@ -288,7 +303,7 @@ private struct SQLQualifiedRelation {
|
||||
|
||||
// Qualify all joins, selection, filter, and ordering, so that all
|
||||
// identifiers can be correctly disambiguated and qualified.
|
||||
joins = relation.joins.mapValues(SQLQualifiedJoin.init)
|
||||
directJoins = relation.directJoins.mapValues(SQLQualifiedJoin.init)
|
||||
ownSelection = relation.selection.map { $0.qualifiedSelectable(with: alias) }
|
||||
filterPromise = relation.filterPromise.map { [alias] in $0?.qualifiedExpression(with: alias) }
|
||||
ownOrdering = relation.ordering.qualified(with: alias)
|
||||
@ -306,7 +321,7 @@ private struct SQLQualifiedRelation {
|
||||
/// relations does not need any row adapter.
|
||||
func rowAdapter(_ db: Database, fromIndex startIndex: Int) throws -> (adapter: RowAdapter, endIndex: Int)? {
|
||||
// Root relation && no join => no need for any adapter
|
||||
if startIndex == 0 && joins.isEmpty {
|
||||
if startIndex == 0 && directJoins.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -320,7 +335,7 @@ private struct SQLQualifiedRelation {
|
||||
// Name them according to the join keys.
|
||||
var endIndex = startIndex + selectionWidth
|
||||
var scopes: [String: RowAdapter] = [:]
|
||||
for (key, join) in joins {
|
||||
for (key, join) in directJoins {
|
||||
if let (joinAdapter, joinEndIndex) = try join.relation.rowAdapter(db, fromIndex: endIndex) {
|
||||
scopes[key] = joinAdapter
|
||||
endIndex = joinEndIndex
|
||||
@ -404,12 +419,12 @@ private enum SQLQualifiedSource {
|
||||
|
||||
/// A "qualified" join, where all tables are identified with a table alias.
|
||||
private struct SQLQualifiedJoin {
|
||||
private let isRequired: Bool
|
||||
private let condition: SQLJoin.Condition
|
||||
private let kind: SQLJoin.Kind
|
||||
private let condition: SQLJoinCondition
|
||||
let relation: SQLQualifiedRelation
|
||||
|
||||
init(_ join: SQLJoin) {
|
||||
self.isRequired = join.isRequired
|
||||
self.kind = join.kind
|
||||
self.condition = join.condition
|
||||
self.relation = SQLQualifiedRelation(join.relation)
|
||||
}
|
||||
@ -418,13 +433,16 @@ private struct SQLQualifiedJoin {
|
||||
var isRequiredAllowed = isRequiredAllowed
|
||||
var sql = ""
|
||||
|
||||
if isRequired {
|
||||
switch kind {
|
||||
case .all:
|
||||
fatalError("Not implemented: chaining an `all` association")
|
||||
case .required:
|
||||
guard isRequiredAllowed else {
|
||||
// TODO: chainOptionalRequired
|
||||
fatalError("Not implemented: chaining a required association behind an optional association")
|
||||
}
|
||||
sql += "JOIN"
|
||||
} else {
|
||||
case .optional:
|
||||
isRequiredAllowed = false
|
||||
sql += "LEFT JOIN"
|
||||
}
|
||||
@ -433,14 +451,14 @@ private struct SQLQualifiedJoin {
|
||||
|
||||
let rightAlias = relation.alias
|
||||
let filters = try [
|
||||
condition.sqlExpression(db, leftAlias: leftAlias, rightAlias: rightAlias),
|
||||
condition.joinExpression(db, leftAlias: leftAlias, rightAlias: rightAlias),
|
||||
relation.filterPromise.resolve(db)
|
||||
].compactMap { $0 }
|
||||
if !filters.isEmpty {
|
||||
sql += " ON " + filters.joined(operator: .and).expressionSQL(&context)
|
||||
}
|
||||
|
||||
for (_, join) in relation.joins {
|
||||
for (_, join) in relation.directJoins {
|
||||
sql += try " " + join.sql(db, &context, leftAlias: rightAlias, isRequiredAllowed: isRequiredAllowed)
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ private struct RowDecoder<Record: FetchableRecord>: Decoder {
|
||||
var userInfo: [CodingUserInfoKey: Any] { return Record.databaseDecodingUserInfo }
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
|
||||
return KeyedDecodingContainer(KeyedContainer<Key>(decoder: self))
|
||||
return KeyedDecodingContainer(KeyedContainer<Record, Key>(decoder: self))
|
||||
}
|
||||
|
||||
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
@ -43,7 +43,7 @@ private struct RowDecoder<Record: FetchableRecord>: Decoder {
|
||||
return ColumnDecoder<Record>(row: row, columnIndex: index, codingPath: codingPath)
|
||||
}
|
||||
|
||||
struct KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
|
||||
struct KeyedContainer<Record: FetchableRecord, Key: CodingKey>: KeyedDecodingContainerProtocol {
|
||||
let decoder: RowDecoder
|
||||
var codingPath: [CodingKey] { return decoder.codingPath }
|
||||
|
||||
@ -137,7 +137,13 @@ private struct RowDecoder<Record: FetchableRecord>: Decoder {
|
||||
return try decode(type, fromRow: scopedRow, codingPath: codingPath + [key])
|
||||
}
|
||||
|
||||
// Key is not a column, and not a scope.
|
||||
// Prefetched Rows?
|
||||
if let prefetchedRows = row.prefetchedRowsTree[keyName] {
|
||||
let decoder = PrefetchedRowsDecoder<Record>(rows: prefetchedRows, codingPath: codingPath)
|
||||
return try T(from: decoder)
|
||||
}
|
||||
|
||||
// Key is not a column, not a scope, not a prefetched rows key
|
||||
//
|
||||
// Should be throw an error? Well... The use case is the following:
|
||||
//
|
||||
@ -235,6 +241,125 @@ private struct RowDecoder<Record: FetchableRecord>: Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PrefetchedRowsDecoder
|
||||
|
||||
private struct PrefetchedRowsDecoder<Record: FetchableRecord>: Decoder {
|
||||
var rows: [Row]
|
||||
var codingPath: [CodingKey]
|
||||
var index: Int
|
||||
var userInfo: [CodingUserInfoKey: Any] { return Record.databaseDecodingUserInfo }
|
||||
|
||||
init(rows: [Row], codingPath: [CodingKey]) {
|
||||
self.rows = rows
|
||||
self.codingPath = codingPath
|
||||
self.index = 0
|
||||
}
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
|
||||
fatalError("keyed decoding from prefetched rows is not supported")
|
||||
}
|
||||
|
||||
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
return self
|
||||
}
|
||||
|
||||
func singleValueContainer() throws -> SingleValueDecodingContainer {
|
||||
fatalError("single value decoding from prefetched rows is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
extension PrefetchedRowsDecoder: UnkeyedDecodingContainer {
|
||||
var count: Int? {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
var isAtEnd: Bool {
|
||||
return index >= rows.endIndex
|
||||
}
|
||||
|
||||
var currentIndex: Int {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decodeNil() throws -> Bool {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Bool.Type) throws -> Bool {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: String.Type) throws -> String {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Double.Type) throws -> Double {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Float.Type) throws -> Float {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int.Type) throws -> Int {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int8.Type) throws -> Int8 {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int16.Type) throws -> Int16 {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int32.Type) throws -> Int32 {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int64.Type) throws -> Int64 {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt.Type) throws -> UInt {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt8.Type) throws -> UInt8 {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt16.Type) throws -> UInt16 {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt32.Type) throws -> UInt32 {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt64.Type) throws -> UInt64 {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
|
||||
defer { index += 1 }
|
||||
let decoder = RowDecoder<Record>(row: rows[index], codingPath: codingPath)
|
||||
return try T(from: decoder)
|
||||
}
|
||||
|
||||
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
|
||||
mutating func superDecoder() throws -> Decoder {
|
||||
fatalError("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ColumnDecoder
|
||||
|
||||
/// The decoder that decodes from a database column
|
||||
|
||||
@ -89,6 +89,22 @@ struct OrderedDictionary<Key: Hashable, Value> {
|
||||
dict.appendValue(try transform(pair.value), forKey: pair.key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new ordered dictionary containing the keys of this dictionary
|
||||
/// with the values transformed by the given closure.
|
||||
///
|
||||
/// - Parameter transform: A closure that transforms a value. `transform`
|
||||
/// accepts each value of the dictionary as its parameter and returns a
|
||||
/// transformed value of the same or of a different type.
|
||||
/// - Returns: A dictionary containing the keys and transformed values of
|
||||
/// this dictionary.
|
||||
func compactMapValues<T>(_ transform: (Value) throws -> T?) rethrows -> OrderedDictionary<Key, T> {
|
||||
return try reduce(into: OrderedDictionary<Key, T>(), { dict, pair in
|
||||
if let value = try transform(pair.value) {
|
||||
dict[pair.key] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
extension OrderedDictionary: Collection {
|
||||
|
||||
@ -9,6 +9,10 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
56071A4E1DB54ED200CA6E47 /* FetchedRecordsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B15D0A1CD4C35100A24C8B /* FetchedRecordsControllerTests.swift */; };
|
||||
56071A4F1DB54ED300CA6E47 /* FetchedRecordsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B15D0A1CD4C35100A24C8B /* FetchedRecordsControllerTests.swift */; };
|
||||
560EBA542245F685000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA522245F685000E3FBC /* FetchableRecord+QueryInterface.swift */; };
|
||||
560EBA552245F685000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA522245F685000E3FBC /* FetchableRecord+QueryInterface.swift */; };
|
||||
560EBA562245F685000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA532245F685000E3FBC /* Row+QueryInterfaceRequest.swift */; };
|
||||
560EBA572245F685000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA532245F685000E3FBC /* Row+QueryInterfaceRequest.swift */; };
|
||||
560FC51C1CB003810014AA8E /* QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56300B6F1C53F592005A543B /* QueryInterfaceRequest.swift */; };
|
||||
560FC51D1CB003810014AA8E /* SQLCollatedExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5605F1891C6B1A8700235C62 /* SQLCollatedExpression.swift */; };
|
||||
560FC51F1CB003810014AA8E /* CGFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5605F1491C672E4000235C62 /* CGFloat.swift */; };
|
||||
@ -279,6 +283,10 @@
|
||||
5644DE8320F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE8120F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift */; };
|
||||
5644DE8420F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE8120F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift */; };
|
||||
5644DE8520F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE8120F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift */; };
|
||||
5646AFE322440F190019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5646AFE122440F190019B5D9 /* AssociationHasManyRowScopeTests.swift */; };
|
||||
5646AFE422440F190019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5646AFE122440F190019B5D9 /* AssociationHasManyRowScopeTests.swift */; };
|
||||
5646AFE522440F190019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5646AFE122440F190019B5D9 /* AssociationHasManyRowScopeTests.swift */; };
|
||||
5646AFE622440F190019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5646AFE122440F190019B5D9 /* AssociationHasManyRowScopeTests.swift */; };
|
||||
564CE44021AA957000652B19 /* ValueObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE43F21AA957000652B19 /* ValueObserver.swift */; };
|
||||
564CE44121AA957000652B19 /* ValueObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE43F21AA957000652B19 /* ValueObserver.swift */; };
|
||||
564CE4DD21B2DEF700652B19 /* ValueObservation+CompactMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE4DC21B2DEF700652B19 /* ValueObservation+CompactMap.swift */; };
|
||||
@ -930,8 +938,8 @@
|
||||
56CEB4FE1EAA2F4D00BFAF62 /* FTS3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB4F81EAA2F4D00BFAF62 /* FTS3.swift */; };
|
||||
56CEB5021EAA2F4D00BFAF62 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB4F91EAA2F4D00BFAF62 /* FTS4.swift */; };
|
||||
56CEB5051EAA2F4D00BFAF62 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB4F91EAA2F4D00BFAF62 /* FTS4.swift */; };
|
||||
56CEB5121EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */; };
|
||||
56CEB5151EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */; };
|
||||
56CEB5121EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */; };
|
||||
56CEB5151EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */; };
|
||||
56CEB51A1EAA328900BFAF62 /* FTS5+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */; };
|
||||
56CEB51D1EAA328900BFAF62 /* FTS5+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */; };
|
||||
56CEB5461EAA359A00BFAF62 /* Column.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5401EAA359A00BFAF62 /* Column.swift */; };
|
||||
@ -1070,6 +1078,8 @@
|
||||
560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertible.swift; sourceTree = "<group>"; };
|
||||
560D92441C672C4B00F4F92B /* PersistableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistableRecord.swift; sourceTree = "<group>"; };
|
||||
560D92461C672C4B00F4F92B /* TableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRecord.swift; sourceTree = "<group>"; };
|
||||
560EBA522245F685000E3FBC /* FetchableRecord+QueryInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FetchableRecord+QueryInterface.swift"; sourceTree = "<group>"; };
|
||||
560EBA532245F685000E3FBC /* Row+QueryInterfaceRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Row+QueryInterfaceRequest.swift"; sourceTree = "<group>"; };
|
||||
560FC54D1CB003810014AA8E /* GRDBCipher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GRDBCipher.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
560FC5501CB004AD0014AA8E /* sqlcipher.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = sqlcipher.xcodeproj; path = SQLCipher/src/sqlcipher.xcodeproj; sourceTree = "<group>"; };
|
||||
560FC5B01CB00B880014AA8E /* GRDBCipherOSXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBCipherOSXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -1133,6 +1143,7 @@
|
||||
564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAfterNextTransactionCommitTests.swift; sourceTree = "<group>"; };
|
||||
5644DE7A20F8C903001FFDDE /* DatabaseValueConversion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversion.swift; sourceTree = "<group>"; };
|
||||
5644DE8120F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversionErrorTests.swift; sourceTree = "<group>"; };
|
||||
5646AFE122440F190019B5D9 /* AssociationHasManyRowScopeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasManyRowScopeTests.swift; sourceTree = "<group>"; };
|
||||
564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseCollationTests.swift; sourceTree = "<group>"; };
|
||||
564CE43F21AA957000652B19 /* ValueObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObserver.swift; sourceTree = "<group>"; };
|
||||
564CE4DC21B2DEF700652B19 /* ValueObservation+CompactMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+CompactMap.swift"; sourceTree = "<group>"; };
|
||||
@ -1338,7 +1349,7 @@
|
||||
56CEB4F01EAA2EFA00BFAF62 /* FetchableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableRecord.swift; sourceTree = "<group>"; };
|
||||
56CEB4F81EAA2F4D00BFAF62 /* FTS3.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS3.swift; sourceTree = "<group>"; };
|
||||
56CEB4F91EAA2F4D00BFAF62 /* FTS4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4.swift; sourceTree = "<group>"; };
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FTS3+QueryInterfaceRequest.swift"; sourceTree = "<group>"; };
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FTS3+QueryInterface.swift"; sourceTree = "<group>"; };
|
||||
56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FTS5+QueryInterface.swift"; sourceTree = "<group>"; };
|
||||
56CEB5401EAA359A00BFAF62 /* Column.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Column.swift; sourceTree = "<group>"; };
|
||||
56CEB5411EAA359A00BFAF62 /* SQLExpressible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLExpressible.swift; sourceTree = "<group>"; };
|
||||
@ -1660,11 +1671,13 @@
|
||||
5653EB8420961FBF00F46237 /* Association */,
|
||||
56CEB5401EAA359A00BFAF62 /* Column.swift */,
|
||||
56D91AAC2205F30200770D8D /* DatabasePromise.swift */,
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */,
|
||||
560EBA522245F685000E3FBC /* FetchableRecord+QueryInterface.swift */,
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */,
|
||||
56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */,
|
||||
56300B6F1C53F592005A543B /* QueryInterfaceRequest.swift */,
|
||||
5653EBA120961FCA00F46237 /* QueryInterfaceRequest+Association.swift */,
|
||||
5616AAF8207CD5A900AC3664 /* RequestProtocols.swift */,
|
||||
560EBA532245F685000E3FBC /* Row+QueryInterfaceRequest.swift */,
|
||||
5605F1891C6B1A8700235C62 /* SQLCollatedExpression.swift */,
|
||||
566475991D97D8A000FF74B8 /* SQLCollection.swift */,
|
||||
56CEB5411EAA359A00BFAF62 /* SQLExpressible.swift */,
|
||||
@ -1745,6 +1758,7 @@
|
||||
5653EBB020961FE700F46237 /* AssociationBelongsToSQLTests.swift */,
|
||||
5653EBAE20961FE700F46237 /* AssociationChainRowScopesTests.swift */,
|
||||
5653EBB120961FE800F46237 /* AssociationChainSQLTests.swift */,
|
||||
5646AFE122440F190019B5D9 /* AssociationHasManyRowScopeTests.swift */,
|
||||
56959638222D059C002CB7C9 /* AssociationHasManySQLTests.swift */,
|
||||
56959622222C459A002CB7C9 /* AssociationHasManyThroughSQLTests.swift */,
|
||||
5653EBAD20961FE700F46237 /* AssociationHasOneSQLDerivationTests.swift */,
|
||||
@ -2285,6 +2299,7 @@
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
@ -2381,7 +2396,7 @@
|
||||
563EF4502161F1A8007DAACD /* Inflections.swift in Sources */,
|
||||
566B910A1FA4C3970012D5B0 /* Database+Statements.swift in Sources */,
|
||||
566B91241FA4CF810012D5B0 /* Database+Schema.swift in Sources */,
|
||||
56CEB5121EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */,
|
||||
56CEB5121EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */,
|
||||
5659F4911EA8D964004A4992 /* ReadWriteBox.swift in Sources */,
|
||||
566A842920413D6F00E50BFD /* DatabaseSnapshot.swift in Sources */,
|
||||
56CEB4F21EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */,
|
||||
@ -2479,6 +2494,7 @@
|
||||
5613ED8521A95E8400DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */,
|
||||
5653EB9520961FC000F46237 /* HasManyAssociation.swift in Sources */,
|
||||
560FC53D1CB003810014AA8E /* NSNumber.swift in Sources */,
|
||||
560EBA562245F685000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */,
|
||||
56E9FADC221053ED00C703A8 /* SQLLiteral.swift in Sources */,
|
||||
5613ED7B21A95E8400DC7A68 /* ValueObservation.swift in Sources */,
|
||||
567071F8208A00D4006AD95A /* SQLiteDateParser.swift in Sources */,
|
||||
@ -2493,6 +2509,7 @@
|
||||
5657AABA1D107001006283EF /* NSData.swift in Sources */,
|
||||
560FC5421CB003810014AA8E /* StatementColumnConvertible.swift in Sources */,
|
||||
56CEB5541EAA359A00BFAF62 /* SQLExpression.swift in Sources */,
|
||||
560EBA542245F685000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */,
|
||||
56B9649E1DA51B4C0002DA19 /* FTS5.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -2571,6 +2588,7 @@
|
||||
567F45A91F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */,
|
||||
5634B1081CF9B970005360B9 /* TransactionObserverSavepointsTests.swift in Sources */,
|
||||
56959626222C459A002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
|
||||
5646AFE322440F190019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */,
|
||||
560FC57D1CB00B880014AA8E /* RecordInitializersTests.swift in Sources */,
|
||||
56176C5F1EACCCC7000F3F2B /* FTS5CustomTokenizerTests.swift in Sources */,
|
||||
565F03C31CE5D3AA00DE108F /* RowAdapterTests.swift in Sources */,
|
||||
@ -2760,6 +2778,7 @@
|
||||
567F45AA1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */,
|
||||
5657AB601D108BA9006283EF /* FoundationNSURLTests.swift in Sources */,
|
||||
56959625222C459A002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
|
||||
5646AFE422440F190019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */,
|
||||
5671563B1CB16729007DC145 /* RecordInitializersTests.swift in Sources */,
|
||||
56176C651EACCCC8000F3F2B /* FTS5CustomTokenizerTests.swift in Sources */,
|
||||
5657AB681D108BA9006283EF /* FoundationURLTests.swift in Sources */,
|
||||
@ -2887,7 +2906,7 @@
|
||||
563EF4512161F1A8007DAACD /* Inflections.swift in Sources */,
|
||||
566B910D1FA4C3970012D5B0 /* Database+Statements.swift in Sources */,
|
||||
566B91271FA4CF810012D5B0 /* Database+Schema.swift in Sources */,
|
||||
56CEB5151EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */,
|
||||
56CEB5151EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */,
|
||||
5659F4941EA8D964004A4992 /* ReadWriteBox.swift in Sources */,
|
||||
566A842A20413D8500E50BFD /* DatabaseSnapshot.swift in Sources */,
|
||||
56CEB4F51EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */,
|
||||
@ -2985,6 +3004,7 @@
|
||||
5613ED8621A95E8400DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */,
|
||||
5653EB9620961FC000F46237 /* HasManyAssociation.swift in Sources */,
|
||||
56AFCA141CB1A8BB00F48B96 /* StandardLibrary.swift in Sources */,
|
||||
560EBA572245F685000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */,
|
||||
56E9FADD221053ED00C703A8 /* SQLLiteral.swift in Sources */,
|
||||
5613ED7C21A95E8400DC7A68 /* ValueObservation.swift in Sources */,
|
||||
56AFCA151CB1A8BB00F48B96 /* PersistableRecord.swift in Sources */,
|
||||
@ -2999,6 +3019,7 @@
|
||||
56AFCA181CB1A8BB00F48B96 /* FetchedRecordsController.swift in Sources */,
|
||||
56AFCA191CB1A8BB00F48B96 /* DatabaseValue.swift in Sources */,
|
||||
56CEB5571EAA359A00BFAF62 /* SQLExpression.swift in Sources */,
|
||||
560EBA552245F685000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */,
|
||||
56B964A11DA51B4C0002DA19 /* FTS5.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -3077,6 +3098,7 @@
|
||||
5657AB631D108BA9006283EF /* FoundationNSURLTests.swift in Sources */,
|
||||
567F45AD1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */,
|
||||
56959627222C459A002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
|
||||
5646AFE522440F190019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */,
|
||||
5657AB6B1D108BA9006283EF /* FoundationURLTests.swift in Sources */,
|
||||
56AFCA4F1CB1AA9900F48B96 /* DataMemoryTests.swift in Sources */,
|
||||
56176C711EACCCCA000F3F2B /* FTS5CustomTokenizerTests.swift in Sources */,
|
||||
@ -3266,6 +3288,7 @@
|
||||
56AFCAA41CB1ABC800F48B96 /* MutablePersistableRecordTests.swift in Sources */,
|
||||
567F45AE1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */,
|
||||
56959624222C459A002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
|
||||
5646AFE622440F190019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */,
|
||||
56AFCAA51CB1ABC800F48B96 /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */,
|
||||
56AFCAA61CB1ABC800F48B96 /* DatabasePoolReadOnlyTests.swift in Sources */,
|
||||
56176C771EACCCCB000F3F2B /* FTS5CustomTokenizerTests.swift in Sources */,
|
||||
|
||||
@ -8,6 +8,10 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
56071A501DB54ED300CA6E47 /* FetchedRecordsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B15D0A1CD4C35100A24C8B /* FetchedRecordsControllerTests.swift */; };
|
||||
560EBA4D2245F663000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA4B2245F662000E3FBC /* FetchableRecord+QueryInterface.swift */; };
|
||||
560EBA4E2245F663000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA4B2245F662000E3FBC /* FetchableRecord+QueryInterface.swift */; };
|
||||
560EBA4F2245F663000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA4C2245F663000E3FBC /* Row+QueryInterfaceRequest.swift */; };
|
||||
560EBA502245F663000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EBA4C2245F663000E3FBC /* Row+QueryInterfaceRequest.swift */; };
|
||||
5613ED6121A95E6100DC7A68 /* ValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5921A95E6100DC7A68 /* ValueObservation.swift */; };
|
||||
5613ED6221A95E6100DC7A68 /* ValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5921A95E6100DC7A68 /* ValueObservation.swift */; };
|
||||
5613ED6321A95E6100DC7A68 /* ValueObservation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5A21A95E6100DC7A68 /* ValueObservation+Combine.swift */; };
|
||||
@ -107,6 +111,8 @@
|
||||
5644DE7920F8C8EA001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */; };
|
||||
5644DE7F20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */; };
|
||||
5644DE8020F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */; };
|
||||
5646AFDF22440EF70019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5646AFDD22440EF70019B5D9 /* AssociationHasManyRowScopeTests.swift */; };
|
||||
5646AFE022440EF70019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5646AFDD22440EF70019B5D9 /* AssociationHasManyRowScopeTests.swift */; };
|
||||
564CE43C21AA955B00652B19 /* ValueObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE43B21AA955B00652B19 /* ValueObserver.swift */; };
|
||||
564CE43D21AA955B00652B19 /* ValueObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE43B21AA955B00652B19 /* ValueObserver.swift */; };
|
||||
564CE4DA21B2DEEB00652B19 /* ValueObservation+CompactMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE4D921B2DEEB00652B19 /* ValueObservation+CompactMap.swift */; };
|
||||
@ -378,8 +384,8 @@
|
||||
56CEB4FF1EAA2F4D00BFAF62 /* FTS3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB4F81EAA2F4D00BFAF62 /* FTS3.swift */; };
|
||||
56CEB5031EAA2F4D00BFAF62 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB4F91EAA2F4D00BFAF62 /* FTS4.swift */; };
|
||||
56CEB5061EAA2F4D00BFAF62 /* FTS4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB4F91EAA2F4D00BFAF62 /* FTS4.swift */; };
|
||||
56CEB5131EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */; };
|
||||
56CEB5161EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */; };
|
||||
56CEB5131EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */; };
|
||||
56CEB5161EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */; };
|
||||
56CEB51B1EAA328900BFAF62 /* FTS5+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */; };
|
||||
56CEB51E1EAA328900BFAF62 /* FTS5+QueryInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */; };
|
||||
56CEB5471EAA359A00BFAF62 /* Column.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CEB5401EAA359A00BFAF62 /* Column.swift */; };
|
||||
@ -691,6 +697,8 @@
|
||||
560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertible.swift; sourceTree = "<group>"; };
|
||||
560D92441C672C4B00F4F92B /* PersistableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistableRecord.swift; sourceTree = "<group>"; };
|
||||
560D92461C672C4B00F4F92B /* TableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRecord.swift; sourceTree = "<group>"; };
|
||||
560EBA4B2245F662000E3FBC /* FetchableRecord+QueryInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FetchableRecord+QueryInterface.swift"; sourceTree = "<group>"; };
|
||||
560EBA4C2245F663000E3FBC /* Row+QueryInterfaceRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Row+QueryInterfaceRequest.swift"; sourceTree = "<group>"; };
|
||||
5613ED5921A95E6100DC7A68 /* ValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservation.swift; sourceTree = "<group>"; };
|
||||
5613ED5A21A95E6100DC7A68 /* ValueObservation+Combine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Combine.swift"; sourceTree = "<group>"; };
|
||||
5613ED5B21A95E6100DC7A68 /* ValueObservation+Map.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Map.swift"; sourceTree = "<group>"; };
|
||||
@ -751,6 +759,7 @@
|
||||
564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAfterNextTransactionCommitTests.swift; sourceTree = "<group>"; };
|
||||
5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversion.swift; sourceTree = "<group>"; };
|
||||
5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversionErrorTests.swift; sourceTree = "<group>"; };
|
||||
5646AFDD22440EF70019B5D9 /* AssociationHasManyRowScopeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasManyRowScopeTests.swift; sourceTree = "<group>"; };
|
||||
564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseCollationTests.swift; sourceTree = "<group>"; };
|
||||
564CE43B21AA955B00652B19 /* ValueObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObserver.swift; sourceTree = "<group>"; };
|
||||
564CE4D921B2DEEB00652B19 /* ValueObservation+CompactMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+CompactMap.swift"; sourceTree = "<group>"; };
|
||||
@ -946,7 +955,7 @@
|
||||
56CEB4F01EAA2EFA00BFAF62 /* FetchableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableRecord.swift; sourceTree = "<group>"; };
|
||||
56CEB4F81EAA2F4D00BFAF62 /* FTS3.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS3.swift; sourceTree = "<group>"; };
|
||||
56CEB4F91EAA2F4D00BFAF62 /* FTS4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4.swift; sourceTree = "<group>"; };
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FTS3+QueryInterfaceRequest.swift"; sourceTree = "<group>"; };
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FTS3+QueryInterface.swift"; sourceTree = "<group>"; };
|
||||
56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FTS5+QueryInterface.swift"; sourceTree = "<group>"; };
|
||||
56CEB5401EAA359A00BFAF62 /* Column.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Column.swift; sourceTree = "<group>"; };
|
||||
56CEB5411EAA359A00BFAF62 /* SQLExpressible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLExpressible.swift; sourceTree = "<group>"; };
|
||||
@ -1250,11 +1259,13 @@
|
||||
5653EB3620961F6000F46237 /* Association */,
|
||||
56CEB5401EAA359A00BFAF62 /* Column.swift */,
|
||||
56D91AA52205F2CF00770D8D /* DatabasePromise.swift */,
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift */,
|
||||
560EBA4B2245F662000E3FBC /* FetchableRecord+QueryInterface.swift */,
|
||||
56CEB5101EAA324B00BFAF62 /* FTS3+QueryInterface.swift */,
|
||||
56CEB5181EAA328900BFAF62 /* FTS5+QueryInterface.swift */,
|
||||
56300B6F1C53F592005A543B /* QueryInterfaceRequest.swift */,
|
||||
5653EB5320961F7D00F46237 /* QueryInterfaceRequest+Association.swift */,
|
||||
5616AAF4207CD59300AC3664 /* RequestProtocols.swift */,
|
||||
560EBA4C2245F663000E3FBC /* Row+QueryInterfaceRequest.swift */,
|
||||
5605F1891C6B1A8700235C62 /* SQLCollatedExpression.swift */,
|
||||
566475991D97D8A000FF74B8 /* SQLCollection.swift */,
|
||||
56CEB5411EAA359A00BFAF62 /* SQLExpressible.swift */,
|
||||
@ -1335,6 +1346,7 @@
|
||||
5653EB6220961FB200F46237 /* AssociationBelongsToSQLTests.swift */,
|
||||
5653EB6020961FB100F46237 /* AssociationChainRowScopesTests.swift */,
|
||||
5653EB6320961FB200F46237 /* AssociationChainSQLTests.swift */,
|
||||
5646AFDD22440EF70019B5D9 /* AssociationHasManyRowScopeTests.swift */,
|
||||
56959635222D058B002CB7C9 /* AssociationHasManySQLTests.swift */,
|
||||
5695961E222C4589002CB7C9 /* AssociationHasManyThroughSQLTests.swift */,
|
||||
5653EB5F20961FB100F46237 /* AssociationHasOneSQLDerivationTests.swift */,
|
||||
@ -1885,6 +1897,7 @@
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
@ -1999,7 +2012,7 @@
|
||||
563EF44E2161F196007DAACD /* Inflections.swift in Sources */,
|
||||
566B910E1FA4C3970012D5B0 /* Database+Statements.swift in Sources */,
|
||||
566B91281FA4CF810012D5B0 /* Database+Schema.swift in Sources */,
|
||||
56CEB5161EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */,
|
||||
56CEB5161EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */,
|
||||
5659F4951EA8D964004A4992 /* ReadWriteBox.swift in Sources */,
|
||||
566A842E20413D9A00E50BFD /* DatabaseSnapshot.swift in Sources */,
|
||||
56CEB4F61EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */,
|
||||
@ -2098,6 +2111,7 @@
|
||||
5613ED6C21A95E6100DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */,
|
||||
5653EB4820961F6100F46237 /* HasManyAssociation.swift in Sources */,
|
||||
F3BA80301CFB289F003DC1BA /* DatabaseMigrator.swift in Sources */,
|
||||
560EBA502245F663000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */,
|
||||
56E9FAD62210538400C703A8 /* SQLLiteral.swift in Sources */,
|
||||
5613ED6221A95E6100DC7A68 /* ValueObservation.swift in Sources */,
|
||||
F3BA80361CFB28A4003DC1BA /* TableRecord.swift in Sources */,
|
||||
@ -2112,6 +2126,7 @@
|
||||
F3BA800E1CFB2876003DC1BA /* DatabaseQueue.swift in Sources */,
|
||||
F3BA80191CFB2876003DC1BA /* Statement.swift in Sources */,
|
||||
56CEB5581EAA359A00BFAF62 /* SQLExpression.swift in Sources */,
|
||||
560EBA4E2245F663000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */,
|
||||
56B964A21DA51B4C0002DA19 /* FTS5.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -2190,6 +2205,7 @@
|
||||
567F45AF1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */,
|
||||
F3BA804C1CFB2B24003DC1BA /* GRDBTestCase.swift in Sources */,
|
||||
56959620222C458A002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
|
||||
5646AFDF22440EF70019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */,
|
||||
5698ACBD1DA6285E0056AF8C /* FTS3TokenizerTests.swift in Sources */,
|
||||
5623931F1DECC02000A6B01F /* RowFetchTests.swift in Sources */,
|
||||
562393701DEE0CD200A6B01F /* FlattenCursorTests.swift in Sources */,
|
||||
@ -2316,7 +2332,7 @@
|
||||
563EF44D2161F196007DAACD /* Inflections.swift in Sources */,
|
||||
566B910B1FA4C3970012D5B0 /* Database+Statements.swift in Sources */,
|
||||
566B91251FA4CF810012D5B0 /* Database+Schema.swift in Sources */,
|
||||
56CEB5131EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */,
|
||||
56CEB5131EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */,
|
||||
5659F4921EA8D964004A4992 /* ReadWriteBox.swift in Sources */,
|
||||
566A842D20413D9A00E50BFD /* DatabaseSnapshot.swift in Sources */,
|
||||
56CEB4F31EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */,
|
||||
@ -2415,6 +2431,7 @@
|
||||
5613ED6B21A95E6100DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */,
|
||||
5653EB4720961F6100F46237 /* HasManyAssociation.swift in Sources */,
|
||||
F3BA808C1CFB2E75003DC1BA /* DatabaseMigrator.swift in Sources */,
|
||||
560EBA4F2245F663000E3FBC /* Row+QueryInterfaceRequest.swift in Sources */,
|
||||
56E9FAD52210538400C703A8 /* SQLLiteral.swift in Sources */,
|
||||
5613ED6121A95E6100DC7A68 /* ValueObservation.swift in Sources */,
|
||||
F3BA80921CFB2E7A003DC1BA /* TableRecord.swift in Sources */,
|
||||
@ -2429,6 +2446,7 @@
|
||||
F3BA806A1CFB2E55003DC1BA /* DatabaseQueue.swift in Sources */,
|
||||
F3BA80751CFB2E55003DC1BA /* Statement.swift in Sources */,
|
||||
56CEB5551EAA359A00BFAF62 /* SQLExpression.swift in Sources */,
|
||||
560EBA4D2245F663000E3FBC /* FetchableRecord+QueryInterface.swift in Sources */,
|
||||
56B9649F1DA51B4C0002DA19 /* FTS5.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -2507,6 +2525,7 @@
|
||||
567F45AB1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */,
|
||||
F3BA80A61CFB2F91003DC1BA /* GRDBTestCase.swift in Sources */,
|
||||
56959621222C458A002CB7C9 /* AssociationHasManyThroughSQLTests.swift in Sources */,
|
||||
5646AFE022440EF70019B5D9 /* AssociationHasManyRowScopeTests.swift in Sources */,
|
||||
5657AB411D108BA9006283EF /* FoundationNSDataTests.swift in Sources */,
|
||||
56B964C61DA521450002DA19 /* FTS5PatternTests.swift in Sources */,
|
||||
562205FA1E420E49005860AC /* DatabasePoolReleaseMemoryTests.swift in Sources */,
|
||||
|
||||
1
TODO.md
1
TODO.md
@ -45,6 +45,7 @@ GRDB 4.0
|
||||
|
||||
- [ ] FTS: prefix queries
|
||||
- [ ] Test NOT TESTED methods
|
||||
- [ ] Remove references to obsolete `TypedRequest`
|
||||
|
||||
Swift 4.2
|
||||
|
||||
|
||||
412
Tests/GRDBTests/AssociationHasManyRowScopeTests.swift
Normal file
412
Tests/GRDBTests/AssociationHasManyRowScopeTests.swift
Normal file
@ -0,0 +1,412 @@
|
||||
import XCTest
|
||||
#if GRDBCIPHER
|
||||
import GRDBCipher
|
||||
#elseif GRDBCUSTOMSQLITE
|
||||
import GRDBCustomSQLite
|
||||
#else
|
||||
import GRDB
|
||||
#endif
|
||||
|
||||
private struct A: TableRecord, FetchableRecord, Decodable {
|
||||
var cola1: Int64
|
||||
var cola2: String
|
||||
}
|
||||
private struct B: TableRecord, FetchableRecord, Decodable {
|
||||
var colb1: Int64
|
||||
var colb2: Int64
|
||||
var colb3: String
|
||||
}
|
||||
private struct C: TableRecord, FetchableRecord, Decodable {
|
||||
var colc1: Int64
|
||||
var colc2: String
|
||||
}
|
||||
private struct D: TableRecord, FetchableRecord, Decodable {
|
||||
var cold1: Int64
|
||||
var cold2: Int64
|
||||
var cold3: String
|
||||
}
|
||||
|
||||
class AssociationHasManyRowScopeTests: GRDBTestCase {
|
||||
override func setup(_ dbWriter: DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "a") { t in
|
||||
t.autoIncrementedPrimaryKey("cola1")
|
||||
t.column("cola2", .text)
|
||||
}
|
||||
try db.create(table: "b") { t in
|
||||
t.autoIncrementedPrimaryKey("colb1")
|
||||
t.column("colb2", .integer).references("a")
|
||||
t.column("colb3", .text)
|
||||
}
|
||||
try db.create(table: "c") { t in
|
||||
t.autoIncrementedPrimaryKey("colc1")
|
||||
t.column("colc2", .integer).references("a")
|
||||
}
|
||||
try db.create(table: "d") { t in
|
||||
t.autoIncrementedPrimaryKey("cold1")
|
||||
t.column("cold2", .integer).references("c")
|
||||
t.column("cold3", .text)
|
||||
}
|
||||
try db.execute(
|
||||
sql: """
|
||||
INSERT INTO a (cola1, cola2) VALUES (?, ?);
|
||||
INSERT INTO a (cola1, cola2) VALUES (?, ?);
|
||||
INSERT INTO a (cola1, cola2) VALUES (?, ?);
|
||||
INSERT INTO b (colb1, colb2, colb3) VALUES (?, ?, ?);
|
||||
INSERT INTO b (colb1, colb2, colb3) VALUES (?, ?, ?);
|
||||
INSERT INTO b (colb1, colb2, colb3) VALUES (?, ?, ?);
|
||||
INSERT INTO c (colc1, colc2) VALUES (?, ?);
|
||||
INSERT INTO c (colc1, colc2) VALUES (?, ?);
|
||||
INSERT INTO c (colc1, colc2) VALUES (?, ?);
|
||||
INSERT INTO d (cold1, cold2, cold3) VALUES (?, ?, ?);
|
||||
INSERT INTO d (cold1, cold2, cold3) VALUES (?, ?, ?);
|
||||
INSERT INTO d (cold1, cold2, cold3) VALUES (?, ?, ?);
|
||||
INSERT INTO d (cold1, cold2, cold3) VALUES (?, ?, ?);
|
||||
""",
|
||||
arguments: [
|
||||
1, "a1",
|
||||
2, "a2",
|
||||
3, "a3",
|
||||
4, 1, "b1",
|
||||
5, 1, "b2",
|
||||
6, 2, "b3",
|
||||
7, 1,
|
||||
8, 2,
|
||||
9, 2,
|
||||
10, 7, "d1",
|
||||
11, 8, "d2",
|
||||
12, 8, "d3",
|
||||
13, 9, "d4",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func testHasMany() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.write { db in
|
||||
let request = A
|
||||
.including(all: A
|
||||
.hasMany(B.self)
|
||||
.orderByPrimaryKey()
|
||||
.forKey("bs")) // TODO: auto-pluralization
|
||||
.orderByPrimaryKey()
|
||||
|
||||
do {
|
||||
sqlQueries.removeAll()
|
||||
let rows = try Row.fetchAll(db, request)
|
||||
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT * FROM "a" ORDER BY "cola1"
|
||||
"""))
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT *, "colb2" AS "grdb_colb2" \
|
||||
FROM "b" \
|
||||
WHERE ("colb2" IN (1, 2, 3)) \
|
||||
ORDER BY "colb1"
|
||||
"""))
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0], ["cola1": 1, "cola2": "a1"])
|
||||
XCTAssertEqual(rows[0].prefetchedRows.count, 1)
|
||||
XCTAssertEqual(rows[0].prefetchedRows["bs"]!.count, 2)
|
||||
XCTAssertEqual(rows[0].prefetchedRows["bs"]![0], ["colb1": 4, "colb2": 1, "colb3": "b1", "grdb_colb2": 1]) // TODO: remove grdb_ column
|
||||
XCTAssertEqual(rows[0].prefetchedRows["bs"]![1], ["colb1": 5, "colb2": 1, "colb3": "b2", "grdb_colb2": 1]) // TODO: remove grdb_ column
|
||||
|
||||
XCTAssertEqual(rows[1], ["cola1": 2, "cola2": "a2"])
|
||||
XCTAssertEqual(rows[1].prefetchedRows.count, 1)
|
||||
XCTAssertEqual(rows[1].prefetchedRows["bs"]!.count, 1)
|
||||
XCTAssertEqual(rows[1].prefetchedRows["bs"]![0], ["colb1": 6, "colb2": 2, "colb3": "b3", "grdb_colb2": 2]) // TODO: remove grdb_ column
|
||||
|
||||
XCTAssertEqual(rows[2], ["cola1": 3, "cola2": "a3"])
|
||||
XCTAssertEqual(rows[2].prefetchedRows.count, 1)
|
||||
XCTAssertEqual(rows[2].prefetchedRows["bs"]!.count, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
struct AInfo: FetchableRecord {
|
||||
var a: A
|
||||
var bs: [B]
|
||||
|
||||
init(row: Row) {
|
||||
a = A(row: row)
|
||||
bs = row["bs"]
|
||||
}
|
||||
}
|
||||
let infos = try request.asRequest(of: AInfo.self).fetchAll(db)
|
||||
|
||||
XCTAssertEqual(infos.count, 3)
|
||||
|
||||
XCTAssertEqual(infos[0].a.cola1, 1)
|
||||
XCTAssertEqual(infos[0].a.cola2, "a1")
|
||||
XCTAssertEqual(infos[0].bs.count, 2)
|
||||
XCTAssertEqual(infos[0].bs[0].colb1, 4)
|
||||
XCTAssertEqual(infos[0].bs[0].colb2, 1)
|
||||
XCTAssertEqual(infos[0].bs[0].colb3, "b1")
|
||||
XCTAssertEqual(infos[0].bs[1].colb1, 5)
|
||||
XCTAssertEqual(infos[0].bs[1].colb2, 1)
|
||||
XCTAssertEqual(infos[0].bs[1].colb3, "b2")
|
||||
|
||||
XCTAssertEqual(infos[1].a.cola1, 2)
|
||||
XCTAssertEqual(infos[1].a.cola2, "a2")
|
||||
XCTAssertEqual(infos[1].bs.count, 1)
|
||||
XCTAssertEqual(infos[1].bs[0].colb1, 6)
|
||||
XCTAssertEqual(infos[1].bs[0].colb2, 2)
|
||||
XCTAssertEqual(infos[1].bs[0].colb3, "b3")
|
||||
|
||||
XCTAssertEqual(infos[2].a.cola1, 3)
|
||||
XCTAssertEqual(infos[2].a.cola2, "a3")
|
||||
XCTAssertEqual(infos[2].bs.count, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
struct AInfo: FetchableRecord, Decodable {
|
||||
var a: A
|
||||
var bs: [B]
|
||||
}
|
||||
let infos = try request.asRequest(of: AInfo.self).fetchAll(db)
|
||||
|
||||
XCTAssertEqual(infos.count, 3)
|
||||
|
||||
XCTAssertEqual(infos[0].a.cola1, 1)
|
||||
XCTAssertEqual(infos[0].a.cola2, "a1")
|
||||
XCTAssertEqual(infos[0].bs.count, 2)
|
||||
XCTAssertEqual(infos[0].bs[0].colb1, 4)
|
||||
XCTAssertEqual(infos[0].bs[0].colb2, 1)
|
||||
XCTAssertEqual(infos[0].bs[0].colb3, "b1")
|
||||
XCTAssertEqual(infos[0].bs[1].colb1, 5)
|
||||
XCTAssertEqual(infos[0].bs[1].colb2, 1)
|
||||
XCTAssertEqual(infos[0].bs[1].colb3, "b2")
|
||||
|
||||
XCTAssertEqual(infos[1].a.cola1, 2)
|
||||
XCTAssertEqual(infos[1].a.cola2, "a2")
|
||||
XCTAssertEqual(infos[1].bs.count, 1)
|
||||
XCTAssertEqual(infos[1].bs[0].colb1, 6)
|
||||
XCTAssertEqual(infos[1].bs[0].colb2, 2)
|
||||
XCTAssertEqual(infos[1].bs[0].colb3, "b3")
|
||||
|
||||
XCTAssertEqual(infos[2].a.cola1, 3)
|
||||
XCTAssertEqual(infos[2].a.cola2, "a3")
|
||||
XCTAssertEqual(infos[2].bs.count, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testHasManyThrough() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.write { db in
|
||||
let request = A
|
||||
.including(all: A
|
||||
.hasMany(D.self, through: A.hasMany(C.self), using: C.hasMany(D.self))
|
||||
.orderByPrimaryKey()
|
||||
.forKey("ds")) // TODO: auto-pluralization
|
||||
.orderByPrimaryKey()
|
||||
|
||||
do {
|
||||
sqlQueries.removeAll()
|
||||
let rows = try Row.fetchAll(db, request)
|
||||
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT * FROM "a" ORDER BY "cola1"
|
||||
"""))
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
|
||||
FROM "d" \
|
||||
JOIN "c" ON (("c"."colc1" = "d"."cold2") AND ("c"."colc2" IN (1, 2, 3))) \
|
||||
ORDER BY "d"."cold1"
|
||||
"""))
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0], ["cola1": 1, "cola2": "a1"])
|
||||
XCTAssertEqual(rows[0].prefetchedRows.count, 1)
|
||||
XCTAssertEqual(rows[0].prefetchedRows["ds"]!.count, 1)
|
||||
XCTAssertEqual(rows[0].prefetchedRows["ds"]![0], ["cold1": 10, "cold2": 7, "cold3": "d1", "grdb_colc2": 1]) // TODO: remove grdb_ column
|
||||
|
||||
XCTAssertEqual(rows[1], ["cola1": 2, "cola2": "a2"])
|
||||
XCTAssertEqual(rows[1].prefetchedRows.count, 1)
|
||||
XCTAssertEqual(rows[1].prefetchedRows["ds"]!.count, 3)
|
||||
XCTAssertEqual(rows[1].prefetchedRows["ds"]![0], ["cold1": 11, "cold2": 8, "cold3": "d2", "grdb_colc2": 2]) // TODO: remove grdb_ column
|
||||
XCTAssertEqual(rows[1].prefetchedRows["ds"]![1], ["cold1": 12, "cold2": 8, "cold3": "d3", "grdb_colc2": 2]) // TODO: remove grdb_ column
|
||||
XCTAssertEqual(rows[1].prefetchedRows["ds"]![2], ["cold1": 13, "cold2": 9, "cold3": "d4", "grdb_colc2": 2]) // TODO: remove grdb_ column
|
||||
|
||||
XCTAssertEqual(rows[2], ["cola1": 3, "cola2": "a3"])
|
||||
XCTAssertEqual(rows[2].prefetchedRows.count, 1)
|
||||
XCTAssertEqual(rows[2].prefetchedRows["ds"]!.count, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
struct AInfo: FetchableRecord, Decodable {
|
||||
var a: A
|
||||
var ds: [D]
|
||||
}
|
||||
let infos = try request.asRequest(of: AInfo.self).fetchAll(db)
|
||||
|
||||
XCTAssertEqual(infos.count, 3)
|
||||
|
||||
XCTAssertEqual(infos[0].a.cola1, 1)
|
||||
XCTAssertEqual(infos[0].a.cola2, "a1")
|
||||
XCTAssertEqual(infos[0].ds.count, 1)
|
||||
XCTAssertEqual(infos[0].ds[0].cold1, 10)
|
||||
XCTAssertEqual(infos[0].ds[0].cold2, 7)
|
||||
XCTAssertEqual(infos[0].ds[0].cold3, "d1")
|
||||
|
||||
XCTAssertEqual(infos[1].a.cola1, 2)
|
||||
XCTAssertEqual(infos[1].a.cola2, "a2")
|
||||
XCTAssertEqual(infos[1].ds.count, 3)
|
||||
XCTAssertEqual(infos[1].ds[0].cold1, 11)
|
||||
XCTAssertEqual(infos[1].ds[0].cold2, 8)
|
||||
XCTAssertEqual(infos[1].ds[0].cold3, "d2")
|
||||
XCTAssertEqual(infos[1].ds[1].cold1, 12)
|
||||
XCTAssertEqual(infos[1].ds[1].cold2, 8)
|
||||
XCTAssertEqual(infos[1].ds[1].cold3, "d3")
|
||||
XCTAssertEqual(infos[1].ds[2].cold1, 13)
|
||||
XCTAssertEqual(infos[1].ds[2].cold2, 9)
|
||||
XCTAssertEqual(infos[1].ds[2].cold3, "d4")
|
||||
|
||||
XCTAssertEqual(infos[2].a.cola1, 3)
|
||||
XCTAssertEqual(infos[2].a.cola2, "a3")
|
||||
XCTAssertEqual(infos[2].ds.count, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testHasManyMergedWithHasManyThrough() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.write { db in
|
||||
let request = A
|
||||
.including(all: A
|
||||
.hasMany(C.self)
|
||||
.orderByPrimaryKey()
|
||||
.forKey("cs")) // TODO: auto-pluralization
|
||||
.including(all: A
|
||||
.hasMany(D.self, through: A.hasMany(C.self).forKey("cs"), using: C.hasMany(D.self))
|
||||
.orderByPrimaryKey()
|
||||
.forKey("ds")) // TODO: auto-pluralization
|
||||
.orderByPrimaryKey()
|
||||
|
||||
do {
|
||||
sqlQueries.removeAll()
|
||||
let rows = try Row.fetchAll(db, request)
|
||||
|
||||
// TODO
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT * FROM "a" ORDER BY "cola1"
|
||||
"""))
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT "d".*, "c"."colc2" AS "grdb_colc2" \
|
||||
FROM "d" \
|
||||
JOIN "c" ON (("c"."colc1" = "d"."cold2") AND ("c"."colc2" IN (1, 2, 3))) \
|
||||
ORDER BY "d"."cold1"
|
||||
"""))
|
||||
print(sqlQueries)
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0], ["cola1": 1, "cola2": "a1"])
|
||||
XCTAssertEqual(rows[0].prefetchedRows.count, 1)
|
||||
XCTAssertEqual(rows[0].prefetchedRows["ds"]!.count, 1)
|
||||
XCTAssertEqual(rows[0].prefetchedRows["ds"]![0], ["cold1": 10, "cold2": 7, "cold3": "d1", "grdb_colc2": 1]) // TODO: remove grdb_ column
|
||||
|
||||
XCTAssertEqual(rows[1], ["cola1": 2, "cola2": "a2"])
|
||||
XCTAssertEqual(rows[1].prefetchedRows.count, 1)
|
||||
XCTAssertEqual(rows[1].prefetchedRows["ds"]!.count, 3)
|
||||
XCTAssertEqual(rows[1].prefetchedRows["ds"]![0], ["cold1": 11, "cold2": 8, "cold3": "d2", "grdb_colc2": 2]) // TODO: remove grdb_ column
|
||||
XCTAssertEqual(rows[1].prefetchedRows["ds"]![1], ["cold1": 12, "cold2": 8, "cold3": "d3", "grdb_colc2": 2]) // TODO: remove grdb_ column
|
||||
XCTAssertEqual(rows[1].prefetchedRows["ds"]![2], ["cold1": 13, "cold2": 9, "cold3": "d4", "grdb_colc2": 2]) // TODO: remove grdb_ column
|
||||
|
||||
XCTAssertEqual(rows[2], ["cola1": 3, "cola2": "a3"])
|
||||
XCTAssertEqual(rows[2].prefetchedRows.count, 1)
|
||||
XCTAssertEqual(rows[2].prefetchedRows["ds"]!.count, 0)
|
||||
}
|
||||
|
||||
do {
|
||||
struct AInfo: FetchableRecord, Decodable {
|
||||
var a: A
|
||||
var ds: [D]
|
||||
}
|
||||
let infos = try request.asRequest(of: AInfo.self).fetchAll(db)
|
||||
|
||||
XCTAssertEqual(infos.count, 3)
|
||||
|
||||
XCTAssertEqual(infos[0].a.cola1, 1)
|
||||
XCTAssertEqual(infos[0].a.cola2, "a1")
|
||||
XCTAssertEqual(infos[0].ds.count, 1)
|
||||
XCTAssertEqual(infos[0].ds[0].cold1, 10)
|
||||
XCTAssertEqual(infos[0].ds[0].cold2, 7)
|
||||
XCTAssertEqual(infos[0].ds[0].cold3, "d1")
|
||||
|
||||
XCTAssertEqual(infos[1].a.cola1, 2)
|
||||
XCTAssertEqual(infos[1].a.cola2, "a2")
|
||||
XCTAssertEqual(infos[1].ds.count, 3)
|
||||
XCTAssertEqual(infos[1].ds[0].cold1, 11)
|
||||
XCTAssertEqual(infos[1].ds[0].cold2, 8)
|
||||
XCTAssertEqual(infos[1].ds[0].cold3, "d2")
|
||||
XCTAssertEqual(infos[1].ds[1].cold1, 12)
|
||||
XCTAssertEqual(infos[1].ds[1].cold2, 8)
|
||||
XCTAssertEqual(infos[1].ds[1].cold3, "d3")
|
||||
XCTAssertEqual(infos[1].ds[2].cold1, 13)
|
||||
XCTAssertEqual(infos[1].ds[2].cold2, 9)
|
||||
XCTAssertEqual(infos[1].ds[2].cold3, "d4")
|
||||
|
||||
XCTAssertEqual(infos[2].a.cola1, 3)
|
||||
XCTAssertEqual(infos[2].a.cola2, "a3")
|
||||
XCTAssertEqual(infos[2].ds.count, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: not implemented
|
||||
// func testAssociationIncludingAll() throws {
|
||||
// let dbQueue = try makeDatabaseQueue()
|
||||
// try dbQueue.write { db in
|
||||
// let request = B
|
||||
// .including(required: B
|
||||
// .belongsTo(A.self)
|
||||
// .including(all: A
|
||||
// .hasMany(C.self)
|
||||
// .orderByPrimaryKey()
|
||||
// .forKey("cs")) // TODO: auto-pluralization
|
||||
// )
|
||||
// .orderByPrimaryKey()
|
||||
//
|
||||
// do {
|
||||
// sqlQueries.removeAll()
|
||||
// let rows = try Row.fetchAll(db, request)
|
||||
//
|
||||
// XCTAssertTrue(sqlQueries.contains("""
|
||||
// SELECT "b".*, "a".* \
|
||||
// FROM "b" \
|
||||
// JOIN "a" ON ("a"."cola1" = "b"."colb2") \
|
||||
// ORDER BY "b"."colb1"
|
||||
// """))
|
||||
//
|
||||
//// XCTAssertEqual(rows.count, 3)
|
||||
////
|
||||
//// XCTAssertEqual(rows[0], ["cola1": 1, "cola2": "a1"])
|
||||
//// XCTAssertEqual(rows[0].prefetchedRows.count, 1)
|
||||
//// XCTAssertEqual(rows[0].prefetchedRows["ds"]!.count, 1)
|
||||
//// XCTAssertEqual(rows[0].prefetchedRows["ds"]![0], ["cold1": 10, "cold2": 7, "cold3": "d1", "grdb_colc2": 1]) // TODO: remove grdb_ column
|
||||
////
|
||||
//// XCTAssertEqual(rows[1], ["cola1": 2, "cola2": "a2"])
|
||||
//// XCTAssertEqual(rows[1].prefetchedRows.count, 1)
|
||||
//// XCTAssertEqual(rows[1].prefetchedRows["ds"]!.count, 3)
|
||||
//// XCTAssertEqual(rows[1].prefetchedRows["ds"]![0], ["cold1": 11, "cold2": 8, "cold3": "d2", "grdb_colc2": 2]) // TODO: remove grdb_ column
|
||||
//// XCTAssertEqual(rows[1].prefetchedRows["ds"]![1], ["cold1": 12, "cold2": 8, "cold3": "d3", "grdb_colc2": 2]) // TODO: remove grdb_ column
|
||||
//// XCTAssertEqual(rows[1].prefetchedRows["ds"]![2], ["cold1": 13, "cold2": 9, "cold3": "d4", "grdb_colc2": 2]) // TODO: remove grdb_ column
|
||||
////
|
||||
//// XCTAssertEqual(rows[2], ["cola1": 3, "cola2": "a3"])
|
||||
//// XCTAssertEqual(rows[2].prefetchedRows.count, 1)
|
||||
//// XCTAssertEqual(rows[2].prefetchedRows["ds"]!.count, 0)
|
||||
// }
|
||||
//
|
||||
// do {
|
||||
// struct BInfo: FetchableRecord, Decodable {
|
||||
// var b: B
|
||||
// var a: A
|
||||
// var cs: [C]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user