diff --git a/CHANGELOG.md b/CHANGELOG.md index a74299f39a..1c0021b91d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Release Notes - [#442](https://github.com/groue/GRDB.swift/pull/442): Reindex - [#443](https://github.com/groue/GRDB.swift/pull/443): In place record update +- [#444](https://github.com/groue/GRDB.swift/pull/444): Combine Value Observations - ValueObservation has three new factory methods that accept an array of database regions, and complete the existing variadic methods (addresses [#441](https://github.com/groue/GRDB.swift/issues/441)): ```swift @@ -22,6 +23,7 @@ Release Notes ### Documentation Diff - [Record Comparison](README.md#record-comparison): this chapter has been updated for the new `updateChanges(_:with:)` method. +- [ValueObservation](README.md#valueobservation): this chapter has been updated for the new `ValueObservation.combine(...)` method. ### API diff @@ -57,6 +59,8 @@ Release Notes + fetchDistinct fetch: @escaping (Database) throws -> Value) + -> ValueObservation> + where Value: Equatable ++ static func combine(_ o1: ValueObservation, ...) ++ -> ValueObservation<...> } ``` diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index b421429039..860edfe48c 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -49,6 +49,29 @@ 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 */; }; + 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 */; }; + 5613ED3F21A95A8B00DC7A68 /* ValueObservation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED3E21A95A8B00DC7A68 /* ValueObservation+Combine.swift */; }; + 5613ED4021A95A8B00DC7A68 /* ValueObservation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED3E21A95A8B00DC7A68 /* ValueObservation+Combine.swift */; }; + 5613ED4121A95A8B00DC7A68 /* ValueObservation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED3E21A95A8B00DC7A68 /* ValueObservation+Combine.swift */; }; + 5613ED4421A95B2C00DC7A68 /* ValueReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4321A95B2C00DC7A68 /* ValueReducer.swift */; }; + 5613ED4521A95B2C00DC7A68 /* ValueReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4321A95B2C00DC7A68 /* ValueReducer.swift */; }; + 5613ED4621A95B2C00DC7A68 /* ValueReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4321A95B2C00DC7A68 /* ValueReducer.swift */; }; + 5613ED4821A95C1200DC7A68 /* ValueObservation+Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4721A95C1200DC7A68 /* ValueObservation+Row.swift */; }; + 5613ED4921A95C1200DC7A68 /* ValueObservation+Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4721A95C1200DC7A68 /* ValueObservation+Row.swift */; }; + 5613ED4A21A95C1200DC7A68 /* ValueObservation+Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4721A95C1200DC7A68 /* ValueObservation+Row.swift */; }; + 5613ED4C21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4B21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift */; }; + 5613ED4D21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4B21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift */; }; + 5613ED4E21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4B21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift */; }; + 5613ED5021A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4F21A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */; }; + 5613ED5121A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4F21A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */; }; + 5613ED5221A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED4F21A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */; }; + 5613ED5421A95DD000DC7A68 /* ValueObservation+Count.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5321A95DD000DC7A68 /* ValueObservation+Count.swift */; }; + 5613ED5521A95DD000DC7A68 /* ValueObservation+Count.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5321A95DD000DC7A68 /* ValueObservation+Count.swift */; }; + 5613ED5621A95DD000DC7A68 /* ValueObservation+Count.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5321A95DD000DC7A68 /* ValueObservation+Count.swift */; }; + 5613EDA621A96A9300DC7A68 /* ValueObservationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613EDA521A96A9200DC7A68 /* ValueObservationCombineTests.swift */; }; + 5613EDA721A96A9300DC7A68 /* ValueObservationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613EDA521A96A9200DC7A68 /* ValueObservationCombineTests.swift */; }; 561667051D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */; }; 5616AAF1207CD45E00AC3664 /* RequestProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5616AAF0207CD45E00AC3664 /* RequestProtocols.swift */; }; 5616AAF2207CD45E00AC3664 /* RequestProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5616AAF0207CD45E00AC3664 /* RequestProtocols.swift */; }; @@ -881,6 +904,14 @@ 560D92441C672C4B00F4F92B /* PersistableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistableRecord.swift; sourceTree = ""; }; 560D92461C672C4B00F4F92B /* TableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRecord.swift; sourceTree = ""; }; 560FC5101CAEEDF10014AA8E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 5613ED3421A95A5C00DC7A68 /* ValueObservation+Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Map.swift"; sourceTree = ""; }; + 5613ED3E21A95A8B00DC7A68 /* ValueObservation+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Combine.swift"; sourceTree = ""; }; + 5613ED4321A95B2C00DC7A68 /* ValueReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueReducer.swift; sourceTree = ""; }; + 5613ED4721A95C1200DC7A68 /* ValueObservation+Row.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Row.swift"; sourceTree = ""; }; + 5613ED4B21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValueObservation+FetchableRecord.swift"; sourceTree = ""; }; + 5613ED4F21A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValueObservation+DatabaseValueConvertible.swift"; sourceTree = ""; }; + 5613ED5321A95DD000DC7A68 /* ValueObservation+Count.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Count.swift"; sourceTree = ""; }; + 5613EDA521A96A9200DC7A68 /* ValueObservationCombineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationCombineTests.swift; sourceTree = ""; }; 561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSDecimalNumberTests.swift; sourceTree = ""; }; 5616AAF0207CD45E00AC3664 /* RequestProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProtocols.swift; sourceTree = ""; }; 562393171DECC02000A6B01F /* RowFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFetchTests.swift; sourceTree = ""; }; @@ -1348,6 +1379,21 @@ name = PersistableRecord; sourceTree = ""; }; + 5613ED4221A95AFB00DC7A68 /* ValueObservation */ = { + isa = PBXGroup; + children = ( + 563B06AA217EF0CC00B38F35 /* ValueObservation.swift */, + 5613ED3E21A95A8B00DC7A68 /* ValueObservation+Combine.swift */, + 5613ED5321A95DD000DC7A68 /* ValueObservation+Count.swift */, + 5613ED4F21A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */, + 5613ED4B21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift */, + 5613ED3421A95A5C00DC7A68 /* ValueObservation+Map.swift */, + 5613ED4721A95C1200DC7A68 /* ValueObservation+Row.swift */, + 5613ED4321A95B2C00DC7A68 /* ValueReducer.swift */, + ); + path = ValueObservation; + sourceTree = ""; + }; 56176C581EACC2D8000F3F2B /* GRDBTests */ = { isa = PBXGroup; children = ( @@ -1361,6 +1407,7 @@ 56300B5C1C53C38F005A543B /* QueryInterface */, 56A238251B9C74A90082EB20 /* Record */, 56176C9F1EACEE15000F3F2B /* Support */, + 563B06BF2185CD1700B38F35 /* ValueObservation */, ); path = GRDBTests; sourceTree = ""; @@ -1484,6 +1531,7 @@ 563B06BF2185CD1700B38F35 /* ValueObservation */ = { isa = PBXGroup; children = ( + 5613EDA521A96A9200DC7A68 /* ValueObservationCombineTests.swift */, 563B06F621861D8300B38F35 /* ValueObservationCountTests.swift */, 563B071721862F4C00B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift */, 563B06C02185D29F00B38F35 /* ValueObservationExtentTests.swift */, @@ -1668,7 +1716,6 @@ 56A238201B9C74A90082EB20 /* Statement */, 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */, 5607EFD11BB8253300605DE3 /* TransactionObserver */, - 563B06BF2185CD1700B38F35 /* ValueObservation */, ); name = Core; sourceTree = ""; @@ -1762,7 +1809,6 @@ 560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */, 5605F1471C672E4000235C62 /* Support */, 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */, - 563B06AA217EF0CC00B38F35 /* ValueObservation.swift */, ); path = Core; sourceTree = ""; @@ -1966,6 +2012,7 @@ 56A2389F1B9C753B0082EB20 /* Record */, DC37743319C8CFCE004FCF85 /* Supporting Files */, 5659F4861EA8D94E004A4992 /* Utils */, + 5613ED4221A95AFB00DC7A68 /* ValueObservation */, ); path = GRDB; sourceTree = ""; @@ -2408,15 +2455,18 @@ 565490E21D5AE252005622CB /* Record.swift in Sources */, 5656BF5120C723E300F98521 /* QueryOrdering.swift in Sources */, 565490C11D5AE236005622CB /* FetchRequest.swift in Sources */, + 5613ED4621A95B2C00DC7A68 /* ValueReducer.swift in Sources */, 5698AD1C1DAAD17F0056AF8C /* FTS5Tokenizer.swift in Sources */, 56CEB5171EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */, 56B964B71DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */, + 5613ED5221A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */, 5653EB0B20944C7C00F46237 /* Association.swift in Sources */, 56F5ABDA1D814330001F60CB /* NSData.swift in Sources */, 56D121601ED34978001347D2 /* Fixits-0.109.0.swift in Sources */, 564F9C341F07611900877A00 /* DatabaseFunction.swift in Sources */, 566475A81D9810A400FF74B8 /* SQLSelectable+QueryInterface.swift in Sources */, 5659F4961EA8D964004A4992 /* ReadWriteBox.swift in Sources */, + 5613ED3721A95A5C00DC7A68 /* ValueObservation+Map.swift in Sources */, 5659F48E1EA8D94E004A4992 /* Utils.swift in Sources */, 56CEB5671EAA359A00BFAF62 /* SQLSelectable.swift in Sources */, 5653EB272094A14400F46237 /* QueryInterfaceRequest+Association.swift in Sources */, @@ -2430,9 +2480,11 @@ 565490D91D5AE252005622CB /* Migration.swift in Sources */, 56CEB5001EAA2F4D00BFAF62 /* FTS3.swift in Sources */, 5698AD271DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, + 5613ED4121A95A8B00DC7A68 /* ValueObservation+Combine.swift in Sources */, 5653EB1120944C7C00F46237 /* AssociationRequest.swift in Sources */, C96C0F2D2084A45A006B2981 /* SQLiteDateParser.swift in Sources */, 566A841C2041146100E50BFD /* DatabaseSnapshot.swift in Sources */, + 5613ED4A21A95C1200DC7A68 /* ValueObservation+Row.swift in Sources */, 565490D51D5AE252005622CB /* DatabaseValueConvertible+RawRepresentable.swift in Sources */, 565490C51D5AE236005622CB /* SerializedDatabase.swift in Sources */, 56F3E7691E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */, @@ -2442,6 +2494,8 @@ 563EF45D2163309F007DAACD /* Inflections.swift in Sources */, 565490BF1D5AE236005622CB /* DatabaseValueConvertible.swift in Sources */, 566B91191FA4C3F50012D5B0 /* DatabaseCollation.swift in Sources */, + 5613ED4E21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */, + 5613ED5621A95DD000DC7A68 /* ValueObservation+Count.swift in Sources */, 56CEB5591EAA359A00BFAF62 /* SQLExpression.swift in Sources */, 5674A6FF1F307F600095F066 /* FetchableRecord+Decodable.swift in Sources */, 56E06F181E85906E008AE2A4 /* Fixits-0.102.0.swift in Sources */, @@ -2549,6 +2603,7 @@ 56CEB5481EAA359A00BFAF62 /* Column.swift in Sources */, 5653EB262094A14400F46237 /* QueryInterfaceRequest+Association.swift in Sources */, 5605F1641C672E4000235C62 /* Date.swift in Sources */, + 5613ED4921A95C1200DC7A68 /* ValueObservation+Row.swift in Sources */, 566B91361FA4D3810012D5B0 /* TransactionObserver.swift in Sources */, 5605F1721C672E4000235C62 /* DatabaseValueConvertible+RawRepresentable.swift in Sources */, 56CEB5141EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */, @@ -2600,6 +2655,7 @@ 566B91161FA4C3F50012D5B0 /* DatabaseCollation.swift in Sources */, 56FC987B1D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */, 56A238881B9C75030082EB20 /* Row.swift in Sources */, + 5613ED4D21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */, 563EF44B2161F179007DAACD /* Inflections.swift in Sources */, 56CEB4F41EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */, 569531201C907A8C00CF1A2B /* DatabaseSchemaCache.swift in Sources */, @@ -2622,15 +2678,20 @@ 5644DE6E20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */, C96C0F2C2084A459006B2981 /* SQLiteDateParser.swift in Sources */, 5605F1681C672E4000235C62 /* NSNumber.swift in Sources */, + 5613ED5521A95DD000DC7A68 /* ValueObservation+Count.swift in Sources */, 56CEB5041EAA2F4D00BFAF62 /* FTS4.swift in Sources */, 5656BF5020C723E300F98521 /* QueryOrdering.swift in Sources */, 5605F1741C672E4000235C62 /* StandardLibrary.swift in Sources */, 560D92481C672C4B00F4F92B /* PersistableRecord.swift in Sources */, + 5613ED4521A95B2C00DC7A68 /* ValueReducer.swift in Sources */, + 5613ED5121A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */, 560D92431C672C3E00F4F92B /* StatementColumnConvertible.swift in Sources */, + 5613ED3621A95A5C00DC7A68 /* ValueObservation+Map.swift in Sources */, 566475A51D9810A400FF74B8 /* SQLSelectable+QueryInterface.swift in Sources */, 5653EB0720944C7C00F46237 /* ForeignKeyRequest.swift in Sources */, 5657AABC1D107001006283EF /* NSData.swift in Sources */, 31A778841C6A4E0600F507F6 /* FetchedRecordsController.swift in Sources */, + 5613ED4021A95A8B00DC7A68 /* ValueObservation+Combine.swift in Sources */, 56A238861B9C75030082EB20 /* DatabaseValue.swift in Sources */, 56CEB55D1EAA359A00BFAF62 /* SQLOrdering.swift in Sources */, 566B91261FA4CF810012D5B0 /* Database+Schema.swift in Sources */, @@ -2739,6 +2800,7 @@ 567A80571D41350C00C7DCEC /* IndexInfoTests.swift in Sources */, 563B06CB2185D2E500B38F35 /* ValueObservationFetchTests.swift in Sources */, 5623935B1DEE013C00A6B01F /* FilterCursorTests.swift in Sources */, + 5613EDA721A96A9300DC7A68 /* ValueObservationCombineTests.swift in Sources */, 5674A72D1F30A9090095F066 /* FetchableRecordDecodableTests.swift in Sources */, 566A84412041914000E50BFD /* MutablePersistableRecordChangesTests.swift in Sources */, 56176C701EACCCC9000F3F2B /* FTS5WrapperTokenizerTests.swift in Sources */, @@ -2915,6 +2977,7 @@ 56D496B81D813465008276D7 /* DataMemoryTests.swift in Sources */, 563B06CA2185D2E500B38F35 /* ValueObservationFetchTests.swift in Sources */, 56D496541D812F5B008276D7 /* SQLExpressionLiteralTests.swift in Sources */, + 5613EDA621A96A9300DC7A68 /* ValueObservationCombineTests.swift in Sources */, 56D496961D81317B008276D7 /* PersistableRecordTests.swift in Sources */, 5698AC9E1DA4B0430056AF8C /* FTS4TableBuilderTests.swift in Sources */, 5674A72A1F30A9090095F066 /* FetchableRecordDecodableTests.swift in Sources */, @@ -3001,13 +3064,16 @@ files = ( 56873BEC1F2CB400004D24B4 /* Fixits-1.2.swift in Sources */, 563EF42D2161180D007DAACD /* AssociationAggregate.swift in Sources */, + 5613ED4C21A95C4300DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */, 56BF6D3D1DEF47DA006039A3 /* Fixits-Swift2.swift in Sources */, 5616AAF1207CD45E00AC3664 /* RequestProtocols.swift in Sources */, + 5613ED4421A95B2C00DC7A68 /* ValueReducer.swift in Sources */, 5636E9BC1D22574100B9B05F /* FetchRequest.swift in Sources */, 56BB6EA91D3009B100A1CA52 /* SchedulingWatchdog.swift in Sources */, 563EF44A2161F179007DAACD /* Inflections.swift in Sources */, 566B91091FA4C3970012D5B0 /* Database+Statements.swift in Sources */, 566B91231FA4CF810012D5B0 /* Database+Schema.swift in Sources */, + 5613ED3F21A95A8B00DC7A68 /* ValueObservation+Combine.swift in Sources */, 563B06AB217EF0CC00B38F35 /* ValueObservation.swift in Sources */, 56CEB5111EAA324B00BFAF62 /* FTS3+QueryInterfaceRequest.swift in Sources */, 5659F4901EA8D964004A4992 /* ReadWriteBox.swift in Sources */, @@ -3016,6 +3082,7 @@ 56CEB4F11EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */, 56300B781C53F592005A543B /* QueryInterfaceRequest.swift in Sources */, 5605F18D1C6B1A8700235C62 /* SQLCollatedExpression.swift in Sources */, + 5613ED5421A95DD000DC7A68 /* ValueObservation+Count.swift in Sources */, 566B91331FA4D3810012D5B0 /* TransactionObserver.swift in Sources */, 56D1215A1ED34978001347D2 /* Fixits-0.109.0.swift in Sources */, 566475D31D981D5E00FF74B8 /* SQLOperators.swift in Sources */, @@ -3061,13 +3128,16 @@ 5671FC201DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */, 56A238A41B9C753B0082EB20 /* Record.swift in Sources */, 56CEB5451EAA359A00BFAF62 /* Column.swift in Sources */, + 5613ED5021A95C6D00DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */, 5657AB0F1D10899D006283EF /* URL.swift in Sources */, 560D924B1C672C4B00F4F92B /* TableRecord.swift in Sources */, + 5613ED4821A95C1200DC7A68 /* ValueObservation+Row.swift in Sources */, 56DAA2DB1DE9C827006E10C8 /* Cursor.swift in Sources */, 5674A6EB1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */, 560A37A41C8F625000949E71 /* DatabasePool.swift in Sources */, 56B7F43A1BEB42D500E39BBF /* Migration.swift in Sources */, 5605F1931C6B1A8700235C62 /* QueryInterfaceQuery.swift in Sources */, + 5613ED3521A95A5C00DC7A68 /* ValueObservation+Map.swift in Sources */, 566B912B1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, 56A2388B1B9C75030082EB20 /* Statement.swift in Sources */, 5690C3401D23E82A00E59934 /* Data.swift in Sources */, diff --git a/GRDB/Core/ValueObservation.swift b/GRDB/Core/ValueObservation.swift deleted file mode 100644 index ede800bd15..0000000000 --- a/GRDB/Core/ValueObservation.swift +++ /dev/null @@ -1,1037 +0,0 @@ -// -// ValueObservation.swift -// GRDB -// -// Created by Gwendal Roué on 23/10/2018. -// Copyright © 2018 Gwendal Roué. All rights reserved. -// - -import Dispatch - -// MARK: - ValueScheduling - -/// ValueScheduling controls how ValueObservation schedules the notifications -/// of fresh values to your application. -public enum ValueScheduling { - /// All values are notified on the main queue. - /// - /// If the observation starts on the main queue, an initial value is - /// notified right upon subscription, synchronously: - /// - /// // On main queue - /// let observation = ValueObservation.trackingAll(Player.all()) - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("fresh players: \(players)") - /// } - /// // <- here "fresh players" is already printed. - /// - /// If the observation does not start on the main queue, an initial value - /// is also notified on the main queue, but asynchronously: - /// - /// // Not on the main queue: "fresh players" is eventually printed - /// // on the main queue. - /// let observation = ValueObservation.trackingAll(Player.all()) - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("fresh players: \(players)") - /// } - /// - /// When the database changes, fresh values are asynchronously notified on - /// the main queue: - /// - /// // Eventually prints "fresh players" on the main queue - /// try dbQueue.write { db in - /// try Player(...).insert(db) - /// } - case mainQueue - - /// All values are asychronously notified on the specified queue. - /// - /// An initial value is fetched and notified if `startImmediately` - /// is true. - case onQueue(DispatchQueue, startImmediately: Bool) - - /// Values are not all notified on the same dispatch queue. - /// - /// If `startImmediately` is true, an initial value is notified right upon - /// subscription, synchronously, on the dispatch queue which starts - /// the observation. - /// - /// // On any queue - /// var observation = ValueObservation.trackingAll(Player.all()) - /// observation.scheduling = .unsafe(startImmediately: true) - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("fresh players: \(players)") - /// } - /// // <- here "fresh players" is already printed. - /// - /// When the database changes, other values are notified on - /// unspecified queues. - case unsafe(startImmediately: Bool) -} - -extension DispatchQueue { - private static var mainKey: DispatchSpecificKey<()> = { - let key = DispatchSpecificKey<()>() - DispatchQueue.main.setSpecific(key: key, value: ()) - return key - }() - - static var isMain: Bool { - return DispatchQueue.getSpecific(key: mainKey) != nil - } -} - -// MARK: - ValueReducer - -/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) -/// -/// The ValueReducer protocol supports ValueObservation. -public protocol ValueReducer { - /// The type of fetched database values - associatedtype Fetched - - /// The type of observed values - associatedtype Value - - /// Feches database values upon changes in an observed database region. - func fetch(_ db: Database) throws -> Fetched - - /// Transforms a fetched value into an eventual observed value. Returns nil - /// when observer should not be notified. - /// - /// This method runs inside a private dispatch queue. - mutating func value(_ fetched: Fetched) -> Value? -} - -extension ValueReducer { - /// Returns a reducer which transforms the values returned by this reducer. - public func map(_ transform: @escaping (Value) -> T?) -> MapValueReducer { - return MapValueReducer(self, transform) - } -} - -/// A ValueReducer whose values consist of those in a Base ValueReducer passed -/// through a transform function. -/// -/// See ValueReducer.map(_:) -/// -/// :nodoc: -public struct MapValueReducer: ValueReducer { - private var base: Base - private let transform: (Base.Value) -> T? - - init(_ base: Base, _ transform: @escaping (Base.Value) -> T?) { - self.base = base - self.transform = transform - } - - public func fetch(_ db: Database) throws -> Base.Fetched { - return try base.fetch(db) - } - - public mutating func value(_ fetched: Base.Fetched) -> T? { - guard let value = base.value(fetched) else { return nil } - return transform(value) - } -} - -/// A type-erased ValueReducer. -/// -/// An AnyValueReducer forwards its operations to an underlying reducer, -/// hiding its specifics. -public struct AnyValueReducer: ValueReducer { - private var _fetch: (Database) throws -> Fetched - private var _value: (Fetched) -> Value? - - /// Creates a reducer whose `fetch(_:)` and `value(_:)` methods wrap and - /// forward operations the argument closures. - /// - /// For example, this reducer counts the number of a times the player table - /// is modified: - /// - /// var count = 0 - /// let reducer = AnyValueReducer( - /// fetch: { _ in }, - /// value: { _ -> Int? in - /// count += 1 - /// return count - /// }) - /// let observer = ValueObservation - /// .tracking(Player.all(), reducer: reducer) - /// .start(in: dbQueue) { count: Int in - /// print("Players have been modified \(count) times.") - /// } - public init(fetch: @escaping (Database) throws -> Fetched, value: @escaping (Fetched) -> Value?) { - self._fetch = fetch - self._value = value - } - - /// Creates a reducer that wraps and forwards operations to `reducer`. - public init(_ reducer: Base) where Base.Fetched == Fetched, Base.Value == Value { - var reducer = reducer - self._fetch = { try reducer.fetch($0) } - self._value = { reducer.value($0) } - } - - /// :nodoc: - public func fetch(_ db: Database) throws -> Fetched { - return try _fetch(db) - } - - /// :nodoc: - public func value(_ fetched: Fetched) -> Value? { - return _value(fetched) - } -} - -public enum ValueReducers { - /// A reducer which outputs raw database values, without any processing. - public struct Raw: ValueReducer { - private let _fetch: (Database) throws -> Value - - init(_ fetch: @escaping (Database) throws -> Value) { - self._fetch = fetch - } - - /// :nodoc: - public func fetch(_ db: Database) throws -> Value { - return try _fetch(db) - } - - /// :nodoc: - public func value(_ fetched: Value) -> Value? { - return fetched - } - } - - /// A reducer which outputs raw database values, filtering out consecutive - /// values that are equal. - public struct Distinct: ValueReducer { - private let _fetch: (Database) throws -> Value - private var previousValue: Value?? - - init(_ fetch: @escaping (Database) throws -> Value) { - self._fetch = fetch - } - - /// :nodoc: - public func fetch(_ db: Database) throws -> Value { - return try _fetch(db) - } - - /// :nodoc: - public mutating func value(_ value: Value) -> Value? { - if let previousValue = previousValue, previousValue == value { - // Don't notify consecutive identical values - return nil - } - self.previousValue = value - return value - } - } - - /// A reducer which outputs arrays of records, filtering out consecutive - /// identical database rows. - public struct Records: ValueReducer { - private let _fetch: (Database) throws -> [Row] - private var previousRows: [Row]? - - init(_ fetch: @escaping (Database) throws -> [Row]) { - self._fetch = fetch - } - - /// :nodoc: - public func fetch(_ db: Database) throws -> [Row] { - return try _fetch(db) - } - - /// :nodoc: - public mutating func value(_ rows: [Row]) -> [Record]? { - if let previousRows = previousRows, previousRows == rows { - // Don't notify consecutive identical row arrays - return nil - } - self.previousRows = rows - return rows.map(Record.init(row:)) - } - } - - /// A reducer which outputs optional records, filtering out consecutive - /// identical database rows. - public struct Record: ValueReducer { - private let _fetch: (Database) throws -> Row? - private var previousRow: Row?? - - init(_ fetch: @escaping (Database) throws -> Row?) { - self._fetch = fetch - } - - /// :nodoc: - public func fetch(_ db: Database) throws -> Row? { - return try _fetch(db) - } - - /// :nodoc: - public mutating func value(_ row: Row?) -> Record?? { - if let previousRow = previousRow, previousRow == row { - // Don't notify consecutive identical rows - return nil - } - self.previousRow = row - return .some(row.map(Record.init(row:))) - } - } - - /// A reducer which outputs arrays of values, filtering out consecutive - /// identical database values. - public struct Values: ValueReducer { - private let _fetch: (Database) throws -> [DatabaseValue] - private var previousDbValues: [DatabaseValue]? - - init(_ fetch: @escaping (Database) throws -> [DatabaseValue]) { - self._fetch = fetch - } - - /// :nodoc: - public func fetch(_ db: Database) throws -> [DatabaseValue] { - return try _fetch(db) - } - - /// :nodoc: - public mutating func value(_ dbValues: [DatabaseValue]) -> [T]? { - if let previousDbValues = previousDbValues, previousDbValues == dbValues { - // Don't notify consecutive identical dbValue arrays - return nil - } - self.previousDbValues = dbValues - return dbValues.map { - T.decode(from: $0, conversionContext: nil) - } - } - } - - /// A reducer which outputs optional values, filtering out consecutive - /// identical database values. - public struct Value: ValueReducer { - private let _fetch: (Database) throws -> DatabaseValue? - private var previousDbValue: DatabaseValue?? - private var previousValueWasNil = false - - init(_ fetch: @escaping (Database) throws -> DatabaseValue?) { - self._fetch = fetch - } - - /// :nodoc: - public func fetch(_ db: Database) throws -> DatabaseValue? { - return try _fetch(db) - } - - /// :nodoc: - public mutating func value(_ dbValue: DatabaseValue?) -> T?? { - if let previousDbValue = previousDbValue, previousDbValue == dbValue { - // Don't notify consecutive identical dbValue - return nil - } - self.previousDbValue = dbValue - if let dbValue = dbValue, - let value = T.decodeIfPresent(from: dbValue, conversionContext: nil) - { - previousValueWasNil = false - return .some(value) - } else if previousValueWasNil { - // Don't notify consecutive nil values - return nil - } else { - previousValueWasNil = true - return .some(nil) - } - } - } - - /// A reducer which outputs arrays of optional values, filtering out consecutive - /// identical database values. - public struct OptionalValues: ValueReducer { - private let _fetch: (Database) throws -> [DatabaseValue] - private var previousDbValues: [DatabaseValue]? - - init(_ fetch: @escaping (Database) throws -> [DatabaseValue]) { - self._fetch = fetch - } - - /// :nodoc: - public func fetch(_ db: Database) throws -> [DatabaseValue] { - return try _fetch(db) - } - - /// :nodoc: - public mutating func value(_ dbValues: [DatabaseValue]) -> [T?]? { - if let previousDbValues = previousDbValues, previousDbValues == dbValues { - // Don't notify consecutive identical dbValue arrays - return nil - } - self.previousDbValues = dbValues - return dbValues.map { - T.decodeIfPresent(from: $0, conversionContext: nil) - } - } - } -} - -// MARK: - ValueObservation - -/// ValueObservation tracks changes in the results of database requests, and -/// notifies fresh values whenever the database changes. -/// -/// For example: -/// -/// let observation = ValueObservation.trackingAll(Player.all) -/// let observer = try observation.start(in: dbQueue) { players: [Player] in -/// print("Players have changed.") -/// } -public struct ValueObservation { - /// A closure that is evaluated when the observation starts, and returns - /// the observed database region. - var observedRegion: (Database) throws -> DatabaseRegion - - /// The reducer is triggered upon each database change in *observedRegion*. - var reducer: Reducer - - /// Default is false. Set this property to true when the observation - /// requires write access in order to fetch fresh values. Fetches are then - /// wrapped inside a savepoint. - /// - /// Don't set this flag to true unless you really need it. A read/write - /// observation is less efficient than a read-only observation. - public var requiresWriteAccess: Bool = false - - /// The extent of the database observation. The default is - /// `.observerLifetime`: the observation lasts until the - /// observer returned by the `start(in:onError:onChange:)` method - /// is deallocated. - public var extent = Database.TransactionObservationExtent.observerLifetime - - /// `scheduling` controls how fresh values are notified. Default - /// is `.mainQueue`. - /// - /// - `.mainQueue`: all values are notified on the main queue. - /// - /// If the observation starts on the main queue, an initial value is - /// notified right upon subscription, synchronously:: - /// - /// // On main queue - /// let observation = ValueObservation.trackingAll(Player.all()) - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("fresh players: \(players)") - /// } - /// // <- here "fresh players" is already printed. - /// - /// If the observation does not start on the main queue, an initial - /// value is also notified on the main queue, but asynchronously: - /// - /// // Not on the main queue: "fresh players" is eventually printed - /// // on the main queue. - /// let observation = ValueObservation.trackingAll(Player.all()) - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("fresh players: \(players)") - /// } - /// - /// When the database changes, fresh values are asynchronously notified: - /// - /// // Eventually prints "fresh players" on the main queue - /// try dbQueue.write { db in - /// try Player(...).insert(db) - /// } - /// - /// - `.onQueue(_:startImmediately:)`: all values are asychronously notified - /// on the specified queue. - /// - /// An initial value is fetched and notified if `startImmediately` - /// is true. - /// - /// - `unsafe(startImmediately:)`: values are not all notified on the same - /// dispatch queue. - /// - /// If `startImmediately` is true, an initial value is notified right - /// upon subscription, synchronously, on the dispatch queue which starts - /// the observation. - /// - /// // On any queue - /// var observation = ValueObservation.trackingAll(Player.all()) - /// observation.scheduling = .unsafe(startImmediately: true) - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("fresh players: \(players)") - /// } - /// // <- here "fresh players" is already printed. - /// - /// When the database changes, other values are notified on - /// unspecified queues. - public var scheduling: ValueScheduling = .mainQueue - - /// The dispatch queue where change callbacks are called. - var notificationQueue: DispatchQueue? { - switch scheduling { - case .mainQueue: - return DispatchQueue.main - case .onQueue(let queue, startImmediately: _): - return queue - case .unsafe: - return nil - } - } - - // Not public. See ValueObservation.tracking(_:reducer:) - init( - tracking region: @escaping (Database) throws -> DatabaseRegion, - reducer: Reducer) - { - self.observedRegion = { db in - // Remove views from the observed region. - // - // We can do it because we are only interested in modifications in - // actual tables. And we want to do it because we have a fast path - // for simple regions that span a single table. - let views = try db.schema().names(ofType: .view) - return try region(db).ignoring(views) - } - self.reducer = reducer - } - - /// Returs a ValueObservation which observes *regions*, and notifies the - /// values returned by the *reducer* whenever one of the observed - /// regions is modified by a database transaction. - /// - /// This method is the most fundamental way to create a ValueObservation. - /// - /// For example, this observation counts the number of a times the player - /// table is modified: - /// - /// var count = 0 - /// let reducer = AnyValueReducer( - /// fetch: { _ in }, - /// value: { _ -> Int? in - /// count += 1 - /// return count - /// }) - /// let observation = ValueObservation.tracking(Player.all(), reducer: reducer) - /// let observer = observation.start(in: dbQueue) { count: Int in - /// print("Players have been modified \(count) times.") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter regions: A list of observed regions. - /// - parameter reducer: A reducer that turns database changes in the - /// modified regions into fresh values. Currently only reducers that adopt - /// the ValueReducer protocol are supported. - public static func tracking( - _ regions: DatabaseRegionConvertible..., - reducer: Reducer) - -> ValueObservation - { - return ValueObservation.tracking(regions, reducer: reducer) - } - - /// Returs a ValueObservation which observes *regions*, and notifies the - /// values returned by the *reducer* whenever one of the observed - /// regions is modified by a database transaction. - /// - /// This method is the most fundamental way to create a ValueObservation. - /// - /// For example, this observation counts the number of a times the player - /// table is modified: - /// - /// var count = 0 - /// let reducer = AnyValueReducer( - /// fetch: { _ in }, - /// value: { _ -> Int? in - /// count += 1 - /// return count - /// }) - /// let observation = ValueObservation.tracking([Player.all()], reducer: reducer) - /// let observer = observation.start(in: dbQueue) { count: Int in - /// print("Players have been modified \(count) times.") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter regions: A list of observed regions. - /// - parameter reducer: A reducer that turns database changes in the - /// modified regions into fresh values. Currently only reducers that adopt - /// the ValueReducer protocol are supported. - public static func tracking( - _ regions: [DatabaseRegionConvertible], - reducer: Reducer) - -> ValueObservation - { - return ValueObservation(tracking: union(regions), reducer: reducer) - } -} - -extension ValueObservation where Reducer: ValueReducer { - /// Returns a ValueObservation which transforms the values returned by - /// this ValueObservation. - public func map(_ transform: @escaping (Reducer.Value) -> T) - -> ValueObservation> - { - return ValueObservation>( - tracking: observedRegion, - reducer: reducer.map(transform)) - } -} - -extension ValueObservation where Reducer == Void { - /// Creates a ValueObservation which observes *regions*, and notifies the - /// values returned by the *fetch* closure whenever one of the observed - /// regions is modified by a database transaction. - /// - /// For example: - /// - /// let observation = ValueObservation.tracking( - /// Player.all(), - /// fetch: { db in return try Player.fetchAll(db) }) - /// - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("Players have changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter regions: A list of observed regions. - /// - parameter fetch: A closure that fetches a value. - public static func tracking( - _ regions: DatabaseRegionConvertible..., - fetch: @escaping (Database) throws -> Value) - -> ValueObservation> - { - return ValueObservation.tracking(regions, fetch: fetch) - } - - /// Creates a ValueObservation which observes *regions*, and notifies the - /// values returned by the *fetch* closure whenever one of the observed - /// regions is modified by a database transaction. - /// - /// For example: - /// - /// let observation = ValueObservation.tracking( - /// [Player.all()], - /// fetch: { db in return try Player.fetchAll(db) }) - /// - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("Players have changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter regions: A list of observed regions. - /// - parameter fetch: A closure that fetches a value. - public static func tracking( - _ regions: [DatabaseRegionConvertible], - fetch: @escaping (Database) throws -> Value) - -> ValueObservation> - { - return ValueObservation>( - tracking: union(regions), - reducer: ValueReducers.Raw(fetch)) - } - - /// Creates a ValueObservation which observes *regions*, and notifies the - /// values returned by the *fetch* closure whenever one of the observed - /// regions is modified by a database transaction. Consecutive equal values - /// are filtered out. - /// - /// For example: - /// - /// let observation = ValueObservation.tracking( - /// Player.all(), - /// fetchDistinct: { db in return try Player.fetchAll(db) }) - /// - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("Players have changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter regions: A list of observed regions. - /// - parameter fetch: A closure that fetches a value. - public static func tracking( - _ regions: DatabaseRegionConvertible..., - fetchDistinct fetch: @escaping (Database) throws -> Value) - -> ValueObservation> - where Value: Equatable - { - return ValueObservation.tracking(regions, fetchDistinct: fetch) - } - - /// Creates a ValueObservation which observes *regions*, and notifies the - /// values returned by the *fetch* closure whenever one of the observed - /// regions is modified by a database transaction. Consecutive equal values - /// are filtered out. - /// - /// For example: - /// - /// let observation = ValueObservation.tracking( - /// [Player.all()], - /// fetchDistinct: { db in return try Player.fetchAll(db) }) - /// - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("Players have changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter regions: A list of observed regions. - /// - parameter fetch: A closure that fetches a value. - public static func tracking( - _ regions: [DatabaseRegionConvertible], - fetchDistinct fetch: @escaping (Database) throws -> Value) - -> ValueObservation> - where Value: Equatable - { - return ValueObservation>( - tracking: union(regions), - reducer: ValueReducers.Distinct(fetch)) - } -} - -private func union(_ regions: [DatabaseRegionConvertible]) -> (Database) throws -> DatabaseRegion { - return { db in - try regions.reduce(into: DatabaseRegion()) { union, region in - try union.formUnion(region.databaseRegion(db)) - } - } -} - -// MARK: - Starting Observation - -extension ValueObservation where Reducer: ValueReducer { - /// Starts the value observation in the provided database reader (such as - /// a database queue or database pool), and returns a transaction observer. - /// - /// - parameter reader: A DatabaseReader. - /// - parameter onError: A closure that is provided eventual errors that - /// happen during observation - /// - parameter onChange: A closure that is provided fresh values - /// - returns: a TransactionObserver - public func start( - in reader: DatabaseReader, - onError: ((Error) -> Void)? = nil, - onChange: @escaping (Reducer.Value) -> Void) throws -> TransactionObserver - { - return try reader.add(observation: self, onError: onError, onChange: onChange) - } -} - -// MARK: - Count Observation - -extension ValueObservation where Reducer == Void { - /// Creates a ValueObservation which observes *request*, and notifies its - /// count whenever it is modified by a database transaction. - /// - /// For example: - /// - /// let request = Player.all() - /// let observation = ValueObservation.trackingCount(request) - /// - /// let observer = try observation.start(in: dbQueue) { count: Int in - /// print("Number of players has changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter request: the observed request. - /// - returns: a ValueObservation. - public static func trackingCount(_ request: Request) - -> ValueObservation> - { - return ValueObservation.tracking(request, fetchDistinct: request.fetchCount) - } -} - -// MARK: - Row Observation - -extension ValueObservation where Reducer == Void { - /// Creates a ValueObservation which observes *request*, and notifies - /// fresh rows whenever the request is modified by a database transaction. - /// - /// For example: - /// - /// let request = SQLRequest("SELECT * FROM player") - /// let observation = ValueObservation.trackingAll(request) - /// - /// let observer = try observation.start(in: dbQueue) { rows: [Row] in - /// print("Players have changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter request: the observed request. - /// - returns: a ValueObservation. - public static func trackingAll(_ request: Request) - -> ValueObservation> - where Request.RowDecoder == Row - { - return ValueObservation.tracking(request, fetchDistinct: request.fetchAll) - } - - /// Creates a ValueObservation which observes *request*, and notifies a - /// fresh row whenever the request is modified by a database transaction. - /// - /// For example: - /// - /// let request = SQLRequest("SELECT * FROM player WHERE id = ?", arguments: [1]) - /// let observation = ValueObservation.trackingOne(request) - /// - /// let observer = try observation.start(in: dbQueue) { row: Row? in - /// print("Players have changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter request: the observed request. - /// - returns: a ValueObservation. - public static func trackingOne(_ request: Request) - -> ValueObservation> - where Request.RowDecoder == Row - { - return ValueObservation.tracking(request, fetchDistinct: request.fetchOne) - } -} - -// MARK: - FetchableRecord Observation - -extension ValueObservation where Reducer == Void { - /// Creates a ValueObservation which observes *request*, and notifies - /// fresh records whenever the request is modified by a - /// database transaction. - /// - /// For example: - /// - /// let request = Player.all() - /// let observation = ValueObservation.trackingAll(request) - /// - /// let observer = try observation.start(in: dbQueue) { players: [Player] in - /// print("Players have changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter request: the observed request. - /// - returns: a ValueObservation. - public static func trackingAll(_ request: Request) - -> ValueObservation> - where Request.RowDecoder: FetchableRecord - { - return ValueObservation>.tracking( - request, - reducer: ValueReducers.Records { try Row.fetchAll($0, request) }) - } - - /// Creates a ValueObservation which observes *request*, and notifies a - /// fresh record whenever the request is modified by a database transaction. - /// - /// For example: - /// - /// let request = Player.filter(key: 1) - /// let observation = ValueObservation.trackingOne(request) - /// - /// let observer = try observation.start(in: dbQueue) { player: Player? in - /// print("Player has changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter request: the observed request. - /// - returns: a ValueObservation. - public static func trackingOne(_ request: Request) -> - ValueObservation> - where Request.RowDecoder: FetchableRecord - { - return ValueObservation>.tracking( - request, - reducer: ValueReducers.Record { try Row.fetchOne($0, request) }) - } -} - -// MARK: - DatabaseValueConvertible Observation - -extension ValueObservation where Reducer == Void { - /// Creates a ValueObservation which observes *request*, and notifies - /// fresh values whenever the request is modified by a - /// database transaction. - /// - /// For example: - /// - /// let request = Player.select(Column("name"), as: String.self) - /// let observation = ValueObservation.trackingAll(request) - /// - /// let observer = try observation.start(in: dbQueue) { names: [String] in - /// print("Player names have changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter request: the observed request. - /// - returns: a ValueObservation. - public static func trackingAll(_ request: Request) - -> ValueObservation> - where Request.RowDecoder: DatabaseValueConvertible - { - return ValueObservation>.tracking( - request, - reducer: ValueReducers.Values { try DatabaseValue.fetchAll($0, request) }) - } - - /// Creates a ValueObservation which observes *request*, and notifies a - /// fresh value whenever the request is modified by a database transaction. - /// - /// For example: - /// - /// let request = Player.select(max(Column("score")), as: Int.self) - /// let observation = ValueObservation.trackingOne(request) - /// - /// let observer = try observation.start(in: dbQueue) { maxScore: Int? in - /// print("Maximum score has changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter request: the observed request. - /// - returns: a ValueObservation. - public static func trackingOne(_ request: Request) - -> ValueObservation> - where Request.RowDecoder: DatabaseValueConvertible - { - return ValueObservation>.tracking( - request, - reducer: ValueReducers.Value { try DatabaseValue.fetchOne($0, request) }) - } -} - -// MARK: - Optional DatabaseValueConvertible Observation - -extension ValueObservation where Reducer == Void { - /// Creates a ValueObservation which observes *request*, and notifies - /// fresh values whenever the request is modified by a - /// database transaction. - /// - /// For example: - /// - /// let request = Player.select(Column("name"), as: Optional.self) - /// let observation = ValueObservation.trackingAll(request) - /// - /// let observer = try observation.start(in: dbQueue) { names: [String?] in - /// print("Player names have changed") - /// } - /// - /// The returned observation has the default configuration: - /// - /// - When started with the `start(in:onError:onChange:)` method, a fresh - /// value is immediately notified on the main queue. - /// - Upon subsequent database changes, fresh values are notified on the - /// main queue. - /// - The observation lasts until the observer returned by - /// `start` is deallocated. - /// - /// - parameter request: the observed request. - /// - returns: a ValueObservation. - public static func trackingAll(_ request: Request) - -> ValueObservation> - where Request.RowDecoder: _OptionalProtocol, - Request.RowDecoder._Wrapped: DatabaseValueConvertible - { - return ValueObservation>.tracking( - request, - reducer: ValueReducers.OptionalValues { try DatabaseValue.fetchAll($0, request) }) - } -} diff --git a/GRDB/Utils/Utils.swift b/GRDB/Utils/Utils.swift index fa808dca4f..484dab35c2 100644 --- a/GRDB/Utils/Utils.swift +++ b/GRDB/Utils/Utils.swift @@ -69,3 +69,15 @@ extension Array { } } } + +extension DispatchQueue { + private static var mainKey: DispatchSpecificKey<()> = { + let key = DispatchSpecificKey<()>() + DispatchQueue.main.setSpecific(key: key, value: ()) + return key + }() + + static var isMain: Bool { + return DispatchQueue.getSpecific(key: mainKey) != nil + } +} diff --git a/GRDB/ValueObservation/ValueObservation+Combine.swift b/GRDB/ValueObservation/ValueObservation+Combine.swift new file mode 100644 index 0000000000..988c6f1d96 --- /dev/null +++ b/GRDB/ValueObservation/ValueObservation+Combine.swift @@ -0,0 +1,291 @@ +// +// ValueObservation+Combine.swift +// GRDB +// +// Created by Gwendal Roué on 24/11/2018. +// Copyright © 2018 Gwendal Roué. All rights reserved. +// + +extension ValueObservation where Reducer == Void { + public static func combine( + _ o1: ValueObservation, + _ o2: ValueObservation) + -> ValueObservation> + { + return ValueObservation>.init( + tracking: union([ + o1.observedRegion, + o2.observedRegion]), + reducer: ValueReducers.combine( + o1.reducer, + o2.reducer)) + } + + public static func combine( + _ o1: ValueObservation, + _ o2: ValueObservation, + _ o3: ValueObservation) + -> ValueObservation> + { + return ValueObservation>.init( + tracking: union([ + o1.observedRegion, + o2.observedRegion, + o3.observedRegion]), + reducer: ValueReducers.combine( + o1.reducer, + o2.reducer, + o3.reducer)) + } + + public static func combine( + _ o1: ValueObservation, + _ o2: ValueObservation, + _ o3: ValueObservation, + _ o4: ValueObservation) + -> ValueObservation> + { + return ValueObservation>.init( + tracking: union([ + o1.observedRegion, + o2.observedRegion, + o3.observedRegion, + o4.observedRegion]), + reducer: ValueReducers.combine( + o1.reducer, + o2.reducer, + o3.reducer, + o4.reducer)) + } + + public static func combine( + _ o1: ValueObservation, + _ o2: ValueObservation, + _ o3: ValueObservation, + _ o4: ValueObservation, + _ o5: ValueObservation) + -> ValueObservation> + { + return ValueObservation>.init( + tracking: union([ + o1.observedRegion, + o2.observedRegion, + o3.observedRegion, + o4.observedRegion, + o5.observedRegion]), + reducer: ValueReducers.combine( + o1.reducer, + o2.reducer, + o3.reducer, + o4.reducer, + o5.reducer)) + } +} + +extension ValueReducers { + static func combine( + _ r1: R1, + _ r2: R2) + -> AnyValueReducer< + (R1.Fetched, R2.Fetched), + (R1.Value, R2.Value)> + { + var r1 = r1 + var r2 = r2 + var prev1: R1.Value? + var prev2: R2.Value? + func fetch(db: Database) throws -> (R1.Fetched, R2.Fetched) { + return try ( + r1.fetch(db), + r2.fetch(db)) + } + func value(tuple: (R1.Fetched, R2.Fetched)) -> (R1.Value, R2.Value)? { + let v1 = r1.value(tuple.0) + let v2 = r2.value(tuple.1) + defer { + if let v1 = v1 { prev1 = v1 } + if let v2 = v2 { prev2 = v2 } + } + if v1 != nil || v2 != nil, + let c1 = v1 ?? prev1, + let c2 = v2 ?? prev2 + { + return (c1, c2) + } else { + return nil + } + } + return AnyValueReducer(fetch: fetch, value: value) + } + + static func combine( + _ r1: R1, + _ r2: R2, + _ r3: R3) + -> AnyValueReducer< + (R1.Fetched, R2.Fetched, R3.Fetched), + (R1.Value, R2.Value, R3.Value)> + { + var r1 = r1 + var r2 = r2 + var r3 = r3 + var prev1: R1.Value? + var prev2: R2.Value? + var prev3: R3.Value? + func fetch(db: Database) throws -> (R1.Fetched, R2.Fetched, R3.Fetched) { + return try ( + r1.fetch(db), + r2.fetch(db), + r3.fetch(db)) + } + func value(tuple: (R1.Fetched, R2.Fetched, R3.Fetched)) -> (R1.Value, R2.Value, R3.Value)? { + let v1 = r1.value(tuple.0) + let v2 = r2.value(tuple.1) + let v3 = r3.value(tuple.2) + defer { + if let v1 = v1 { prev1 = v1 } + if let v2 = v2 { prev2 = v2 } + if let v3 = v3 { prev3 = v3 } + } + if v1 != nil || v2 != nil || v3 != nil, + let c1 = v1 ?? prev1, + let c2 = v2 ?? prev2, + let c3 = v3 ?? prev3 + { + return (c1, c2, c3) + } else { + return nil + } + } + return AnyValueReducer(fetch: fetch, value: value) + } + + static func combine( + _ r1: R1, + _ r2: R2, + _ r3: R3, + _ r4: R4) + -> AnyValueReducer< + (R1.Fetched, R2.Fetched, R3.Fetched, R4.Fetched), + (R1.Value, R2.Value, R3.Value, R4.Value)> + { + var r1 = r1 + var r2 = r2 + var r3 = r3 + var r4 = r4 + var prev1: R1.Value? + var prev2: R2.Value? + var prev3: R3.Value? + var prev4: R4.Value? + func fetch(db: Database) throws -> (R1.Fetched, R2.Fetched, R3.Fetched, R4.Fetched) { + return try ( + r1.fetch(db), + r2.fetch(db), + r3.fetch(db), + r4.fetch(db)) + } + func value(tuple: (R1.Fetched, R2.Fetched, R3.Fetched, R4.Fetched)) -> (R1.Value, R2.Value, R3.Value, R4.Value)? { + let v1 = r1.value(tuple.0) + let v2 = r2.value(tuple.1) + let v3 = r3.value(tuple.2) + let v4 = r4.value(tuple.3) + defer { + if let v1 = v1 { prev1 = v1 } + if let v2 = v2 { prev2 = v2 } + if let v3 = v3 { prev3 = v3 } + if let v4 = v4 { prev4 = v4 } + } + if v1 != nil || v2 != nil || v3 != nil || v4 != nil, + let c1 = v1 ?? prev1, + let c2 = v2 ?? prev2, + let c3 = v3 ?? prev3, + let c4 = v4 ?? prev4 + { + return (c1, c2, c3, c4) + } else { + return nil + } + } + return AnyValueReducer(fetch: fetch, value: value) + } + + static func combine( + _ r1: R1, + _ r2: R2, + _ r3: R3, + _ r4: R4, + _ r5: R5) + -> AnyValueReducer< + (R1.Fetched, R2.Fetched, R3.Fetched, R4.Fetched, R5.Fetched), + (R1.Value, R2.Value, R3.Value, R4.Value, R5.Value)> + { + var r1 = r1 + var r2 = r2 + var r3 = r3 + var r4 = r4 + var r5 = r5 + var prev1: R1.Value? + var prev2: R2.Value? + var prev3: R3.Value? + var prev4: R4.Value? + var prev5: R5.Value? + func fetch(db: Database) throws -> (R1.Fetched, R2.Fetched, R3.Fetched, R4.Fetched, R5.Fetched) { + return try ( + r1.fetch(db), + r2.fetch(db), + r3.fetch(db), + r4.fetch(db), + r5.fetch(db)) + } + func value(tuple: (R1.Fetched, R2.Fetched, R3.Fetched, R4.Fetched, R5.Fetched)) -> (R1.Value, R2.Value, R3.Value, R4.Value, R5.Value)? { + let v1 = r1.value(tuple.0) + let v2 = r2.value(tuple.1) + let v3 = r3.value(tuple.2) + let v4 = r4.value(tuple.3) + let v5 = r5.value(tuple.4) + defer { + if let v1 = v1 { prev1 = v1 } + if let v2 = v2 { prev2 = v2 } + if let v3 = v3 { prev3 = v3 } + if let v4 = v4 { prev4 = v4 } + if let v5 = v5 { prev5 = v5 } + } + if v1 != nil || v2 != nil || v3 != nil || v4 != nil || v5 != nil, + let c1 = v1 ?? prev1, + let c2 = v2 ?? prev2, + let c3 = v3 ?? prev3, + let c4 = v4 ?? prev4, + let c5 = v5 ?? prev5 + { + return (c1, c2, c3, c4, c5) + } else { + return nil + } + } + return AnyValueReducer(fetch: fetch, value: value) + } +} + +private func union(_ regions: [(Database) throws -> DatabaseRegion]) -> (Database) throws -> DatabaseRegion { + return { db in + try regions.reduce(into: DatabaseRegion()) { union, region in + try union.formUnion(region(db)) + } + } +} diff --git a/GRDB/ValueObservation/ValueObservation+Count.swift b/GRDB/ValueObservation/ValueObservation+Count.swift new file mode 100644 index 0000000000..cdde780cc6 --- /dev/null +++ b/GRDB/ValueObservation/ValueObservation+Count.swift @@ -0,0 +1,41 @@ +// +// ValueObservation+Count.swift +// GRDB +// +// Created by Gwendal Roué on 24/11/2018. +// Copyright © 2018 Gwendal Roué. All rights reserved. +// + +extension ValueObservation where Reducer == Void { + + // MARK: - Count Observation + + /// Creates a ValueObservation which observes *request*, and notifies its + /// count whenever it is modified by a database transaction. + /// + /// For example: + /// + /// let request = Player.all() + /// let observation = ValueObservation.trackingCount(request) + /// + /// let observer = try observation.start(in: dbQueue) { count: Int in + /// print("Number of players has changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter request: the observed request. + /// - returns: a ValueObservation. + public static func trackingCount(_ request: Request) + -> ValueObservation> + { + return ValueObservation.tracking(request, fetchDistinct: request.fetchCount) + } +} diff --git a/GRDB/ValueObservation/ValueObservation+DatabaseValueConvertible.swift b/GRDB/ValueObservation/ValueObservation+DatabaseValueConvertible.swift new file mode 100644 index 0000000000..c440567e65 --- /dev/null +++ b/GRDB/ValueObservation/ValueObservation+DatabaseValueConvertible.swift @@ -0,0 +1,207 @@ +// +// ValueObservation+DatabaseValueConvertible.swift +// GRDB +// +// Created by Gwendal Roué on 24/11/2018. +// Copyright © 2018 Gwendal Roué. All rights reserved. +// + +extension ValueObservation where Reducer == Void { + + // MARK: - DatabaseValueConvertible Observation + + /// Creates a ValueObservation which observes *request*, and notifies + /// fresh values whenever the request is modified by a + /// database transaction. + /// + /// For example: + /// + /// let request = Player.select(Column("name"), as: String.self) + /// let observation = ValueObservation.trackingAll(request) + /// + /// let observer = try observation.start(in: dbQueue) { names: [String] in + /// print("Player names have changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter request: the observed request. + /// - returns: a ValueObservation. + public static func trackingAll(_ request: Request) + -> ValueObservation> + where Request.RowDecoder: DatabaseValueConvertible + { + return ValueObservation>.tracking( + request, + reducer: ValueReducers.Values { try DatabaseValue.fetchAll($0, request) }) + } + + /// Creates a ValueObservation which observes *request*, and notifies a + /// fresh value whenever the request is modified by a database transaction. + /// + /// For example: + /// + /// let request = Player.select(max(Column("score")), as: Int.self) + /// let observation = ValueObservation.trackingOne(request) + /// + /// let observer = try observation.start(in: dbQueue) { maxScore: Int? in + /// print("Maximum score has changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter request: the observed request. + /// - returns: a ValueObservation. + public static func trackingOne(_ request: Request) + -> ValueObservation> + where Request.RowDecoder: DatabaseValueConvertible + { + return ValueObservation>.tracking( + request, + reducer: ValueReducers.Value { try DatabaseValue.fetchOne($0, request) }) + } + + /// Creates a ValueObservation which observes *request*, and notifies + /// fresh values whenever the request is modified by a + /// database transaction. + /// + /// For example: + /// + /// let request = Player.select(Column("name"), as: Optional.self) + /// let observation = ValueObservation.trackingAll(request) + /// + /// let observer = try observation.start(in: dbQueue) { names: [String?] in + /// print("Player names have changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter request: the observed request. + /// - returns: a ValueObservation. + public static func trackingAll(_ request: Request) + -> ValueObservation> + where Request.RowDecoder: _OptionalProtocol, + Request.RowDecoder._Wrapped: DatabaseValueConvertible + { + return ValueObservation>.tracking( + request, + reducer: ValueReducers.OptionalValues { try DatabaseValue.fetchAll($0, request) }) + } +} + +extension ValueReducers { + /// A reducer which outputs arrays of values, filtering out consecutive + /// identical database values. + public struct Values: ValueReducer { + private let _fetch: (Database) throws -> [DatabaseValue] + private var previousDbValues: [DatabaseValue]? + + init(_ fetch: @escaping (Database) throws -> [DatabaseValue]) { + self._fetch = fetch + } + + /// :nodoc: + public func fetch(_ db: Database) throws -> [DatabaseValue] { + return try _fetch(db) + } + + /// :nodoc: + public mutating func value(_ dbValues: [DatabaseValue]) -> [T]? { + if let previousDbValues = previousDbValues, previousDbValues == dbValues { + // Don't notify consecutive identical dbValue arrays + return nil + } + self.previousDbValues = dbValues + return dbValues.map { + T.decode(from: $0, conversionContext: nil) + } + } + } + + /// A reducer which outputs optional values, filtering out consecutive + /// identical database values. + public struct Value: ValueReducer { + private let _fetch: (Database) throws -> DatabaseValue? + private var previousDbValue: DatabaseValue?? + private var previousValueWasNil = false + + init(_ fetch: @escaping (Database) throws -> DatabaseValue?) { + self._fetch = fetch + } + + /// :nodoc: + public func fetch(_ db: Database) throws -> DatabaseValue? { + return try _fetch(db) + } + + /// :nodoc: + public mutating func value(_ dbValue: DatabaseValue?) -> T?? { + if let previousDbValue = previousDbValue, previousDbValue == dbValue { + // Don't notify consecutive identical dbValue + return nil + } + self.previousDbValue = dbValue + if let dbValue = dbValue, + let value = T.decodeIfPresent(from: dbValue, conversionContext: nil) + { + previousValueWasNil = false + return .some(value) + } else if previousValueWasNil { + // Don't notify consecutive nil values + return nil + } else { + previousValueWasNil = true + return .some(nil) + } + } + } + + /// A reducer which outputs arrays of optional values, filtering out consecutive + /// identical database values. + public struct OptionalValues: ValueReducer { + private let _fetch: (Database) throws -> [DatabaseValue] + private var previousDbValues: [DatabaseValue]? + + init(_ fetch: @escaping (Database) throws -> [DatabaseValue]) { + self._fetch = fetch + } + + /// :nodoc: + public func fetch(_ db: Database) throws -> [DatabaseValue] { + return try _fetch(db) + } + + /// :nodoc: + public mutating func value(_ dbValues: [DatabaseValue]) -> [T?]? { + if let previousDbValues = previousDbValues, previousDbValues == dbValues { + // Don't notify consecutive identical dbValue arrays + return nil + } + self.previousDbValues = dbValues + return dbValues.map { + T.decodeIfPresent(from: $0, conversionContext: nil) + } + } + } +} diff --git a/GRDB/ValueObservation/ValueObservation+FetchableRecord.swift b/GRDB/ValueObservation/ValueObservation+FetchableRecord.swift new file mode 100644 index 0000000000..6ca6ca16fd --- /dev/null +++ b/GRDB/ValueObservation/ValueObservation+FetchableRecord.swift @@ -0,0 +1,131 @@ +// +// ValueObservation+FetchableRecord.swift +// GRDB +// +// Created by Gwendal Roué on 24/11/2018. +// Copyright © 2018 Gwendal Roué. All rights reserved. +// + +extension ValueObservation where Reducer == Void { + + // MARK: - FetchableRecord Observation + + /// Creates a ValueObservation which observes *request*, and notifies + /// fresh records whenever the request is modified by a + /// database transaction. + /// + /// For example: + /// + /// let request = Player.all() + /// let observation = ValueObservation.trackingAll(request) + /// + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("Players have changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter request: the observed request. + /// - returns: a ValueObservation. + public static func trackingAll(_ request: Request) + -> ValueObservation> + where Request.RowDecoder: FetchableRecord + { + return ValueObservation>.tracking( + request, + reducer: ValueReducers.Records { try Row.fetchAll($0, request) }) + } + + /// Creates a ValueObservation which observes *request*, and notifies a + /// fresh record whenever the request is modified by a database transaction. + /// + /// For example: + /// + /// let request = Player.filter(key: 1) + /// let observation = ValueObservation.trackingOne(request) + /// + /// let observer = try observation.start(in: dbQueue) { player: Player? in + /// print("Player has changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter request: the observed request. + /// - returns: a ValueObservation. + public static func trackingOne(_ request: Request) -> + ValueObservation> + where Request.RowDecoder: FetchableRecord + { + return ValueObservation>.tracking( + request, + reducer: ValueReducers.Record { try Row.fetchOne($0, request) }) + } +} + +extension ValueReducers { + /// A reducer which outputs arrays of records, filtering out consecutive + /// identical database rows. + public struct Records: ValueReducer { + private let _fetch: (Database) throws -> [Row] + private var previousRows: [Row]? + + init(_ fetch: @escaping (Database) throws -> [Row]) { + self._fetch = fetch + } + + /// :nodoc: + public func fetch(_ db: Database) throws -> [Row] { + return try _fetch(db) + } + + /// :nodoc: + public mutating func value(_ rows: [Row]) -> [Record]? { + if let previousRows = previousRows, previousRows == rows { + // Don't notify consecutive identical row arrays + return nil + } + self.previousRows = rows + return rows.map(Record.init(row:)) + } + } + + /// A reducer which outputs optional records, filtering out consecutive + /// identical database rows. + public struct Record: ValueReducer { + private let _fetch: (Database) throws -> Row? + private var previousRow: Row?? + + init(_ fetch: @escaping (Database) throws -> Row?) { + self._fetch = fetch + } + + /// :nodoc: + public func fetch(_ db: Database) throws -> Row? { + return try _fetch(db) + } + + /// :nodoc: + public mutating func value(_ row: Row?) -> Record?? { + if let previousRow = previousRow, previousRow == row { + // Don't notify consecutive identical rows + return nil + } + self.previousRow = row + return .some(row.map(Record.init(row:))) + } + } +} diff --git a/GRDB/ValueObservation/ValueObservation+Map.swift b/GRDB/ValueObservation/ValueObservation+Map.swift new file mode 100644 index 0000000000..24cde5bcd2 --- /dev/null +++ b/GRDB/ValueObservation/ValueObservation+Map.swift @@ -0,0 +1,51 @@ +// +// ValueObservation+Map.swift +// GRDB +// +// Created by Gwendal Roué on 24/11/2018. +// Copyright © 2018 Gwendal Roué. All rights reserved. +// + +extension ValueObservation where Reducer: ValueReducer { + /// Returns a ValueObservation which transforms the values returned by + /// this ValueObservation. + public func map(_ transform: @escaping (Reducer.Value) -> T) + -> ValueObservation> + { + return ValueObservation>( + tracking: observedRegion, + reducer: reducer.map(transform)) + } +} + +extension ValueReducer { + /// Returns a reducer which transforms the values returned by this reducer. + public func map(_ transform: @escaping (Value) -> T?) -> MapValueReducer { + return MapValueReducer(self, transform) + } +} + +/// A ValueReducer whose values consist of those in a Base ValueReducer passed +/// through a transform function. +/// +/// See ValueReducer.map(_:) +/// +/// :nodoc: +public struct MapValueReducer: ValueReducer { + private var base: Base + private let transform: (Base.Value) -> T? + + init(_ base: Base, _ transform: @escaping (Base.Value) -> T?) { + self.base = base + self.transform = transform + } + + public func fetch(_ db: Database) throws -> Base.Fetched { + return try base.fetch(db) + } + + public mutating func value(_ fetched: Base.Fetched) -> T? { + guard let value = base.value(fetched) else { return nil } + return transform(value) + } +} diff --git a/GRDB/ValueObservation/ValueObservation+Row.swift b/GRDB/ValueObservation/ValueObservation+Row.swift new file mode 100644 index 0000000000..1b3cc03009 --- /dev/null +++ b/GRDB/ValueObservation/ValueObservation+Row.swift @@ -0,0 +1,72 @@ +// +// ValueObservation+Row.swift +// GRDB +// +// Created by Gwendal Roué on 24/11/2018. +// Copyright © 2018 Gwendal Roué. All rights reserved. +// + +extension ValueObservation where Reducer == Void { + + // MARK: - Row Observation + + /// Creates a ValueObservation which observes *request*, and notifies + /// fresh rows whenever the request is modified by a database transaction. + /// + /// For example: + /// + /// let request = SQLRequest("SELECT * FROM player") + /// let observation = ValueObservation.trackingAll(request) + /// + /// let observer = try observation.start(in: dbQueue) { rows: [Row] in + /// print("Players have changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter request: the observed request. + /// - returns: a ValueObservation. + public static func trackingAll(_ request: Request) + -> ValueObservation> + where Request.RowDecoder == Row + { + return ValueObservation.tracking(request, fetchDistinct: request.fetchAll) + } + + /// Creates a ValueObservation which observes *request*, and notifies a + /// fresh row whenever the request is modified by a database transaction. + /// + /// For example: + /// + /// let request = SQLRequest("SELECT * FROM player WHERE id = ?", arguments: [1]) + /// let observation = ValueObservation.trackingOne(request) + /// + /// let observer = try observation.start(in: dbQueue) { row: Row? in + /// print("Players have changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter request: the observed request. + /// - returns: a ValueObservation. + public static func trackingOne(_ request: Request) + -> ValueObservation> + where Request.RowDecoder == Row + { + return ValueObservation.tracking(request, fetchDistinct: request.fetchOne) + } +} diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift new file mode 100644 index 0000000000..22da33b42b --- /dev/null +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -0,0 +1,453 @@ +// +// ValueObservation.swift +// GRDB +// +// Created by Gwendal Roué on 23/10/2018. +// Copyright © 2018 Gwendal Roué. All rights reserved. +// + +import Dispatch + +// MARK: - ValueScheduling + +/// ValueScheduling controls how ValueObservation schedules the notifications +/// of fresh values to your application. +public enum ValueScheduling { + /// All values are notified on the main queue. + /// + /// If the observation starts on the main queue, an initial value is + /// notified right upon subscription, synchronously: + /// + /// // On main queue + /// let observation = ValueObservation.trackingAll(Player.all()) + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("fresh players: \(players)") + /// } + /// // <- here "fresh players" is already printed. + /// + /// If the observation does not start on the main queue, an initial value + /// is also notified on the main queue, but asynchronously: + /// + /// // Not on the main queue: "fresh players" is eventually printed + /// // on the main queue. + /// let observation = ValueObservation.trackingAll(Player.all()) + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("fresh players: \(players)") + /// } + /// + /// When the database changes, fresh values are asynchronously notified on + /// the main queue: + /// + /// // Eventually prints "fresh players" on the main queue + /// try dbQueue.write { db in + /// try Player(...).insert(db) + /// } + case mainQueue + + /// All values are asychronously notified on the specified queue. + /// + /// An initial value is fetched and notified if `startImmediately` + /// is true. + case onQueue(DispatchQueue, startImmediately: Bool) + + /// Values are not all notified on the same dispatch queue. + /// + /// If `startImmediately` is true, an initial value is notified right upon + /// subscription, synchronously, on the dispatch queue which starts + /// the observation. + /// + /// // On any queue + /// var observation = ValueObservation.trackingAll(Player.all()) + /// observation.scheduling = .unsafe(startImmediately: true) + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("fresh players: \(players)") + /// } + /// // <- here "fresh players" is already printed. + /// + /// When the database changes, other values are notified on + /// unspecified queues. + case unsafe(startImmediately: Bool) +} + +// MARK: - ValueObservation + +/// ValueObservation tracks changes in the results of database requests, and +/// notifies fresh values whenever the database changes. +/// +/// For example: +/// +/// let observation = ValueObservation.trackingAll(Player.all) +/// let observer = try observation.start(in: dbQueue) { players: [Player] in +/// print("Players have changed.") +/// } +public struct ValueObservation { + /// A closure that is evaluated when the observation starts, and returns + /// the observed database region. + var observedRegion: (Database) throws -> DatabaseRegion + + /// The reducer is triggered upon each database change in *observedRegion*. + var reducer: Reducer + + /// Default is false. Set this property to true when the observation + /// requires write access in order to fetch fresh values. Fetches are then + /// wrapped inside a savepoint. + /// + /// Don't set this flag to true unless you really need it. A read/write + /// observation is less efficient than a read-only observation. + public var requiresWriteAccess: Bool = false + + /// The extent of the database observation. The default is + /// `.observerLifetime`: the observation lasts until the + /// observer returned by the `start(in:onError:onChange:)` method + /// is deallocated. + public var extent = Database.TransactionObservationExtent.observerLifetime + + /// `scheduling` controls how fresh values are notified. Default + /// is `.mainQueue`. + /// + /// - `.mainQueue`: all values are notified on the main queue. + /// + /// If the observation starts on the main queue, an initial value is + /// notified right upon subscription, synchronously:: + /// + /// // On main queue + /// let observation = ValueObservation.trackingAll(Player.all()) + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("fresh players: \(players)") + /// } + /// // <- here "fresh players" is already printed. + /// + /// If the observation does not start on the main queue, an initial + /// value is also notified on the main queue, but asynchronously: + /// + /// // Not on the main queue: "fresh players" is eventually printed + /// // on the main queue. + /// let observation = ValueObservation.trackingAll(Player.all()) + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("fresh players: \(players)") + /// } + /// + /// When the database changes, fresh values are asynchronously notified: + /// + /// // Eventually prints "fresh players" on the main queue + /// try dbQueue.write { db in + /// try Player(...).insert(db) + /// } + /// + /// - `.onQueue(_:startImmediately:)`: all values are asychronously notified + /// on the specified queue. + /// + /// An initial value is fetched and notified if `startImmediately` + /// is true. + /// + /// - `unsafe(startImmediately:)`: values are not all notified on the same + /// dispatch queue. + /// + /// If `startImmediately` is true, an initial value is notified right + /// upon subscription, synchronously, on the dispatch queue which starts + /// the observation. + /// + /// // On any queue + /// var observation = ValueObservation.trackingAll(Player.all()) + /// observation.scheduling = .unsafe(startImmediately: true) + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("fresh players: \(players)") + /// } + /// // <- here "fresh players" is already printed. + /// + /// When the database changes, other values are notified on + /// unspecified queues. + public var scheduling: ValueScheduling = .mainQueue + + /// The dispatch queue where change callbacks are called. + var notificationQueue: DispatchQueue? { + switch scheduling { + case .mainQueue: + return DispatchQueue.main + case .onQueue(let queue, startImmediately: _): + return queue + case .unsafe: + return nil + } + } + + // Not public. See ValueObservation.tracking(_:reducer:) + init( + tracking region: @escaping (Database) throws -> DatabaseRegion, + reducer: Reducer) + { + self.observedRegion = { db in + // Remove views from the observed region. + // + // We can do it because we are only interested in modifications in + // actual tables. And we want to do it because we have a fast path + // for simple regions that span a single table. + let views = try db.schema().names(ofType: .view) + return try region(db).ignoring(views) + } + self.reducer = reducer + } +} + +extension ValueObservation where Reducer: ValueReducer { + + // MARK: - Starting Observation + + /// Starts the value observation in the provided database reader (such as + /// a database queue or database pool), and returns a transaction observer. + /// + /// - parameter reader: A DatabaseReader. + /// - parameter onError: A closure that is provided eventual errors that + /// happen during observation + /// - parameter onChange: A closure that is provided fresh values + /// - returns: a TransactionObserver + public func start( + in reader: DatabaseReader, + onError: ((Error) -> Void)? = nil, + onChange: @escaping (Reducer.Value) -> Void) throws -> TransactionObserver + { + return try reader.add(observation: self, onError: onError, onChange: onChange) + } +} + +extension ValueObservation { + + // MARK: - Creating ValueObservation from ValueReducer + + /// Returs a ValueObservation which observes *regions*, and notifies the + /// values returned by the *reducer* whenever one of the observed + /// regions is modified by a database transaction. + /// + /// This method is the most fundamental way to create a ValueObservation. + /// + /// For example, this observation counts the number of a times the player + /// table is modified: + /// + /// var count = 0 + /// let reducer = AnyValueReducer( + /// fetch: { _ in }, + /// value: { _ -> Int? in + /// count += 1 + /// return count + /// }) + /// let observation = ValueObservation.tracking(Player.all(), reducer: reducer) + /// let observer = observation.start(in: dbQueue) { count: Int in + /// print("Players have been modified \(count) times.") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter regions: A list of observed regions. + /// - parameter reducer: A reducer that turns database changes in the + /// modified regions into fresh values. Currently only reducers that adopt + /// the ValueReducer protocol are supported. + public static func tracking( + _ regions: DatabaseRegionConvertible..., + reducer: Reducer) + -> ValueObservation + { + return ValueObservation.tracking(regions, reducer: reducer) + } + + /// Returs a ValueObservation which observes *regions*, and notifies the + /// values returned by the *reducer* whenever one of the observed + /// regions is modified by a database transaction. + /// + /// This method is the most fundamental way to create a ValueObservation. + /// + /// For example, this observation counts the number of a times the player + /// table is modified: + /// + /// var count = 0 + /// let reducer = AnyValueReducer( + /// fetch: { _ in }, + /// value: { _ -> Int? in + /// count += 1 + /// return count + /// }) + /// let observation = ValueObservation.tracking([Player.all()], reducer: reducer) + /// let observer = observation.start(in: dbQueue) { count: Int in + /// print("Players have been modified \(count) times.") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter regions: A list of observed regions. + /// - parameter reducer: A reducer that turns database changes in the + /// modified regions into fresh values. Currently only reducers that adopt + /// the ValueReducer protocol are supported. + public static func tracking( + _ regions: [DatabaseRegionConvertible], + reducer: Reducer) + -> ValueObservation + { + return ValueObservation(tracking: union(regions), reducer: reducer) + } +} + +extension ValueObservation where Reducer == Void { + + // MARK: - Creating ValueObservation from Fetch Closures + + /// Creates a ValueObservation which observes *regions*, and notifies the + /// values returned by the *fetch* closure whenever one of the observed + /// regions is modified by a database transaction. + /// + /// For example: + /// + /// let observation = ValueObservation.tracking( + /// Player.all(), + /// fetch: { db in return try Player.fetchAll(db) }) + /// + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("Players have changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter regions: A list of observed regions. + /// - parameter fetch: A closure that fetches a value. + public static func tracking( + _ regions: DatabaseRegionConvertible..., + fetch: @escaping (Database) throws -> Value) + -> ValueObservation> + { + return ValueObservation.tracking(regions, fetch: fetch) + } + + /// Creates a ValueObservation which observes *regions*, and notifies the + /// values returned by the *fetch* closure whenever one of the observed + /// regions is modified by a database transaction. + /// + /// For example: + /// + /// let observation = ValueObservation.tracking( + /// [Player.all()], + /// fetch: { db in return try Player.fetchAll(db) }) + /// + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("Players have changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter regions: A list of observed regions. + /// - parameter fetch: A closure that fetches a value. + public static func tracking( + _ regions: [DatabaseRegionConvertible], + fetch: @escaping (Database) throws -> Value) + -> ValueObservation> + { + return ValueObservation>( + tracking: union(regions), + reducer: ValueReducers.Raw(fetch)) + } + + /// Creates a ValueObservation which observes *regions*, and notifies the + /// values returned by the *fetch* closure whenever one of the observed + /// regions is modified by a database transaction. Consecutive equal values + /// are filtered out. + /// + /// For example: + /// + /// let observation = ValueObservation.tracking( + /// Player.all(), + /// fetchDistinct: { db in return try Player.fetchAll(db) }) + /// + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("Players have changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter regions: A list of observed regions. + /// - parameter fetch: A closure that fetches a value. + public static func tracking( + _ regions: DatabaseRegionConvertible..., + fetchDistinct fetch: @escaping (Database) throws -> Value) + -> ValueObservation> + where Value: Equatable + { + return ValueObservation.tracking(regions, fetchDistinct: fetch) + } + + /// Creates a ValueObservation which observes *regions*, and notifies the + /// values returned by the *fetch* closure whenever one of the observed + /// regions is modified by a database transaction. Consecutive equal values + /// are filtered out. + /// + /// For example: + /// + /// let observation = ValueObservation.tracking( + /// [Player.all()], + /// fetchDistinct: { db in return try Player.fetchAll(db) }) + /// + /// let observer = try observation.start(in: dbQueue) { players: [Player] in + /// print("Players have changed") + /// } + /// + /// The returned observation has the default configuration: + /// + /// - When started with the `start(in:onError:onChange:)` method, a fresh + /// value is immediately notified on the main queue. + /// - Upon subsequent database changes, fresh values are notified on the + /// main queue. + /// - The observation lasts until the observer returned by + /// `start` is deallocated. + /// + /// - parameter regions: A list of observed regions. + /// - parameter fetch: A closure that fetches a value. + public static func tracking( + _ regions: [DatabaseRegionConvertible], + fetchDistinct fetch: @escaping (Database) throws -> Value) + -> ValueObservation> + where Value: Equatable + { + return ValueObservation>( + tracking: union(regions), + reducer: ValueReducers.Distinct(fetch)) + } +} + +private func union(_ regions: [DatabaseRegionConvertible]) -> (Database) throws -> DatabaseRegion { + return { db in + try regions.reduce(into: DatabaseRegion()) { union, region in + try union.formUnion(region.databaseRegion(db)) + } + } +} diff --git a/GRDB/ValueObservation/ValueReducer.swift b/GRDB/ValueObservation/ValueReducer.swift new file mode 100644 index 0000000000..91bcd05a01 --- /dev/null +++ b/GRDB/ValueObservation/ValueReducer.swift @@ -0,0 +1,126 @@ +// +// ValueReducer.swift +// GRDB +// +// Created by Gwendal Roué on 24/11/2018. +// Copyright © 2018 Gwendal Roué. All rights reserved. +// + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// The ValueReducer protocol supports ValueObservation. +public protocol ValueReducer { + /// The type of fetched database values + associatedtype Fetched + + /// The type of observed values + associatedtype Value + + /// Feches database values upon changes in an observed database region. + func fetch(_ db: Database) throws -> Fetched + + /// Transforms a fetched value into an eventual observed value. Returns nil + /// when observer should not be notified. + /// + /// This method runs inside a private dispatch queue. + mutating func value(_ fetched: Fetched) -> Value? +} + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +/// +/// A type-erased ValueReducer. +/// +/// An AnyValueReducer forwards its operations to an underlying reducer, +/// hiding its specifics. +public struct AnyValueReducer: ValueReducer { + private var _fetch: (Database) throws -> Fetched + private var _value: (Fetched) -> Value? + + /// Creates a reducer whose `fetch(_:)` and `value(_:)` methods wrap and + /// forward operations the argument closures. + /// + /// For example, this reducer counts the number of a times the player table + /// is modified: + /// + /// var count = 0 + /// let reducer = AnyValueReducer( + /// fetch: { _ in }, + /// value: { _ -> Int? in + /// count += 1 + /// return count + /// }) + /// let observer = ValueObservation + /// .tracking(Player.all(), reducer: reducer) + /// .start(in: dbQueue) { count: Int in + /// print("Players have been modified \(count) times.") + /// } + public init(fetch: @escaping (Database) throws -> Fetched, value: @escaping (Fetched) -> Value?) { + self._fetch = fetch + self._value = value + } + + /// Creates a reducer that wraps and forwards operations to `reducer`. + public init(_ reducer: Base) where Base.Fetched == Fetched, Base.Value == Value { + var reducer = reducer + self._fetch = { try reducer.fetch($0) } + self._value = { reducer.value($0) } + } + + /// :nodoc: + public func fetch(_ db: Database) throws -> Fetched { + return try _fetch(db) + } + + /// :nodoc: + public func value(_ fetched: Fetched) -> Value? { + return _value(fetched) + } +} + +/// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) +public enum ValueReducers { + /// A reducer which outputs raw database values, without any processing. + public struct Raw: ValueReducer { + private let _fetch: (Database) throws -> Value + + init(_ fetch: @escaping (Database) throws -> Value) { + self._fetch = fetch + } + + /// :nodoc: + public func fetch(_ db: Database) throws -> Value { + return try _fetch(db) + } + + /// :nodoc: + public func value(_ fetched: Value) -> Value? { + return fetched + } + } + + /// A reducer which outputs raw database values, filtering out consecutive + /// values that are equal. + public struct Distinct: ValueReducer { + private let _fetch: (Database) throws -> Value + private var previousValue: Value?? + + init(_ fetch: @escaping (Database) throws -> Value) { + self._fetch = fetch + } + + /// :nodoc: + public func fetch(_ db: Database) throws -> Value { + return try _fetch(db) + } + + /// :nodoc: + public mutating func value(_ value: Value) -> Value? { + if let previousValue = previousValue, previousValue == value { + // Don't notify consecutive identical values + return nil + } + self.previousValue = value + return value + } + } +} diff --git a/GRDBCipher.xcodeproj/project.pbxproj b/GRDBCipher.xcodeproj/project.pbxproj index f6541248d7..d0d4296705 100755 --- a/GRDBCipher.xcodeproj/project.pbxproj +++ b/GRDBCipher.xcodeproj/project.pbxproj @@ -89,6 +89,26 @@ 560FC5A71CB00B880014AA8E /* GRDBTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */; }; 560FC5B21CB031E30014AA8E /* StatementColumnConvertibleFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */; }; 560FC5B31CB031EA0014AA8E /* DataMemoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EB0AB11BCD787300A3DC55 /* DataMemoryTests.swift */; }; + 5613ED7B21A95E8400DC7A68 /* ValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7321A95E8400DC7A68 /* ValueObservation.swift */; }; + 5613ED7C21A95E8400DC7A68 /* ValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7321A95E8400DC7A68 /* ValueObservation.swift */; }; + 5613ED7D21A95E8400DC7A68 /* ValueObservation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7421A95E8400DC7A68 /* ValueObservation+Combine.swift */; }; + 5613ED7E21A95E8400DC7A68 /* ValueObservation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7421A95E8400DC7A68 /* ValueObservation+Combine.swift */; }; + 5613ED7F21A95E8400DC7A68 /* ValueObservation+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7521A95E8400DC7A68 /* ValueObservation+Map.swift */; }; + 5613ED8021A95E8400DC7A68 /* ValueObservation+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7521A95E8400DC7A68 /* ValueObservation+Map.swift */; }; + 5613ED8121A95E8400DC7A68 /* ValueObservation+Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7621A95E8400DC7A68 /* ValueObservation+Row.swift */; }; + 5613ED8221A95E8400DC7A68 /* ValueObservation+Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7621A95E8400DC7A68 /* ValueObservation+Row.swift */; }; + 5613ED8321A95E8400DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7721A95E8400DC7A68 /* ValueObservation+FetchableRecord.swift */; }; + 5613ED8421A95E8400DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7721A95E8400DC7A68 /* ValueObservation+FetchableRecord.swift */; }; + 5613ED8521A95E8400DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7821A95E8400DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */; }; + 5613ED8621A95E8400DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7821A95E8400DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */; }; + 5613ED8721A95E8400DC7A68 /* ValueReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7921A95E8400DC7A68 /* ValueReducer.swift */; }; + 5613ED8821A95E8400DC7A68 /* ValueReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7921A95E8400DC7A68 /* ValueReducer.swift */; }; + 5613ED8921A95E8400DC7A68 /* ValueObservation+Count.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7A21A95E8400DC7A68 /* ValueObservation+Count.swift */; }; + 5613ED8A21A95E8400DC7A68 /* ValueObservation+Count.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED7A21A95E8400DC7A68 /* ValueObservation+Count.swift */; }; + 5613ED9E21A96A6F00DC7A68 /* ValueObservationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED9D21A96A6F00DC7A68 /* ValueObservationCombineTests.swift */; }; + 5613ED9F21A96A6F00DC7A68 /* ValueObservationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED9D21A96A6F00DC7A68 /* ValueObservationCombineTests.swift */; }; + 5613EDA021A96A6F00DC7A68 /* ValueObservationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED9D21A96A6F00DC7A68 /* ValueObservationCombineTests.swift */; }; + 5613EDA121A96A6F00DC7A68 /* ValueObservationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED9D21A96A6F00DC7A68 /* ValueObservationCombineTests.swift */; }; 561667021D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */; }; 561667031D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */; }; 561667061D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */; }; @@ -180,8 +200,6 @@ 5634B10C1CF9B970005360B9 /* TransactionObserverSavepointsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5634B1061CF9B970005360B9 /* TransactionObserverSavepointsTests.swift */; }; 5636E9BD1D22574100B9B05F /* FetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5636E9BB1D22574100B9B05F /* FetchRequest.swift */; }; 5636E9C01D22574100B9B05F /* FetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5636E9BB1D22574100B9B05F /* FetchRequest.swift */; }; - 563B06B6217F3C5A00B38F35 /* ValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06B4217F3C5900B38F35 /* ValueObservation.swift */; }; - 563B06B7217F3C5A00B38F35 /* ValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06B4217F3C5900B38F35 /* ValueObservation.swift */; }; 563B06E12185E07200B38F35 /* ValueObservationExtentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06DC2185E07100B38F35 /* ValueObservationExtentTests.swift */; }; 563B06E22185E07200B38F35 /* ValueObservationExtentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06DC2185E07100B38F35 /* ValueObservationExtentTests.swift */; }; 563B06E32185E07300B38F35 /* ValueObservationExtentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06DC2185E07100B38F35 /* ValueObservationExtentTests.swift */; }; @@ -998,6 +1016,15 @@ 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 = ""; }; 560FC5B01CB00B880014AA8E /* GRDBCipherOSXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBCipherOSXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5613ED7321A95E8400DC7A68 /* ValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservation.swift; sourceTree = ""; }; + 5613ED7421A95E8400DC7A68 /* ValueObservation+Combine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Combine.swift"; sourceTree = ""; }; + 5613ED7521A95E8400DC7A68 /* ValueObservation+Map.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Map.swift"; sourceTree = ""; }; + 5613ED7621A95E8400DC7A68 /* ValueObservation+Row.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Row.swift"; sourceTree = ""; }; + 5613ED7721A95E8400DC7A68 /* ValueObservation+FetchableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+FetchableRecord.swift"; sourceTree = ""; }; + 5613ED7821A95E8400DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+DatabaseValueConvertible.swift"; sourceTree = ""; }; + 5613ED7921A95E8400DC7A68 /* ValueReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueReducer.swift; sourceTree = ""; }; + 5613ED7A21A95E8400DC7A68 /* ValueObservation+Count.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Count.swift"; sourceTree = ""; }; + 5613ED9D21A96A6F00DC7A68 /* ValueObservationCombineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationCombineTests.swift; sourceTree = ""; }; 561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSDecimalNumberTests.swift; sourceTree = ""; }; 5616AAF8207CD5A900AC3664 /* RequestProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestProtocols.swift; sourceTree = ""; }; 562393171DECC02000A6B01F /* RowFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFetchTests.swift; sourceTree = ""; }; @@ -1026,7 +1053,6 @@ 563363D41C94484E000BE133 /* DatabaseQueueReleaseMemoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueReleaseMemoryTests.swift; sourceTree = ""; }; 5634B1061CF9B970005360B9 /* TransactionObserverSavepointsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionObserverSavepointsTests.swift; sourceTree = ""; }; 5636E9BB1D22574100B9B05F /* FetchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequest.swift; sourceTree = ""; }; - 563B06B4217F3C5900B38F35 /* ValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservation.swift; sourceTree = ""; }; 563B06DC2185E07100B38F35 /* ValueObservationExtentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationExtentTests.swift; sourceTree = ""; }; 563B06DD2185E07200B38F35 /* ValueObservationReadonlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationReadonlyTests.swift; sourceTree = ""; }; 563B06DE2185E07200B38F35 /* ValueObservationFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationFetchTests.swift; sourceTree = ""; }; @@ -1434,6 +1460,21 @@ name = Products; sourceTree = ""; }; + 5613ED7221A95E8400DC7A68 /* ValueObservation */ = { + isa = PBXGroup; + children = ( + 5613ED7321A95E8400DC7A68 /* ValueObservation.swift */, + 5613ED7421A95E8400DC7A68 /* ValueObservation+Combine.swift */, + 5613ED7521A95E8400DC7A68 /* ValueObservation+Map.swift */, + 5613ED7621A95E8400DC7A68 /* ValueObservation+Row.swift */, + 5613ED7721A95E8400DC7A68 /* ValueObservation+FetchableRecord.swift */, + 5613ED7821A95E8400DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */, + 5613ED7921A95E8400DC7A68 /* ValueReducer.swift */, + 5613ED7A21A95E8400DC7A68 /* ValueObservation+Count.swift */, + ); + path = ValueObservation; + sourceTree = ""; + }; 56176C581EACC2D8000F3F2B /* GRDBTests */ = { isa = PBXGroup; children = ( @@ -1447,6 +1488,7 @@ 56300B5C1C53C38F005A543B /* QueryInterface */, 56A238251B9C74A90082EB20 /* Record */, 56176C9F1EACEE15000F3F2B /* Support */, + 563B06F52185E07C00B38F35 /* ValueObservation */, ); path = GRDBTests; sourceTree = ""; @@ -1570,6 +1612,7 @@ 563B06F52185E07C00B38F35 /* ValueObservation */ = { isa = PBXGroup; children = ( + 5613ED9D21A96A6F00DC7A68 /* ValueObservationCombineTests.swift */, 563B06F921861D8B00B38F35 /* ValueObservationCountTests.swift */, 563B071D21862F5D00B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift */, 563B06DC2185E07100B38F35 /* ValueObservationExtentTests.swift */, @@ -1739,7 +1782,6 @@ 56A238201B9C74A90082EB20 /* Statement */, 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */, 5607EFD11BB8253300605DE3 /* TransactionObserver */, - 563B06F52185E07C00B38F35 /* ValueObservation */, ); name = Core; sourceTree = ""; @@ -1833,7 +1875,6 @@ 560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */, 5605F1471C672E4000235C62 /* Support */, 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */, - 563B06B4217F3C5900B38F35 /* ValueObservation.swift */, ); path = Core; sourceTree = ""; @@ -1971,6 +2012,7 @@ 56A2389F1B9C753B0082EB20 /* Record */, DC37743319C8CFCE004FCF85 /* Supporting Files */, 5659F4861EA8D94E004A4992 /* Utils */, + 5613ED7221A95E8400DC7A68 /* ValueObservation */, ); path = GRDB; sourceTree = ""; @@ -2256,8 +2298,10 @@ 560FC51C1CB003810014AA8E /* QueryInterfaceRequest.swift in Sources */, 560FC51D1CB003810014AA8E /* SQLCollatedExpression.swift in Sources */, 5653EB9920961FC000F46237 /* ForeignKeyRequest.swift in Sources */, + 5613ED8121A95E8400DC7A68 /* ValueObservation+Row.swift in Sources */, 566B91341FA4D3810012D5B0 /* TransactionObserver.swift in Sources */, 56D1215B1ED34978001347D2 /* Fixits-0.109.0.swift in Sources */, + 5613ED8921A95E8400DC7A68 /* ValueObservation+Count.swift in Sources */, 566475D41D981D5E00FF74B8 /* SQLOperators.swift in Sources */, 5653EB9120961FC000F46237 /* HasOneAssociation.swift in Sources */, 56CEB5621EAA359A00BFAF62 /* SQLSelectable.swift in Sources */, @@ -2299,6 +2343,7 @@ 5656BF5E20C7248700F98521 /* QueryOrdering.swift in Sources */, 56D51D011EA789FA0074638A /* FetchableRecord+TableRecord.swift in Sources */, 566475BB1D981AD200FF74B8 /* SQLSpecificExpressible+QueryInterface.swift in Sources */, + 5613ED8721A95E8400DC7A68 /* ValueReducer.swift in Sources */, 560FC52B1CB003810014AA8E /* DatabaseValue.swift in Sources */, 5671FC211DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */, 560FC52E1CB003810014AA8E /* Record.swift in Sources */, @@ -2307,10 +2352,10 @@ 560FC5311CB003810014AA8E /* TableRecord.swift in Sources */, 56DAA2DC1DE9C827006E10C8 /* Cursor.swift in Sources */, 5674A6F11F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */, + 5613ED8321A95E8400DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */, 5644DE7C20F8C903001FFDDE /* DatabaseValueConversion.swift in Sources */, 560FC5321CB003810014AA8E /* DatabasePool.swift in Sources */, 560FC5331CB003810014AA8E /* Migration.swift in Sources */, - 563B06B6217F3C5A00B38F35 /* ValueObservation.swift in Sources */, 560FC5341CB003810014AA8E /* QueryInterfaceQuery.swift in Sources */, 566B912C1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, 560FC5351CB003810014AA8E /* Statement.swift in Sources */, @@ -2330,15 +2375,19 @@ 5698AC791DA37DCB0056AF8C /* VirtualTableModule.swift in Sources */, 560FC53B1CB003810014AA8E /* Database.swift in Sources */, 566AD8B31D5318F4002EC1A8 /* TableDefinition.swift in Sources */, + 5613ED7F21A95E8400DC7A68 /* ValueObservation+Map.swift in Sources */, 5698AD221DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, 560FC53C1CB003810014AA8E /* DatabaseQueue.swift in Sources */, 5653EB9720961FC000F46237 /* ForeignKey.swift in Sources */, 569A98FC2039B72D008D7DBF /* Fixits-3.0.swift in Sources */, + 5613ED8521A95E8400DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */, 5653EB9520961FC000F46237 /* HasManyAssociation.swift in Sources */, 560FC53D1CB003810014AA8E /* NSNumber.swift in Sources */, + 5613ED7B21A95E8400DC7A68 /* ValueObservation.swift in Sources */, 567071F8208A00D4006AD95A /* SQLiteDateParser.swift in Sources */, 560FC53F1CB003810014AA8E /* Row.swift in Sources */, 560FC5401CB003810014AA8E /* StandardLibrary.swift in Sources */, + 5613ED7D21A95E8400DC7A68 /* ValueObservation+Combine.swift in Sources */, 5653EC1A209873E600F46237 /* SQLGenerationContext.swift in Sources */, 560FC5411CB003810014AA8E /* PersistableRecord.swift in Sources */, 566475A31D9810A400FF74B8 /* SQLSelectable+QueryInterface.swift in Sources */, @@ -2447,6 +2496,7 @@ 5653EBB820961FE800F46237 /* AssociationParallelSQLTests.swift in Sources */, 560FC5841CB00B880014AA8E /* PersistableRecordTests.swift in Sources */, 560FC5871CB00B880014AA8E /* RecordSubClassTests.swift in Sources */, + 5613ED9E21A96A6F00DC7A68 /* ValueObservationCombineTests.swift in Sources */, 5653EBBC20961FE800F46237 /* AssociationBelongsToFetchableRecordTests.swift in Sources */, 560FC5891CB00B880014AA8E /* TransactionObserverTests.swift in Sources */, 5674A72C1F30A9090095F066 /* FetchableRecordDecodableTests.swift in Sources */, @@ -2623,6 +2673,7 @@ 5653EBB920961FE800F46237 /* AssociationParallelSQLTests.swift in Sources */, 567156461CB16729007DC145 /* TransactionObserverTests.swift in Sources */, 567A80551D41350C00C7DCEC /* IndexInfoTests.swift in Sources */, + 5613ED9F21A96A6F00DC7A68 /* ValueObservationCombineTests.swift in Sources */, 5653EBBD20961FE800F46237 /* AssociationBelongsToFetchableRecordTests.swift in Sources */, 567156481CB16729007DC145 /* DatabaseValueTests.swift in Sources */, 5674A72E1F30A9090095F066 /* FetchableRecordDecodableTests.swift in Sources */, @@ -2724,8 +2775,10 @@ 56AFC9F11CB1A8BB00F48B96 /* QueryInterfaceRequest.swift in Sources */, 56AFC9F21CB1A8BB00F48B96 /* SQLCollatedExpression.swift in Sources */, 5653EB9A20961FC000F46237 /* ForeignKeyRequest.swift in Sources */, + 5613ED8221A95E8400DC7A68 /* ValueObservation+Row.swift in Sources */, 566B91371FA4D3810012D5B0 /* TransactionObserver.swift in Sources */, 56D1215E1ED34978001347D2 /* Fixits-0.109.0.swift in Sources */, + 5613ED8A21A95E8400DC7A68 /* ValueObservation+Count.swift in Sources */, 566475D71D981D5E00FF74B8 /* SQLOperators.swift in Sources */, 5653EB9220961FC000F46237 /* HasOneAssociation.swift in Sources */, 56CEB5651EAA359A00BFAF62 /* SQLSelectable.swift in Sources */, @@ -2767,6 +2820,7 @@ 5656BF5F20C7248700F98521 /* QueryOrdering.swift in Sources */, 56D51D041EA789FA0074638A /* FetchableRecord+TableRecord.swift in Sources */, 566475BE1D981AD200FF74B8 /* SQLSpecificExpressible+QueryInterface.swift in Sources */, + 5613ED8821A95E8400DC7A68 /* ValueReducer.swift in Sources */, 56AFCA001CB1A8BB00F48B96 /* Record.swift in Sources */, 5671FC241DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */, 56AFCA031CB1A8BB00F48B96 /* TableRecord.swift in Sources */, @@ -2775,10 +2829,10 @@ 56AFCA061CB1A8BB00F48B96 /* Configuration.swift in Sources */, 56DAA2DF1DE9C827006E10C8 /* Cursor.swift in Sources */, 5674A6EE1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */, + 5613ED8421A95E8400DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */, 5644DE7D20F8C903001FFDDE /* DatabaseValueConversion.swift in Sources */, 56AFCA071CB1A8BB00F48B96 /* DatabasePool.swift in Sources */, 56AFCA081CB1A8BB00F48B96 /* Statement.swift in Sources */, - 563B06B7217F3C5A00B38F35 /* ValueObservation.swift in Sources */, 56AFCA091CB1A8BB00F48B96 /* QueryInterfaceQuery.swift in Sources */, 566B912F1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, 56AFCA0A1CB1A8BB00F48B96 /* Migration.swift in Sources */, @@ -2798,15 +2852,19 @@ 5698AC7C1DA37DCB0056AF8C /* VirtualTableModule.swift in Sources */, 56AFCA111CB1A8BB00F48B96 /* DatabaseError.swift in Sources */, 566AD8B61D5318F4002EC1A8 /* TableDefinition.swift in Sources */, + 5613ED8021A95E8400DC7A68 /* ValueObservation+Map.swift in Sources */, 5698AD251DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, 56AFCA121CB1A8BB00F48B96 /* NSNumber.swift in Sources */, 5653EB9820961FC000F46237 /* ForeignKey.swift in Sources */, 569A98FD2039B72D008D7DBF /* Fixits-3.0.swift in Sources */, + 5613ED8621A95E8400DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */, 5653EB9620961FC000F46237 /* HasManyAssociation.swift in Sources */, 56AFCA141CB1A8BB00F48B96 /* StandardLibrary.swift in Sources */, + 5613ED7C21A95E8400DC7A68 /* ValueObservation.swift in Sources */, 56AFCA151CB1A8BB00F48B96 /* PersistableRecord.swift in Sources */, 567071F9208A00D4006AD95A /* SQLiteDateParser.swift in Sources */, 56AFCA161CB1A8BB00F48B96 /* StatementColumnConvertible.swift in Sources */, + 5613ED7E21A95E8400DC7A68 /* ValueObservation+Combine.swift in Sources */, 5653EC1B209873E600F46237 /* SQLGenerationContext.swift in Sources */, 566475A61D9810A400FF74B8 /* SQLSelectable+QueryInterface.swift in Sources */, 5657AABD1D107001006283EF /* NSData.swift in Sources */, @@ -2915,6 +2973,7 @@ 5653EBBA20961FE800F46237 /* AssociationParallelSQLTests.swift in Sources */, 56176C761EACCCCA000F3F2B /* FTS5WrapperTokenizerTests.swift in Sources */, 5623935C1DEE013C00A6B01F /* FilterCursorTests.swift in Sources */, + 5613EDA021A96A6F00DC7A68 /* ValueObservationCombineTests.swift in Sources */, 5653EBBE20961FE800F46237 /* AssociationBelongsToFetchableRecordTests.swift in Sources */, 5698AC851DA380A20056AF8C /* VirtualTableModuleTests.swift in Sources */, 5698AC451DA2BED90056AF8C /* FTS3PatternTests.swift in Sources */, @@ -3091,6 +3150,7 @@ 5653EBBB20961FE800F46237 /* AssociationParallelSQLTests.swift in Sources */, 56176C7C1EACCCCB000F3F2B /* FTS5WrapperTokenizerTests.swift in Sources */, 5698AC861DA380A20056AF8C /* VirtualTableModuleTests.swift in Sources */, + 5613EDA121A96A6F00DC7A68 /* ValueObservationCombineTests.swift in Sources */, 5653EBBF20961FE800F46237 /* AssociationBelongsToFetchableRecordTests.swift in Sources */, 5698AC461DA2BED90056AF8C /* FTS3PatternTests.swift in Sources */, 56AF74711D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift in Sources */, diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 7b4462c5f3..c2bdf9df65 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -8,6 +8,24 @@ /* Begin PBXBuildFile section */ 56071A501DB54ED300CA6E47 /* FetchedRecordsControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B15D0A1CD4C35100A24C8B /* FetchedRecordsControllerTests.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 */; }; + 5613ED6421A95E6100DC7A68 /* ValueObservation+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5A21A95E6100DC7A68 /* ValueObservation+Combine.swift */; }; + 5613ED6521A95E6100DC7A68 /* ValueObservation+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5B21A95E6100DC7A68 /* ValueObservation+Map.swift */; }; + 5613ED6621A95E6100DC7A68 /* ValueObservation+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5B21A95E6100DC7A68 /* ValueObservation+Map.swift */; }; + 5613ED6721A95E6100DC7A68 /* ValueObservation+Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5C21A95E6100DC7A68 /* ValueObservation+Row.swift */; }; + 5613ED6821A95E6100DC7A68 /* ValueObservation+Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5C21A95E6100DC7A68 /* ValueObservation+Row.swift */; }; + 5613ED6921A95E6100DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5D21A95E6100DC7A68 /* ValueObservation+FetchableRecord.swift */; }; + 5613ED6A21A95E6100DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5D21A95E6100DC7A68 /* ValueObservation+FetchableRecord.swift */; }; + 5613ED6B21A95E6100DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5E21A95E6100DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */; }; + 5613ED6C21A95E6100DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5E21A95E6100DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */; }; + 5613ED6D21A95E6100DC7A68 /* ValueReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5F21A95E6100DC7A68 /* ValueReducer.swift */; }; + 5613ED6E21A95E6100DC7A68 /* ValueReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED5F21A95E6100DC7A68 /* ValueReducer.swift */; }; + 5613ED6F21A95E6100DC7A68 /* ValueObservation+Count.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED6021A95E6100DC7A68 /* ValueObservation+Count.swift */; }; + 5613ED7021A95E6100DC7A68 /* ValueObservation+Count.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613ED6021A95E6100DC7A68 /* ValueObservation+Count.swift */; }; + 5613EDA321A96A8100DC7A68 /* ValueObservationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613EDA221A96A8000DC7A68 /* ValueObservationCombineTests.swift */; }; + 5613EDA421A96A8100DC7A68 /* ValueObservationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5613EDA221A96A8000DC7A68 /* ValueObservationCombineTests.swift */; }; 561667041D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */; }; 561667081D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */; }; 5616AAF6207CD59400AC3664 /* RequestProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5616AAF4207CD59300AC3664 /* RequestProtocols.swift */; }; @@ -43,8 +61,6 @@ 562EA8361F17B9EB00FA528C /* CompilationSubClassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 562EA82E1F17B9EB00FA528C /* CompilationSubClassTests.swift */; }; 5636E9BE1D22574100B9B05F /* FetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5636E9BB1D22574100B9B05F /* FetchRequest.swift */; }; 5636E9C11D22574100B9B05F /* FetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5636E9BB1D22574100B9B05F /* FetchRequest.swift */; }; - 563B06BA217F3C6E00B38F35 /* ValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06B8217F3C6E00B38F35 /* ValueObservation.swift */; }; - 563B06BB217F3C6E00B38F35 /* ValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06B8217F3C6E00B38F35 /* ValueObservation.swift */; }; 563B06D12185E04600B38F35 /* ValueObservationExtentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06CC2185E04500B38F35 /* ValueObservationExtentTests.swift */; }; 563B06D22185E04600B38F35 /* ValueObservationExtentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06CC2185E04500B38F35 /* ValueObservationExtentTests.swift */; }; 563B06D32185E04600B38F35 /* ValueObservationReadonlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06CD2185E04600B38F35 /* ValueObservationReadonlyTests.swift */; }; @@ -641,6 +657,15 @@ 560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertible.swift; sourceTree = ""; }; 560D92441C672C4B00F4F92B /* PersistableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistableRecord.swift; sourceTree = ""; }; 560D92461C672C4B00F4F92B /* TableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRecord.swift; sourceTree = ""; }; + 5613ED5921A95E6100DC7A68 /* ValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservation.swift; sourceTree = ""; }; + 5613ED5A21A95E6100DC7A68 /* ValueObservation+Combine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Combine.swift"; sourceTree = ""; }; + 5613ED5B21A95E6100DC7A68 /* ValueObservation+Map.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Map.swift"; sourceTree = ""; }; + 5613ED5C21A95E6100DC7A68 /* ValueObservation+Row.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Row.swift"; sourceTree = ""; }; + 5613ED5D21A95E6100DC7A68 /* ValueObservation+FetchableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+FetchableRecord.swift"; sourceTree = ""; }; + 5613ED5E21A95E6100DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+DatabaseValueConvertible.swift"; sourceTree = ""; }; + 5613ED5F21A95E6100DC7A68 /* ValueReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueReducer.swift; sourceTree = ""; }; + 5613ED6021A95E6100DC7A68 /* ValueObservation+Count.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ValueObservation+Count.swift"; sourceTree = ""; }; + 5613EDA221A96A8000DC7A68 /* ValueObservationCombineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationCombineTests.swift; sourceTree = ""; }; 561667001D08A49900ADD404 /* FoundationNSDecimalNumberTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSDecimalNumberTests.swift; sourceTree = ""; }; 5616AAF4207CD59300AC3664 /* RequestProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestProtocols.swift; sourceTree = ""; }; 562393171DECC02000A6B01F /* RowFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowFetchTests.swift; sourceTree = ""; }; @@ -669,7 +694,6 @@ 563363D41C94484E000BE133 /* DatabaseQueueReleaseMemoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueReleaseMemoryTests.swift; sourceTree = ""; }; 5634B1061CF9B970005360B9 /* TransactionObserverSavepointsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionObserverSavepointsTests.swift; sourceTree = ""; }; 5636E9BB1D22574100B9B05F /* FetchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequest.swift; sourceTree = ""; }; - 563B06B8217F3C6E00B38F35 /* ValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservation.swift; sourceTree = ""; }; 563B06CC2185E04500B38F35 /* ValueObservationExtentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationExtentTests.swift; sourceTree = ""; }; 563B06CD2185E04600B38F35 /* ValueObservationReadonlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationReadonlyTests.swift; sourceTree = ""; }; 563B06CE2185E04600B38F35 /* ValueObservationFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationFetchTests.swift; sourceTree = ""; }; @@ -1043,6 +1067,21 @@ name = PersistableRecord; sourceTree = ""; }; + 5613ED5821A95E6100DC7A68 /* ValueObservation */ = { + isa = PBXGroup; + children = ( + 5613ED5921A95E6100DC7A68 /* ValueObservation.swift */, + 5613ED5A21A95E6100DC7A68 /* ValueObservation+Combine.swift */, + 5613ED5B21A95E6100DC7A68 /* ValueObservation+Map.swift */, + 5613ED5C21A95E6100DC7A68 /* ValueObservation+Row.swift */, + 5613ED5D21A95E6100DC7A68 /* ValueObservation+FetchableRecord.swift */, + 5613ED5E21A95E6100DC7A68 /* ValueObservation+DatabaseValueConvertible.swift */, + 5613ED5F21A95E6100DC7A68 /* ValueReducer.swift */, + 5613ED6021A95E6100DC7A68 /* ValueObservation+Count.swift */, + ); + path = ValueObservation; + sourceTree = ""; + }; 5614DEEE1BA9D6F9003163B3 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1063,6 +1102,7 @@ 56300B5C1C53C38F005A543B /* QueryInterface */, 56A238251B9C74A90082EB20 /* Record */, 56176C9F1EACEE15000F3F2B /* Support */, + 563B06DB2185E04E00B38F35 /* ValueObservation */, ); path = GRDBTests; sourceTree = ""; @@ -1186,6 +1226,7 @@ 563B06DB2185E04E00B38F35 /* ValueObservation */ = { isa = PBXGroup; children = ( + 5613EDA221A96A8000DC7A68 /* ValueObservationCombineTests.swift */, 563B06FE21861D9D00B38F35 /* ValueObservationCountTests.swift */, 563B071A21862F5600B38F35 /* ValueObservationDatabaseValueConvertibleTests.swift */, 563B06CC2185E04500B38F35 /* ValueObservationExtentTests.swift */, @@ -1355,7 +1396,6 @@ 56A238201B9C74A90082EB20 /* Statement */, 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */, 5607EFD11BB8253300605DE3 /* TransactionObserver */, - 563B06DB2185E04E00B38F35 /* ValueObservation */, ); name = Core; sourceTree = ""; @@ -1449,7 +1489,6 @@ 560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */, 5605F1471C672E4000235C62 /* Support */, 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */, - 563B06B8217F3C6E00B38F35 /* ValueObservation.swift */, ); path = Core; sourceTree = ""; @@ -1586,6 +1625,7 @@ 56A2389F1B9C753B0082EB20 /* Record */, DC37743319C8CFCE004FCF85 /* Supporting Files */, 5659F4861EA8D94E004A4992 /* Utils */, + 5613ED5821A95E6100DC7A68 /* ValueObservation */, ); path = GRDB; sourceTree = ""; @@ -1901,8 +1941,10 @@ 56BB6EAE1D3009B100A1CA52 /* SchedulingWatchdog.swift in Sources */, F3BA801A1CFB2876003DC1BA /* StatementColumnConvertible.swift in Sources */, 5653EB4C20961F6100F46237 /* ForeignKeyRequest.swift in Sources */, + 5613ED6821A95E6100DC7A68 /* ValueObservation+Row.swift in Sources */, 566B91381FA4D3810012D5B0 /* TransactionObserver.swift in Sources */, 56D1215F1ED34978001347D2 /* Fixits-0.109.0.swift in Sources */, + 5613ED7021A95E6100DC7A68 /* ValueObservation+Count.swift in Sources */, 566475D81D981D5E00FF74B8 /* SQLOperators.swift in Sources */, 5653EB4420961F6100F46237 /* HasOneAssociation.swift in Sources */, 56CEB5661EAA359A00BFAF62 /* SQLSelectable.swift in Sources */, @@ -1944,6 +1986,7 @@ 5656BF5B20C7247100F98521 /* QueryOrdering.swift in Sources */, 56D51D051EA789FA0074638A /* FetchableRecord+TableRecord.swift in Sources */, F3BA80151CFB2876003DC1BA /* DatabaseWriter.swift in Sources */, + 5613ED6E21A95E6100DC7A68 /* ValueReducer.swift in Sources */, F3BA800A1CFB286A003DC1BA /* Configuration.swift in Sources */, 566475BF1D981AD200FF74B8 /* SQLSpecificExpressible+QueryInterface.swift in Sources */, 5671FC251DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */, @@ -1952,10 +1995,10 @@ F3BA801D1CFB288C003DC1BA /* DatabaseDateComponents.swift in Sources */, 5674A6ED1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */, 5657AB141D10899D006283EF /* URL.swift in Sources */, + 5613ED6A21A95E6100DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */, 5644DE7920F8C8EA001FFDDE /* DatabaseValueConversion.swift in Sources */, 56DAA2E01DE9C827006E10C8 /* Cursor.swift in Sources */, F3BA800C1CFB286F003DC1BA /* DatabaseError.swift in Sources */, - 563B06BB217F3C6E00B38F35 /* ValueObservation.swift in Sources */, 566B91301FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, F3BA80221CFB288C003DC1BA /* NSNumber.swift in Sources */, 5653EB4620961F6100F46237 /* AssociationQuery.swift in Sources */, @@ -1975,15 +2018,19 @@ F3BA80141CFB2876003DC1BA /* DatabaseValueConvertible.swift in Sources */, 5698AC7D1DA37DCB0056AF8C /* VirtualTableModule.swift in Sources */, 566AD8B71D5318F4002EC1A8 /* TableDefinition.swift in Sources */, + 5613ED6621A95E6100DC7A68 /* ValueObservation+Map.swift in Sources */, 5698AD261DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, F3BA802C1CFB289B003DC1BA /* SQLCollatedExpression.swift in Sources */, 5653EB4A20961F6100F46237 /* ForeignKey.swift in Sources */, 569A98F92039B716008D7DBF /* Fixits-3.0.swift in Sources */, + 5613ED6C21A95E6100DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */, 5653EB4820961F6100F46237 /* HasManyAssociation.swift in Sources */, F3BA80301CFB289F003DC1BA /* DatabaseMigrator.swift in Sources */, + 5613ED6221A95E6100DC7A68 /* ValueObservation.swift in Sources */, F3BA80361CFB28A4003DC1BA /* TableRecord.swift in Sources */, 567071F5208A00BE006AD95A /* SQLiteDateParser.swift in Sources */, F3BA80271CFB2891003DC1BA /* DatabaseValueConvertible+RawRepresentable.swift in Sources */, + 5613ED6421A95E6100DC7A68 /* ValueObservation+Combine.swift in Sources */, 5653EC092098737400F46237 /* SQLGenerationContext.swift in Sources */, 566475A71D9810A400FF74B8 /* SQLSelectable+QueryInterface.swift in Sources */, 5657AABE1D107001006283EF /* NSData.swift in Sources */, @@ -2092,6 +2139,7 @@ 5653EB6920961FB200F46237 /* AssociationParallelSQLTests.swift in Sources */, F3BA80B71CFB2FCD003DC1BA /* DatabaseErrorTests.swift in Sources */, 5698ACCC1DA62A2D0056AF8C /* FTS5TokenizerTests.swift in Sources */, + 5613EDA421A96A8100DC7A68 /* ValueObservationCombineTests.swift in Sources */, 5653EB6B20961FB200F46237 /* AssociationBelongsToFetchableRecordTests.swift in Sources */, 5690C33E1D23E7D200E59934 /* FoundationDateTests.swift in Sources */, 567A805A1D41350C00C7DCEC /* IndexInfoTests.swift in Sources */, @@ -2193,8 +2241,10 @@ 56BB6EAB1D3009B100A1CA52 /* SchedulingWatchdog.swift in Sources */, F3BA80761CFB2E55003DC1BA /* StatementColumnConvertible.swift in Sources */, 5653EB4B20961F6100F46237 /* ForeignKeyRequest.swift in Sources */, + 5613ED6721A95E6100DC7A68 /* ValueObservation+Row.swift in Sources */, 566B91351FA4D3810012D5B0 /* TransactionObserver.swift in Sources */, 56D1215C1ED34978001347D2 /* Fixits-0.109.0.swift in Sources */, + 5613ED6F21A95E6100DC7A68 /* ValueObservation+Count.swift in Sources */, 566475D51D981D5E00FF74B8 /* SQLOperators.swift in Sources */, 5653EB4320961F6100F46237 /* HasOneAssociation.swift in Sources */, 56CEB5631EAA359A00BFAF62 /* SQLSelectable.swift in Sources */, @@ -2236,6 +2286,7 @@ 5656BF5A20C7247100F98521 /* QueryOrdering.swift in Sources */, 56D51D021EA789FA0074638A /* FetchableRecord+TableRecord.swift in Sources */, F3BA80711CFB2E55003DC1BA /* DatabaseWriter.swift in Sources */, + 5613ED6D21A95E6100DC7A68 /* ValueReducer.swift in Sources */, F3BA80661CFB2E55003DC1BA /* Configuration.swift in Sources */, 566475BC1D981AD200FF74B8 /* SQLSpecificExpressible+QueryInterface.swift in Sources */, 5671FC221DA3CAC9003BF4FF /* FTS3TokenizerDescriptor.swift in Sources */, @@ -2244,10 +2295,10 @@ F3BA80791CFB2E61003DC1BA /* DatabaseDateComponents.swift in Sources */, 5674A6EC1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */, 5657AB111D10899D006283EF /* URL.swift in Sources */, + 5613ED6921A95E6100DC7A68 /* ValueObservation+FetchableRecord.swift in Sources */, 5644DE7820F8C8EA001FFDDE /* DatabaseValueConversion.swift in Sources */, 56DAA2DD1DE9C827006E10C8 /* Cursor.swift in Sources */, F3BA80681CFB2E55003DC1BA /* DatabaseError.swift in Sources */, - 563B06BA217F3C6E00B38F35 /* ValueObservation.swift in Sources */, 566B912D1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, F3BA807E1CFB2E61003DC1BA /* NSNumber.swift in Sources */, 5653EB4520961F6100F46237 /* AssociationQuery.swift in Sources */, @@ -2267,15 +2318,19 @@ F3BA80701CFB2E55003DC1BA /* DatabaseValueConvertible.swift in Sources */, 5698AC7A1DA37DCB0056AF8C /* VirtualTableModule.swift in Sources */, 566AD8B41D5318F4002EC1A8 /* TableDefinition.swift in Sources */, + 5613ED6521A95E6100DC7A68 /* ValueObservation+Map.swift in Sources */, 5698AD231DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, F3BA80881CFB2E70003DC1BA /* SQLCollatedExpression.swift in Sources */, 5653EB4920961F6100F46237 /* ForeignKey.swift in Sources */, 569A98F82039B716008D7DBF /* Fixits-3.0.swift in Sources */, + 5613ED6B21A95E6100DC7A68 /* ValueObservation+DatabaseValueConvertible.swift in Sources */, 5653EB4720961F6100F46237 /* HasManyAssociation.swift in Sources */, F3BA808C1CFB2E75003DC1BA /* DatabaseMigrator.swift in Sources */, + 5613ED6121A95E6100DC7A68 /* ValueObservation.swift in Sources */, F3BA80921CFB2E7A003DC1BA /* TableRecord.swift in Sources */, 567071F4208A00BE006AD95A /* SQLiteDateParser.swift in Sources */, F3BA80831CFB2E67003DC1BA /* DatabaseValueConvertible+RawRepresentable.swift in Sources */, + 5613ED6321A95E6100DC7A68 /* ValueObservation+Combine.swift in Sources */, 5653EC082098737400F46237 /* SQLGenerationContext.swift in Sources */, 566475A41D9810A400FF74B8 /* SQLSelectable+QueryInterface.swift in Sources */, 5657AABB1D107001006283EF /* NSData.swift in Sources */, @@ -2384,6 +2439,7 @@ 5653EB6820961FB200F46237 /* AssociationParallelSQLTests.swift in Sources */, F3BA80F21CFB301A003DC1BA /* DatabaseSavepointTests.swift in Sources */, F3BA80B81CFB2FCD003DC1BA /* DatabaseErrorTests.swift in Sources */, + 5613EDA321A96A8100DC7A68 /* ValueObservationCombineTests.swift in Sources */, 5653EB6A20961FB200F46237 /* AssociationBelongsToFetchableRecordTests.swift in Sources */, 5698AC061D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift in Sources */, F3BA80FA1CFB3021003DC1BA /* SelectStatementTests.swift in Sources */, diff --git a/README.md b/README.md index 3279733bc0..12d17bdb7a 100644 --- a/README.md +++ b/README.md @@ -6301,6 +6301,7 @@ The observer returned by the `start` method is stored in a property of the view > :bulb: **Tip**: see the [Demo Application](DemoApps/GRDBDemoiOS/README.md) for a sample app that uses ValueObservation. - [ValueObservation.trackingCount, trackingOne, trackingAll](#valueobservationtrackingcount-trackingone-trackingall) +- [ValueObservation.combine(...)](#valueobservationcombine) - [ValueObservation.tracking(_:fetch:)](#valueobservationtracking_fetch) - [ValueObservation.tracking(_:reducer:)](#valueobservationtracking_reducer) - [ValueObservation Options](#valueobservation-options) @@ -6367,77 +6368,137 @@ Those observations match the `fetchCount`, `fetchOne`, and `fetchAll` request me } ``` - -### ValueObservation.tracking(_:fetch:) +### ValueObservation.combine(...) Sometimes you need to observe several requests at the same time. For example, you need to observe changes in both a team and its players. -When this happens, you create a ValueObservation with the `ValueObservation.tracking(_:fetch:)` method, which accepts two parameters: - -1. The list of observed requests. -2. A closure that fetches a fresh value whenever one of the observed requests are modified. - -The fetch closure is granted an immutable view of the last committed state of the database: this means that fetched values are guaranteed to be **consistent**. - -For example: +When this happens, **combine** several observations together with the `ValueObservation.combine(...)` method: ```swift // The two observed requests (the team and its players) let teamRequest = Team.filter(key: 1) let playersRequest = Player.filter(Column("teamId") == 1) -// The fetched value -struct TeamInfo { - var team: Team - var players: [Player] +// Two observations +let teamObservation = ValueObservation.trackingOne(teamRequest) +let playersObservation = ValueObservation.trackingAll(playersRequest) + +// The combined observation +let observation = ValueObservation.combine(teamObservation, playersObservation) + +// Start tracking players and teams +let observer = observation.start(in: dbQueue) { team: Team?, players: [Player] in + print("Team or players have changed.") } +``` + +Combining observations provides the guarantee that notified values are [**consistent**](https://en.wikipedia.org/wiki/Consistency_(database_systems)). + +> :point_up: **Note**: you can combine up to five observations together. Please submit a pull request if you need more. +> +> :point_up: **Note**: readers who are familiar with Reactive Programming will recognize the [CombineLatest](http://reactivex.io/documentation/operators/combinelatest.html) operator in the `ValueObservation.combine` method. The reactive operator does not care about data consistency, though: if you use a Reactive layer such as [RxGRDB], compose observations with `ValueObservation.combine`, not with the CombineLatest operator. + + +### ValueObservation.tracking(_:fetch:) + +Observing the database is not always easy to express with simple requests, or the combination of several observations, as we have seen above. + +For example, let's say that we have a struct that defines a "Hall of Fame": + +```swift +struct HallOfFame { + var totalPlayerCount: Int + var bestPlayers: [Player] + + /// Fetch a HallOfFame + static fetch(_ db: Database) throws -> HallOfFame { + let totalPlayerCount = try Player.fetchCount(db) + let bestPlayers = try Player + .order(Column("score").desc) + .limit(10) + .fetchAll(db) + return HallOfFame( + totalPlayerCount: totalPlayerCount, + bestPlayers: bestPlayers) + } +} + +let hallOfFame = try dbQueue.read { try HallOfFame.fetch($0) } +print(""" + Best players out of \(hallOfFame.totalPlayerCount): + \(hallOfFame.bestPlayers) + """) +``` + +In order to track changes in the Hall of Fame, we'll use the `ValueObservation.tracking(_:fetch:)` method. It accepts two parameters: + +1. A list of observed requests. +2. A closure that fetches a fresh value whenever one of the observed requests are modified. + +In our case, any change to the `player` table can impact the Hall of Fame. We thus track the request for all players, `Player.all()`, and fetch a new Hall of Fame whenever players change: +```swift let observation = ValueObservation.tracking( - teamRequest, playersRequest, - fetch: { db -> TeamInfo? in - guard let team = try teamRequest.fetchOne(db) else { - return nil - } - let players = try playersRequest.fetchAll(db) - return TeamInfo(team: team, players: players) + Player.all(), + fetch: { db -> HallOfFame in + return HallOfFame.fetch(db) }) -let observer = observation.start(in: dbQueue) { teamInfo: TeamInfo? in - print("Team and players have changed.") +let observer = observation.start(in: dbQueue) { hallOfFame: HallOfFame in + print(""" + Best players out of \(hallOfFame.totalPlayerCount): + \(hallOfFame.bestPlayers) + """) } ``` -It may happen that a database change does not modify the fetched values. In this case, you'll be notified with consecutive identical values. You can filter out those duplicates with the `ValueObservation.tracking(_:fetchDistinct:)` method. It requires the fetched value to adopt the Equatable protocol: + +#### Filtering out Consecutive Identical Values + +It may happen that a database change does not modify the observed values. The Hall of Fame, for example, is not affected by changes that happen to the worst players. + +When such a database change happens, `ValueObservation.tracking(_:fetch:)` notifies identical consecutive values. + +You can filter out those duplicates with the `ValueObservation.tracking(_:fetchDistinct:)` method. It requires the observed value to adopt the Equatable protocol: ```swift -// When the `player` table is changed, fetch the total number of players, and -// the ten best ones: -struct HallOfFame: Equatable { - var count: Int - var players: [Player] -} +extension HallOfFame: Equatable { ... } let observation = ValueObservation.tracking( Player.all(), fetchDistinct: { db -> HallOfFame in - let count = try Player.fetchCount(db) - let players = try Player - .order(Column("score").desc) - .limit(10) - .fetchAll(db) - return HallOfFame(count: count, players: players) + return HallOfFame.fetch(db) }) let observer = observation.start(in: dbQueue) { hallOfFame: HallOfFame in - print("Best players out of \(hallOfFame.count): \(hallOfFame.players)") + print(""" + Best players out of \(hallOfFame.totalPlayerCount): + \(hallOfFame.bestPlayers) + """) } ``` + +#### The DatabaseRegionConvertible Protocol + The initial parameter of the `ValueObservation.tracking(_:fetch:)` and `ValueObservation.tracking(_:fetchDistinct:)` methods can be fed with requests, and generally speaking, values that adopt the **DatabaseRegionConvertible** protocol. -Use this protocol when you want to encapsulate your complex requests in a dedicated type. Our example above can be rewritten as below: +```swift +protocol DatabaseRegionConvertible { + func databaseRegion(_ db: Database) throws -> DatabaseRegion +} +``` + +[DatabaseRegion](#databaseregion) is a type that helps observing the database. + +Use this protocol when you want to encapsulate your complex requests in a dedicated type. In the sample code below, `TeamInfoRequest` is not only able to fetch a team and its players, but also to be observed. ```swift +struct TeamInfo { + var team: Team + var players: [Player] +} + struct TeamInfoRequest: DatabaseRegionConvertible { var teamId: Int64 @@ -6449,13 +6510,14 @@ struct TeamInfoRequest: DatabaseRegionConvertible { return Player.filter(Column("teamId") == teamId) } - // DatabaseRegionConvertible adoption + /// DatabaseRegionConvertible adoption func databaseRegion(_ db: Database) throws -> DatabaseRegion { let teamRegion = try teamRequest.databaseRegion(db) let playersRegion = try playersRequest.databaseRegion(db) return teamRegion.union(playersRegion) } + /// Fetch a TeamInfo func fetch(_ db: Database) throws -> TeamInfo? { guard let team = try teamRequest.fetchOne(db) else { return nil @@ -6466,15 +6528,18 @@ struct TeamInfoRequest: DatabaseRegionConvertible { } let request = TeamInfoRequest(teamId: 1) + +// Simple fetch +let teamInfo: TeamInfo? = try dbQueue.read(request.fetch) + +// Observatin let observer = ValueObservation .tracking(request, fetch: request.fetch) .start(in: dbQueue) { teamInfo: TeamInfo? in - print("Team and players have hanged.") + print("Team and its players have hanged.") } ``` -See [DatabaseRegion](#databaseregion) for more information. - ### ValueObservation.tracking(_:reducer:) diff --git a/Tests/GRDBTests/ValueObservationCombineTests.swift b/Tests/GRDBTests/ValueObservationCombineTests.swift new file mode 100644 index 0000000000..32b89c8602 --- /dev/null +++ b/Tests/GRDBTests/ValueObservationCombineTests.swift @@ -0,0 +1,383 @@ +import XCTest +#if GRDBCIPHER + import GRDBCipher +#elseif GRDBCUSTOMSQLITE + import GRDBCustomSQLite +#else + #if SWIFT_PACKAGE + import CSQLite + #else + import SQLite3 + #endif + import GRDB +#endif + +class ValueObservationCombineTests: GRDBTestCase { + func testCombine2() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { + try $0.execute(""" + CREATE TABLE t1(id INTEGER PRIMARY KEY AUTOINCREMENT); + CREATE TABLE t2(id INTEGER PRIMARY KEY AUTOINCREMENT); + """) + } + + var values: [(Int, Int)] = [] + let notificationExpectation = expectation(description: "notification") + notificationExpectation.assertForOverFulfill = true + notificationExpectation.expectedFulfillmentCount = 4 + + struct T1: TableRecord { } + struct T2: TableRecord { } + let observation1 = ValueObservation.trackingCount(T1.all()) + let observation2 = ValueObservation.trackingCount(T2.all()) + var observation = ValueObservation.combine(observation1, observation2) + observation.extent = .databaseLifetime + _ = try observation.start(in: dbQueue) { value in + values.append(value) + notificationExpectation.fulfill() + } + + try dbQueue.write { db in + try db.execute("INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t1") + try db.execute("INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t2") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t1") + try db.execute("DELETE FROM t2") + try db.execute("INSERT INTO t1 DEFAULT VALUES") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t1 DEFAULT VALUES") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + } + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(values.count, 4) + XCTAssert(values[0] == (0, 0)) + XCTAssert(values[1] == (1, 0)) + XCTAssert(values[2] == (1, 1)) + XCTAssert(values[3] == (2, 2)) + } + + func testCombine3() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { + try $0.execute(""" + CREATE TABLE t1(id INTEGER PRIMARY KEY AUTOINCREMENT); + CREATE TABLE t2(id INTEGER PRIMARY KEY AUTOINCREMENT); + CREATE TABLE t3(id INTEGER PRIMARY KEY AUTOINCREMENT); + """) + } + + var values: [(Int, Int, Int)] = [] + let notificationExpectation = expectation(description: "notification") + notificationExpectation.assertForOverFulfill = true + notificationExpectation.expectedFulfillmentCount = 5 + + struct T1: TableRecord { } + struct T2: TableRecord { } + struct T3: TableRecord { } + let observation1 = ValueObservation.trackingCount(T1.all()) + let observation2 = ValueObservation.trackingCount(T2.all()) + let observation3 = ValueObservation.trackingCount(T3.all()) + var observation = ValueObservation.combine(observation1, observation2, observation3) + observation.extent = .databaseLifetime + _ = try observation.start(in: dbQueue) { value in + values.append(value) + notificationExpectation.fulfill() + } + + try dbQueue.write { db in + try db.execute("INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t1") + try db.execute("INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t2") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t3") + try db.execute("INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t1") + try db.execute("DELETE FROM t2") + try db.execute("DELETE FROM t3") + try db.execute("INSERT INTO t1 DEFAULT VALUES") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + try db.execute("INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t1 DEFAULT VALUES") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + try db.execute("INSERT INTO t3 DEFAULT VALUES") + } + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(values.count, 5) + XCTAssert(values[0] == (0, 0, 0)) + XCTAssert(values[1] == (1, 0, 0)) + XCTAssert(values[2] == (1, 1, 0)) + XCTAssert(values[3] == (1, 1, 1)) + XCTAssert(values[4] == (2, 2, 2)) + } + + func testCombine4() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { + try $0.execute(""" + CREATE TABLE t1(id INTEGER PRIMARY KEY AUTOINCREMENT); + CREATE TABLE t2(id INTEGER PRIMARY KEY AUTOINCREMENT); + CREATE TABLE t3(id INTEGER PRIMARY KEY AUTOINCREMENT); + CREATE TABLE t4(id INTEGER PRIMARY KEY AUTOINCREMENT); + """) + } + + var values: [(Int, Int, Int, Int)] = [] + let notificationExpectation = expectation(description: "notification") + notificationExpectation.assertForOverFulfill = true + notificationExpectation.expectedFulfillmentCount = 6 + + struct T1: TableRecord { } + struct T2: TableRecord { } + struct T3: TableRecord { } + struct T4: TableRecord { } + let observation1 = ValueObservation.trackingCount(T1.all()) + let observation2 = ValueObservation.trackingCount(T2.all()) + let observation3 = ValueObservation.trackingCount(T3.all()) + let observation4 = ValueObservation.trackingCount(T4.all()) + var observation = ValueObservation.combine(observation1, observation2, observation3, observation4) + observation.extent = .databaseLifetime + _ = try observation.start(in: dbQueue) { value in + values.append(value) + notificationExpectation.fulfill() + } + + try dbQueue.write { db in + try db.execute("INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t4 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t1") + try db.execute("INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t2") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t3") + try db.execute("INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t4") + try db.execute("INSERT INTO t4 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t1") + try db.execute("DELETE FROM t2") + try db.execute("DELETE FROM t3") + try db.execute("DELETE FROM t4") + try db.execute("INSERT INTO t1 DEFAULT VALUES") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + try db.execute("INSERT INTO t3 DEFAULT VALUES") + try db.execute("INSERT INTO t4 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t1 DEFAULT VALUES") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + try db.execute("INSERT INTO t3 DEFAULT VALUES") + try db.execute("INSERT INTO t4 DEFAULT VALUES") + } + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(values.count, 6) + XCTAssert(values[0] == (0, 0, 0, 0)) + XCTAssert(values[1] == (1, 0, 0, 0)) + XCTAssert(values[2] == (1, 1, 0, 0)) + XCTAssert(values[3] == (1, 1, 1, 0)) + XCTAssert(values[4] == (1, 1, 1, 1)) + XCTAssert(values[5] == (2, 2, 2, 2)) + } + + func testCombine5() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { + try $0.execute(""" + CREATE TABLE t1(id INTEGER PRIMARY KEY AUTOINCREMENT); + CREATE TABLE t2(id INTEGER PRIMARY KEY AUTOINCREMENT); + CREATE TABLE t3(id INTEGER PRIMARY KEY AUTOINCREMENT); + CREATE TABLE t4(id INTEGER PRIMARY KEY AUTOINCREMENT); + CREATE TABLE t5(id INTEGER PRIMARY KEY AUTOINCREMENT); + """) + } + + var values: [(Int, Int, Int, Int, Int)] = [] + let notificationExpectation = expectation(description: "notification") + notificationExpectation.assertForOverFulfill = true + notificationExpectation.expectedFulfillmentCount = 7 + + struct T1: TableRecord { } + struct T2: TableRecord { } + struct T3: TableRecord { } + struct T4: TableRecord { } + struct T5: TableRecord { } + let observation1 = ValueObservation.trackingCount(T1.all()) + let observation2 = ValueObservation.trackingCount(T2.all()) + let observation3 = ValueObservation.trackingCount(T3.all()) + let observation4 = ValueObservation.trackingCount(T4.all()) + let observation5 = ValueObservation.trackingCount(T5.all()) + var observation = ValueObservation.combine(observation1, observation2, observation3, observation4, observation5) + observation.extent = .databaseLifetime + _ = try observation.start(in: dbQueue) { value in + values.append(value) + notificationExpectation.fulfill() + } + + try dbQueue.write { db in + try db.execute("INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t4 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t5 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t1") + try db.execute("INSERT INTO t1 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t2") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t3") + try db.execute("INSERT INTO t3 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t4") + try db.execute("INSERT INTO t4 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t5") + try db.execute("INSERT INTO t5 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("DELETE FROM t1") + try db.execute("DELETE FROM t2") + try db.execute("DELETE FROM t3") + try db.execute("DELETE FROM t4") + try db.execute("DELETE FROM t5") + try db.execute("INSERT INTO t1 DEFAULT VALUES") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + try db.execute("INSERT INTO t3 DEFAULT VALUES") + try db.execute("INSERT INTO t4 DEFAULT VALUES") + try db.execute("INSERT INTO t5 DEFAULT VALUES") + } + try dbQueue.write { db in + try db.execute("INSERT INTO t1 DEFAULT VALUES") + try db.execute("INSERT INTO t2 DEFAULT VALUES") + try db.execute("INSERT INTO t3 DEFAULT VALUES") + try db.execute("INSERT INTO t4 DEFAULT VALUES") + try db.execute("INSERT INTO t5 DEFAULT VALUES") + } + waitForExpectations(timeout: 1, handler: nil) + XCTAssertEqual(values.count, 7) + XCTAssert(values[0] == (0, 0, 0, 0, 0)) + XCTAssert(values[1] == (1, 0, 0, 0, 0)) + XCTAssert(values[2] == (1, 1, 0, 0, 0)) + XCTAssert(values[3] == (1, 1, 1, 0, 0)) + XCTAssert(values[4] == (1, 1, 1, 1, 0)) + XCTAssert(values[5] == (1, 1, 1, 1, 1)) + XCTAssert(values[6] == (2, 2, 2, 2, 2)) + } + + func testHeterogeneusCombine2() throws { + struct V1 { } + struct V2 { } + let observation1 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V1() }) + let observation2 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V2() }) + let observation = ValueObservation.combine(observation1, observation2) + var value: (V1, V2)? + _ = try observation.start(in: makeDatabaseQueue()) { value = $0 } + XCTAssertNotNil(value) + } + + func testHeterogeneusCombine3() throws { + struct V1 { } + struct V2 { } + struct V3 { } + let observation1 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V1() }) + let observation2 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V2() }) + let observation3 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V3() }) + let observation = ValueObservation.combine(observation1, observation2, observation3) + var value: (V1, V2, V3)? + _ = try observation.start(in: makeDatabaseQueue()) { value = $0 } + XCTAssertNotNil(value) + } + + func testHeterogeneusCombine4() throws { + struct V1 { } + struct V2 { } + struct V3 { } + struct V4 { } + let observation1 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V1() }) + let observation2 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V2() }) + let observation3 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V3() }) + let observation4 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V4() }) + let observation = ValueObservation.combine(observation1, observation2, observation3, observation4) + var value: (V1, V2, V3, V4)? + _ = try observation.start(in: makeDatabaseQueue()) { value = $0 } + XCTAssertNotNil(value) + } + + func testHeterogeneusCombine5() throws { + struct V1 { } + struct V2 { } + struct V3 { } + struct V4 { } + struct V5 { } + let observation1 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V1() }) + let observation2 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V2() }) + let observation3 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V3() }) + let observation4 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V4() }) + let observation5 = ValueObservation.tracking(DatabaseRegion.fullDatabase, fetch: { _ in V5() }) + let observation = ValueObservation.combine(observation1, observation2, observation3, observation4, observation5) + var value: (V1, V2, V3, V4, V5)? + _ = try observation.start(in: makeDatabaseQueue()) { value = $0 } + XCTAssertNotNil(value) + } +}