Compare commits

...

23 Commits

Author SHA1 Message Date
Gwendal Roué
1e8ddeb691 More failing tests 2019-03-24 11:29:36 +01:00
Gwendal Roué
e2c7b5cf73 Prefetch WIP 2019-03-24 11:27:00 +01:00
Gwendal Roué
d6075fb310 "associated rows" -> "prefetched rows" 2019-03-23 18:45:07 +01:00
Gwendal Roué
21b299b6fa Failing test 2019-03-23 15:33:58 +01:00
Gwendal Roué
8d38ecf4da Stronger tests 2019-03-23 06:25:59 +01:00
Gwendal Roué
5d78ea6b84 Query interface code goes in its own folder 2019-03-23 06:09:05 +01:00
Gwendal Roué
cf12b6415c Cleanup 2019-03-22 18:31:20 +01:00
Gwendal Roué
e30b28347d Cleanup 2019-03-22 18:25:25 +01:00
Gwendal Roué
b982bfa4ea including(all:): support for HasManyThrough association 2019-03-22 18:10:19 +01:00
Gwendal Roué
e5af115c08 Cleanup 2019-03-22 15:47:42 +01:00
Gwendal Roué
ca35d35b64 Cleanup 2019-03-22 15:45:05 +01:00
Gwendal Roué
c3f72f1531 First successful decoding of including(all:) 🎉 2019-03-22 15:27:22 +01:00
Gwendal Roué
507b7e77ee Cleanup 2019-03-22 14:26:21 +01:00
Gwendal Roué
e96e347583 Cleanup 2019-03-22 12:24:10 +01:00
Gwendal Roué
81cfca7480 group indirect rows 2019-03-22 11:59:16 +01:00
Gwendal Roué
d70c2e9c11 Simplify SQLJoinExpression 2019-03-22 11:45:39 +01:00
Gwendal Roué
b0fb840143 SQLExpression.resolvedExpression(inContext:) is no longer needed 2019-03-22 08:50:45 +01:00
Gwendal Roué
0c6193fdcf Necessary refactoring for including(all:) 2019-03-22 08:43:39 +01:00
Gwendal Roué
3392c5bbd0 TODO 2019-03-21 17:53:46 +01:00
Gwendal Roué
495b714c05 The right spot for multi requests 2019-03-21 17:53:14 +01:00
Gwendal Roué
58ebc1c49c TODO 2019-03-21 17:52:56 +01:00
Gwendal Roué
ede4a5fd1e including(all:) 2019-03-21 17:31:39 +01:00
Gwendal Roué
12a9e84ce3 Rename SQLJoin.Condition to SQLJoinCondition 2019-03-21 16:34:44 +01:00
24 changed files with 1387 additions and 258 deletions

View File

@ -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 */,

View File

@ -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

View File

@ -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?

View File

@ -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)
}
}

View File

@ -146,7 +146,7 @@ extension TableRecord {
destinationTable: Destination.databaseTableName,
foreignKey: foreignKey)
let condition = SQLJoin.Condition(
let condition = SQLJoinCondition(
foreignKeyRequest: foreignKeyRequest,
originIsLeft: true)

View File

@ -146,7 +146,7 @@ extension TableRecord {
destinationTable: databaseTableName,
foreignKey: foreignKey)
let condition = SQLJoin.Condition(
let condition = SQLJoinCondition(
foreignKeyRequest: foreignKeyRequest,
originIsLeft: false)

View File

@ -148,7 +148,7 @@ extension TableRecord {
destinationTable: databaseTableName,
foreignKey: foreignKey)
let condition = SQLJoin.Condition(
let condition = SQLJoinCondition(
foreignKeyRequest: foreignKeyRequest,
originIsLeft: false)

View File

@ -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:

View 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)
}
}

View File

@ -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.

View File

@ -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)
}
}

View 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)
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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
}
}
}

View File

@ -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() }
}

View File

@ -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)
}

View File

@ -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

View File

@ -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 {

View File

@ -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 */,

View File

@ -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 */,

View File

@ -45,6 +45,7 @@ GRDB 4.0
- [ ] FTS: prefix queries
- [ ] Test NOT TESTED methods
- [ ] Remove references to obsolete `TypedRequest`
Swift 4.2

View 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]
// }
// }
// }
// }
}