diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..d82189eac3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*.swift] +indent_style = space +indent_size = 4 +tab_width = 4 +end_of_line = lf +insert_final_newline = true +max_line_length = 76 +trim_trailing_whitespace = true diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6a9f3ec808..54a73ddacb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,4 @@ -# https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md +# https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md name: "GRDB CI" @@ -40,42 +40,18 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 destination: "platform=macOS" name: "macOS" - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - destination: "OS=16.4,name=iPhone 14 Pro" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + destination: "OS=18.1,name=iPhone 15 Pro" name: "iOS" - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - destination: "OS=16.4,name=Apple TV" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + destination: "OS=18.0,name=Apple TV" name: "tvOS" - - xcode: "Xcode_14.2.app" - runsOn: macOS-13 - destination: "platform=macOS" - name: "macOS" - - xcode: "Xcode_14.2.app" - runsOn: macOS-13 - destination: "OS=16.2,name=iPhone 14" - name: "iOS" - - xcode: "Xcode_14.1.app" - runsOn: macOS-13 - destination: "platform=macOS" - name: "macOS" - - xcode: "Xcode_14.1.app" - runsOn: macOS-13 - destination: "OS=16.1,name=iPhone 14" - name: "iOS" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - destination: "platform=macOS" - name: "macOS" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - destination: "OS=16.0,name=iPhone 14" - name: "iOS" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -90,18 +66,9 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - name: "Xcode 14.3.1" - - xcode: "Xcode_14.2.app" - runsOn: macOS-13 - name: "Xcode 14.2" - - xcode: "Xcode_14.1.app" - runsOn: macOS-13 - name: "Xcode 14.1" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - name: "Xcode 14.0.1" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + name: "Xcode 16.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -116,12 +83,9 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - name: "Xcode 14.3.1" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - name: "Xcode 14.0.1" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + name: "Xcode 16.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -136,12 +100,9 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - name: "Xcode 14.3.1" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - name: "Xcode 14.0.1" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + name: "Xcode 16.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -156,12 +117,9 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - name: "Xcode 14.3.1" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - name: "Xcode 14.0.1" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + name: "Xcode 16.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -176,12 +134,9 @@ jobs: fail-fast: false matrix: include: - - xcode: "Xcode_14.3.1.app" - runsOn: macOS-13 - name: "Xcode 14.3.1" - - xcode: "Xcode_14.0.1.app" - runsOn: macOS-12 - name: "Xcode 14.0.1" + - xcode: "Xcode_16.1.app" + runsOn: macOS-14 + name: "Xcode 16.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/GRDB-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/GRDB-Package.xcscheme deleted file mode 100644 index b60345c202..0000000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/GRDB-Package.xcscheme +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f846842e..099b355ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,25 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: --- +## Next Release + +- **Breaking Change**: Bump requirements by [@groue](https://github.com/groue) in [#1598](https://github.com/groue/GRDB.swift/pull/1598) and [#1634](https://github.com/groue/GRDB.swift/pull/1634) +- **Breaking Change**: Enhance ergonomics of record methods that insert/save/upsert and fetch by [@groue](https://github.com/groue) in [#1599](https://github.com/groue/GRDB.swift/pull/1599) +- **Breaking Change**: Rename the CSQLite module to GRDBSQLite, and stop exporting the C SQLite functions by [@groue](https://github.com/groue) in [#1600](https://github.com/groue/GRDB.swift/pull/1600) +- **Breaking Change**: Perform all writes with immediate transactions by default by [@groue](https://github.com/groue) in [#1602](https://github.com/groue/GRDB.swift/pull/1602) +- **Breaking Change**: Remove DatabasePool.concurrentRead by [@groue](https://github.com/groue) in [#1603](https://github.com/groue/GRDB.swift/pull/1603) +- **Breaking Change**: Coding strategies depend on the column by [@groue](https://github.com/groue) in [#1606](https://github.com/groue/GRDB.swift/pull/1606) +- **Breaking Change**: Add missing Sendable conformances by [@groue](https://github.com/groue) in [#1607](https://github.com/groue/GRDB.swift/pull/1607) and [#1639](https://github.com/groue/GRDB.swift/pull/1639) +- **Breaking Change**: Async database accesses honor Task cancellation by [@groue](https://github.com/groue) in [#1610](https://github.com/groue/GRDB.swift/pull/1610) +- **Breaking Change**: Prefer Collection over Sequence for filter(keys:) and related APIs by [@groue](https://github.com/groue) in [#1617](https://github.com/groue/GRDB.swift/pull/1617) +- **Breaking Change**: MainActor ValueObservation scheduling by [@groue](https://github.com/groue) in [#1633](https://github.com/groue/GRDB.swift/pull/1633) +- **Breaking Change**: Prefer any DatabaseReader and DatabaseWriter by [@groue](https://github.com/groue) in [#1635](https://github.com/groue/GRDB.swift/pull/1635) +- **New**: Sendable database accesses by [@groue](https://github.com/groue) in [#1618](https://github.com/groue/GRDB.swift/pull/1618) +- **New**: DatabaseCursor has a primary associated type by [@groue](https://github.com/groue) in [#1605](https://github.com/groue/GRDB.swift/pull/1605) +- **Documentation Update**: [Migrating From GRDB 6 to GRDB 7](Documentation/GRDB7MigrationGuide.md) describes in detail how to bump the GRDB version in your application. +- **Documentation Update**: The new [Swift Concurrency and GRDB](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/swiftconcurrency) guide explains how to best integrate GRDB and Swift Concurrency. +- **Documentation Update**: The [demo app](Documentation/DemoApps/) was rewritten from scratch in a brand new Xcode 16 project. + ## 6.29.3 Released September 7, 2024 diff --git a/Documentation/AssociationsBasics.md b/Documentation/AssociationsBasics.md index 50e241674f..e37565ce75 100644 --- a/Documentation/AssociationsBasics.md +++ b/Documentation/AssociationsBasics.md @@ -137,9 +137,7 @@ Before we dive in, please remember that associations can not generate all possib **Associations are available on types that adopt the necessary supporting protocols.** -When your record type is a subclass of the [Record class], all necessary protocols are already setup and ready: you can skip this chapter. - -Generally speaking, associations use the [TableRecord], [FetchableRecord], and [EncodableRecord] protocols: +Associations are based on the [TableRecord], [FetchableRecord], and [EncodableRecord] protocols: - **[TableRecord]** is the protocol that lets you declare associations between record types: @@ -3005,7 +3003,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. [TableRecord]: ../README.md#tablerecord-protocol [Recommended Practices for Designing Record Types]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices [regular aggregating methods]: ../README.md#fetching-aggregated-values -[Record class]: ../README.md#record-class [EncodableRecord]: ../README.md#persistablerecord-protocol [PersistableRecord]: ../README.md#persistablerecord-protocol [Codable Records]: ../README.md#codable-records diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.pbxproj b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.pbxproj deleted file mode 100644 index 7b04e5bebc..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.pbxproj +++ /dev/null @@ -1,611 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */; }; - 56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */; }; - 56519DCA274FC8E900ED16D8 /* GRDBQuery in Frameworks */ = {isa = PBXBuildFile; productRef = 56519DC9274FC8E900ED16D8 /* GRDBQuery */; }; - 5671723A261B23C800423B6F /* PlayerList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717239261B23C800423B6F /* PlayerList.swift */; }; - 56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717251261B334D00423B6F /* PlayerRequestTests.swift */; }; - 567C3E1A2520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E192520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift */; }; - 567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */; }; - 567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */; }; - 567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E532520B75C0011F6E9 /* Player.swift */; }; - 567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E542520B75C0011F6E9 /* Persistence.swift */; }; - 567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E592520B75C0011F6E9 /* PlayerFormView.swift */; }; - 567C3E622520B75D0011F6E9 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5A2520B75C0011F6E9 /* AppView.swift */; }; - 567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */; }; - 567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */; }; - 567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E652520B7880011F6E9 /* AppDatabase.swift */; }; - 567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E752520BB650011F6E9 /* Localizable.stringsdict */; }; - 567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */; }; - 56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B6D1082619EC1B003CC455 /* PlayerRequest.swift */; }; - 56F8A13527359A5A0011ACBE /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56F8A13427359A5A0011ACBE /* GRDB */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 56026C9D25B8A7D000D1DF3F /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 567C3E0E2520B6DE0011F6E9 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 567C3E152520B6DE0011F6E9; - remoteInfo = GRDBAsyncDemo; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 567C3E502520B70E0011F6E9 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 56026C9825B8A7D000D1DF3F /* GRDBAsyncDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBAsyncDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 56026C9C25B8A7D000D1DF3F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = ""; }; - 56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabaseTests.swift; sourceTree = ""; }; - 56717239261B23C800423B6F /* PlayerList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerList.swift; sourceTree = ""; }; - 56717251261B334D00423B6F /* PlayerRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequestTests.swift; sourceTree = ""; }; - 567C3E162520B6DE0011F6E9 /* GRDBAsyncDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBAsyncDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 567C3E192520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBAsyncDemoApp.swift; sourceTree = ""; }; - 567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 567C3E222520B6DF0011F6E9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 567C3E532520B75C0011F6E9 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; - 567C3E542520B75C0011F6E9 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; - 567C3E592520B75C0011F6E9 /* PlayerFormView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerFormView.swift; sourceTree = ""; }; - 567C3E5A2520B75C0011F6E9 /* AppView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = ""; }; - 567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerCreationView.swift; sourceTree = ""; }; - 567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerEditionView.swift; sourceTree = ""; }; - 567C3E652520B7880011F6E9 /* AppDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = ""; }; - 567C3E762520BB650011F6E9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; - 567C3E782520BB650011F6E9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 56B6D1082619EC1B003CC455 /* PlayerRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequest.swift; sourceTree = ""; }; - 56F8A12E27359A350011ACBE /* GRDB.swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = GRDB.swift; path = ../../..; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 56026C9525B8A7D000D1DF3F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 567C3E132520B6DE0011F6E9 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 56519DCA274FC8E900ED16D8 /* GRDBQuery in Frameworks */, - 56F8A13527359A5A0011ACBE /* GRDB in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 56026C9925B8A7D000D1DF3F /* GRDBAsyncDemoTests */ = { - isa = PBXGroup; - children = ( - 56026C9C25B8A7D000D1DF3F /* Info.plist */, - 56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */, - 56717251261B334D00423B6F /* PlayerRequestTests.swift */, - 56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */, - ); - path = GRDBAsyncDemoTests; - sourceTree = ""; - }; - 56185BC125B8047D00B9C30F /* Resources */ = { - isa = PBXGroup; - children = ( - 567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */, - 567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */, - 567C3E752520BB650011F6E9 /* Localizable.stringsdict */, - ); - path = Resources; - sourceTree = ""; - }; - 567C3E0D2520B6DE0011F6E9 = { - isa = PBXGroup; - children = ( - 567C3E182520B6DE0011F6E9 /* GRDBAsyncDemo */, - 56026C9925B8A7D000D1DF3F /* GRDBAsyncDemoTests */, - 567C3E172520B6DE0011F6E9 /* Products */, - 567C3E4D2520B70E0011F6E9 /* Frameworks */, - 56F8A12E27359A350011ACBE /* GRDB.swift */, - ); - sourceTree = ""; - }; - 567C3E172520B6DE0011F6E9 /* Products */ = { - isa = PBXGroup; - children = ( - 567C3E162520B6DE0011F6E9 /* GRDBAsyncDemo.app */, - 56026C9825B8A7D000D1DF3F /* GRDBAsyncDemoTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 567C3E182520B6DE0011F6E9 /* GRDBAsyncDemo */ = { - isa = PBXGroup; - children = ( - 567C3E222520B6DF0011F6E9 /* Info.plist */, - 567C3E652520B7880011F6E9 /* AppDatabase.swift */, - 567C3E192520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift */, - 567C3E542520B75C0011F6E9 /* Persistence.swift */, - 567C3E532520B75C0011F6E9 /* Player.swift */, - 56B6D1082619EC1B003CC455 /* PlayerRequest.swift */, - 567C3E1F2520B6DF0011F6E9 /* Preview Content */, - 56185BC125B8047D00B9C30F /* Resources */, - 567C3E582520B75C0011F6E9 /* Views */, - ); - path = GRDBAsyncDemo; - sourceTree = ""; - }; - 567C3E1F2520B6DF0011F6E9 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 567C3E4D2520B70E0011F6E9 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; - 567C3E582520B75C0011F6E9 /* Views */ = { - isa = PBXGroup; - children = ( - 567C3E5A2520B75C0011F6E9 /* AppView.swift */, - 567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */, - 567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */, - 567C3E592520B75C0011F6E9 /* PlayerFormView.swift */, - 56717239261B23C800423B6F /* PlayerList.swift */, - ); - path = Views; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 56026C9725B8A7D000D1DF3F /* GRDBAsyncDemoTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 56026CA825B8A7D000D1DF3F /* Build configuration list for PBXNativeTarget "GRDBAsyncDemoTests" */; - buildPhases = ( - 56026C9425B8A7D000D1DF3F /* Sources */, - 56026C9525B8A7D000D1DF3F /* Frameworks */, - 56026C9625B8A7D000D1DF3F /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 56026C9E25B8A7D000D1DF3F /* PBXTargetDependency */, - ); - name = GRDBAsyncDemoTests; - productName = GRDBAsyncDemoTests; - productReference = 56026C9825B8A7D000D1DF3F /* GRDBAsyncDemoTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 567C3E152520B6DE0011F6E9 /* GRDBAsyncDemo */ = { - isa = PBXNativeTarget; - buildConfigurationList = 567C3E252520B6DF0011F6E9 /* Build configuration list for PBXNativeTarget "GRDBAsyncDemo" */; - buildPhases = ( - 567C3E122520B6DE0011F6E9 /* Sources */, - 567C3E132520B6DE0011F6E9 /* Frameworks */, - 567C3E142520B6DE0011F6E9 /* Resources */, - 567C3E502520B70E0011F6E9 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 56F8A13127359A540011ACBE /* PBXTargetDependency */, - ); - name = GRDBAsyncDemo; - packageProductDependencies = ( - 56F8A13427359A5A0011ACBE /* GRDB */, - 56519DC9274FC8E900ED16D8 /* GRDBQuery */, - ); - productName = GRBCombineDemo; - productReference = 567C3E162520B6DE0011F6E9 /* GRDBAsyncDemo.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 567C3E0E2520B6DE0011F6E9 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 1200; - TargetAttributes = { - 56026C9725B8A7D000D1DF3F = { - CreatedOnToolsVersion = 12.3; - TestTargetID = 567C3E152520B6DE0011F6E9; - }; - 567C3E152520B6DE0011F6E9 = { - CreatedOnToolsVersion = 12.0; - }; - }; - }; - buildConfigurationList = 567C3E112520B6DE0011F6E9 /* Build configuration list for PBXProject "GRDBAsyncDemo" */; - compatibilityVersion = "Xcode 12.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 567C3E0D2520B6DE0011F6E9; - packageReferences = ( - 56519DC8274FC8E900ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */, - ); - productRefGroup = 567C3E172520B6DE0011F6E9 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 567C3E152520B6DE0011F6E9 /* GRDBAsyncDemo */, - 56026C9725B8A7D000D1DF3F /* GRDBAsyncDemoTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 56026C9625B8A7D000D1DF3F /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 567C3E142520B6DE0011F6E9 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */, - 567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */, - 567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */, - 567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 56026C9425B8A7D000D1DF3F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */, - 56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */, - 56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 567C3E122520B6DE0011F6E9 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */, - 567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */, - 56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */, - 5671723A261B23C800423B6F /* PlayerList.swift in Sources */, - 567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */, - 567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */, - 567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */, - 567C3E622520B75D0011F6E9 /* AppView.swift in Sources */, - 567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */, - 567C3E1A2520B6DE0011F6E9 /* GRDBAsyncDemoApp.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 56026C9E25B8A7D000D1DF3F /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 567C3E152520B6DE0011F6E9 /* GRDBAsyncDemo */; - targetProxy = 56026C9D25B8A7D000D1DF3F /* PBXContainerItemProxy */; - }; - 56F8A13127359A540011ACBE /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = 56F8A13027359A540011ACBE /* GRDB */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 567C3E752520BB650011F6E9 /* Localizable.stringsdict */ = { - isa = PBXVariantGroup; - children = ( - 567C3E762520BB650011F6E9 /* en */, - ); - name = Localizable.stringsdict; - sourceTree = ""; - }; - 567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 567C3E782520BB650011F6E9 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 56026C9F25B8A7D000D1DF3F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = GRDBAsyncDemoTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemoTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBAsyncDemo.app/GRDBAsyncDemo"; - }; - name = Debug; - }; - 56026CA025B8A7D000D1DF3F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = GRDBAsyncDemoTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemoTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBAsyncDemo.app/GRDBAsyncDemo"; - }; - name = Release; - }; - 567C3E232520B6DF0011F6E9 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 567C3E242520B6DF0011F6E9 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 567C3E262520B6DF0011F6E9 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"GRDBAsyncDemo/Preview Content\""; - DEVELOPMENT_TEAM = ""; - ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = GRDBAsyncDemo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemo; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Debug; - }; - 567C3E272520B6DF0011F6E9 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"GRDBAsyncDemo/Preview Content\""; - DEVELOPMENT_TEAM = ""; - ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = GRDBAsyncDemo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBAsyncDemo; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 56026CA825B8A7D000D1DF3F /* Build configuration list for PBXNativeTarget "GRDBAsyncDemoTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 56026C9F25B8A7D000D1DF3F /* Debug */, - 56026CA025B8A7D000D1DF3F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 567C3E112520B6DE0011F6E9 /* Build configuration list for PBXProject "GRDBAsyncDemo" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 567C3E232520B6DF0011F6E9 /* Debug */, - 567C3E242520B6DF0011F6E9 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 567C3E252520B6DF0011F6E9 /* Build configuration list for PBXNativeTarget "GRDBAsyncDemo" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 567C3E262520B6DF0011F6E9 /* Debug */, - 567C3E272520B6DF0011F6E9 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 56519DC8274FC8E900ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/groue/GRDBQuery"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.6.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 56519DC9274FC8E900ED16D8 /* GRDBQuery */ = { - isa = XCSwiftPackageProductDependency; - package = 56519DC8274FC8E900ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */; - productName = GRDBQuery; - }; - 56F8A13027359A540011ACBE /* GRDB */ = { - isa = XCSwiftPackageProductDependency; - productName = GRDB; - }; - 56F8A13427359A5A0011ACBE /* GRDB */ = { - isa = XCSwiftPackageProductDependency; - productName = GRDB; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 567C3E0E2520B6DE0011F6E9 /* Project object */; -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 216295601d..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pins" : [ - { - "identity" : "grdbquery", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/GRDBQuery", - "state" : { - "revision" : "a6c46dd38ecf11a5c37732870dc03a384d582fba", - "version" : "0.9.0" - } - } - ], - "version" : 2 -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/xcshareddata/xcschemes/GRDBAsyncDemo.xcscheme b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/xcshareddata/xcschemes/GRDBAsyncDemo.xcscheme deleted file mode 100644 index aeb65e487a..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/xcshareddata/xcschemes/GRDBAsyncDemo.xcscheme +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/AppDatabase.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/AppDatabase.swift deleted file mode 100644 index e19cc7af7a..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/AppDatabase.swift +++ /dev/null @@ -1,235 +0,0 @@ -import Foundation -import GRDB -import os.log - -/// A database of players. -/// -/// You create an `AppDatabase` with a connection to an SQLite database -/// (see ). -/// -/// Create those connections with a configuration returned from -/// `AppDatabase/makeConfiguration(_:)`. -/// -/// For example: -/// -/// ```swift -/// // Create an in-memory AppDatabase -/// let config = AppDatabase.makeConfiguration() -/// let dbQueue = try DatabaseQueue(configuration: config) -/// let appDatabase = try AppDatabase(dbQueue) -/// ``` -struct AppDatabase { - /// Creates an `AppDatabase`, and makes sure the database schema - /// is ready. - /// - /// - important: Create the `DatabaseWriter` with a configuration - /// returned by ``makeConfiguration(_:)``. - init(_ dbWriter: any DatabaseWriter) throws { - self.dbWriter = dbWriter - try migrator.migrate(dbWriter) - } - - /// Provides access to the database. - /// - /// Application can use a `DatabasePool`, while SwiftUI previews and tests - /// can use a fast in-memory `DatabaseQueue`. - /// - /// See - private let dbWriter: any DatabaseWriter -} - -// MARK: - Database Configuration - -extension AppDatabase { - private static let sqlLogger = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SQL") - - /// Returns a database configuration suited for `PlayerRepository`. - /// - /// SQL statements are logged if the `SQL_TRACE` environment variable - /// is set. - /// - /// - parameter base: A base configuration. - public static func makeConfiguration(_ base: Configuration = Configuration()) -> Configuration { - var config = base - - // An opportunity to add required custom SQL functions or - // collations, if needed: - // config.prepareDatabase { db in - // db.add(function: ...) - // } - - // Log SQL statements if the `SQL_TRACE` environment variable is set. - // See - if ProcessInfo.processInfo.environment["SQL_TRACE"] != nil { - config.prepareDatabase { db in - db.trace { - // It's ok to log statements publicly. Sensitive - // information (statement arguments) are not logged - // unless config.publicStatementArguments is set - // (see below). - os_log("%{public}@", log: sqlLogger, type: .debug, String(describing: $0)) - } - } - } - -#if DEBUG - // Protect sensitive information by enabling verbose debugging in - // DEBUG builds only. - // See - config.publicStatementArguments = true -#endif - - return config - } -} - -// MARK: - Database Migrations - -extension AppDatabase { - /// The DatabaseMigrator that defines the database schema. - /// - /// See - private var migrator: DatabaseMigrator { - var migrator = DatabaseMigrator() - -#if DEBUG - // Speed up development by nuking the database when migrations change - // See - migrator.eraseDatabaseOnSchemaChange = true -#endif - - migrator.registerMigration("createPlayer") { db in - // Create a table - // See - try db.create(table: "player") { t in - t.autoIncrementedPrimaryKey("id") - t.column("name", .text).notNull() - t.column("score", .integer).notNull() - } - } - - // Migrations for future application versions will be inserted here: - // migrator.registerMigration(...) { db in - // ... - // } - - return migrator - } -} - -// MARK: - Database Access: Writes -// The write methods execute invariant-preserving database transactions. - -extension AppDatabase { - /// A validation error that prevents some players from being saved into - /// the database. - enum ValidationError: LocalizedError { - case missingName - - var errorDescription: String? { - switch self { - case .missingName: - return "Please provide a name" - } - } - } - - /// Saves (inserts or updates) a player. When the method returns, the - /// player is present in the database, and its id is not nil. - func savePlayer(_ player: inout Player) async throws { - if player.name.isEmpty { - throw ValidationError.missingName - } - player = try await dbWriter.write { [player] db in - try player.saved(db) - } - } - - /// Delete the specified players - func deletePlayers(ids: [Int64]) async throws { - try await dbWriter.write { db in - _ = try Player.deleteAll(db, ids: ids) - } - } - - /// Delete all players - func deleteAllPlayers() async throws { - try await dbWriter.write { db in - _ = try Player.deleteAll(db) - } - } - - /// Refresh all players (by performing some random changes, for demo purpose). - func refreshPlayers() async throws { - try await dbWriter.write { db in - if try Player.all().isEmpty(db) { - // When database is empty, insert new random players - try createRandomPlayers(db) - } else { - // Insert a player - if Bool.random() { - _ = try Player.makeRandom().inserted(db) // insert but ignore inserted id - } - - // Delete a random player - if Bool.random() { - try Player.order(sql: "RANDOM()").limit(1).deleteAll(db) - } - - // Update some players - for var player in try Player.fetchAll(db) where Bool.random() { - try player.updateChanges(db) { - $0.score = Player.randomScore() - } - } - } - } - } - - /// Create random players if the database is empty. - func createRandomPlayersIfEmpty() throws { - try dbWriter.write { db in - if try Player.all().isEmpty(db) { - try createRandomPlayers(db) - } - } - } - - private static let uiTestPlayers = [ - Player(id: nil, name: "Arthur", score: 5), - Player(id: nil, name: "Barbara", score: 6), - Player(id: nil, name: "Craig", score: 8), - Player(id: nil, name: "David", score: 4), - Player(id: nil, name: "Elena", score: 1), - Player(id: nil, name: "Frederik", score: 2), - Player(id: nil, name: "Gilbert", score: 7), - Player(id: nil, name: "Henriette", score: 3)] - - func createPlayersForUITests() throws { - try dbWriter.write { db in - try AppDatabase.uiTestPlayers.forEach { player in - _ = try player.inserted(db) // insert but ignore inserted id - } - } - } - - /// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`. - private func createRandomPlayers(_ db: Database) throws { - for _ in 0..<8 { - _ = try Player.makeRandom().inserted(db) // insert but ignore inserted id - } - } -} - -// MARK: - Database Access: Reads - -// This demo app does not provide any specific reading method, and instead -// gives an unrestricted read-only access to the rest of the application. -// In your app, you are free to choose another path, and define focused -// reading methods. -extension AppDatabase { - /// Provides a read-only access to the database - var reader: DatabaseReader { - dbWriter - } -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/GRDBAsyncDemoApp.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/GRDBAsyncDemoApp.swift deleted file mode 100644 index 748b10a900..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/GRDBAsyncDemoApp.swift +++ /dev/null @@ -1,32 +0,0 @@ -import GRDBQuery -import SwiftUI - -@main -struct GRDBAsyncDemoApp: App { - var body: some Scene { - WindowGroup { - AppView().appDatabase(.shared) - } - } -} - -// MARK: - Give SwiftUI access to the database - -private struct AppDatabaseKey: EnvironmentKey { - static var defaultValue: AppDatabase { .empty() } -} - -extension EnvironmentValues { - var appDatabase: AppDatabase { - get { self[AppDatabaseKey.self] } - set { self[AppDatabaseKey.self] = newValue } - } -} - -extension View { - func appDatabase(_ appDatabase: AppDatabase) -> some View { - self - .environment(\.appDatabase, appDatabase) - .databaseContext(.readOnly { appDatabase.reader }) - } -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Info.plist b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Info.plist deleted file mode 100644 index 4754bb8682..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Info.plist +++ /dev/null @@ -1,50 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - - UIApplicationSupportsIndirectInputEvents - - UILaunchScreen - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/PlayerRequest.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/PlayerRequest.swift deleted file mode 100644 index 4d1d924605..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/PlayerRequest.swift +++ /dev/null @@ -1,35 +0,0 @@ -import GRDB -import GRDBQuery - -/// A player request can be used with the `@Query` property wrapper in order to -/// feed a view with a list of players. -/// -/// For example: -/// -/// struct MyView: View { -/// @Query(PlayerRequest(ordering: .byName)) private var players: [Player] -/// -/// var body: some View { -/// List(players) { player in ... ) -/// } -/// } -struct PlayerRequest: ValueObservationQueryable { - enum Ordering { - case byScore - case byName - } - - static var defaultValue: [Player] { [] } - - /// The ordering used by the player request. - var ordering: Ordering - - func fetch(_ db: Database) throws -> [Player] { - switch ordering { - case .byScore: - return try Player.all().orderedByScore().fetchAll(db) - case .byName: - return try Player.all().orderedByName().fetchAll(db) - } - } -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 29d91251df..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "icon_20pt@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "icon_20pt@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "icon_29pt@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "icon_29pt@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "icon_40pt@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "icon_40pt@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "icon_60pt@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "icon_60pt@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "icon_20pt.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "icon_20pt@2x-1.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "icon_29pt.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "icon_29pt@2x-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "icon_40pt.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "icon_40pt@2x-1.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "icon_76pt.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "icon_76pt@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "icon_83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png deleted file mode 100644 index 66b1931a14..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png deleted file mode 100644 index 90648b3f40..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png deleted file mode 100644 index a077a6f490..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png deleted file mode 100644 index a077a6f490..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png deleted file mode 100644 index 600bdbd9cd..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png deleted file mode 100644 index 8e04af0dd8..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png deleted file mode 100644 index 686e8d99e2..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png deleted file mode 100644 index 686e8d99e2..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png deleted file mode 100644 index 1d013c3d33..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png deleted file mode 100644 index a077a6f490..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png deleted file mode 100644 index da66b9ba82..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png deleted file mode 100644 index da66b9ba82..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png deleted file mode 100644 index 59346ef4b6..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png deleted file mode 100644 index 59346ef4b6..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png deleted file mode 100644 index d4640afc9a..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png deleted file mode 100644 index e3a04522bf..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png deleted file mode 100644 index 593ebd783d..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png deleted file mode 100644 index ca02cd03bc..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json deleted file mode 100644 index 2cbe59d5ec..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "LaunchIcon.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Base.lproj/LaunchScreen.storyboard b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 79f85a1dd9..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/en.lproj/Localizable.stringsdict b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/en.lproj/Localizable.stringsdict deleted file mode 100644 index 2d9aa217c4..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/en.lproj/Localizable.stringsdict +++ /dev/null @@ -1,42 +0,0 @@ - - - - - %lld Players - - NSStringLocalizedFormatKey - %#@VARIABLE@ - VARIABLE - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - lld - zero - No Player - one - 1 Player - other - %lld Players - - - %lld points - - NSStringLocalizedFormatKey - %#@VARIABLE@ - VARIABLE - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - lld - zero - 0 point - one - 1 point - other - %lld points - - - - diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/AppView.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/AppView.swift deleted file mode 100644 index 562b4aee8a..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/AppView.swift +++ /dev/null @@ -1,154 +0,0 @@ -import GRDBQuery -import SwiftUI - -/// The main application view -struct AppView: View { - /// Write access to the database - @Environment(\.appDatabase) private var appDatabase - - /// The `players` property is automatically updated when the database changes - @Query(PlayerRequest(ordering: .byScore)) private var players: [Player] - - /// We'll need to leave edit mode in several occasions. - @State private var editMode = EditMode.inactive - - /// Tracks the presentation of the player creation sheet. - @State private var newPlayerIsPresented = false - - // If you want to define the query on initialization, you will prefer: - // - // @Query private var players: [Player] - // - // init(initialOrdering: PlayerRequest.Ordering) { - // _players = Query(PlayerRequest(ordering: initialOrdering)) - // } - - var body: some View { - NavigationView { - PlayerList(players: players) - .navigationBarTitle(Text("\(players.count) Players")) - .navigationBarItems( - leading: HStack { - EditButton() - newPlayerButton - }, - trailing: ToggleOrderingButton( - ordering: $players.ordering, - willChange: { - // onChange(of: $players.wrappedValue.ordering) - // is not able to leave the editing mode during - // the animation of the list content. - // Workaround: stop editing before the ordering - // is changed, and the list content is updated. - stopEditing() - })) - .toolbar { toolbarContent } - .onChange(of: players) { - if players.isEmpty { - stopEditing() - } - } - .environment(\.editMode, $editMode) - } - } - - private var toolbarContent: some ToolbarContent { - ToolbarItemGroup(placement: .bottomBar) { - Button { - // Don't stopEditing() here because this is - // performed `onChange(of: players)` - Task { - try? await appDatabase.deleteAllPlayers() - } - } label: { - Image(systemName: "trash").imageScale(.large) - } - - Spacer() - - Button { - stopEditing() - Task { - try? await appDatabase.refreshPlayers() - } - } label: { - Image(systemName: "arrow.clockwise").imageScale(.large) - } - - Spacer() - - Button { - stopEditing() - // Perform 50 refreshes in parallel - Task { - try? await withThrowingTaskGroup(of: Void.self) { group in - for _ in 0..<50 { - _ = group.addTaskUnlessCancelled { - try await appDatabase.refreshPlayers() - } - } - try await group.waitForAll() - } - } - } label: { - Image(systemName: "tornado").imageScale(.large) - } - } - } - - /// The button that presents the player creation sheet. - private var newPlayerButton: some View { - Button { - stopEditing() - newPlayerIsPresented = true - } label: { - Image(systemName: "plus") - } - .accessibility(label: Text("New Player")) - .sheet(isPresented: $newPlayerIsPresented) { - PlayerCreationView() - } - } - - private func stopEditing() { - withAnimation { - editMode = .inactive - } - } -} - -private struct ToggleOrderingButton: View { - @Binding var ordering: PlayerRequest.Ordering - let willChange: () -> Void - - var body: some View { - switch ordering { - case .byName: - Button { - willChange() - ordering = .byScore - } label: { - Label("Name", systemImage: "arrowtriangle.up.fill").labelStyle(.titleAndIcon) - } - case .byScore: - Button { - willChange() - ordering = .byName - } label: { - Label("Score", systemImage: "arrowtriangle.down.fill").labelStyle(.titleAndIcon) - } - } - } -} - -// MARK: - Previews - -#Preview("Empty") { - // Preview the default, empty database - AppView() -} - -#Preview("Populated") { - // Preview a database of random players - AppView().appDatabase(.random()) -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerCreationView.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerCreationView.swift deleted file mode 100644 index a47fdd04df..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerCreationView.swift +++ /dev/null @@ -1,50 +0,0 @@ -import SwiftUI - -/// The view that creates a new player. -struct PlayerCreationView: View { - /// Write access to the database - @Environment(\.appDatabase) private var appDatabase - @Environment(\.dismiss) private var dismiss - @State private var form = PlayerForm(name: "", score: "") - @State private var errorAlertIsPresented = false - @State private var errorAlertTitle = "" - - var body: some View { - NavigationView { - PlayerFormView(form: $form) - .alert( - isPresented: $errorAlertIsPresented, - content: { Alert(title: Text(errorAlertTitle)) }) - .navigationBarTitle("New Player") - .navigationBarItems( - leading: Button(role: .cancel) { - dismiss() - } label: { - Text("Cancel") - }, - trailing: Button { - Task { await save() } - } label: { - Text("Save") - }) - } - } - - private func save() async { - do { - var player = Player(id: nil, name: "", score: 0) - form.apply(to: &player) - try await appDatabase.savePlayer(&player) - dismiss() - } catch { - errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred" - errorAlertIsPresented = true - } - } -} - -// MARK: - Previews - -#Preview { - PlayerCreationView() -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerEditionView.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerEditionView.swift deleted file mode 100644 index 1ab97677dc..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerEditionView.swift +++ /dev/null @@ -1,40 +0,0 @@ -import SwiftUI - -/// The view that edits an existing player. -struct PlayerEditionView: View { - /// Write access to the database - @Environment(\.appDatabase) private var appDatabase - @Environment(\.isPresented) private var isPresented - private let player: Player - @State private var form: PlayerForm - - init(player: Player) { - self.player = player - self.form = PlayerForm(player) - } - - var body: some View { - PlayerFormView(form: $form) - .onChange(of: isPresented) { - // Save when back button is pressed - if !isPresented { - Task { - var savedPlayer = player - form.apply(to: &savedPlayer) - // Ignore error because I don't know how to cancel the - // back button and present the error - try? await appDatabase.savePlayer(&savedPlayer) - } - } - } - } -} - -// MARK: - Previews - -#Preview { - NavigationView { - PlayerEditionView(player: Player.makeRandom()) - .navigationBarTitle("Player Edition") - } -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerFormView.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerFormView.swift deleted file mode 100644 index 590506d465..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerFormView.swift +++ /dev/null @@ -1,48 +0,0 @@ -import SwiftUI - -/// The Player editing form, embedded in both -/// `PlayerCreationView` and `PlayerEditionView`. -struct PlayerFormView: View { - @Binding var form: PlayerForm - - var body: some View { - List { - TextField("Name", text: $form.name) - .accessibility(label: Text("Player Name")) - TextField("Score", text: $form.score).keyboardType(.numberPad) - .accessibility(label: Text("Player Score")) - } - .listStyle(InsetGroupedListStyle()) - } -} - -struct PlayerForm { - var name: String - var score: String -} - -extension PlayerForm { - init(_ player: Player) { - self.name = player.name - self.score = "\(player.score)" - } - - func apply(to player: inout Player) { - player.name = name - player.score = Int(score) ?? 0 - } -} - -// MARK: - Previews - -#Preview("Empty") { - PlayerFormView(form: .constant(PlayerForm( - name: "", - score: ""))) -} - -#Preview("Prefilled") { - PlayerFormView(form: .constant(PlayerForm( - name: Player.randomName(), - score: "\(Player.randomScore())"))) -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerList.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerList.swift deleted file mode 100644 index f156f786c0..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Views/PlayerList.swift +++ /dev/null @@ -1,59 +0,0 @@ -import SwiftUI - -struct PlayerList: View { - /// Write access to the database - @Environment(\.appDatabase) private var appDatabase - - /// The players in the list - var players: [Player] - - var body: some View { - List { - ForEach(players) { player in - NavigationLink(destination: editionView(for: player)) { - PlayerRow(player: player) - // Don't animate player update - .animation(nil, value: player) - } - } - .onDelete { offsets in - let playerIds = offsets.compactMap { players[$0].id } - Task { - try? await appDatabase.deletePlayers(ids: playerIds) - } - } - } - // Animate list updates - .animation(.default, value: players) - .listStyle(.plain) - } - - /// The view that edits a player in the list. - private func editionView(for player: Player) -> some View { - PlayerEditionView(player: player).navigationBarTitle(player.name) - } -} - -private struct PlayerRow: View { - var player: Player - - var body: some View { - HStack { - Text(player.name) - Spacer() - Text("\(player.score) points").foregroundColor(.gray) - } - } -} - -// MARK: - Previews - -#Preview { - NavigationView { - PlayerList(players: [ - Player(id: 1, name: "Arthur", score: 100), - Player(id: 2, name: "Barbara", score: 1000), - ]) - .navigationTitle("Preview") - } -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/AppDatabaseTests.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/AppDatabaseTests.swift deleted file mode 100644 index b736e90774..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/AppDatabaseTests.swift +++ /dev/null @@ -1,144 +0,0 @@ -import XCTest -import GRDB -@testable import GRDBAsyncDemo - -class AppDatabaseTests: XCTestCase { - func test_database_schema() throws { - // Given an empty database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - - // When we instantiate an AppDatabase - _ = try AppDatabase(dbQueue) - - // Then the player table exists, with id, name & score columns - try dbQueue.read { db in - try XCTAssert(db.tableExists("player")) - let columns = try db.columns(in: "player") - let columnNames = Set(columns.map { $0.name }) - XCTAssertEqual(columnNames, ["id", "name", "score"]) - } - } - - func test_savePlayer_inserts() async throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - - // When we save a new player - var player = Player(id: nil, name: "Arthur", score: 100) - try await appDatabase.savePlayer(&player) - - // Then the player exists in the database - let playerExists = try await dbQueue.read { [player] in try player.exists($0) } - XCTAssertTrue(playerExists) - } - - func test_savePlayer_updates() async throws { - // Given a players database that contains a player - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - var player = try await dbQueue.write { db in - try Player(id: nil, name: "Arthur", score: 100).inserted(db) - } - - // When we modify and save the player - player.name = "Barbara" - player.score = 1000 - try await appDatabase.savePlayer(&player) - - // Then the player has been updated in the database - let fetchedPlayer = try await dbQueue.read { [player] db in - try XCTUnwrap(Player.fetchOne(db, key: player.id)) - } - XCTAssertEqual(fetchedPlayer, player) - } - - func test_deletePlayers() async throws { - // Given a players database that contains four players - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - let playerIds: [Int64] = try await dbQueue.write { db in - _ = try Player(id: nil, name: "Arthur", score: 100).inserted(db) - _ = try Player(id: nil, name: "Barbara", score: 200).inserted(db) - _ = try Player(id: nil, name: "Craig", score: 150).inserted(db) - _ = try Player(id: nil, name: "David", score: 120).inserted(db) - return try Player.selectPrimaryKey().fetchAll(db) - } - - // When we delete two players - let deletedId1 = playerIds[0] - let deletedId2 = playerIds[2] - try await appDatabase.deletePlayers(ids: [deletedId1, deletedId2]) - - // Then the deleted players no longer exist - try await dbQueue.read { db in - try XCTAssertFalse(Player.exists(db, id: deletedId1)) - try XCTAssertFalse(Player.exists(db, id: deletedId2)) - } - - // Then the database still contains two players - let count = try await dbQueue.read { try Player.fetchCount($0) } - XCTAssertEqual(count, 2) - } - - func test_deleteAllPlayers() async throws { - // Given a players database that contains players - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - try await dbQueue.write { db in - _ = try Player(id: nil, name: "Arthur", score: 100).inserted(db) - _ = try Player(id: nil, name: "Barbara", score: 200).inserted(db) - _ = try Player(id: nil, name: "Craig", score: 150).inserted(db) - _ = try Player(id: nil, name: "David", score: 120).inserted(db) - } - - // When we delete all players - try await appDatabase.deleteAllPlayers() - - // Then the database does not contain any player - let count = try await dbQueue.read { try Player.fetchCount($0) } - XCTAssertEqual(count, 0) - } - - func test_refreshPlayers_populates_an_empty_database() async throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - - // When we refresh players - try await appDatabase.refreshPlayers() - - // Then the database is not empty - let count = try await dbQueue.read { try Player.fetchCount($0) } - XCTAssert(count > 0) - } - - func test_createRandomPlayersIfEmpty_populates_an_empty_database() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - - // When we create random players - try appDatabase.createRandomPlayersIfEmpty() - - // Then the database is not empty - try XCTAssert(dbQueue.read(Player.fetchCount) > 0) - } - - func test_createRandomPlayersIfEmpty_does_not_modify_a_non_empty_database() throws { - // Given a players database that contains one player - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - var player = Player(id: nil, name: "Arthur", score: 100) - try dbQueue.write { db in - try player.insert(db) - } - - // When we create random players - try appDatabase.createRandomPlayersIfEmpty() - - // Then the database still only contains the original player - let players = try dbQueue.read(Player.fetchAll) - XCTAssertEqual(players, [player]) - } -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/Info.plist b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/Info.plist deleted file mode 100644 index 64d65ca495..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/PlayerRequestTests.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/PlayerRequestTests.swift deleted file mode 100644 index 71d82ab409..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/PlayerRequestTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -import XCTest -import GRDB -@testable import GRDBAsyncDemo - -class PlayerRequestTests: XCTestCase { - func test_PlayerRequest_byName_fetches_well_ordered_players() throws { - // Given a players database that contains two players - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: nil, name: "Arthur", score: 100) - var player2 = Player(id: nil, name: "Barbara", score: 1000) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - } - - // When we fetch players ordered by name - let playerRequest = PlayerRequest(ordering: .byName) - let players = try dbQueue.read(playerRequest.fetch) - - // Then the players are the two players ordered by name - XCTAssertEqual(players, [player1, player2]) - } - - func test_PlayerRequest_byScore_fetches_well_ordered_players() throws { - // Given a players database that contains two players - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: nil, name: "Arthur", score: 100) - var player2 = Player(id: nil, name: "Barbara", score: 1000) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - } - - // When we fetch players ordered by score - let playerRequest = PlayerRequest(ordering: .byScore) - let players = try dbQueue.read(playerRequest.fetch) - - // Then the players are the two players ordered by score descending - XCTAssertEqual(players, [player2, player1]) - } -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/PlayerTests.swift b/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/PlayerTests.swift deleted file mode 100644 index edd8726845..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemoTests/PlayerTests.swift +++ /dev/null @@ -1,108 +0,0 @@ -import XCTest -import GRDB -@testable import GRDBAsyncDemo - -class PlayerTests: XCTestCase { - // MARK: - CRUD - // Test that our Player type properly talks to GRDB. - - func testInsert() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - - // When we insert a player - var player = Player(id: nil, name: "Arthur", score: 100) - try dbQueue.write { db in - try player.insert(db) - } - - // Then the player gets a non-nil id - XCTAssertNotNil(player.id) - } - - func testRoundtrip() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - - // When we insert a player and fetch the player with the same id - var insertedPlayer = Player(id: nil, name: "Arthur", score: 100) - let fetchedPlayer: Player? = try dbQueue.write { db in - try insertedPlayer.insert(db) - return try Player.fetchOne(db, key: insertedPlayer.id) - } - - // Then the fetched player is equal to the inserted player - XCTAssertEqual(insertedPlayer, fetchedPlayer) - } - - // MARK: - Requests - // Test that requests defined on the Player type behave as expected. - - func testOrderedByScore() throws { - // Given a players database that contains players with distinct scores - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: 1, name: "Arthur", score: 100) - var player2 = Player(id: 2, name: "Barbara", score: 200) - var player3 = Player(id: 3, name: "Craig", score: 150) - var player4 = Player(id: 4, name: "David", score: 120) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we fetch players ordered by score - let players = try dbQueue.read(Player.all().orderedByScore().fetchAll) - - // Then fetched players are ordered by score descending - XCTAssertEqual(players, [player2, player3, player4, player1]) - } - - func testOrderedByScoreSortsIdenticalScoresByName() throws { - // Given a players database that contains players with common scores - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: 1, name: "Arthur", score: 100) - var player2 = Player(id: 2, name: "Barbara", score: 200) - var player3 = Player(id: 3, name: "Craig", score: 200) - var player4 = Player(id: 4, name: "David", score: 200) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we fetch players ordered by score - let players = try dbQueue.read(Player.all().orderedByScore().fetchAll) - - // Then fetched players are ordered by score descending and by name - XCTAssertEqual(players, [player2, player3, player4, player1]) - } - - func testOrderedByName() throws { - // Given a players database that contains players with distinct names - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: 1, name: "Arthur", score: 100) - var player2 = Player(id: 2, name: "Barbara", score: 200) - var player3 = Player(id: 3, name: "Craig", score: 150) - var player4 = Player(id: 4, name: "David", score: 120) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we fetch players ordered by name - let players = try dbQueue.read(Player.all().orderedByName().fetchAll) - - // Then fetched players are ordered by name - XCTAssertEqual(players, [player1, player2, player3, player4]) - } -} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/README.md b/Documentation/DemoApps/GRDBAsyncDemo/README.md deleted file mode 100644 index 79389fdb21..0000000000 --- a/Documentation/DemoApps/GRDBAsyncDemo/README.md +++ /dev/null @@ -1,52 +0,0 @@ -Async/Await + SwiftUI Demo Application -====================================== - - - -**This demo application is an Async/Await + SwiftUI application.** For a demo application that uses UIKit, see [GRDBDemoiOS](../GRDBDemoiOS/README.md), and for Combine + SwiftUI, see [GRDBCombineDemo](../GRDBCombineDemo/README.md). - -**Requirements**: iOS 15.0+ / Xcode 13.1+ - -> **Note**: This demo app is not a project template. Do not copy it as a starting point for your application. Instead, create a new project, choose a GRDB [installation method](../../../README.md#installation), and use the demo as an inspiration. - -The topics covered in this demo are: - -- How to setup a database in an iOS app. -- How to define a simple [Codable Record](../../../README.md#codable-records). -- How to track database changes and animate a SwiftUI List with [ValueObservation](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation) Combine publishers. -- How to apply the recommendations of [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices). -- How to perform `async` database accesses. -- How to feed SwiftUI previews with a transient database. - -**Files of interest:** - -- [GRDBAsyncDemoApp.swift](GRDBAsyncDemo/GRDBAsyncDemoApp.swift) - - `GRDBAsyncDemoApp` feeds the app views with a database, through the SwiftUI environment. - -- [AppDatabase.swift](GRDBAsyncDemo/AppDatabase.swift) - - `AppDatabase` is the type that grants database access. It uses [DatabaseMigrator](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasemigrator) in order to setup the database schema. - -- [Persistence.swift](GRDBAsyncDemo/Persistence.swift) - - This file instantiates various `AppDatabase` for the various projects needs: one database on disk for the application, and in-memory databases for SwiftUI previews. - -- [Player.swift](GRDBAsyncDemo/Player.swift) - - `Player` is a [Record](../../../README.md#records) type, able to read and write in the database. It conforms to the standard Codable protocol in order to gain all advantages of [Codable Records](../../../README.md#codable-records). - -- [PlayerRequest.swift](GRDBAsyncDemo/PlayerRequest.swift), [AppView.swift](GRDBAsyncDemo/Views/AppView.swift) - - `PlayerRequest` defines the player requests used by the app (sorted by score, or by name). - - `PlayerRequest` feeds the `@Query` property wrapper (`@Query`, defined in [GRDBQuery](https://github.com/groue/GRDBQuery), allows SwiftUI views to display up-to-date database content). - - `AppView` is the SwiftUI view that uses `@Query` in order to feed its player list. - -- [GRDBAsyncDemoTests](GRDBAsyncDemoTests) - - - Test the database schema - - Test the `Player` record and its requests - - Test the `PlayerRequest` methods that feed the list of players. - - Test the `AppDatabase` methods that let the app access the database. diff --git a/Documentation/DemoApps/GRDBAsyncDemo/Screenshot.png b/Documentation/DemoApps/GRDBAsyncDemo/Screenshot.png deleted file mode 100644 index 0536821854..0000000000 Binary files a/Documentation/DemoApps/GRDBAsyncDemo/Screenshot.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.pbxproj b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.pbxproj deleted file mode 100644 index f1892a540c..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.pbxproj +++ /dev/null @@ -1,611 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */; }; - 56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */; }; - 56519DC7274FC85600ED16D8 /* GRDBQuery in Frameworks */ = {isa = PBXBuildFile; productRef = 56519DC6274FC85600ED16D8 /* GRDBQuery */; }; - 5671723A261B23C800423B6F /* PlayerList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717239261B23C800423B6F /* PlayerList.swift */; }; - 56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56717251261B334D00423B6F /* PlayerRequestTests.swift */; }; - 567C3E1A2520B6DE0011F6E9 /* GRDBCombineDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E192520B6DE0011F6E9 /* GRDBCombineDemoApp.swift */; }; - 567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */; }; - 567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */; }; - 567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E532520B75C0011F6E9 /* Player.swift */; }; - 567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E542520B75C0011F6E9 /* Persistence.swift */; }; - 567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E592520B75C0011F6E9 /* PlayerFormView.swift */; }; - 567C3E622520B75D0011F6E9 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5A2520B75C0011F6E9 /* AppView.swift */; }; - 567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */; }; - 567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */; }; - 567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567C3E652520B7880011F6E9 /* AppDatabase.swift */; }; - 567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E752520BB650011F6E9 /* Localizable.stringsdict */; }; - 567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */; }; - 56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B6D1082619EC1B003CC455 /* PlayerRequest.swift */; }; - 56F8A1202735989D0011ACBE /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56F8A11F2735989D0011ACBE /* GRDB */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 56026C9D25B8A7D000D1DF3F /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 567C3E0E2520B6DE0011F6E9 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 567C3E152520B6DE0011F6E9; - remoteInfo = GRDBCombineDemo; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 567C3E502520B70E0011F6E9 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 56026C9825B8A7D000D1DF3F /* GRDBCombineDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBCombineDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 56026C9C25B8A7D000D1DF3F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = ""; }; - 56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabaseTests.swift; sourceTree = ""; }; - 56717239261B23C800423B6F /* PlayerList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerList.swift; sourceTree = ""; }; - 56717251261B334D00423B6F /* PlayerRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequestTests.swift; sourceTree = ""; }; - 567C3E162520B6DE0011F6E9 /* GRDBCombineDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBCombineDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 567C3E192520B6DE0011F6E9 /* GRDBCombineDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBCombineDemoApp.swift; sourceTree = ""; }; - 567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 567C3E222520B6DF0011F6E9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 567C3E532520B75C0011F6E9 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; - 567C3E542520B75C0011F6E9 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; - 567C3E592520B75C0011F6E9 /* PlayerFormView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerFormView.swift; sourceTree = ""; }; - 567C3E5A2520B75C0011F6E9 /* AppView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = ""; }; - 567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerCreationView.swift; sourceTree = ""; }; - 567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerEditionView.swift; sourceTree = ""; }; - 567C3E652520B7880011F6E9 /* AppDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = ""; }; - 567C3E762520BB650011F6E9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; - 567C3E782520BB650011F6E9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 56B6D1082619EC1B003CC455 /* PlayerRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerRequest.swift; sourceTree = ""; }; - 56F8A11C2735988F0011ACBE /* GRDB.swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = GRDB.swift; path = ../../..; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 56026C9525B8A7D000D1DF3F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 567C3E132520B6DE0011F6E9 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 56519DC7274FC85600ED16D8 /* GRDBQuery in Frameworks */, - 56F8A1202735989D0011ACBE /* GRDB in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 56026C9925B8A7D000D1DF3F /* GRDBCombineDemoTests */ = { - isa = PBXGroup; - children = ( - 56026C9C25B8A7D000D1DF3F /* Info.plist */, - 56026CAB25B8A7EF00D1DF3F /* AppDatabaseTests.swift */, - 56717251261B334D00423B6F /* PlayerRequestTests.swift */, - 56026CAA25B8A7EF00D1DF3F /* PlayerTests.swift */, - ); - path = GRDBCombineDemoTests; - sourceTree = ""; - }; - 56185BC125B8047D00B9C30F /* Resources */ = { - isa = PBXGroup; - children = ( - 567C3E1D2520B6DF0011F6E9 /* Assets.xcassets */, - 567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */, - 567C3E752520BB650011F6E9 /* Localizable.stringsdict */, - ); - path = Resources; - sourceTree = ""; - }; - 567C3E0D2520B6DE0011F6E9 = { - isa = PBXGroup; - children = ( - 567C3E182520B6DE0011F6E9 /* GRDBCombineDemo */, - 56026C9925B8A7D000D1DF3F /* GRDBCombineDemoTests */, - 567C3E172520B6DE0011F6E9 /* Products */, - 567C3E4D2520B70E0011F6E9 /* Frameworks */, - 56F8A11C2735988F0011ACBE /* GRDB.swift */, - ); - sourceTree = ""; - }; - 567C3E172520B6DE0011F6E9 /* Products */ = { - isa = PBXGroup; - children = ( - 567C3E162520B6DE0011F6E9 /* GRDBCombineDemo.app */, - 56026C9825B8A7D000D1DF3F /* GRDBCombineDemoTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 567C3E182520B6DE0011F6E9 /* GRDBCombineDemo */ = { - isa = PBXGroup; - children = ( - 567C3E222520B6DF0011F6E9 /* Info.plist */, - 567C3E652520B7880011F6E9 /* AppDatabase.swift */, - 567C3E192520B6DE0011F6E9 /* GRDBCombineDemoApp.swift */, - 567C3E542520B75C0011F6E9 /* Persistence.swift */, - 567C3E532520B75C0011F6E9 /* Player.swift */, - 56B6D1082619EC1B003CC455 /* PlayerRequest.swift */, - 567C3E1F2520B6DF0011F6E9 /* Preview Content */, - 56185BC125B8047D00B9C30F /* Resources */, - 567C3E582520B75C0011F6E9 /* Views */, - ); - path = GRDBCombineDemo; - sourceTree = ""; - }; - 567C3E1F2520B6DF0011F6E9 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 567C3E202520B6DF0011F6E9 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - 567C3E4D2520B70E0011F6E9 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; - 567C3E582520B75C0011F6E9 /* Views */ = { - isa = PBXGroup; - children = ( - 567C3E5A2520B75C0011F6E9 /* AppView.swift */, - 567C3E5B2520B75C0011F6E9 /* PlayerCreationView.swift */, - 567C3E5C2520B75C0011F6E9 /* PlayerEditionView.swift */, - 567C3E592520B75C0011F6E9 /* PlayerFormView.swift */, - 56717239261B23C800423B6F /* PlayerList.swift */, - ); - path = Views; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 56026C9725B8A7D000D1DF3F /* GRDBCombineDemoTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 56026CA825B8A7D000D1DF3F /* Build configuration list for PBXNativeTarget "GRDBCombineDemoTests" */; - buildPhases = ( - 56026C9425B8A7D000D1DF3F /* Sources */, - 56026C9525B8A7D000D1DF3F /* Frameworks */, - 56026C9625B8A7D000D1DF3F /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 56026C9E25B8A7D000D1DF3F /* PBXTargetDependency */, - ); - name = GRDBCombineDemoTests; - productName = GRDBCombineDemoTests; - productReference = 56026C9825B8A7D000D1DF3F /* GRDBCombineDemoTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 567C3E152520B6DE0011F6E9 /* GRDBCombineDemo */ = { - isa = PBXNativeTarget; - buildConfigurationList = 567C3E252520B6DF0011F6E9 /* Build configuration list for PBXNativeTarget "GRDBCombineDemo" */; - buildPhases = ( - 567C3E122520B6DE0011F6E9 /* Sources */, - 567C3E132520B6DE0011F6E9 /* Frameworks */, - 567C3E142520B6DE0011F6E9 /* Resources */, - 567C3E502520B70E0011F6E9 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 56F8A11E273598960011ACBE /* PBXTargetDependency */, - ); - name = GRDBCombineDemo; - packageProductDependencies = ( - 56F8A11F2735989D0011ACBE /* GRDB */, - 56519DC6274FC85600ED16D8 /* GRDBQuery */, - ); - productName = GRBCombineDemo; - productReference = 567C3E162520B6DE0011F6E9 /* GRDBCombineDemo.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 567C3E0E2520B6DE0011F6E9 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 1200; - TargetAttributes = { - 56026C9725B8A7D000D1DF3F = { - CreatedOnToolsVersion = 12.3; - TestTargetID = 567C3E152520B6DE0011F6E9; - }; - 567C3E152520B6DE0011F6E9 = { - CreatedOnToolsVersion = 12.0; - }; - }; - }; - buildConfigurationList = 567C3E112520B6DE0011F6E9 /* Build configuration list for PBXProject "GRDBCombineDemo" */; - compatibilityVersion = "Xcode 12.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 567C3E0D2520B6DE0011F6E9; - packageReferences = ( - 56519DC5274FC85600ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */, - ); - productRefGroup = 567C3E172520B6DE0011F6E9 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 567C3E152520B6DE0011F6E9 /* GRDBCombineDemo */, - 56026C9725B8A7D000D1DF3F /* GRDBCombineDemoTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 56026C9625B8A7D000D1DF3F /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 567C3E142520B6DE0011F6E9 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 567C3E212520B6DF0011F6E9 /* Preview Assets.xcassets in Resources */, - 567C3E7A2520BB650011F6E9 /* LaunchScreen.storyboard in Resources */, - 567C3E1E2520B6DF0011F6E9 /* Assets.xcassets in Resources */, - 567C3E792520BB650011F6E9 /* Localizable.stringsdict in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 56026C9425B8A7D000D1DF3F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 56026CAC25B8A7EF00D1DF3F /* PlayerTests.swift in Sources */, - 56026CAD25B8A7EF00D1DF3F /* AppDatabaseTests.swift in Sources */, - 56717252261B334D00423B6F /* PlayerRequestTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 567C3E122520B6DE0011F6E9 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 567C3E5E2520B75C0011F6E9 /* Persistence.swift in Sources */, - 567C3E5D2520B75C0011F6E9 /* Player.swift in Sources */, - 56B6D1092619EC1B003CC455 /* PlayerRequest.swift in Sources */, - 5671723A261B23C800423B6F /* PlayerList.swift in Sources */, - 567C3E612520B75D0011F6E9 /* PlayerFormView.swift in Sources */, - 567C3E632520B75D0011F6E9 /* PlayerCreationView.swift in Sources */, - 567C3E662520B7880011F6E9 /* AppDatabase.swift in Sources */, - 567C3E622520B75D0011F6E9 /* AppView.swift in Sources */, - 567C3E642520B75D0011F6E9 /* PlayerEditionView.swift in Sources */, - 567C3E1A2520B6DE0011F6E9 /* GRDBCombineDemoApp.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 56026C9E25B8A7D000D1DF3F /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 567C3E152520B6DE0011F6E9 /* GRDBCombineDemo */; - targetProxy = 56026C9D25B8A7D000D1DF3F /* PBXContainerItemProxy */; - }; - 56F8A11E273598960011ACBE /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = 56F8A11D273598960011ACBE /* GRDB */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 567C3E752520BB650011F6E9 /* Localizable.stringsdict */ = { - isa = PBXVariantGroup; - children = ( - 567C3E762520BB650011F6E9 /* en */, - ); - name = Localizable.stringsdict; - sourceTree = ""; - }; - 567C3E772520BB650011F6E9 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 567C3E782520BB650011F6E9 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 56026C9F25B8A7D000D1DF3F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = GRDBCombineDemoTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBCombineDemoTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBCombineDemo.app/GRDBCombineDemo"; - }; - name = Debug; - }; - 56026CA025B8A7D000D1DF3F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = GRDBCombineDemoTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBCombineDemoTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBCombineDemo.app/GRDBCombineDemo"; - }; - name = Release; - }; - 567C3E232520B6DF0011F6E9 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 567C3E242520B6DF0011F6E9 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 567C3E262520B6DF0011F6E9 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"GRDBCombineDemo/Preview Content\""; - DEVELOPMENT_TEAM = ""; - ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = GRDBCombineDemo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBCombineDemo; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Debug; - }; - 567C3E272520B6DF0011F6E9 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = "\"GRDBCombineDemo/Preview Content\""; - DEVELOPMENT_TEAM = ""; - ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = GRDBCombineDemo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBCombineDemo; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 56026CA825B8A7D000D1DF3F /* Build configuration list for PBXNativeTarget "GRDBCombineDemoTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 56026C9F25B8A7D000D1DF3F /* Debug */, - 56026CA025B8A7D000D1DF3F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 567C3E112520B6DE0011F6E9 /* Build configuration list for PBXProject "GRDBCombineDemo" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 567C3E232520B6DF0011F6E9 /* Debug */, - 567C3E242520B6DF0011F6E9 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 567C3E252520B6DF0011F6E9 /* Build configuration list for PBXNativeTarget "GRDBCombineDemo" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 567C3E262520B6DF0011F6E9 /* Debug */, - 567C3E272520B6DF0011F6E9 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 56519DC5274FC85600ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/groue/GRDBQuery"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.6.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 56519DC6274FC85600ED16D8 /* GRDBQuery */ = { - isa = XCSwiftPackageProductDependency; - package = 56519DC5274FC85600ED16D8 /* XCRemoteSwiftPackageReference "GRDBQuery" */; - productName = GRDBQuery; - }; - 56F8A11D273598960011ACBE /* GRDB */ = { - isa = XCSwiftPackageProductDependency; - productName = GRDB; - }; - 56F8A11F2735989D0011ACBE /* GRDB */ = { - isa = XCSwiftPackageProductDependency; - productName = GRDB; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 567C3E0E2520B6DE0011F6E9 /* Project object */; -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 216295601d..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pins" : [ - { - "identity" : "grdbquery", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/GRDBQuery", - "state" : { - "revision" : "a6c46dd38ecf11a5c37732870dc03a384d582fba", - "version" : "0.9.0" - } - } - ], - "version" : 2 -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/xcshareddata/xcschemes/GRDBCombineDemo.xcscheme b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/xcshareddata/xcschemes/GRDBCombineDemo.xcscheme deleted file mode 100644 index 0cbf7bf5c1..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/xcshareddata/xcschemes/GRDBCombineDemo.xcscheme +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/AppDatabase.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/AppDatabase.swift deleted file mode 100644 index c4b5a5e920..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/AppDatabase.swift +++ /dev/null @@ -1,235 +0,0 @@ -import Foundation -import GRDB -import os.log - -/// A database of players. -/// -/// You create an `AppDatabase` with a connection to an SQLite database -/// (see ). -/// -/// Create those connections with a configuration returned from -/// `AppDatabase/makeConfiguration(_:)`. -/// -/// For example: -/// -/// ```swift -/// // Create an in-memory AppDatabase -/// let config = AppDatabase.makeConfiguration() -/// let dbQueue = try DatabaseQueue(configuration: config) -/// let appDatabase = try AppDatabase(dbQueue) -/// ``` -struct AppDatabase { - /// Creates an `AppDatabase`, and makes sure the database schema - /// is ready. - /// - /// - important: Create the `DatabaseWriter` with a configuration - /// returned by ``makeConfiguration(_:)``. - init(_ dbWriter: any DatabaseWriter) throws { - self.dbWriter = dbWriter - try migrator.migrate(dbWriter) - } - - /// Provides access to the database. - /// - /// Application can use a `DatabasePool`, while SwiftUI previews and tests - /// can use a fast in-memory `DatabaseQueue`. - /// - /// See - private let dbWriter: any DatabaseWriter -} - -// MARK: - Database Configuration - -extension AppDatabase { - private static let sqlLogger = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SQL") - - /// Returns a database configuration suited for `PlayerRepository`. - /// - /// SQL statements are logged if the `SQL_TRACE` environment variable - /// is set. - /// - /// - parameter base: A base configuration. - public static func makeConfiguration(_ base: Configuration = Configuration()) -> Configuration { - var config = base - - // An opportunity to add required custom SQL functions or - // collations, if needed: - // config.prepareDatabase { db in - // db.add(function: ...) - // } - - // Log SQL statements if the `SQL_TRACE` environment variable is set. - // See - if ProcessInfo.processInfo.environment["SQL_TRACE"] != nil { - config.prepareDatabase { db in - db.trace { - // It's ok to log statements publicly. Sensitive - // information (statement arguments) are not logged - // unless config.publicStatementArguments is set - // (see below). - os_log("%{public}@", log: sqlLogger, type: .debug, String(describing: $0)) - } - } - } - -#if DEBUG - // Protect sensitive information by enabling verbose debugging in - // DEBUG builds only. - // See - config.publicStatementArguments = true -#endif - - return config - } -} - -// MARK: - Database Migrations - -extension AppDatabase { - /// The DatabaseMigrator that defines the database schema. - /// - /// See - private var migrator: DatabaseMigrator { - var migrator = DatabaseMigrator() - -#if DEBUG - // Speed up development by nuking the database when migrations change - // See - migrator.eraseDatabaseOnSchemaChange = true -#endif - - migrator.registerMigration("createPlayer") { db in - // Create a table - // See - try db.create(table: "player") { t in - t.autoIncrementedPrimaryKey("id") - t.column("name", .text).notNull() - t.column("score", .integer).notNull() - } - } - - // Migrations for future application versions will be inserted here: - // migrator.registerMigration(...) { db in - // ... - // } - - return migrator - } -} - -// MARK: - Database Access: Writes -// The write methods execute invariant-preserving database transactions. - -extension AppDatabase { - /// A validation error that prevents some players from being saved into - /// the database. - enum ValidationError: LocalizedError { - case missingName - - var errorDescription: String? { - switch self { - case .missingName: - return "Please provide a name" - } - } - } - - /// Saves (inserts or updates) a player. When the method returns, the - /// player is present in the database, and its id is not nil. - func savePlayer(_ player: inout Player) throws { - if player.name.isEmpty { - throw ValidationError.missingName - } - try dbWriter.write { db in - try player.save(db) - } - } - - /// Delete the specified players - func deletePlayers(ids: [Int64]) throws { - try dbWriter.write { db in - _ = try Player.deleteAll(db, ids: ids) - } - } - - /// Delete all players - func deleteAllPlayers() throws { - try dbWriter.write { db in - _ = try Player.deleteAll(db) - } - } - - /// Refresh all players (by performing some random changes, for demo purpose). - func refreshPlayers() throws { - try dbWriter.write { db in - if try Player.all().isEmpty(db) { - // When database is empty, insert new random players - try createRandomPlayers(db) - } else { - // Insert a player - if Bool.random() { - _ = try Player.makeRandom().inserted(db) // insert but ignore inserted id - } - - // Delete a random player - if Bool.random() { - try Player.order(sql: "RANDOM()").limit(1).deleteAll(db) - } - - // Update some players - for var player in try Player.fetchAll(db) where Bool.random() { - try player.updateChanges(db) { - $0.score = Player.randomScore() - } - } - } - } - } - - /// Create random players if the database is empty. - func createRandomPlayersIfEmpty() throws { - try dbWriter.write { db in - if try Player.all().isEmpty(db) { - try createRandomPlayers(db) - } - } - } - - private static let uiTestPlayers = [ - Player(id: nil, name: "Arthur", score: 5), - Player(id: nil, name: "Barbara", score: 6), - Player(id: nil, name: "Craig", score: 8), - Player(id: nil, name: "David", score: 4), - Player(id: nil, name: "Elena", score: 1), - Player(id: nil, name: "Frederik", score: 2), - Player(id: nil, name: "Gilbert", score: 7), - Player(id: nil, name: "Henriette", score: 3)] - - func createPlayersForUITests() throws { - try dbWriter.write { db in - try AppDatabase.uiTestPlayers.forEach { player in - _ = try player.inserted(db) // insert but ignore inserted id - } - } - } - - /// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`. - private func createRandomPlayers(_ db: Database) throws { - for _ in 0..<8 { - _ = try Player.makeRandom().inserted(db) // insert but ignore inserted id - } - } -} - -// MARK: - Database Access: Reads - -// This demo app does not provide any specific reading method, and instead -// gives an unrestricted read-only access to the rest of the application. -// In your app, you are free to choose another path, and define focused -// reading methods. -extension AppDatabase { - /// Provides a read-only access to the database - var reader: DatabaseReader { - dbWriter - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/GRDBCombineDemoApp.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/GRDBCombineDemoApp.swift deleted file mode 100644 index 27db2c5ea4..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/GRDBCombineDemoApp.swift +++ /dev/null @@ -1,32 +0,0 @@ -import GRDBQuery -import SwiftUI - -@main -struct GRDBCombineDemoApp: App { - var body: some Scene { - WindowGroup { - AppView().appDatabase(.shared) - } - } -} - -// MARK: - Give SwiftUI access to the database - -private struct AppDatabaseKey: EnvironmentKey { - static var defaultValue: AppDatabase { .empty() } -} - -extension EnvironmentValues { - var appDatabase: AppDatabase { - get { self[AppDatabaseKey.self] } - set { self[AppDatabaseKey.self] = newValue } - } -} - -extension View { - func appDatabase(_ appDatabase: AppDatabase) -> some View { - self - .environment(\.appDatabase, appDatabase) - .databaseContext(.readOnly { appDatabase.reader }) - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Info.plist b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Info.plist deleted file mode 100644 index 4754bb8682..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Info.plist +++ /dev/null @@ -1,50 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - - UIApplicationSupportsIndirectInputEvents - - UILaunchScreen - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Persistence.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Persistence.swift deleted file mode 100644 index 05b0115789..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Persistence.swift +++ /dev/null @@ -1,77 +0,0 @@ -import Foundation -import GRDB - -extension AppDatabase { - /// The database for the application - static let shared = makeShared() - - private static func makeShared() -> AppDatabase { - do { - // Apply recommendations from - // - // - // Create the "Application Support/Database" directory if needed - let fileManager = FileManager.default - let appSupportURL = try fileManager.url( - for: .applicationSupportDirectory, in: .userDomainMask, - appropriateFor: nil, create: true) - let directoryURL = appSupportURL.appendingPathComponent("Database", isDirectory: true) - - // Support for tests: delete the database if requested - if CommandLine.arguments.contains("-reset") { - try? fileManager.removeItem(at: directoryURL) - } - - // Create the database folder if needed - try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true) - - // Open or create the database - let databaseURL = directoryURL.appendingPathComponent("db.sqlite") - NSLog("Database stored at \(databaseURL.path)") - let dbPool = try DatabasePool( - path: databaseURL.path, - // Use default AppDatabase configuration - configuration: AppDatabase.makeConfiguration()) - - // Create the AppDatabase - let appDatabase = try AppDatabase(dbPool) - - // Prepare the database with test fixtures if requested - if CommandLine.arguments.contains("-fixedTestData") { - try appDatabase.createPlayersForUITests() - } else { - // Otherwise, populate the database if it is empty, for better - // demo purpose. - try appDatabase.createRandomPlayersIfEmpty() - } - - return appDatabase - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. - // - // Typical reasons for an error here include: - // * The parent directory cannot be created, or disallows writing. - // * The database is not accessible, due to permissions or data protection when the device is locked. - // * The device is out of space. - // * The database could not be migrated to its latest schema version. - // Check the error message to determine what the actual problem was. - fatalError("Unresolved error \(error)") - } - } - - /// Creates an empty database for SwiftUI previews - static func empty() -> AppDatabase { - // Connect to an in-memory database - // See https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections - let dbQueue = try! DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - return try! AppDatabase(dbQueue) - } - - /// Creates a database full of random players for SwiftUI previews - static func random() -> AppDatabase { - let appDatabase = empty() - try! appDatabase.createRandomPlayersIfEmpty() - return appDatabase - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Player.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Player.swift deleted file mode 100644 index 7876b049df..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Player.swift +++ /dev/null @@ -1,105 +0,0 @@ -import GRDB - -/// The Player struct. -/// -/// Identifiable conformance supports SwiftUI list animations, and type-safe -/// GRDB primary key methods. -/// Equatable conformance supports tests. -struct Player: Identifiable, Equatable { - /// The player id. - /// - /// Int64 is the recommended type for auto-incremented database ids. - /// Use nil for players that are not inserted yet in the database. - var id: Int64? - var name: String - var score: Int -} - -extension Player { - private static let names = [ - "Arthur", "Anita", "Barbara", "Bernard", "Craig", "Chiara", "David", - "Dean", "Éric", "Elena", "Fatima", "Frederik", "Gilbert", "Georgette", - "Henriette", "Hassan", "Ignacio", "Irene", "Julie", "Jack", "Karl", - "Kristel", "Louis", "Liz", "Masashi", "Mary", "Noam", "Nicole", - "Ophelie", "Oleg", "Pascal", "Patricia", "Quentin", "Quinn", "Raoul", - "Rachel", "Stephan", "Susie", "Tristan", "Tatiana", "Ursule", "Urbain", - "Victor", "Violette", "Wilfried", "Wilhelmina", "Yvon", "Yann", - "Zazie", "Zoé"] - - /// Creates a new player with empty name and zero score - static func new() -> Player { - Player(id: nil, name: "", score: 0) - } - - /// Creates a new player with random name and random score - static func makeRandom() -> Player { - Player(id: nil, name: randomName(), score: randomScore()) - } - - /// Returns a random name - static func randomName() -> String { - names.randomElement()! - } - - /// Returns a random score - static func randomScore() -> Int { - 10 * Int.random(in: 0...100) - } -} - -// MARK: - Persistence - -/// Make Player a Codable Record. -/// -/// See -extension Player: Codable, FetchableRecord, MutablePersistableRecord { - // Define database columns from CodingKeys - fileprivate enum Columns { - static let name = Column(CodingKeys.name) - static let score = Column(CodingKeys.score) - } - - /// Updates a player id after it has been inserted in the database. - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } -} - -// MARK: - Player Database Requests - -/// Define some player requests used by the application. -/// -/// See -extension DerivableRequest { - /// A request of players ordered by name. - /// - /// For example: - /// - /// let players: [Player] = try dbWriter.read { db in - /// try Player.all().orderedByName().fetchAll(db) - /// } - func orderedByName() -> Self { - // Sort by name in a localized case insensitive fashion - // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison - order(Player.Columns.name.collating(.localizedCaseInsensitiveCompare)) - } - - /// A request of players ordered by score. - /// - /// For example: - /// - /// let players: [Player] = try dbWriter.read { db in - /// try Player.all().orderedByScore().fetchAll(db) - /// } - /// let bestPlayer: Player? = try dbWriter.read { db in - /// try Player.all().orderedByScore().fetchOne(db) - /// } - func orderedByScore() -> Self { - // Sort by descending score, and then by name, in a - // localized case insensitive fashion - // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison - order( - Player.Columns.score.desc, - Player.Columns.name.collating(.localizedCaseInsensitiveCompare)) - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/PlayerRequest.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/PlayerRequest.swift deleted file mode 100644 index 4d1d924605..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/PlayerRequest.swift +++ /dev/null @@ -1,35 +0,0 @@ -import GRDB -import GRDBQuery - -/// A player request can be used with the `@Query` property wrapper in order to -/// feed a view with a list of players. -/// -/// For example: -/// -/// struct MyView: View { -/// @Query(PlayerRequest(ordering: .byName)) private var players: [Player] -/// -/// var body: some View { -/// List(players) { player in ... ) -/// } -/// } -struct PlayerRequest: ValueObservationQueryable { - enum Ordering { - case byScore - case byName - } - - static var defaultValue: [Player] { [] } - - /// The ordering used by the player request. - var ordering: Ordering - - func fetch(_ db: Database) throws -> [Player] { - switch ordering { - case .byScore: - return try Player.all().orderedByScore().fetchAll(db) - case .byName: - return try Player.all().orderedByName().fetchAll(db) - } - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 29d91251df..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "icon_20pt@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "icon_20pt@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "icon_29pt@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "icon_29pt@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "icon_40pt@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "icon_40pt@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "icon_60pt@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "icon_60pt@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "icon_20pt.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "icon_20pt@2x-1.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "icon_29pt.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "icon_29pt@2x-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "icon_40pt.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "icon_40pt@2x-1.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "icon_76pt.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "icon_76pt@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "icon_83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png deleted file mode 100644 index 66b1931a14..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png deleted file mode 100644 index 90648b3f40..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png deleted file mode 100644 index a077a6f490..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png deleted file mode 100644 index a077a6f490..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png deleted file mode 100644 index 600bdbd9cd..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png deleted file mode 100644 index 8e04af0dd8..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png deleted file mode 100644 index 686e8d99e2..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png deleted file mode 100644 index 686e8d99e2..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png deleted file mode 100644 index 1d013c3d33..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png deleted file mode 100644 index a077a6f490..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png deleted file mode 100644 index da66b9ba82..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png deleted file mode 100644 index da66b9ba82..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png deleted file mode 100644 index 59346ef4b6..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png deleted file mode 100644 index 59346ef4b6..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png deleted file mode 100644 index d4640afc9a..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png deleted file mode 100644 index e3a04522bf..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png deleted file mode 100644 index 593ebd783d..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png deleted file mode 100644 index ca02cd03bc..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json deleted file mode 100644 index 2cbe59d5ec..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "LaunchIcon.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf deleted file mode 100644 index 2660891492..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf and /dev/null differ diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Base.lproj/LaunchScreen.storyboard b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 79f85a1dd9..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/en.lproj/Localizable.stringsdict b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/en.lproj/Localizable.stringsdict deleted file mode 100644 index 2d9aa217c4..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/en.lproj/Localizable.stringsdict +++ /dev/null @@ -1,42 +0,0 @@ - - - - - %lld Players - - NSStringLocalizedFormatKey - %#@VARIABLE@ - VARIABLE - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - lld - zero - No Player - one - 1 Player - other - %lld Players - - - %lld points - - NSStringLocalizedFormatKey - %#@VARIABLE@ - VARIABLE - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - lld - zero - 0 point - one - 1 point - other - %lld points - - - - diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/AppView.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/AppView.swift deleted file mode 100644 index 7c93e15cb8..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/AppView.swift +++ /dev/null @@ -1,145 +0,0 @@ -import GRDBQuery -import SwiftUI - -/// The main application view -struct AppView: View { - /// Write access to the database - @Environment(\.appDatabase) private var appDatabase - - /// The `players` property is automatically updated when the database changes - @Query(PlayerRequest(ordering: .byScore)) private var players: [Player] - - /// We'll need to leave edit mode in several occasions. - @State private var editMode = EditMode.inactive - - /// Tracks the presentation of the player creation sheet. - @State private var newPlayerIsPresented = false - - // If you want to define the query on initialization, you will prefer: - // - // @Query private var players: [Player] - // - // init(initialOrdering: PlayerRequest.Ordering) { - // _players = Query(PlayerRequest(ordering: initialOrdering)) - // } - - var body: some View { - NavigationView { - PlayerList(players: players) - .navigationBarTitle(Text("\(players.count) Players")) - .navigationBarItems( - leading: HStack { - EditButton() - newPlayerButton - }, - trailing: ToggleOrderingButton( - ordering: $players.ordering, - willChange: { - // onChange(of: $players.wrappedValue.ordering) - // is not able to leave the editing mode during - // the animation of the list content. - // Workaround: stop editing before the ordering - // is changed, and the list content is updated. - stopEditing() - })) - .toolbar { toolbarContent } - .onChange(of: players) { - if players.isEmpty { - stopEditing() - } - } - .environment(\.editMode, $editMode) - } - } - - private var toolbarContent: some ToolbarContent { - ToolbarItemGroup(placement: .bottomBar) { - Button { - // Don't stopEditing() here because this is - // performed `onChange(of: players)` - try! appDatabase.deleteAllPlayers() - } label: { - Image(systemName: "trash").imageScale(.large) - } - - Spacer() - - Button { - stopEditing() - try! appDatabase.refreshPlayers() - } label: { - Image(systemName: "arrow.clockwise").imageScale(.large) - } - - Spacer() - - Button { - stopEditing() - // Perform 50 refreshes in parallel - for _ in 0..<50 { - DispatchQueue.global().async { - try! AppDatabase.shared.refreshPlayers() - } - } - } label: { - Image(systemName: "tornado").imageScale(.large) - } - } - } - - /// The button that presents the player creation sheet. - private var newPlayerButton: some View { - Button { - stopEditing() - newPlayerIsPresented = true - } label: { - Image(systemName: "plus") - } - .accessibility(label: Text("New Player")) - .sheet(isPresented: $newPlayerIsPresented) { - PlayerCreationView() - } - } - - private func stopEditing() { - withAnimation { - editMode = .inactive - } - } -} - -private struct ToggleOrderingButton: View { - @Binding var ordering: PlayerRequest.Ordering - let willChange: () -> Void - - var body: some View { - switch ordering { - case .byName: - Button { - willChange() - ordering = .byScore - } label: { - Label("Name", systemImage: "arrowtriangle.up.fill").labelStyle(.titleAndIcon) - } - case .byScore: - Button { - willChange() - ordering = .byName - } label: { - Label("Score", systemImage: "arrowtriangle.down.fill").labelStyle(.titleAndIcon) - } - } - } -} - -// MARK: - Previews - -#Preview("Empty") { - // Preview the default, empty database - AppView() -} - -#Preview("Populated") { - // Preview a database of random players - AppView().appDatabase(.random()) -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerCreationView.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerCreationView.swift deleted file mode 100644 index f86876fd9f..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerCreationView.swift +++ /dev/null @@ -1,50 +0,0 @@ -import SwiftUI - -/// The view that creates a new player. -struct PlayerCreationView: View { - /// Write access to the database - @Environment(\.appDatabase) private var appDatabase - @Environment(\.dismiss) private var dismiss - @State private var form = PlayerForm(name: "", score: "") - @State private var errorAlertIsPresented = false - @State private var errorAlertTitle = "" - - var body: some View { - NavigationView { - PlayerFormView(form: $form) - .alert( - isPresented: $errorAlertIsPresented, - content: { Alert(title: Text(errorAlertTitle)) }) - .navigationBarTitle("New Player") - .navigationBarItems( - leading: Button(role: .cancel) { - dismiss() - } label: { - Text("Cancel") - }, - trailing: Button { - save() - } label: { - Text("Save") - }) - } - } - - private func save() { - do { - var player = Player(id: nil, name: "", score: 0) - form.apply(to: &player) - try appDatabase.savePlayer(&player) - dismiss() - } catch { - errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred" - errorAlertIsPresented = true - } - } -} - -// MARK: - Previews - -#Preview { - PlayerCreationView() -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerEditionView.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerEditionView.swift deleted file mode 100644 index a3c559a1af..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerEditionView.swift +++ /dev/null @@ -1,38 +0,0 @@ -import SwiftUI - -/// The view that edits an existing player. -struct PlayerEditionView: View { - /// Write access to the database - @Environment(\.appDatabase) private var appDatabase - @Environment(\.isPresented) private var isPresented - private let player: Player - @State private var form: PlayerForm - - init(player: Player) { - self.player = player - self.form = PlayerForm(player) - } - - var body: some View { - PlayerFormView(form: $form) - .onChange(of: isPresented) { - // Save when back button is pressed - if !isPresented { - var savedPlayer = player - form.apply(to: &savedPlayer) - // Ignore error because I don't know how to cancel the - // back button and present the error - try? appDatabase.savePlayer(&savedPlayer) - } - } - } -} - -// MARK: - Previews - -#Preview { - NavigationView { - PlayerEditionView(player: Player.makeRandom()) - .navigationBarTitle("Player Edition") - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerFormView.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerFormView.swift deleted file mode 100644 index 590506d465..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerFormView.swift +++ /dev/null @@ -1,48 +0,0 @@ -import SwiftUI - -/// The Player editing form, embedded in both -/// `PlayerCreationView` and `PlayerEditionView`. -struct PlayerFormView: View { - @Binding var form: PlayerForm - - var body: some View { - List { - TextField("Name", text: $form.name) - .accessibility(label: Text("Player Name")) - TextField("Score", text: $form.score).keyboardType(.numberPad) - .accessibility(label: Text("Player Score")) - } - .listStyle(InsetGroupedListStyle()) - } -} - -struct PlayerForm { - var name: String - var score: String -} - -extension PlayerForm { - init(_ player: Player) { - self.name = player.name - self.score = "\(player.score)" - } - - func apply(to player: inout Player) { - player.name = name - player.score = Int(score) ?? 0 - } -} - -// MARK: - Previews - -#Preview("Empty") { - PlayerFormView(form: .constant(PlayerForm( - name: "", - score: ""))) -} - -#Preview("Prefilled") { - PlayerFormView(form: .constant(PlayerForm( - name: Player.randomName(), - score: "\(Player.randomScore())"))) -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerList.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerList.swift deleted file mode 100644 index 7913c73c60..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Views/PlayerList.swift +++ /dev/null @@ -1,57 +0,0 @@ -import SwiftUI - -struct PlayerList: View { - /// Write access to the database - @Environment(\.appDatabase) private var appDatabase - - /// The players in the list - var players: [Player] - - var body: some View { - List { - ForEach(players) { player in - NavigationLink(destination: editionView(for: player)) { - PlayerRow(player: player) - // Don't animate player update - .animation(nil, value: player) - } - } - .onDelete { offsets in - let playerIds = offsets.compactMap { players[$0].id } - try? appDatabase.deletePlayers(ids: playerIds) - } - } - // Animate list updates - .animation(.default, value: players) - .listStyle(.plain) - } - - /// The view that edits a player in the list. - private func editionView(for player: Player) -> some View { - PlayerEditionView(player: player).navigationBarTitle(player.name) - } -} - -private struct PlayerRow: View { - var player: Player - - var body: some View { - HStack { - Text(player.name) - Spacer() - Text("\(player.score) points").foregroundColor(.gray) - } - } -} - -// MARK: - Previews - -#Preview { - NavigationView { - PlayerList(players: [ - Player(id: 1, name: "Arthur", score: 100), - Player(id: 2, name: "Barbara", score: 1000), - ]) - .navigationTitle("Preview") - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/AppDatabaseTests.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/AppDatabaseTests.swift deleted file mode 100644 index f9343671e1..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/AppDatabaseTests.swift +++ /dev/null @@ -1,146 +0,0 @@ -import XCTest -import GRDB -@testable import GRDBCombineDemo - -class AppDatabaseTests: XCTestCase { - func test_database_schema() throws { - // Given an empty database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - - // When we instantiate an AppDatabase - _ = try AppDatabase(dbQueue) - - // Then the player table exists, with id, name & score columns - try dbQueue.read { db in - try XCTAssert(db.tableExists("player")) - let columns = try db.columns(in: "player") - let columnNames = Set(columns.map { $0.name }) - XCTAssertEqual(columnNames, ["id", "name", "score"]) - } - } - - func test_savePlayer_inserts() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - - // When we save a new player - var player = Player(id: nil, name: "Arthur", score: 100) - try appDatabase.savePlayer(&player) - - // Then the player exists in the database - try XCTAssertTrue(dbQueue.read(player.exists)) - } - - func test_savePlayer_updates() throws { - // Given a players database that contains a player - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - var player = Player(id: nil, name: "Arthur", score: 100) - try dbQueue.write { db in - try player.insert(db) - } - - // When we modify and save the player - player.name = "Barbara" - player.score = 1000 - try appDatabase.savePlayer(&player) - - // Then the player has been updated in the database - let fetchedPlayer = try dbQueue.read { db in - try XCTUnwrap(Player.fetchOne(db, key: player.id)) - } - XCTAssertEqual(fetchedPlayer, player) - } - - func test_deletePlayers() throws { - // Given a players database that contains four players - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - var player1 = Player(id: nil, name: "Arthur", score: 100) - var player2 = Player(id: nil, name: "Barbara", score: 200) - var player3 = Player(id: nil, name: "Craig", score: 150) - var player4 = Player(id: nil, name: "David", score: 120) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we delete two players - try appDatabase.deletePlayers(ids: [player1.id!, player3.id!]) - - // Then the deleted players no longer exist - try dbQueue.read { db in - try XCTAssertFalse(player1.exists(db)) - try XCTAssertFalse(player3.exists(db)) - } - - // Then the database still contains two players - try XCTAssertEqual(dbQueue.read(Player.fetchCount), 2) - } - - func test_deleteAllPlayers() throws { - // Given a players database that contains players - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - var player1 = Player(id: nil, name: "Arthur", score: 100) - var player2 = Player(id: nil, name: "Barbara", score: 200) - var player3 = Player(id: nil, name: "Craig", score: 150) - var player4 = Player(id: nil, name: "David", score: 120) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we delete all players - try appDatabase.deleteAllPlayers() - - // Then the database does not contain any player - try XCTAssertEqual(dbQueue.read(Player.fetchCount), 0) - } - - func test_refreshPlayers_populates_an_empty_database() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - - // When we refresh players - try appDatabase.refreshPlayers() - - // Then the database is not empty - try XCTAssert(dbQueue.read(Player.fetchCount) > 0) - } - - func test_createRandomPlayersIfEmpty_populates_an_empty_database() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - - // When we create random players - try appDatabase.createRandomPlayersIfEmpty() - - // Then the database is not empty - try XCTAssert(dbQueue.read(Player.fetchCount) > 0) - } - - func test_createRandomPlayersIfEmpty_does_not_modify_a_non_empty_database() throws { - // Given a players database that contains one player - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - var player = Player(id: nil, name: "Arthur", score: 100) - try dbQueue.write { db in - try player.insert(db) - } - - // When we create random players - try appDatabase.createRandomPlayersIfEmpty() - - // Then the database still only contains the original player - let players = try dbQueue.read(Player.fetchAll) - XCTAssertEqual(players, [player]) - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/Info.plist b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/Info.plist deleted file mode 100644 index 64d65ca495..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/PlayerRequestTests.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/PlayerRequestTests.swift deleted file mode 100644 index c9e58da1ed..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/PlayerRequestTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -import XCTest -import GRDB -@testable import GRDBCombineDemo - -class PlayerRequestTests: XCTestCase { - func test_PlayerRequest_byName_fetches_well_ordered_players() throws { - // Given a players database that contains two players - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: nil, name: "Arthur", score: 100) - var player2 = Player(id: nil, name: "Barbara", score: 1000) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - } - - // When we fetch players ordered by name - let playerRequest = PlayerRequest(ordering: .byName) - let players = try dbQueue.read(playerRequest.fetch) - - // Then the players are the two players ordered by name - XCTAssertEqual(players, [player1, player2]) - } - - func test_PlayerRequest_byScore_fetches_well_ordered_players() throws { - // Given a players database that contains two players - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: nil, name: "Arthur", score: 100) - var player2 = Player(id: nil, name: "Barbara", score: 1000) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - } - - // When we fetch players ordered by score - let playerRequest = PlayerRequest(ordering: .byScore) - let players = try dbQueue.read(playerRequest.fetch) - - // Then the players are the two players ordered by score descending - XCTAssertEqual(players, [player2, player1]) - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/PlayerTests.swift b/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/PlayerTests.swift deleted file mode 100644 index 4262d80235..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemoTests/PlayerTests.swift +++ /dev/null @@ -1,108 +0,0 @@ -import XCTest -import GRDB -@testable import GRDBCombineDemo - -class PlayerTests: XCTestCase { - // MARK: - CRUD - // Test that our Player type properly talks to GRDB. - - func testInsert() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - - // When we insert a player - var player = Player(id: nil, name: "Arthur", score: 100) - try dbQueue.write { db in - try player.insert(db) - } - - // Then the player gets a non-nil id - XCTAssertNotNil(player.id) - } - - func testRoundtrip() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - - // When we insert a player and fetch the player with the same id - var insertedPlayer = Player(id: nil, name: "Arthur", score: 100) - let fetchedPlayer: Player? = try dbQueue.write { db in - try insertedPlayer.insert(db) - return try Player.fetchOne(db, key: insertedPlayer.id) - } - - // Then the fetched player is equal to the inserted player - XCTAssertEqual(insertedPlayer, fetchedPlayer) - } - - // MARK: - Requests - // Test that requests defined on the Player type behave as expected. - - func testOrderedByScore() throws { - // Given a players database that contains players with distinct scores - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: 1, name: "Arthur", score: 100) - var player2 = Player(id: 2, name: "Barbara", score: 200) - var player3 = Player(id: 3, name: "Craig", score: 150) - var player4 = Player(id: 4, name: "David", score: 120) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we fetch players ordered by score - let players = try dbQueue.read(Player.all().orderedByScore().fetchAll) - - // Then fetched players are ordered by score descending - XCTAssertEqual(players, [player2, player3, player4, player1]) - } - - func testOrderedByScoreSortsIdenticalScoresByName() throws { - // Given a players database that contains players with common scores - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: 1, name: "Arthur", score: 100) - var player2 = Player(id: 2, name: "Barbara", score: 200) - var player3 = Player(id: 3, name: "Craig", score: 200) - var player4 = Player(id: 4, name: "David", score: 200) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we fetch players ordered by score - let players = try dbQueue.read(Player.all().orderedByScore().fetchAll) - - // Then fetched players are ordered by score descending and by name - XCTAssertEqual(players, [player2, player3, player4, player1]) - } - - func testOrderedByName() throws { - // Given a players database that contains players with distinct names - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: 1, name: "Arthur", score: 100) - var player2 = Player(id: 2, name: "Barbara", score: 200) - var player3 = Player(id: 3, name: "Craig", score: 150) - var player4 = Player(id: 4, name: "David", score: 120) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we fetch players ordered by name - let players = try dbQueue.read(Player.all().orderedByName().fetchAll) - - // Then fetched players are ordered by name - XCTAssertEqual(players, [player1, player2, player3, player4]) - } -} diff --git a/Documentation/DemoApps/GRDBCombineDemo/README.md b/Documentation/DemoApps/GRDBCombineDemo/README.md deleted file mode 100644 index 6b2ccec0f7..0000000000 --- a/Documentation/DemoApps/GRDBCombineDemo/README.md +++ /dev/null @@ -1,51 +0,0 @@ -Combine + SwiftUI Demo Application -================================== - - - -**This demo application is a Combine + SwiftUI application.** For a demo application that uses UIKit, see [GRDBDemoiOS](../GRDBDemoiOS/README.md), and for Async/Await + SwiftUI, see [GRDBAsyncDemo](../GRDBAsyncDemo/README.md). - -**Requirements**: iOS 15.0+ / Xcode 12+ - -> **Note**: This demo app is not a project template. Do not copy it as a starting point for your application. Instead, create a new project, choose a GRDB [installation method](../../../README.md#installation), and use the demo as an inspiration. - -The topics covered in this demo are: - -- How to setup a database in an iOS app. -- How to define a simple [Codable Record](../../../README.md#codable-records). -- How to track database changes and animate a SwiftUI List with [ValueObservation](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation) Combine publishers. -- How to apply the recommendations of [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices). -- How to feed SwiftUI previews with a transient database. - -**Files of interest:** - -- [GRDBCombineDemoApp.swift](GRDBCombineDemo/GRDBCombineDemoApp.swift) - - `GRDBCombineDemoApp` feeds the app views with a database, through the SwiftUI environment. - -- [AppDatabase.swift](GRDBCombineDemo/AppDatabase.swift) - - `AppDatabase` is the type that grants database access. It uses [DatabaseMigrator](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasemigrator) in order to setup the database schema. - -- [Persistence.swift](GRDBCombineDemo/Persistence.swift) - - This file instantiates various `AppDatabase` for the various projects needs: one database on disk for the application, and in-memory databases for SwiftUI previews. - -- [Player.swift](GRDBCombineDemo/Player.swift) - - `Player` is a [Record](../../../README.md#records) type, able to read and write in the database. It conforms to the standard Codable protocol in order to gain all advantages of [Codable Records](../../../README.md#codable-records). - -- [PlayerRequest.swift](GRDBCombineDemo/PlayerRequest.swift), [AppView.swift](GRDBCombineDemo/Views/AppView.swift) - - `PlayerRequest` defines the player requests used by the app (sorted by score, or by name). - - `PlayerRequest` feeds the `@Query` property wrapper (`@Query`, defined in [GRDBQuery](https://github.com/groue/GRDBQuery), allows SwiftUI views to display up-to-date database content). - - `AppView` is the SwiftUI view that uses `@Query` in order to feed its player list. - -- [GRDBCombineDemoTests](GRDBCombineDemoTests) - - - Test the database schema - - Test the `Player` record and its requests - - Test the `PlayerRequest` methods that feed the list of players. - - Test the `AppDatabase` methods that let the app access the database. diff --git a/Documentation/DemoApps/GRDBCombineDemo/Screenshot.png b/Documentation/DemoApps/GRDBCombineDemo/Screenshot.png deleted file mode 100644 index 0536821854..0000000000 Binary files a/Documentation/DemoApps/GRDBCombineDemo/Screenshot.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo.xcodeproj/project.pbxproj b/Documentation/DemoApps/GRDBDemo/GRDBDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..47782e0625 --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo.xcodeproj/project.pbxproj @@ -0,0 +1,503 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 56CFC6772C9F1E1B000B5023 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56CFC6762C9F1E1B000B5023 /* GRDB */; }; + 56CFC6E42C9F5AEA000B5023 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56CFC6E32C9F5AEA000B5023 /* GRDB */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 56CFC6552C9F1DCA000B5023 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 56CFC63C2C9F1DC9000B5023 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 56CFC6432C9F1DC9000B5023; + remoteInfo = GRDBDemo; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 56CFC6442C9F1DC9000B5023 /* GRDBDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 56CFC6542C9F1DCA000B5023 /* GRDBDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 56CFC6B82C9F544D000B5023 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 56CFC6AA2C9F3ADB000B5023 /* Exceptions for "GRDBDemo" folder in "GRDBDemo" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 56CFC6432C9F1DC9000B5023 /* GRDBDemo */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 56CFC6462C9F1DC9000B5023 /* GRDBDemo */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 56CFC6AA2C9F3ADB000B5023 /* Exceptions for "GRDBDemo" folder in "GRDBDemo" target */, + ); + path = GRDBDemo; + sourceTree = ""; + }; + 56CFC6572C9F1DCA000B5023 /* GRDBDemoTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = GRDBDemoTests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 56CFC6412C9F1DC9000B5023 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 56CFC6772C9F1E1B000B5023 /* GRDB in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 56CFC6512C9F1DCA000B5023 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 56CFC6E42C9F5AEA000B5023 /* GRDB in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 56CFC63B2C9F1DC9000B5023 = { + isa = PBXGroup; + children = ( + 56CFC6B82C9F544D000B5023 /* README.md */, + 56CFC6462C9F1DC9000B5023 /* GRDBDemo */, + 56CFC6572C9F1DCA000B5023 /* GRDBDemoTests */, + 56CFC6E22C9F5AEA000B5023 /* Frameworks */, + 56CFC6452C9F1DC9000B5023 /* Products */, + ); + sourceTree = ""; + }; + 56CFC6452C9F1DC9000B5023 /* Products */ = { + isa = PBXGroup; + children = ( + 56CFC6442C9F1DC9000B5023 /* GRDBDemo.app */, + 56CFC6542C9F1DCA000B5023 /* GRDBDemoTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 56CFC6E22C9F5AEA000B5023 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 56CFC6432C9F1DC9000B5023 /* GRDBDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 56CFC6682C9F1DCB000B5023 /* Build configuration list for PBXNativeTarget "GRDBDemo" */; + buildPhases = ( + 56CFC6402C9F1DC9000B5023 /* Sources */, + 56CFC6412C9F1DC9000B5023 /* Frameworks */, + 56CFC6422C9F1DC9000B5023 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 56CFC6462C9F1DC9000B5023 /* GRDBDemo */, + ); + name = GRDBDemo; + packageProductDependencies = ( + 56CFC6762C9F1E1B000B5023 /* GRDB */, + ); + productName = GRDBDemo; + productReference = 56CFC6442C9F1DC9000B5023 /* GRDBDemo.app */; + productType = "com.apple.product-type.application"; + }; + 56CFC6532C9F1DCA000B5023 /* GRDBDemoTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 56CFC66B2C9F1DCB000B5023 /* Build configuration list for PBXNativeTarget "GRDBDemoTests" */; + buildPhases = ( + 56CFC6502C9F1DCA000B5023 /* Sources */, + 56CFC6512C9F1DCA000B5023 /* Frameworks */, + 56CFC6522C9F1DCA000B5023 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 56CFC6562C9F1DCA000B5023 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 56CFC6572C9F1DCA000B5023 /* GRDBDemoTests */, + ); + name = GRDBDemoTests; + packageProductDependencies = ( + 56CFC6E32C9F5AEA000B5023 /* GRDB */, + ); + productName = GRDBDemoTests; + productReference = 56CFC6542C9F1DCA000B5023 /* GRDBDemoTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 56CFC63C2C9F1DC9000B5023 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + 56CFC6432C9F1DC9000B5023 = { + CreatedOnToolsVersion = 16.0; + }; + 56CFC6532C9F1DCA000B5023 = { + CreatedOnToolsVersion = 16.0; + TestTargetID = 56CFC6432C9F1DC9000B5023; + }; + }; + }; + buildConfigurationList = 56CFC63F2C9F1DC9000B5023 /* Build configuration list for PBXProject "GRDBDemo" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 56CFC63B2C9F1DC9000B5023; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 56CFC6752C9F1E1B000B5023 /* XCLocalSwiftPackageReference "../../../../GRDB.swift" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 56CFC6452C9F1DC9000B5023 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 56CFC6432C9F1DC9000B5023 /* GRDBDemo */, + 56CFC6532C9F1DCA000B5023 /* GRDBDemoTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 56CFC6422C9F1DC9000B5023 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 56CFC6522C9F1DCA000B5023 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 56CFC6402C9F1DC9000B5023 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 56CFC6502C9F1DCA000B5023 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 56CFC6562C9F1DCA000B5023 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 56CFC6432C9F1DC9000B5023 /* GRDBDemo */; + targetProxy = 56CFC6552C9F1DCA000B5023 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 56CFC6662C9F1DCB000B5023 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 56CFC6672C9F1DCB000B5023 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 56CFC6692C9F1DCB000B5023 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"GRDBDemo/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = GRDBDemo/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "GRDB Demo"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 56CFC66A2C9F1DCB000B5023 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"GRDBDemo/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = GRDBDemo/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "GRDB Demo"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 56CFC66C2C9F1DCB000B5023 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GRDBDemo"; + }; + name = Debug; + }; + 56CFC66D2C9F1DCB000B5023 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/GRDBDemo"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 56CFC63F2C9F1DC9000B5023 /* Build configuration list for PBXProject "GRDBDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 56CFC6662C9F1DCB000B5023 /* Debug */, + 56CFC6672C9F1DCB000B5023 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 56CFC6682C9F1DCB000B5023 /* Build configuration list for PBXNativeTarget "GRDBDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 56CFC6692C9F1DCB000B5023 /* Debug */, + 56CFC66A2C9F1DCB000B5023 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 56CFC66B2C9F1DCB000B5023 /* Build configuration list for PBXNativeTarget "GRDBDemoTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 56CFC66C2C9F1DCB000B5023 /* Debug */, + 56CFC66D2C9F1DCB000B5023 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 56CFC6752C9F1E1B000B5023 /* XCLocalSwiftPackageReference "../../../../GRDB.swift" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../../GRDB.swift; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 56CFC6762C9F1E1B000B5023 /* GRDB */ = { + isa = XCSwiftPackageProductDependency; + productName = GRDB; + }; + 56CFC6E32C9F5AEA000B5023 /* GRDB */ = { + isa = XCSwiftPackageProductDependency; + package = 56CFC6752C9F1E1B000B5023 /* XCLocalSwiftPackageReference "../../../../GRDB.swift" */; + productName = GRDB; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 56CFC63C2C9F1DC9000B5023 /* Project object */; +} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Documentation/DemoApps/GRDBDemo/GRDBDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Documentation/DemoApps/GRDBDemo/GRDBDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoiOS.xcscheme b/Documentation/DemoApps/GRDBDemo/GRDBDemo.xcodeproj/xcshareddata/xcschemes/GRDBDemo.xcscheme similarity index 70% rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoiOS.xcscheme rename to Documentation/DemoApps/GRDBDemo/GRDBDemo.xcodeproj/xcshareddata/xcschemes/GRDBDemo.xcscheme index 20c4615d25..386a9790f8 100644 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoiOS.xcscheme +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo.xcodeproj/xcshareddata/xcschemes/GRDBDemo.xcscheme @@ -1,10 +1,11 @@ + LastUpgradeVersion = "1600" + version = "1.7"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> + BlueprintIdentifier = "56CFC6432C9F1DC9000B5023" + BuildableName = "GRDBDemo.app" + BlueprintName = "GRDBDemo" + ReferencedContainer = "container:GRDBDemo.xcodeproj"> @@ -26,17 +27,18 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + BlueprintIdentifier = "56CFC6532C9F1DCA000B5023" + BuildableName = "GRDBDemoTests.xctest" + BlueprintName = "GRDBDemoTests" + ReferencedContainer = "container:GRDBDemo.xcodeproj"> @@ -55,10 +57,10 @@ runnableDebuggingMode = "0"> + BlueprintIdentifier = "56CFC6432C9F1DC9000B5023" + BuildableName = "GRDBDemo.app" + BlueprintName = "GRDBDemo" + ReferencedContainer = "container:GRDBDemo.xcodeproj"> @@ -79,10 +81,10 @@ runnableDebuggingMode = "0"> + BlueprintIdentifier = "56CFC6432C9F1DC9000B5023" + BuildableName = "GRDBDemo.app" + BlueprintName = "GRDBDemo" + ReferencedContainer = "container:GRDBDemo.xcodeproj"> diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/AppDatabase.swift similarity index 67% rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift rename to Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/AppDatabase.swift index 8c38cd2a77..fca3988424 100644 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDatabase.swift +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/AppDatabase.swift @@ -1,90 +1,33 @@ +import Foundation import GRDB import os.log -/// `AppDatabase` lets the application access the database. -/// -/// You create an `AppDatabase` with a connection to an SQLite database -/// (see ). -/// -/// Create those connections with a configuration returned from -/// `AppDatabase/makeConfiguration(_:)`. +/// The type that provides access to the application database. /// /// For example: /// /// ```swift -/// // Create an in-memory AppDatabase +/// // Create an empty, in-memory, AppDatabase /// let config = AppDatabase.makeConfiguration() /// let dbQueue = try DatabaseQueue(configuration: config) /// let appDatabase = try AppDatabase(dbQueue) /// ``` -struct AppDatabase { - /// Creates an `AppDatabase`, and makes sure the database schema +final class AppDatabase: Sendable { + /// Access to the database. + /// + /// See + private let dbWriter: any DatabaseWriter + + /// Creates a `AppDatabase`, and makes sure the database schema /// is ready. /// /// - important: Create the `DatabaseWriter` with a configuration /// returned by ``makeConfiguration(_:)``. - init(_ dbWriter: any DatabaseWriter) throws { + init(_ dbWriter: any GRDB.DatabaseWriter) throws { self.dbWriter = dbWriter try migrator.migrate(dbWriter) } - /// Provides access to the database. - /// - /// Application can use a `DatabasePool`, and tests can use a fast - /// in-memory `DatabaseQueue`. - /// - /// See - private let dbWriter: any DatabaseWriter -} - -// MARK: - Database Configuration - -extension AppDatabase { - private static let sqlLogger = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SQL") - - /// Returns a database configuration suited for `PlayerRepository`. - /// - /// SQL statements are logged if the `SQL_TRACE` environment variable - /// is set. - /// - /// - parameter base: A base configuration. - public static func makeConfiguration(_ base: Configuration = Configuration()) -> Configuration { - var config = base - - // An opportunity to add required custom SQL functions or - // collations, if needed: - // config.prepareDatabase { db in - // db.add(function: ...) - // } - - // Log SQL statements if the `SQL_TRACE` environment variable is set. - // See - if ProcessInfo.processInfo.environment["SQL_TRACE"] != nil { - config.prepareDatabase { db in - db.trace { - // It's ok to log statements publicly. Sensitive - // information (statement arguments) are not logged - // unless config.publicStatementArguments is set - // (see below). - os_log("%{public}@", log: sqlLogger, type: .debug, String(describing: $0)) - } - } - } - -#if DEBUG - // Protect sensitive information by enabling verbose debugging in - // DEBUG builds only. - // See - config.publicStatementArguments = true -#endif - - return config - } -} - -// MARK: - Database Migrations - -extension AppDatabase { /// The DatabaseMigrator that defines the database schema. /// /// See @@ -93,11 +36,11 @@ extension AppDatabase { #if DEBUG // Speed up development by nuking the database when migrations change - // See + // See migrator.eraseDatabaseOnSchemaChange = true #endif - migrator.registerMigration("createPlayer") { db in + migrator.registerMigration("v1") { db in // Create a table // See try db.create(table: "player") { t in @@ -116,8 +59,54 @@ extension AppDatabase { } } +// MARK: - Database Configuration + +extension AppDatabase { + // Uncomment for enabling SQL logging + // private static let sqlLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "SQL") + + /// Returns a database configuration suited for `AppDatabase`. + /// + /// SQL statements are logged if the `SQL_TRACE` environment variable + /// is set. + /// + /// - parameter base: A base configuration. + static func makeConfiguration(_ base: Configuration = Configuration()) -> Configuration { + var config = base + + // Add custom SQL functions or collations, if needed: + // config.prepareDatabase { db in + // db.add(function: ...) + // } + + // Uncomment for enabling SQL logging if the `SQL_TRACE` environment variable is set. + // See + // if ProcessInfo.processInfo.environment["SQL_TRACE"] != nil { + // config.prepareDatabase { db in + // let dbName = db.description + // db.trace { event in + // // Sensitive information (statement arguments) is not + // // logged unless config.publicStatementArguments is set + // // (see below). + // sqlLogger.debug("\(dbName): \(event)") + // } + // } + // } + // + // #if DEBUG + // // Protect sensitive information by enabling verbose debugging in + // // DEBUG builds only. + // // See + // config.publicStatementArguments = true + // #endif + + return config + } +} + // MARK: - Database Access: Writes // The write methods execute invariant-preserving database transactions. +// In this demo repository, they are pretty simple. extension AppDatabase { /// Saves (inserts or updates) a player. When the method returns, the @@ -131,7 +120,7 @@ extension AppDatabase { /// Delete the specified players func deletePlayers(ids: [Int64]) throws { try dbWriter.write { db in - _ = try Player.deleteAll(db, ids: ids) + _ = try Player.deleteAll(db, keys: ids) } } @@ -143,8 +132,8 @@ extension AppDatabase { } /// Refresh all players (by performing some random changes, for demo purpose). - func refreshPlayers() throws { - try dbWriter.write { db in + func refreshPlayers() async throws { + try await dbWriter.write { [self] db in if try Player.all().isEmpty(db) { // When database is empty, insert new random players try createRandomPlayers(db) @@ -193,8 +182,8 @@ extension AppDatabase { // In your app, you are free to choose another path, and define focused // reading methods. extension AppDatabase { - /// Provides a read-only access to the database - var reader: DatabaseReader { + /// Provides a read-only access to the database. + var reader: any GRDB.DatabaseReader { dbWriter } } diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Player.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/Models/Player.swift similarity index 90% rename from Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Player.swift rename to Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/Models/Player.swift index 7876b049df..0c63186f95 100644 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Player.swift +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/Models/Player.swift @@ -5,7 +5,7 @@ import GRDB /// Identifiable conformance supports SwiftUI list animations, and type-safe /// GRDB primary key methods. /// Equatable conformance supports tests. -struct Player: Identifiable, Equatable { +struct Player: Equatable { /// The player id. /// /// Int64 is the recommended type for auto-incremented database ids. @@ -47,14 +47,14 @@ extension Player { } } -// MARK: - Persistence +// MARK: - Database /// Make Player a Codable Record. /// /// See extension Player: Codable, FetchableRecord, MutablePersistableRecord { // Define database columns from CodingKeys - fileprivate enum Columns { + enum Columns { static let name = Column(CodingKeys.name) static let score = Column(CodingKeys.score) } @@ -65,6 +65,9 @@ extension Player: Codable, FetchableRecord, MutablePersistableRecord { } } +// Convenience access to player columns in this file +private typealias Columns = Player.Columns + // MARK: - Player Database Requests /// Define some player requests used by the application. @@ -81,7 +84,7 @@ extension DerivableRequest { func orderedByName() -> Self { // Sort by name in a localized case insensitive fashion // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison - order(Player.Columns.name.collating(.localizedCaseInsensitiveCompare)) + order(Columns.name.collating(.localizedCaseInsensitiveCompare)) } /// A request of players ordered by score. @@ -99,7 +102,7 @@ extension DerivableRequest { // localized case insensitive fashion // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison order( - Player.Columns.score.desc, - Player.Columns.name.collating(.localizedCaseInsensitiveCompare)) + Columns.score.desc, + Columns.name.collating(.localizedCaseInsensitiveCompare)) } } diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Persistence.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/Persistence.swift similarity index 72% rename from Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Persistence.swift rename to Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/Persistence.swift index 05b0115789..94ab0ae79d 100644 --- a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Persistence.swift +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/Persistence.swift @@ -9,41 +9,25 @@ extension AppDatabase { do { // Apply recommendations from // - // + // Create the "Application Support/Database" directory if needed let fileManager = FileManager.default let appSupportURL = try fileManager.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let directoryURL = appSupportURL.appendingPathComponent("Database", isDirectory: true) - - // Support for tests: delete the database if requested - if CommandLine.arguments.contains("-reset") { - try? fileManager.removeItem(at: directoryURL) - } - - // Create the database folder if needed try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true) // Open or create the database let databaseURL = directoryURL.appendingPathComponent("db.sqlite") - NSLog("Database stored at \(databaseURL.path)") - let dbPool = try DatabasePool( - path: databaseURL.path, - // Use default AppDatabase configuration - configuration: AppDatabase.makeConfiguration()) + let config = AppDatabase.makeConfiguration() + let dbPool = try DatabasePool(path: databaseURL.path, configuration: config) // Create the AppDatabase let appDatabase = try AppDatabase(dbPool) - // Prepare the database with test fixtures if requested - if CommandLine.arguments.contains("-fixedTestData") { - try appDatabase.createPlayersForUITests() - } else { - // Otherwise, populate the database if it is empty, for better - // demo purpose. - try appDatabase.createRandomPlayersIfEmpty() - } + // Populate the database if it is empty, for better demo purpose. + try appDatabase.createRandomPlayersIfEmpty() return appDatabase } catch { diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/GRDBDemoApp.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemo/GRDBDemoApp.swift new file mode 100644 index 0000000000..a08232a76e --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/GRDBDemoApp.swift @@ -0,0 +1,22 @@ +import SwiftUI + +@main +struct GRDBDemoApp: App { + var body: some Scene { + WindowGroup { + PlayersNavigationView().appDatabase(.shared) + } + } +} + +// MARK: - Give SwiftUI access to the database + +extension EnvironmentValues { + @Entry var appDatabase = AppDatabase.empty() +} + +extension View { + func appDatabase(_ appDatabase: AppDatabase) -> some View { + self.environment(\.appDatabase, appDatabase) + } +} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Info.plist similarity index 65% rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Documentation/DemoApps/GRDBDemo/GRDBDemo/Info.plist index 18d981003d..0427f524a1 100644 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Info.plist @@ -2,7 +2,10 @@ - IDEDidComputeMac32BitWarning - + UILaunchScreen + + UIImageName + LaunchScreen + diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Preview Content/Preview Assets.xcassets/Contents.json b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Preview Content/Preview Assets.xcassets/Contents.json rename to Documentation/DemoApps/GRDBDemo/GRDBDemo/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json rename to Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..3395dad77a --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,37 @@ +{ + "images" : [ + { + "filename" : "Icon-Light-1024×1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Icon-Dark-1024×1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git "a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Dark-1024\303\2271024.png" "b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Dark-1024\303\2271024.png" new file mode 100644 index 0000000000..d20dbec5c8 Binary files /dev/null and "b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Dark-1024\303\2271024.png" differ diff --git "a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Light-1024\303\2271024.png" "b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Light-1024\303\2271024.png" new file mode 100644 index 0000000000..f456cd22f7 Binary files /dev/null and "b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/AppIcon.appiconset/Icon-Light-1024\303\2271024.png" differ diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/Contents.json b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/Contents.json rename to Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/Contents.json diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/Contents.json b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/Contents.json new file mode 100644 index 0000000000..8988614712 --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "LaunchIcon.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LaunchIcon~Dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/LaunchIcon.pdf similarity index 100% rename from Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf rename to Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/LaunchIcon.pdf diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/LaunchIcon~Dark.pdf b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/LaunchIcon~Dark.pdf new file mode 100644 index 0000000000..cc67d97963 Binary files /dev/null and b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Assets.xcassets/LaunchScreen.imageset/LaunchIcon~Dark.pdf differ diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Localizable.xcstrings b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Localizable.xcstrings new file mode 100644 index 0000000000..8d20dbef02 --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Resources/Localizable.xcstrings @@ -0,0 +1,89 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "" : { + + }, + "%lld Players" : { + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 Player" + } + }, + "other" : { + "stringUnit" : { + "state" : "new", + "value" : "%lld Players" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Player" + } + } + } + } + } + } + }, + "%lld points" : { + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 point" + } + }, + "other" : { + "stringUnit" : { + "state" : "new", + "value" : "%lld points" + } + }, + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "0 point" + } + } + } + } + } + } + }, + "Add Player" : { + + }, + "Anonymous" : { + + }, + "Cancel" : { + + }, + "Name" : { + + }, + "New Player" : { + + }, + "Save" : { + + }, + "Score" : { + + }, + "The team is empty!" : { + + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerCreationSheet.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerCreationSheet.swift new file mode 100644 index 0000000000..c526348041 --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerCreationSheet.swift @@ -0,0 +1,39 @@ +import SwiftUI + +/// A view that creates a `Player`. Display it as a sheet. +struct PlayerCreationSheet: View { + @Environment(\.appDatabase) var appDatabase + @Environment(\.dismiss) var dismiss + @State var form = PlayerForm(name: "", score: nil) + + var body: some View { + NavigationStack { + Form { + PlayerFormView(form: $form) + } + .navigationTitle("New Player") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + save() + } + } + } + } + } + + private func save() { + var player = Player(name: form.name, score: form.score ?? 0) + try? appDatabase.savePlayer(&player) + dismiss() + } +} + +// MARK: - Previews + +#Preview { + PlayerCreationSheet() +} diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerEditionView.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerEditionView.swift new file mode 100644 index 0000000000..0bbe76aa76 --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerEditionView.swift @@ -0,0 +1,41 @@ +import SwiftUI + +struct PlayerEditionView: View { + @Environment(\.isPresented) var isPresented + @Environment(\.appDatabase) var appDatabase + @State var form: PlayerForm + var player: Player + + init(player: Player) { + self.player = player + self._form = State(initialValue: PlayerForm(name: player.name, score: player.score)) + } + + var body: some View { + Form { + PlayerFormView(form: $form) + } + .navigationTitle(player.name) + .onChange(of: isPresented) { + if !isPresented { + // Back button was pressed + save() + } + } + } + + private func save() { + var player = player + player.name = form.name + player.score = form.score ?? 0 + try? appDatabase.savePlayer(&player) + } +} + +// MARK: - Previews + +#Preview { + NavigationStack { + PlayerEditionView(player: .makeRandom()) + } +} diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerFormView.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerFormView.swift new file mode 100644 index 0000000000..32172b020e --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerFormView.swift @@ -0,0 +1,64 @@ +import SwiftUI + +/// A view that edits a `PlayerForm`. +struct PlayerFormView: View { + @Binding var form: PlayerForm + + private enum FocusElement { + case name + case score + } + @FocusState private var focusedElement: FocusElement? + + var body: some View { + Group { + LabeledContent { + TextField(text: $form.name) { EmptyView() } + .textInputAutocapitalization(.words) + .autocorrectionDisabled() + .submitLabel(.next) + .focused($focusedElement, equals: .name) + .labelsHidden() + .onSubmit { + focusedElement = .score + } + } label: { + Text("Name").foregroundStyle(.secondary) + } + + LabeledContent { + TextField(value: $form.score, format: .number) { EmptyView() } + .keyboardType(.numberPad) + .focused($focusedElement, equals: .score) + .labelsHidden() + } label: { + Text("Score").foregroundStyle(.secondary) + } + } + .onAppear { focusedElement = .name } + } +} + +/// The model edited by `PlayerFormView`. +struct PlayerForm { + var name: String + var score: Int? +} + +// MARK: - Previews + +#Preview("Prefilled") { + @Previewable @State var form = PlayerForm(name: "John", score: 100) + + Form { + PlayerFormView(form: $form) + } +} + +#Preview("Empty") { + @Previewable @State var form = PlayerForm(name: "", score: nil) + + Form { + PlayerFormView(form: $form) + } +} diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerListModel.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerListModel.swift new file mode 100644 index 0000000000..74dc501ece --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerListModel.swift @@ -0,0 +1,88 @@ +import Foundation +import Observation +import GRDB + +/// The observable model that drives the main navigation view. +/// +/// It observes the database in order to always display an up-to-date list +/// of players. +/// +/// This class is testable. See `PlayerListModelTests.swift`. +@Observable @MainActor final class PlayerListModel { + /// A player ordering + enum Ordering { + case byName + case byScore + } + + /// The player ordering + var ordering = Ordering.byScore { + didSet { observePlayers() } + } + + /// The players. + /// + /// The array remains empty until `observePlayers()` is called. + var players: [Player] = [] + + private let appDatabase: AppDatabase + @ObservationIgnored private var cancellable: AnyDatabaseCancellable? + + // MARK: - Initialization + + /// Creates a `PlayerListModel`. + init(appDatabase: AppDatabase) { + self.appDatabase = appDatabase + } + + /// Start observing the database. + func observePlayers() { + // We observe all players, sorted according to `ordering`. + let observation = ValueObservation.tracking { [ordering] db in + switch ordering { + case .byName: + try Player.all().orderedByName().fetchAll(db) + case .byScore: + try Player.all().orderedByScore().fetchAll(db) + } + } + + // Start observing the database. + // Previous observation, if any, is cancelled. + cancellable = observation.start(in: appDatabase.reader) { error in + // Handle error + } onChange: { [unowned self] players in + self.players = players + } + } + + // MARK: - Actions + + /// Delete players at specified indexes in `self.players`. + func deletePlayers(at offsets: IndexSet) throws { + let playerIds = offsets.compactMap { players[$0].id } + try appDatabase.deletePlayers(ids: playerIds) + } + + /// Delete all players. + func deleteAllPlayers() throws { + try appDatabase.deleteAllPlayers() + } + + /// Refresh all players (by performing some random changes, for demo purpose). + func refreshPlayers() async throws { + try await appDatabase.refreshPlayers() + } + + /// Perform 50 refreshes in parallel, for demo purpose. + func refreshPlayersManyTimes() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + for _ in 0..<50 { + group.addTask { + try await AppDatabase.shared.refreshPlayers() + } + } + for try await _ in group { } + } + } +} diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerListView.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerListView.swift new file mode 100644 index 0000000000..0f3316512c --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayerListView.swift @@ -0,0 +1,76 @@ +import SwiftUI + +/// A view that displays a list of players. +struct PlayerListView: View { + @Bindable var model: PlayerListModel + + var body: some View { + List { + ForEach(model.players, id: \.id) { player in + NavigationLink { + PlayerEditionView(player: player) + } label: { + PlayerRow(player: player) + } + } + .onDelete { offsets in + try? model.deletePlayers(at: offsets) + } + } + .animation(.default, value: model.players) + .listStyle(.plain) + .navigationTitle("\(model.players.count) Players") + } +} + +struct PlayerRow: View { + var player: Player + + var body: some View { + HStack { + Group { + if player.name.isEmpty { + Text("Anonymous").italic() + } else { + Text(player.name) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + + Text("\(player.score) points") + .monospacedDigit() + .foregroundStyle(.secondary) + } + } +} + +// MARK: - Previews + +#Preview { + struct Preview: View { + @Environment(\.appDatabase) var appDatabase + + var body: some View { + // This technique makes it possible to create an observable object + // (PlayerListModel) from the SwiftUI environment. + ContentView(appDatabase: appDatabase) + } + } + + struct ContentView: View { + @State var model: PlayerListModel + + init(appDatabase: AppDatabase) { + _model = State(initialValue: PlayerListModel(appDatabase: appDatabase)) + } + + var body: some View { + NavigationStack { + PlayerListView(model: model) + } + .onAppear { model.observePlayers() } + } + } + + return Preview().appDatabase(.random()) +} diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayersNavigationView.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayersNavigationView.swift new file mode 100644 index 0000000000..b6e96ac2d6 --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemo/Views/PlayersNavigationView.swift @@ -0,0 +1,181 @@ +import SwiftUI + +/// The main navigation view. +struct PlayersNavigationView: View { + @Environment(\.appDatabase) var appDatabase + + var body: some View { + // This technique makes it possible to create an observable object + // (PlayerListModel) from the SwiftUI environment. + ContentView(appDatabase: appDatabase) + } +} + +private struct ContentView: View { + /// The model for the player list. + @State var model: PlayerListModel + + /// Tracks the edit mode of the player list. + @State var editMode = EditMode.inactive + + /// Tracks the presentation of the player creation sheet. + @State var presentsCreationSheet = false + + init(appDatabase: AppDatabase) { + _model = State(initialValue: PlayerListModel(appDatabase: appDatabase)) + } + + var body: some View { + NavigationStack { + contentView + .toolbar { bottomBarContent } + .environment(\.editMode, $editMode) + } + .onAppear { + model.observePlayers() + } + .onChange(of: model.ordering) { + // Stop editing when ordering is modified + stopEditing() + } + .onChange(of: model.players.isEmpty) { + // Stop editing when the last player is deleted. + if model.players.isEmpty { + stopEditing() + } + } + .sheet(isPresented: $presentsCreationSheet) { + PlayerCreationSheet() + } + } + + @ViewBuilder + private var contentView: some View { + if model.players.isEmpty { + emptyPlayersView + } else { + PlayerListView(model: model) + .toolbar { + ToolbarItemGroup(placement: .topBarLeading) { + presentCreationSheetButton + EditButton() + } + ToolbarItem(placement: .topBarTrailing) { + ToggleOrderingButton(ordering: $model.ordering) + } + } + } + } + + private var emptyPlayersView: some View { + ContentUnavailableView { + Label("The team is empty!", systemImage: "person.slash") + } actions: { + Button("Add Player") { + presentsCreationSheet = true + } + .buttonStyle(.borderedProminent) + } + // Hide the title, but set a string anyway in order to avoid + // an odd relayout when player list becomes empty during + // the tornado. + .navigationTitle("") + } + + @ToolbarContentBuilder + private var bottomBarContent: some ToolbarContent { + ToolbarItemGroup(placement: .bottomBar) { + deleteAllButton + Spacer() + refreshButton + Spacer() + tornadoButton + } + } + + private var presentCreationSheetButton: some View { + Button { + stopEditing() + presentsCreationSheet = true + } label: { + Image(systemName: "plus") + } + } + + private var deleteAllButton: some View { + Button { + try? model.deleteAllPlayers() + } label: { + Image(systemName: "trash") + } + } + + private var refreshButton: some View { + Button { + Task { + stopEditing() + try? await model.refreshPlayers() + } + } label: { + Image(systemName: "arrow.clockwise") + } + } + + private var tornadoButton: some View { + Button { + Task { + stopEditing() + try? await model.refreshPlayersManyTimes() + } + } label: { + Image(systemName: "tornado") + } + } + + private func stopEditing() { + withAnimation { + editMode = .inactive + } + } +} + +private struct ToggleOrderingButton: View { + @Binding var ordering: PlayerListModel.Ordering + + var body: some View { + switch ordering { + case .byName: + Button { + ordering = .byScore + } label: { + buttonLabel("Name", systemImage: "arrowtriangle.up.fill") + } + case .byScore: + Button { + ordering = .byName + } label: { + buttonLabel("Score", systemImage: "arrowtriangle.down.fill") + } + } + } + + private func buttonLabel(_ title: LocalizedStringKey, systemImage: String) -> some View { + HStack { + Text(title) + Image(systemName: systemImage) + .imageScale(.medium) + } + } +} + +// MARK: - Previews + +#Preview("Populated") { + PlayersNavigationView() + .appDatabase(.random()) +} + +#Preview("Empty") { + PlayersNavigationView() + .appDatabase(.empty()) +} diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemoTests/AppDatabaseTests.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemoTests/AppDatabaseTests.swift new file mode 100644 index 0000000000..29b22ad86b --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemoTests/AppDatabaseTests.swift @@ -0,0 +1,58 @@ +import Testing +import GRDB +@testable import GRDBDemo + +struct AppDatabaseTests { + @Test func insert() throws { + // Given an empty database + let appDatabase = try makeEmptyTestDatabase() + + // When we insert a player + var insertedPlayer = Player(name: "Arthur", score: 1000) + try appDatabase.savePlayer(&insertedPlayer) + + // Then the inserted player has an id + #expect(insertedPlayer.id != nil) + + // Then the inserted player exists in the database + let fetchedPlayer = try appDatabase.reader.read(Player.fetchOne) + #expect(fetchedPlayer == insertedPlayer) + } + + @Test func update() throws { + // Given a database that contains a player + let appDatabase = try makeEmptyTestDatabase() + var insertedPlayer = Player(name: "Arthur", score: 1000) + try appDatabase.savePlayer(&insertedPlayer) + + // When we update a player + var updatedPlayer = insertedPlayer + updatedPlayer.name = "Barbara" + updatedPlayer.score = 0 + try appDatabase.savePlayer(&updatedPlayer) + + // Then the player is updated + let fetchedPlayer = try appDatabase.reader.read(Player.fetchOne) + #expect(fetchedPlayer == updatedPlayer) + } + + @Test func deleteAll() throws { + // Given a database that contains a player + let appDatabase = try makeEmptyTestDatabase() + var player = Player(name: "Arthur", score: 1000) + try appDatabase.savePlayer(&player) + + // When we delete all players + try appDatabase.deleteAllPlayers() + + // Then no player exists + let count = try appDatabase.reader.read(Player.fetchCount(_:)) + #expect(count == 0) + } + + /// Return an empty, in-memory, `AppDatabase`. + private func makeEmptyTestDatabase() throws -> AppDatabase { + let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) + return try AppDatabase(dbQueue) + } +} diff --git a/Documentation/DemoApps/GRDBDemo/GRDBDemoTests/PlayerListModelTests.swift b/Documentation/DemoApps/GRDBDemo/GRDBDemoTests/PlayerListModelTests.swift new file mode 100644 index 0000000000..18c386a44e --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/GRDBDemoTests/PlayerListModelTests.swift @@ -0,0 +1,83 @@ +import Testing +import GRDB +@testable import GRDBDemo + +struct PlayerListModelTests { + // MARK: - PlayerListModel.observePlayers tests + + @Test(.timeLimit(.minutes(1))) + @MainActor func observation_started_after_player_creation() async throws { + // Given a PlayerListModel on a database that contains a player + let appDatabase = try makeEmptyTestDatabase() + var player = Player(name: "Arthur", score: 1000) + try appDatabase.savePlayer(&player) + let model = PlayerListModel(appDatabase: appDatabase) + + // When we start observing the database + model.observePlayers() + + // Then the model eventually fetches the player. + // We poll because we do not know when the model will update its players. + await pollUntil { + model.players.isEmpty == false + } + #expect(model.players == [player]) + } + + @Test(.timeLimit(.minutes(1))) + @MainActor func observation_started_before_player_creation() async throws { + // Given a PlayerListModel that observes a empty database + let appDatabase = try makeEmptyTestDatabase() + let model = PlayerListModel(appDatabase: appDatabase) + model.observePlayers() + + // When we insert a player + var player = Player(name: "Arthur", score: 1000) + try appDatabase.savePlayer(&player) + + // Then the model eventually fetches the player. + // We poll because we do not know when the model will update its players. + await pollUntil { + model.players.isEmpty == false + } + #expect(model.players == [player]) + } + + @Test + @MainActor func test_deleteAllPlayers_deletes_players_in_the_database() async throws { + // Given a PlayerListModel on a database that contains a player + let appDatabase = try makeEmptyTestDatabase() + var player = Player(name: "Arthur", score: 1000) + try appDatabase.savePlayer(&player) + let model = PlayerListModel(appDatabase: appDatabase) + + // When we delete all players + try model.deleteAllPlayers() + + // Then the database is empty. + let playerCount = try await appDatabase.reader.read { db in + try Player.fetchCount(db) + } + #expect(playerCount == 0) + } + + /// Return an empty, in-memory, `AppDatabase`. + private func makeEmptyTestDatabase() throws -> AppDatabase { + let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) + return try AppDatabase(dbQueue) + } + + /// Convenience method that loops until a condition is met. + private func pollUntil(condition: @escaping @MainActor () async -> Bool) async { + await confirmation { confirmation in + while true { + if await condition() { + confirmation() + return + } else { + await Task.yield() + } + } + } + } +} diff --git a/Documentation/DemoApps/GRDBDemo/README.md b/Documentation/DemoApps/GRDBDemo/README.md new file mode 100644 index 0000000000..24ecf51a2c --- /dev/null +++ b/Documentation/DemoApps/GRDBDemo/README.md @@ -0,0 +1,36 @@ +GRDBDemo Application +==================== + + + +**GRDBDemo demonstrates how GRDB can fuel a SwiftUI application.** + +> **Note**: This demo app is not a project template. Do not copy it as a starting point for your application. Instead, create a new project, choose a GRDB [installation method](../../../README.md#installation), and use the demo as an inspiration. + +The topics covered in this demo are: + +- How to setup a database in an iOS app. +- How to define a simple [Codable Record](../../../README.md#codable-records). +- How to track database changes and animate a SwiftUI List with [ValueObservation](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation). +- How to apply the recommendations of [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices). +- How to feed SwiftUI previews with a transient database. + +**Files of interest:** + +- [GRDBDemoApp.swift](GRDBDemo/GRDBDemoApp.swift) + + `GRDBDemoApp` feeds the SwiftUI app with a database, through the SwiftUI environment. + +- [AppDatabase.swift](GRDBDemo/Database/AppDatabase.swift) + + `AppDatabase` is the type that grants database access. It uses [DatabaseMigrator](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasemigrator) in order to setup the database schema, and provides methods that read and write. + +- [Persistence.swift](GRDBDemo/Database/Persistence.swift) + + This file instantiates various `AppDatabase` for the various projects needs: one database on disk for the application, and in-memory databases for SwiftUI previews. + +- [Player.swift](GRDBDemo/Database/Models/Player.swift) + + `Player` is a [Record](../../../README.md#records) type, able to read and write in the database. It conforms to the standard Codable protocol in order to gain all advantages of [Codable Records](../../../README.md#codable-records). + +- [GRDBDemoTests](GRDBDemoTests) diff --git a/Documentation/DemoApps/GRDBDemo/Screenshot.png b/Documentation/DemoApps/GRDBDemo/Screenshot.png new file mode 100644 index 0000000000..ded3f50acd Binary files /dev/null and b/Documentation/DemoApps/GRDBDemo/Screenshot.png differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json deleted file mode 100644 index 9be9adbf7d..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "screenWidth" : "{130,145}", - "scale" : "2x" - }, - { - "idiom" : "watch", - "screenWidth" : "{146,165}", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Contents.json b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Contents.json deleted file mode 100644 index 2eca9a1f46..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "assets" : [ - { - "idiom" : "watch", - "filename" : "Circular.imageset", - "role" : "circular" - }, - { - "idiom" : "watch", - "filename" : "Extra Large.imageset", - "role" : "extra-large" - }, - { - "idiom" : "watch", - "filename" : "Modular.imageset", - "role" : "modular" - }, - { - "idiom" : "watch", - "filename" : "Utilitarian.imageset", - "role" : "utilitarian" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json deleted file mode 100644 index 9be9adbf7d..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "screenWidth" : "{130,145}", - "scale" : "2x" - }, - { - "idiom" : "watch", - "screenWidth" : "{146,165}", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json deleted file mode 100644 index 9be9adbf7d..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "screenWidth" : "{130,145}", - "scale" : "2x" - }, - { - "idiom" : "watch", - "screenWidth" : "{146,165}", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json deleted file mode 100644 index 9be9adbf7d..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "screenWidth" : "{130,145}", - "scale" : "2x" - }, - { - "idiom" : "watch", - "screenWidth" : "{146,165}", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/ExtensionDelegate.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/ExtensionDelegate.swift deleted file mode 100644 index 320f84a6b2..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/ExtensionDelegate.swift +++ /dev/null @@ -1,48 +0,0 @@ -import WatchKit - -class ExtensionDelegate: NSObject, WKExtensionDelegate { - - func applicationDidFinishLaunching() { - // Perform any final initialization of your application. - } - - func applicationDidBecomeActive() { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillResignActive() { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, etc. - } - - func handle(_ backgroundTasks: Set) { - // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one. - for task in backgroundTasks { - // Use a switch statement to check the task type - switch task { - case let backgroundTask as WKApplicationRefreshBackgroundTask: - // Be sure to complete the background task once you’re done. - backgroundTask.setTaskCompletedWithSnapshot(false) - case let snapshotTask as WKSnapshotRefreshBackgroundTask: - // Snapshot tasks have a unique completion call, make sure to set your expiration date - snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil) - case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask: - // Be sure to complete the connectivity task once you’re done. - connectivityTask.setTaskCompletedWithSnapshot(false) - case let urlSessionTask as WKURLSessionRefreshBackgroundTask: - // Be sure to complete the URL session task once you’re done. - urlSessionTask.setTaskCompletedWithSnapshot(false) - case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask: - // Be sure to complete the relevant-shortcut task once you're done. - relevantShortcutTask.setTaskCompletedWithSnapshot(false) - case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask: - // Be sure to complete the intent-did-run task once you're done. - intentDidRunTask.setTaskCompletedWithSnapshot(false) - default: - // make sure to complete unhandled task types - task.setTaskCompletedWithSnapshot(false) - } - } - } - -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Info.plist b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Info.plist deleted file mode 100644 index 0881b8b30e..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/Info.plist +++ /dev/null @@ -1,36 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - GRDBDemoWatchOS Extension - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - XPC! - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - NSExtension - - NSExtensionAttributes - - WKAppBundleIdentifier - com.github.groue.GRDBDemoiOS2.watchkitapp - - NSExtensionPointIdentifier - com.apple.watchkit - - WKExtensionDelegateClassName - $(PRODUCT_MODULE_NAME).ExtensionDelegate - - diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/InterfaceController.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/InterfaceController.swift deleted file mode 100644 index 71d59c7948..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS Extension/InterfaceController.swift +++ /dev/null @@ -1,15 +0,0 @@ -import WatchKit -import Foundation -import GRDB - -class InterfaceController: WKInterfaceController { - - @IBOutlet var versionLabel: WKInterfaceLabel! - - override func awake(withContext context: Any?) { - super.awake(withContext: context) - - let sqliteVersion = String(cString: sqlite3_libversion(), encoding: .utf8) - versionLabel.setText(sqliteVersion) - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index dd221ba54d..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "images" : [ - { - "size" : "24x24", - "idiom" : "watch", - "scale" : "2x", - "role" : "notificationCenter", - "subtype" : "38mm" - }, - { - "size" : "27.5x27.5", - "idiom" : "watch", - "scale" : "2x", - "role" : "notificationCenter", - "subtype" : "42mm" - }, - { - "size" : "29x29", - "idiom" : "watch", - "role" : "companionSettings", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "watch", - "role" : "companionSettings", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "watch", - "scale" : "2x", - "role" : "appLauncher", - "subtype" : "38mm" - }, - { - "size" : "86x86", - "idiom" : "watch", - "scale" : "2x", - "role" : "quickLook", - "subtype" : "38mm" - }, - { - "size" : "98x98", - "idiom" : "watch", - "scale" : "2x", - "role" : "quickLook", - "subtype" : "42mm" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS/Base.lproj/Interface.storyboard b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS/Base.lproj/Interface.storyboard deleted file mode 100644 index 4988b4b688..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS/Base.lproj/Interface.storyboard +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS/Info.plist b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS/Info.plist deleted file mode 100644 index 0478fa15f8..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoWatchOS/Info.plist +++ /dev/null @@ -1,33 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - GRDBDemoiOS - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - - WKCompanionAppBundleIdentifier - com.github.groue.GRDBDemoiOS2 - WKWatchKitApp - - - diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.pbxproj b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.pbxproj deleted file mode 100644 index 827e5c560c..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.pbxproj +++ /dev/null @@ -1,912 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 56185BBF25B8036100B9C30F /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56185BBE25B8036100B9C30F /* Persistence.swift */; }; - 56185BF325B80B8900B9C30F /* AppDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56185BF225B80B8900B9C30F /* AppDatabaseTests.swift */; }; - 56185C0725B80CEC00B9C30F /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56185C0625B80CEC00B9C30F /* PlayerTests.swift */; }; - 567940C528BA33D2004A0298 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56606D0A2355931F00185962 /* GRDB.framework */; }; - 567940C628BA33D2004A0298 /* GRDB.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 56606D0A2355931F00185962 /* GRDB.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 567940C728BA33DE004A0298 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56606D0A2355931F00185962 /* GRDB.framework */; }; - 567940C828BA33DE004A0298 /* GRDB.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 56606D0A2355931F00185962 /* GRDB.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 568E5FC31E926430002582E0 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 568E5FC11E926430002582E0 /* Interface.storyboard */; }; - 568E5FC51E926430002582E0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 568E5FC41E926430002582E0 /* Assets.xcassets */; }; - 568E5FCC1E926430002582E0 /* GRDBDemoWatchOS Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 568E5FCB1E926430002582E0 /* GRDBDemoWatchOS Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 568E5FD11E926430002582E0 /* InterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568E5FD01E926430002582E0 /* InterfaceController.swift */; }; - 568E5FD31E926430002582E0 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568E5FD21E926430002582E0 /* ExtensionDelegate.swift */; }; - 568E5FD51E926430002582E0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 568E5FD41E926430002582E0 /* Assets.xcassets */; }; - 568E5FD91E926430002582E0 /* GRDBDemoWatchOS.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 568E5FBF1E926430002582E0 /* GRDBDemoWatchOS.app */; }; - 56B036071E8D9EBE003B6DA4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B036061E8D9EBE003B6DA4 /* AppDelegate.swift */; }; - 56B0360C1E8D9EBE003B6DA4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 56B0360A1E8D9EBE003B6DA4 /* Main.storyboard */; }; - 56B0360E1E8D9EBE003B6DA4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56B0360D1E8D9EBE003B6DA4 /* Assets.xcassets */; }; - 56B036111E8D9EBE003B6DA4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 56B0360F1E8D9EBE003B6DA4 /* LaunchScreen.storyboard */; }; - 56B0361B1E8D9F38003B6DA4 /* AppDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B036191E8D9F38003B6DA4 /* AppDatabase.swift */; }; - 56B0361C1E8D9F38003B6DA4 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B0361A1E8D9F38003B6DA4 /* Player.swift */; }; - 56B036231E8D9F4C003B6DA4 /* PlayerEditionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B0361F1E8D9F4C003B6DA4 /* PlayerEditionViewController.swift */; }; - 56B036241E8D9F4C003B6DA4 /* PlayerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B036201E8D9F4C003B6DA4 /* PlayerListViewController.swift */; }; - 56FE6F2624A90CE400711EDF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FE6F2524A90CE400711EDF /* SceneDelegate.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 56185BF525B80B8900B9C30F /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 56B035FB1E8D9EBE003B6DA4 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 56B036021E8D9EBE003B6DA4; - remoteInfo = GRDBDemoiOS; - }; - 5642252F29A2390800D714BF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = DC3773F219C8CBB3004FCF85; - remoteInfo = GRDB; - }; - 5642253329A2391900D714BF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = DC3773F219C8CBB3004FCF85; - remoteInfo = GRDB; - }; - 56606D092355931F00185962 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = DC3773F319C8CBB3004FCF85; - remoteInfo = GRDBOSX; - }; - 56606D0B2355931F00185962 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 56E5D7F91B4D422D00430942; - remoteInfo = GRDBOSXTests; - }; - 568E5FCD1E926430002582E0 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 56B035FB1E8D9EBE003B6DA4 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 568E5FCA1E926430002582E0; - remoteInfo = "GRDBDemoWatchOS Extension"; - }; - 568E5FD71E926430002582E0 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 56B035FB1E8D9EBE003B6DA4 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 568E5FBE1E926430002582E0; - remoteInfo = GRDBDemoWatchOS; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 568E5FDD1E926430002582E0 /* Embed Foundation Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - 568E5FCC1E926430002582E0 /* GRDBDemoWatchOS Extension.appex in Embed Foundation Extensions */, - ); - name = "Embed Foundation Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; - 568E5FE11E926430002582E0 /* Embed Watch Content */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; - dstSubfolderSpec = 16; - files = ( - 568E5FD91E926430002582E0 /* GRDBDemoWatchOS.app in Embed Watch Content */, - ); - name = "Embed Watch Content"; - runOnlyForDeploymentPostprocessing = 0; - }; - 568E5FE81E926547002582E0 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 567940C828BA33DE004A0298 /* GRDB.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - 56B036621E8D9FB1003B6DA4 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 567940C628BA33D2004A0298 /* GRDB.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 56185BBE25B8036100B9C30F /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; - 56185BF025B80B8900B9C30F /* GRDBDemoiOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBDemoiOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 56185BF225B80B8900B9C30F /* AppDatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDatabaseTests.swift; sourceTree = ""; }; - 56185BF425B80B8900B9C30F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 56185C0625B80CEC00B9C30F /* PlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = ""; }; - 568E5FBF1E926430002582E0 /* GRDBDemoWatchOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBDemoWatchOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 568E5FC21E926430002582E0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; - 568E5FC41E926430002582E0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 568E5FC61E926430002582E0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 568E5FCB1E926430002582E0 /* GRDBDemoWatchOS Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "GRDBDemoWatchOS Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; - 568E5FD01E926430002582E0 /* InterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceController.swift; sourceTree = ""; }; - 568E5FD21E926430002582E0 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = ""; }; - 568E5FD41E926430002582E0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 568E5FD61E926430002582E0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 56B036031E8D9EBE003B6DA4 /* GRDBDemoiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBDemoiOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 56B036061E8D9EBE003B6DA4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 56B0360B1E8D9EBE003B6DA4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 56B0360D1E8D9EBE003B6DA4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 56B036101E8D9EBE003B6DA4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 56B036121E8D9EBE003B6DA4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 56B036191E8D9F38003B6DA4 /* AppDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDatabase.swift; sourceTree = ""; }; - 56B0361A1E8D9F38003B6DA4 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; - 56B0361F1E8D9F4C003B6DA4 /* PlayerEditionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerEditionViewController.swift; sourceTree = ""; }; - 56B036201E8D9F4C003B6DA4 /* PlayerListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerListViewController.swift; sourceTree = ""; }; - 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GRDB.xcodeproj; path = ../../../GRDB.xcodeproj; sourceTree = ""; }; - 56FE6F2524A90CE400711EDF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 56185BED25B80B8900B9C30F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 568E5FC81E926430002582E0 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 567940C728BA33DE004A0298 /* GRDB.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 56B036001E8D9EBE003B6DA4 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 567940C528BA33D2004A0298 /* GRDB.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 56185BF125B80B8900B9C30F /* GRDBDemoiOSTests */ = { - isa = PBXGroup; - children = ( - 56185BF425B80B8900B9C30F /* Info.plist */, - 56185BF225B80B8900B9C30F /* AppDatabaseTests.swift */, - 56185C0625B80CEC00B9C30F /* PlayerTests.swift */, - ); - path = GRDBDemoiOSTests; - sourceTree = ""; - }; - 56606CFD2355931F00185962 /* Products */ = { - isa = PBXGroup; - children = ( - 56606D0A2355931F00185962 /* GRDB.framework */, - 56606D0C2355931F00185962 /* GRDBTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 56606D1F2355938A00185962 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; - 568E5FC01E926430002582E0 /* GRDBDemoWatchOS */ = { - isa = PBXGroup; - children = ( - 568E5FC11E926430002582E0 /* Interface.storyboard */, - 568E5FC41E926430002582E0 /* Assets.xcassets */, - 568E5FC61E926430002582E0 /* Info.plist */, - ); - path = GRDBDemoWatchOS; - sourceTree = ""; - }; - 568E5FCF1E926430002582E0 /* GRDBDemoWatchOS Extension */ = { - isa = PBXGroup; - children = ( - 568E5FD01E926430002582E0 /* InterfaceController.swift */, - 568E5FD21E926430002582E0 /* ExtensionDelegate.swift */, - 568E5FD41E926430002582E0 /* Assets.xcassets */, - 568E5FD61E926430002582E0 /* Info.plist */, - ); - path = "GRDBDemoWatchOS Extension"; - sourceTree = ""; - }; - 56B035FA1E8D9EBE003B6DA4 = { - isa = PBXGroup; - children = ( - 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */, - 56B036051E8D9EBE003B6DA4 /* GRDBDemoiOS */, - 568E5FC01E926430002582E0 /* GRDBDemoWatchOS */, - 568E5FCF1E926430002582E0 /* GRDBDemoWatchOS Extension */, - 56185BF125B80B8900B9C30F /* GRDBDemoiOSTests */, - 56B036041E8D9EBE003B6DA4 /* Products */, - 56606D1F2355938A00185962 /* Frameworks */, - ); - sourceTree = ""; - }; - 56B036041E8D9EBE003B6DA4 /* Products */ = { - isa = PBXGroup; - children = ( - 56B036031E8D9EBE003B6DA4 /* GRDBDemoiOS.app */, - 568E5FBF1E926430002582E0 /* GRDBDemoWatchOS.app */, - 568E5FCB1E926430002582E0 /* GRDBDemoWatchOS Extension.appex */, - 56185BF025B80B8900B9C30F /* GRDBDemoiOSTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 56B036051E8D9EBE003B6DA4 /* GRDBDemoiOS */ = { - isa = PBXGroup; - children = ( - 56B036121E8D9EBE003B6DA4 /* Info.plist */, - 56B036191E8D9F38003B6DA4 /* AppDatabase.swift */, - 56B036061E8D9EBE003B6DA4 /* AppDelegate.swift */, - 56185BBE25B8036100B9C30F /* Persistence.swift */, - 56B0361A1E8D9F38003B6DA4 /* Player.swift */, - 56FE6F2524A90CE400711EDF /* SceneDelegate.swift */, - 56FE6F4E24A9112900711EDF /* Resources */, - 56FE6F4F24A9114200711EDF /* ViewControllers */, - ); - path = GRDBDemoiOS; - sourceTree = ""; - }; - 56FE6F4E24A9112900711EDF /* Resources */ = { - isa = PBXGroup; - children = ( - 56B0360D1E8D9EBE003B6DA4 /* Assets.xcassets */, - 56B0360F1E8D9EBE003B6DA4 /* LaunchScreen.storyboard */, - 56B0360A1E8D9EBE003B6DA4 /* Main.storyboard */, - ); - path = Resources; - sourceTree = ""; - }; - 56FE6F4F24A9114200711EDF /* ViewControllers */ = { - isa = PBXGroup; - children = ( - 56B0361F1E8D9F4C003B6DA4 /* PlayerEditionViewController.swift */, - 56B036201E8D9F4C003B6DA4 /* PlayerListViewController.swift */, - ); - path = ViewControllers; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 56185BEF25B80B8900B9C30F /* GRDBDemoiOSTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 56185BFF25B80B8900B9C30F /* Build configuration list for PBXNativeTarget "GRDBDemoiOSTests" */; - buildPhases = ( - 56185BEC25B80B8900B9C30F /* Sources */, - 56185BED25B80B8900B9C30F /* Frameworks */, - 56185BEE25B80B8900B9C30F /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 56185BF625B80B8900B9C30F /* PBXTargetDependency */, - ); - name = GRDBDemoiOSTests; - productName = GRDBDemoiOSTests; - productReference = 56185BF025B80B8900B9C30F /* GRDBDemoiOSTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 568E5FBE1E926430002582E0 /* GRDBDemoWatchOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 568E5FDE1E926430002582E0 /* Build configuration list for PBXNativeTarget "GRDBDemoWatchOS" */; - buildPhases = ( - 568E5FBD1E926430002582E0 /* Resources */, - 568E5FDD1E926430002582E0 /* Embed Foundation Extensions */, - ); - buildRules = ( - ); - dependencies = ( - 568E5FCE1E926430002582E0 /* PBXTargetDependency */, - ); - name = GRDBDemoWatchOS; - productName = GRDBDemoWatchOS; - productReference = 568E5FBF1E926430002582E0 /* GRDBDemoWatchOS.app */; - productType = "com.apple.product-type.application.watchapp2"; - }; - 568E5FCA1E926430002582E0 /* GRDBDemoWatchOS Extension */ = { - isa = PBXNativeTarget; - buildConfigurationList = 568E5FDA1E926430002582E0 /* Build configuration list for PBXNativeTarget "GRDBDemoWatchOS Extension" */; - buildPhases = ( - 568E5FC71E926430002582E0 /* Sources */, - 568E5FC81E926430002582E0 /* Frameworks */, - 568E5FC91E926430002582E0 /* Resources */, - 568E5FE81E926547002582E0 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 5642253429A2391900D714BF /* PBXTargetDependency */, - ); - name = "GRDBDemoWatchOS Extension"; - productName = "GRDBDemoWatchOS Extension"; - productReference = 568E5FCB1E926430002582E0 /* GRDBDemoWatchOS Extension.appex */; - productType = "com.apple.product-type.watchkit2-extension"; - }; - 56B036021E8D9EBE003B6DA4 /* GRDBDemoiOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 56B036151E8D9EBE003B6DA4 /* Build configuration list for PBXNativeTarget "GRDBDemoiOS" */; - buildPhases = ( - 56B035FF1E8D9EBE003B6DA4 /* Sources */, - 56B036001E8D9EBE003B6DA4 /* Frameworks */, - 56B036011E8D9EBE003B6DA4 /* Resources */, - 56B036621E8D9FB1003B6DA4 /* Embed Frameworks */, - 568E5FE11E926430002582E0 /* Embed Watch Content */, - ); - buildRules = ( - ); - dependencies = ( - 5642253029A2390800D714BF /* PBXTargetDependency */, - 568E5FD81E926430002582E0 /* PBXTargetDependency */, - ); - name = GRDBDemoiOS; - productName = GRDBDemoiOS; - productReference = 56B036031E8D9EBE003B6DA4 /* GRDBDemoiOS.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 56B035FB1E8D9EBE003B6DA4 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1230; - LastUpgradeCheck = 1400; - ORGANIZATIONNAME = "Gwendal Roué"; - TargetAttributes = { - 56185BEF25B80B8900B9C30F = { - CreatedOnToolsVersion = 12.3; - DevelopmentTeam = AMD8W895CT; - ProvisioningStyle = Automatic; - TestTargetID = 56B036021E8D9EBE003B6DA4; - }; - 568E5FBE1E926430002582E0 = { - CreatedOnToolsVersion = 8.3; - DevelopmentTeam = AMD8W895CT; - ProvisioningStyle = Automatic; - }; - 568E5FCA1E926430002582E0 = { - CreatedOnToolsVersion = 8.3; - DevelopmentTeam = AMD8W895CT; - LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; - }; - 56B036021E8D9EBE003B6DA4 = { - CreatedOnToolsVersion = 8.3; - DevelopmentTeam = AMD8W895CT; - LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; - }; - }; - }; - buildConfigurationList = 56B035FE1E8D9EBE003B6DA4 /* Build configuration list for PBXProject "GRDBDemoiOS" */; - compatibilityVersion = "Xcode 12.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 56B035FA1E8D9EBE003B6DA4; - productRefGroup = 56B036041E8D9EBE003B6DA4 /* Products */; - projectDirPath = ""; - projectReferences = ( - { - ProductGroup = 56606CFD2355931F00185962 /* Products */; - ProjectRef = 56B036261E8D9F79003B6DA4 /* GRDB.xcodeproj */; - }, - ); - projectRoot = ""; - targets = ( - 56B036021E8D9EBE003B6DA4 /* GRDBDemoiOS */, - 568E5FBE1E926430002582E0 /* GRDBDemoWatchOS */, - 568E5FCA1E926430002582E0 /* GRDBDemoWatchOS Extension */, - 56185BEF25B80B8900B9C30F /* GRDBDemoiOSTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXReferenceProxy section */ - 56606D0A2355931F00185962 /* GRDB.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = GRDB.framework; - remoteRef = 56606D092355931F00185962 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 56606D0C2355931F00185962 /* GRDBTests.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = GRDBTests.xctest; - remoteRef = 56606D0B2355931F00185962 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - -/* Begin PBXResourcesBuildPhase section */ - 56185BEE25B80B8900B9C30F /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 568E5FBD1E926430002582E0 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 568E5FC51E926430002582E0 /* Assets.xcassets in Resources */, - 568E5FC31E926430002582E0 /* Interface.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 568E5FC91E926430002582E0 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 568E5FD51E926430002582E0 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 56B036011E8D9EBE003B6DA4 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 56B036111E8D9EBE003B6DA4 /* LaunchScreen.storyboard in Resources */, - 56B0360E1E8D9EBE003B6DA4 /* Assets.xcassets in Resources */, - 56B0360C1E8D9EBE003B6DA4 /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 56185BEC25B80B8900B9C30F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 56185C0725B80CEC00B9C30F /* PlayerTests.swift in Sources */, - 56185BF325B80B8900B9C30F /* AppDatabaseTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 568E5FC71E926430002582E0 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 568E5FD31E926430002582E0 /* ExtensionDelegate.swift in Sources */, - 568E5FD11E926430002582E0 /* InterfaceController.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 56B035FF1E8D9EBE003B6DA4 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 56FE6F2624A90CE400711EDF /* SceneDelegate.swift in Sources */, - 56B0361C1E8D9F38003B6DA4 /* Player.swift in Sources */, - 56B0361B1E8D9F38003B6DA4 /* AppDatabase.swift in Sources */, - 56185BBF25B8036100B9C30F /* Persistence.swift in Sources */, - 56B036071E8D9EBE003B6DA4 /* AppDelegate.swift in Sources */, - 56B036241E8D9F4C003B6DA4 /* PlayerListViewController.swift in Sources */, - 56B036231E8D9F4C003B6DA4 /* PlayerEditionViewController.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 56185BF625B80B8900B9C30F /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 56B036021E8D9EBE003B6DA4 /* GRDBDemoiOS */; - targetProxy = 56185BF525B80B8900B9C30F /* PBXContainerItemProxy */; - }; - 5642253029A2390800D714BF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = GRDB; - targetProxy = 5642252F29A2390800D714BF /* PBXContainerItemProxy */; - }; - 5642253429A2391900D714BF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = GRDB; - targetProxy = 5642253329A2391900D714BF /* PBXContainerItemProxy */; - }; - 568E5FCE1E926430002582E0 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 568E5FCA1E926430002582E0 /* GRDBDemoWatchOS Extension */; - targetProxy = 568E5FCD1E926430002582E0 /* PBXContainerItemProxy */; - }; - 568E5FD81E926430002582E0 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 568E5FBE1E926430002582E0 /* GRDBDemoWatchOS */; - targetProxy = 568E5FD71E926430002582E0 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 568E5FC11E926430002582E0 /* Interface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 568E5FC21E926430002582E0 /* Base */, - ); - name = Interface.storyboard; - sourceTree = ""; - }; - 56B0360A1E8D9EBE003B6DA4 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 56B0360B1E8D9EBE003B6DA4 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 56B0360F1E8D9EBE003B6DA4 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 56B036101E8D9EBE003B6DA4 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 56185BF725B80B8900B9C30F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = AMD8W895CT; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = GRDBDemoiOSTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOSTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBDemoiOS.app/GRDBDemoiOS"; - }; - name = Debug; - }; - 56185BF825B80B8900B9C30F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = AMD8W895CT; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = GRDBDemoiOSTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOSTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GRDBDemoiOS.app/GRDBDemoiOS"; - }; - name = Release; - }; - 568E5FDB1E926430002582E0 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; - CLANG_ENABLE_OBJC_WEAK = YES; - DEVELOPMENT_TEAM = AMD8W895CT; - INFOPLIST_FILE = "GRDBDemoWatchOS Extension/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp.watchkitextension; - PRODUCT_NAME = "${TARGET_NAME}"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Debug; - }; - 568E5FDC1E926430002582E0 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; - CLANG_ENABLE_OBJC_WEAK = YES; - DEVELOPMENT_TEAM = AMD8W895CT; - INFOPLIST_FILE = "GRDBDemoWatchOS Extension/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp.watchkitextension; - PRODUCT_NAME = "${TARGET_NAME}"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Release; - }; - 568E5FDF1E926430002582E0 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_OBJC_WEAK = YES; - DEVELOPMENT_TEAM = AMD8W895CT; - IBSC_MODULE = GRDBDemoWatchOS_Extension; - INFOPLIST_FILE = GRDBDemoWatchOS/Info.plist; - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Debug; - }; - 568E5FE01E926430002582E0 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_OBJC_WEAK = YES; - DEVELOPMENT_TEAM = AMD8W895CT; - IBSC_MODULE = GRDBDemoWatchOS_Extension; - INFOPLIST_FILE = GRDBDemoWatchOS/Info.plist; - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2.watchkitapp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Release; - }; - 56B036131E8D9EBE003B6DA4 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - WATCHOS_DEPLOYMENT_TARGET = 8.0; - }; - name = Debug; - }; - 56B036141E8D9EBE003B6DA4 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; - VALIDATE_PRODUCT = YES; - WATCHOS_DEPLOYMENT_TARGET = 8.0; - }; - name = Release; - }; - 56B036161E8D9EBE003B6DA4 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_OBJC_WEAK = YES; - DEVELOPMENT_TEAM = AMD8W895CT; - INFOPLIST_FILE = GRDBDemoiOS/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 56B036171E8D9EBE003B6DA4 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_OBJC_WEAK = YES; - DEVELOPMENT_TEAM = AMD8W895CT; - INFOPLIST_FILE = GRDBDemoiOS/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBDemoiOS2; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 56185BFF25B80B8900B9C30F /* Build configuration list for PBXNativeTarget "GRDBDemoiOSTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 56185BF725B80B8900B9C30F /* Debug */, - 56185BF825B80B8900B9C30F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 568E5FDA1E926430002582E0 /* Build configuration list for PBXNativeTarget "GRDBDemoWatchOS Extension" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 568E5FDB1E926430002582E0 /* Debug */, - 568E5FDC1E926430002582E0 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 568E5FDE1E926430002582E0 /* Build configuration list for PBXNativeTarget "GRDBDemoWatchOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 568E5FDF1E926430002582E0 /* Debug */, - 568E5FE01E926430002582E0 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 56B035FE1E8D9EBE003B6DA4 /* Build configuration list for PBXProject "GRDBDemoiOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 56B036131E8D9EBE003B6DA4 /* Debug */, - 56B036141E8D9EBE003B6DA4 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 56B036151E8D9EBE003B6DA4 /* Build configuration list for PBXNativeTarget "GRDBDemoiOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 56B036161E8D9EBE003B6DA4 /* Debug */, - 56B036171E8D9EBE003B6DA4 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 56B035FB1E8D9EBE003B6DA4 /* Project object */; -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoWatchOS.xcscheme b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoWatchOS.xcscheme deleted file mode 100644 index bd03393c0d..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/xcshareddata/xcschemes/GRDBDemoWatchOS.xcscheme +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDelegate.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDelegate.swift deleted file mode 100644 index 07edceeed8..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/AppDelegate.swift +++ /dev/null @@ -1,22 +0,0 @@ -import UIKit - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Info.plist b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Info.plist deleted file mode 100644 index b5f9c0796b..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Info.plist +++ /dev/null @@ -1,62 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Persistence.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Persistence.swift deleted file mode 100644 index 96267e6846..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Persistence.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -import GRDB - -extension AppDatabase { - /// The database for the application - static let shared = makeShared() - - private static func makeShared() -> AppDatabase { - do { - // Apply recommendations from - // - // - // Create the "Application Support/Database" directory if needed - let fileManager = FileManager.default - let appSupportURL = try fileManager.url( - for: .applicationSupportDirectory, in: .userDomainMask, - appropriateFor: nil, create: true) - let directoryURL = appSupportURL.appendingPathComponent("Database", isDirectory: true) - try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true) - - // Open or create the database - let databaseURL = directoryURL.appendingPathComponent("db.sqlite") - NSLog("Database stored at \(databaseURL.path)") - let dbPool = try DatabasePool( - path: databaseURL.path, - // Use default AppDatabase configuration - configuration: AppDatabase.makeConfiguration()) - - // Create the AppDatabase - let appDatabase = try AppDatabase(dbPool) - - // Populate the database if it is empty, for better demo purpose. - try appDatabase.createRandomPlayersIfEmpty() - - return appDatabase - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. - // - // Typical reasons for an error here include: - // * The parent directory cannot be created, or disallows writing. - // * The database is not accessible, due to permissions or data protection when the device is locked. - // * The device is out of space. - // * The database could not be migrated to its latest schema version. - // Check the error message to determine what the actual problem was. - fatalError("Unresolved error \(error)") - } - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift deleted file mode 100644 index 82204b62c4..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Player.swift +++ /dev/null @@ -1,104 +0,0 @@ -import GRDB - -/// The Player struct. -/// -/// Identifiable conformance supports type-safe GRDB primary key methods. -/// Hashable conformance supports table view updates -struct Player: Identifiable, Hashable { - /// The player id. - /// - /// Int64 is the recommended type for auto-incremented database ids. - /// Use nil for players that are not inserted yet in the database. - var id: Int64? - var name: String - var score: Int -} - -extension Player { - private static let names = [ - "Arthur", "Anita", "Barbara", "Bernard", "Craig", "Chiara", "David", - "Dean", "Éric", "Elena", "Fatima", "Frederik", "Gilbert", "Georgette", - "Henriette", "Hassan", "Ignacio", "Irene", "Julie", "Jack", "Karl", - "Kristel", "Louis", "Liz", "Masashi", "Mary", "Noam", "Nicole", - "Ophelie", "Oleg", "Pascal", "Patricia", "Quentin", "Quinn", "Raoul", - "Rachel", "Stephan", "Susie", "Tristan", "Tatiana", "Ursule", "Urbain", - "Victor", "Violette", "Wilfried", "Wilhelmina", "Yvon", "Yann", - "Zazie", "Zoé"] - - /// Creates a new player with empty name and zero score - static func new() -> Player { - Player(id: nil, name: "", score: 0) - } - - /// Creates a new player with random name and random score - static func makeRandom() -> Player { - Player(id: nil, name: randomName(), score: randomScore()) - } - - /// Returns a random name - static func randomName() -> String { - names.randomElement()! - } - - /// Returns a random score - static func randomScore() -> Int { - 10 * Int.random(in: 0...100) - } -} - -// MARK: - Persistence - -/// Make Player a Codable Record. -/// -/// See -extension Player: Codable, FetchableRecord, MutablePersistableRecord { - // Define database columns from CodingKeys - fileprivate enum Columns { - static let name = Column(CodingKeys.name) - static let score = Column(CodingKeys.score) - } - - /// Updates a player id after it has been inserted in the database. - mutating func didInsert(_ inserted: InsertionSuccess) { - id = inserted.rowID - } -} - -// MARK: - Player Database Requests - -/// Define some player requests used by the application. -/// -/// See -extension DerivableRequest { - /// A request of players ordered by name. - /// - /// For example: - /// - /// let players: [Player] = try dbWriter.read { db in - /// try Player.all().orderedByName().fetchAll(db) - /// } - func orderedByName() -> Self { - // Sort by name in a localized case insensitive fashion - // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison - order(Player.Columns.name.collating(.localizedCaseInsensitiveCompare)) - } - - /// A request of players ordered by score. - /// - /// For example: - /// - /// let players: [Player] = try dbWriter.read { db in - /// try Player.all().orderedByScore().fetchAll(db) - /// } - /// let bestPlayer: Player? = try dbWriter.read { db in - /// try Player.all().orderedByScore().fetchOne(db) - /// } - func orderedByScore() -> Self { - // Sort by descending score, and then by name, in a - // localized case insensitive fashion - // See https://github.com/groue/GRDB.swift/blob/master/README.md#string-comparison - order( - Player.Columns.score.desc, - Player.Columns.name.collating(.localizedCaseInsensitiveCompare)) - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 29d91251df..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "icon_20pt@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "icon_20pt@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "icon_29pt@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "icon_29pt@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "icon_40pt@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "icon_40pt@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "icon_60pt@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "icon_60pt@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "icon_20pt.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "icon_20pt@2x-1.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "icon_29pt.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "icon_29pt@2x-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "icon_40pt.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "icon_40pt@2x-1.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "icon_76pt.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "icon_76pt@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "icon_83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png deleted file mode 100644 index 66b1931a14..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png deleted file mode 100644 index 90648b3f40..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png deleted file mode 100644 index a077a6f490..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png deleted file mode 100644 index a077a6f490..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png deleted file mode 100644 index 600bdbd9cd..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png deleted file mode 100644 index 8e04af0dd8..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png deleted file mode 100644 index 686e8d99e2..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png deleted file mode 100644 index 686e8d99e2..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png deleted file mode 100644 index 1d013c3d33..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png deleted file mode 100644 index a077a6f490..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png deleted file mode 100644 index da66b9ba82..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png deleted file mode 100644 index da66b9ba82..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png deleted file mode 100644 index 59346ef4b6..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png deleted file mode 100644 index 59346ef4b6..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png deleted file mode 100644 index d4640afc9a..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png deleted file mode 100644 index e3a04522bf..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png deleted file mode 100644 index 593ebd783d..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png deleted file mode 100644 index ca02cd03bc..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf deleted file mode 100644 index 2660891492..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/LaunchIcon.pdf and /dev/null differ diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/LaunchScreen.storyboard b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 307d45b11d..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/Main.storyboard b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/Main.storyboard deleted file mode 100644 index 0f36128da4..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Base.lproj/Main.storyboard +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/SceneDelegate.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/SceneDelegate.swift deleted file mode 100644 index b484ed11dc..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/SceneDelegate.swift +++ /dev/null @@ -1,40 +0,0 @@ -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerEditionViewController.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerEditionViewController.swift deleted file mode 100644 index e9988462ae..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerEditionViewController.swift +++ /dev/null @@ -1,128 +0,0 @@ -import UIKit - -class PlayerEditionViewController: UITableViewController { - enum Mode { - /// Edition ends with the "Commit" unwind segue. - case creation - - /// Edition ends when user hits the back button. - case edition - } - - /// The edited player - private(set) var player: Player - - /// The presentation mode - let mode: Mode - - @IBOutlet private weak var cancelButtonItem: UIBarButtonItem! - @IBOutlet private weak var saveButtonItem: UIBarButtonItem! - @IBOutlet private weak var nameCell: UITableViewCell! - @IBOutlet private weak var nameTextField: UITextField! - @IBOutlet private weak var scoreCell: UITableViewCell! - @IBOutlet private weak var scoreTextField: UITextField! - - init?(_ coder: NSCoder, mode: Mode, player: Player) { - self.mode = mode - self.player = player - super.init(coder: coder) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - configureNavigationItem() - configureForm() - } -} - -// MARK: - Navigation - -extension PlayerEditionViewController { - override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { - // Force keyboard to dismiss early - view.endEditing(true) - return true - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == "Commit" { - saveChanges() - } - } - - override func willMove(toParent parent: UIViewController?) { - super.willMove(toParent: parent) - - if mode == .edition, parent == nil { - // Self is popping from its navigation controller - saveChanges() - } - } - - private func configureNavigationItem() { - switch mode { - case .creation: - navigationItem.title = "New Player" - navigationItem.leftBarButtonItem = cancelButtonItem - navigationItem.rightBarButtonItem = saveButtonItem - case .edition: - navigationItem.title = player.name - navigationItem.leftBarButtonItem = nil - navigationItem.rightBarButtonItem = nil - } - } -} - -// MARK: - Form - -extension PlayerEditionViewController: UITextFieldDelegate { - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - nameTextField.becomeFirstResponder() - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: false) - let cell = tableView.cellForRow(at: indexPath) - if cell === nameCell { - nameTextField.becomeFirstResponder() - } else if cell === scoreCell { - scoreTextField.becomeFirstResponder() - } - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField == nameTextField { - scoreTextField.becomeFirstResponder() - } - return false - } - - @IBAction func textFieldDidChange(_ textField: UITextField) { - // User has edited the player: prevent interactive dismissal - isModalInPresentation = true - } - - private func configureForm() { - nameTextField.text = player.name - - if player.score == 0 && player.id == nil { - scoreTextField.text = "" - } else { - scoreTextField.text = "\(player.score)" - } - } - - private func saveChanges() { - var player = self.player - player.name = nameTextField.text ?? "" - player.score = scoreTextField.text.flatMap { Int($0) } ?? 0 - try! AppDatabase.shared.savePlayer(&player) - self.player = player - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerListViewController.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerListViewController.swift deleted file mode 100644 index 0ef0c52d4b..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/ViewControllers/PlayerListViewController.swift +++ /dev/null @@ -1,194 +0,0 @@ -import UIKit -import GRDB - -/// PlayerListViewController displays the list of players. -class PlayerListViewController: UITableViewController { - private enum PlayerOrdering { - case byName - case byScore - } - - @IBOutlet private weak var newPlayerButtonItem: UIBarButtonItem! - private var dataSource: PlayerDataSource! - private var playersCancellable: DatabaseCancellable? - private var playerOrdering: PlayerOrdering = .byScore { - didSet { - configureOrderingBarButtonItem() - observePlayers() - } - } - - override func viewDidLoad() { - super.viewDidLoad() - configureToolbar() - configureNavigationItem() - configureDataSource() - observePlayers() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.isToolbarHidden = false - } - - private func configureToolbar() { - toolbarItems = [ - UIBarButtonItem(systemItem: .trash, primaryAction: UIAction { [unowned self] _ in - setEditing(false, animated: true) - try! AppDatabase.shared.deleteAllPlayers() - }), - UIBarButtonItem(systemItem: .flexibleSpace), - UIBarButtonItem(systemItem: .refresh, primaryAction: UIAction { [unowned self] _ in - setEditing(false, animated: true) - try! AppDatabase.shared.refreshPlayers() - }), - UIBarButtonItem(systemItem: .flexibleSpace), - UIBarButtonItem(image: UIImage(systemName: "tornado"), primaryAction: UIAction { [unowned self] _ in - setEditing(false, animated: true) - for _ in 0..<50 { - DispatchQueue.global().async { - try! AppDatabase.shared.refreshPlayers() - } - } - }), - ] - } - - private func configureNavigationItem() { - navigationItem.backBarButtonItem = UIBarButtonItem(title: "Players") - navigationItem.leftBarButtonItems = [editButtonItem, newPlayerButtonItem] - configureOrderingBarButtonItem() - } - - private func configureOrderingBarButtonItem() { - switch playerOrdering { - case .byScore: - navigationItem.rightBarButtonItem = UIBarButtonItem( - title: "Score ▼", - primaryAction: UIAction { [unowned self] _ in - setEditing(false, animated: true) - playerOrdering = .byName - }) - case .byName: - navigationItem.rightBarButtonItem = UIBarButtonItem( - title: "Name ▲", - primaryAction: UIAction { [unowned self] _ in - setEditing(false, animated: true) - playerOrdering = .byScore - }) - } - } - - private func configureDataSource() { - dataSource = PlayerDataSource(tableView: tableView) { (tableView, indexPath, player) in - let cell = tableView.dequeueReusableCell(withIdentifier: "Player", for: indexPath) - if player.name.isEmpty { - cell.textLabel?.text = "(anonymous)" - } else { - cell.textLabel?.text = player.name - } - cell.detailTextLabel?.text = abs(player.score) > 1 ? "\(player.score) points" : "0 point" - return cell - } - dataSource.defaultRowAnimation = .fade - tableView.dataSource = dataSource - } - - private func configureTitle(from players: [Player]) { - switch players.count { - case 0: - navigationItem.title = "No Player" - case 1: - navigationItem.title = "1 Player" - case let count: - navigationItem.title = "\(count) Players" - } - } - - private func configureDataSource(from players: [Player]) { - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([0]) - snapshot.appendItems(players, toSection: 0) - - // Remember selection - let selectedPlayerId = tableView.indexPathForSelectedRow.flatMap { - dataSource.itemIdentifier(for: $0)?.id - } - - // Avoid a UIKit warning; don't animate when popping from edition - let animated = view.window != nil - - dataSource.apply(snapshot, animatingDifferences: animated, completion: { - // Restore selection - if let index = players.firstIndex(where: { $0.id == selectedPlayerId }) { - self.tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none) - } - }) - } - - private func observePlayers() { - let request: QueryInterfaceRequest - switch playerOrdering { - case .byName: - request = Player.all().orderedByName() - case .byScore: - request = Player.all().orderedByScore() - } - - playersCancellable = ValueObservation - .tracking(request.fetchAll(_:)) - .start( - in: AppDatabase.shared.reader, - // Immediate scheduling feeds the data source right on subscription, - // and avoids an undesired animation when the application starts. - scheduling: .immediate, - onError: { error in fatalError("Unexpected error: \(error)") }, - onChange: { [weak self] players in - guard let self else { return } - self.configureTitle(from: players) - self.configureDataSource(from: players) - }) - } -} - - -// MARK: - Navigation - -extension PlayerListViewController { - @IBSegueAction func makePlayerEditionViewController(_ coder: NSCoder) -> PlayerEditionViewController? { - guard let indexPath = tableView.indexPathForSelectedRow, - let player = dataSource.itemIdentifier(for: indexPath) - else { return nil } - return PlayerEditionViewController(coder, mode: .edition, player: player) - } - - @IBSegueAction func makePlayerCreationViewController(_ coder: NSCoder) -> PlayerEditionViewController? { - let player = Player(id: nil, name: "", score: 0) - return PlayerEditionViewController(coder, mode: .creation, player: player) - } - - @IBAction func cancelPlayerEdition(_ segue: UIStoryboardSegue) { - // Player creation cancelled - } - - @IBAction func commitPlayerEdition(_ segue: UIStoryboardSegue) { - // Player creation committed - } -} - - -// MARK: - UITableViewDataSource - -/// Subclass of UITableViewDiffableDataSource that supports row deletion -private class PlayerDataSource: UITableViewDiffableDataSource { - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - true - } - - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - // Delete the player - if let player = itemIdentifier(for: indexPath), let id = player.id { - try! AppDatabase.shared.deletePlayers(ids: [id]) - } - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOSTests/AppDatabaseTests.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOSTests/AppDatabaseTests.swift deleted file mode 100644 index 2c359a8988..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOSTests/AppDatabaseTests.swift +++ /dev/null @@ -1,146 +0,0 @@ -import XCTest -import GRDB -@testable import GRDBDemoiOS - -class AppDatabaseTests: XCTestCase { - func test_database_schema() throws { - // Given an empty database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - - // When we instantiate an AppDatabase - _ = try AppDatabase(dbQueue) - - // Then the player table exists, with id, name & score columns - try dbQueue.read { db in - try XCTAssert(db.tableExists("player")) - let columns = try db.columns(in: "player") - let columnNames = Set(columns.map { $0.name }) - XCTAssertEqual(columnNames, ["id", "name", "score"]) - } - } - - func test_savePlayer_inserts() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - - // When we save a new player - var player = Player(id: nil, name: "Arthur", score: 100) - try appDatabase.savePlayer(&player) - - // Then the player exists in the database - try XCTAssertTrue(dbQueue.read(player.exists)) - } - - func test_savePlayer_updates() throws { - // Given a players database that contains a player - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - var player = Player(id: nil, name: "Arthur", score: 100) - try dbQueue.write { db in - try player.insert(db) - } - - // When we modify and save the player - player.name = "Barbara" - player.score = 1000 - try appDatabase.savePlayer(&player) - - // Then the player has been updated in the database - let fetchedPlayer = try dbQueue.read { db in - try XCTUnwrap(Player.fetchOne(db, key: player.id)) - } - XCTAssertEqual(fetchedPlayer, player) - } - - func test_deletePlayers() throws { - // Given a players database that contains four players - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - var player1 = Player(id: nil, name: "Arthur", score: 100) - var player2 = Player(id: nil, name: "Barbara", score: 200) - var player3 = Player(id: nil, name: "Craig", score: 150) - var player4 = Player(id: nil, name: "David", score: 120) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we delete two players - try appDatabase.deletePlayers(ids: [player1.id!, player3.id!]) - - // Then the deleted players no longer exist - try dbQueue.read { db in - try XCTAssertFalse(player1.exists(db)) - try XCTAssertFalse(player3.exists(db)) - } - - // Then the database still contains two players - try XCTAssertEqual(dbQueue.read(Player.fetchCount), 2) - } - - func test_deleteAllPlayers() throws { - // Given a players database that contains players - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - var player1 = Player(id: nil, name: "Arthur", score: 100) - var player2 = Player(id: nil, name: "Barbara", score: 200) - var player3 = Player(id: nil, name: "Craig", score: 150) - var player4 = Player(id: nil, name: "David", score: 120) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we delete all players - try appDatabase.deleteAllPlayers() - - // Then the database does not contain any player - try XCTAssertEqual(dbQueue.read(Player.fetchCount), 0) - } - - func test_refreshPlayers_populates_an_empty_database() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - - // When we refresh players - try appDatabase.refreshPlayers() - - // Then the database is not empty - try XCTAssert(dbQueue.read(Player.fetchCount) > 0) - } - - func test_createRandomPlayersIfEmpty_populates_an_empty_database() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - - // When we create random players - try appDatabase.createRandomPlayersIfEmpty() - - // Then the database is not empty - try XCTAssert(dbQueue.read(Player.fetchCount) > 0) - } - - func test_createRandomPlayersIfEmpty_does_not_modify_a_non_empty_database() throws { - // Given a players database that contains one player - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - let appDatabase = try AppDatabase(dbQueue) - var player = Player(id: nil, name: "Arthur", score: 100) - try dbQueue.write { db in - try player.insert(db) - } - - // When we create random players - try appDatabase.createRandomPlayersIfEmpty() - - // Then the database still only contains the original player - let players = try dbQueue.read(Player.fetchAll) - XCTAssertEqual(players, [player]) - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOSTests/Info.plist b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOSTests/Info.plist deleted file mode 100644 index 64d65ca495..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOSTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOSTests/PlayerTests.swift b/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOSTests/PlayerTests.swift deleted file mode 100644 index 64d1c1d2b3..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOSTests/PlayerTests.swift +++ /dev/null @@ -1,108 +0,0 @@ -import XCTest -import GRDB -@testable import GRDBDemoiOS - -class PlayerTests: XCTestCase { - // MARK: - CRUD - // Test that our Player type properly talks to GRDB. - - func testInsert() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - - // When we insert a player - var player = Player(id: nil, name: "Arthur", score: 100) - try dbQueue.write { db in - try player.insert(db) - } - - // Then the player gets a non-nil id - XCTAssertNotNil(player.id) - } - - func testRoundtrip() throws { - // Given an empty players database - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - - // When we insert a player and fetch the player with the same id - var insertedPlayer = Player(id: nil, name: "Arthur", score: 100) - let fetchedPlayer: Player? = try dbQueue.write { db in - try insertedPlayer.insert(db) - return try Player.fetchOne(db, key: insertedPlayer.id) - } - - // Then the fetched player is equal to the inserted player - XCTAssertEqual(insertedPlayer, fetchedPlayer) - } - - // MARK: - Requests - // Test that requests defined on the Player type behave as expected. - - func testOrderedByScore() throws { - // Given a players database that contains players with distinct scores - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: 1, name: "Arthur", score: 100) - var player2 = Player(id: 2, name: "Barbara", score: 200) - var player3 = Player(id: 3, name: "Craig", score: 150) - var player4 = Player(id: 4, name: "David", score: 120) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we fetch players ordered by score - let players = try dbQueue.read(Player.all().orderedByScore().fetchAll) - - // Then fetched players are ordered by score descending - XCTAssertEqual(players, [player2, player3, player4, player1]) - } - - func testOrderedByScoreSortsIdenticalScoresByName() throws { - // Given a players database that contains players with common scores - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: 1, name: "Arthur", score: 100) - var player2 = Player(id: 2, name: "Barbara", score: 200) - var player3 = Player(id: 3, name: "Craig", score: 200) - var player4 = Player(id: 4, name: "David", score: 200) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we fetch players ordered by score - let players = try dbQueue.read(Player.all().orderedByScore().fetchAll) - - // Then fetched players are ordered by score descending and by name - XCTAssertEqual(players, [player2, player3, player4, player1]) - } - - func testOrderedByName() throws { - // Given a players database that contains players with distinct names - let dbQueue = try DatabaseQueue(configuration: AppDatabase.makeConfiguration()) - _ = try AppDatabase(dbQueue) - var player1 = Player(id: 1, name: "Arthur", score: 100) - var player2 = Player(id: 2, name: "Barbara", score: 200) - var player3 = Player(id: 3, name: "Craig", score: 150) - var player4 = Player(id: 4, name: "David", score: 120) - try dbQueue.write { db in - try player1.insert(db) - try player2.insert(db) - try player3.insert(db) - try player4.insert(db) - } - - // When we fetch players ordered by name - let players = try dbQueue.read(Player.all().orderedByName().fetchAll) - - // Then fetched players are ordered by name - XCTAssertEqual(players, [player1, player2, player3, player4]) - } -} diff --git a/Documentation/DemoApps/GRDBDemoiOS/README.md b/Documentation/DemoApps/GRDBDemoiOS/README.md deleted file mode 100644 index ab854697c7..0000000000 --- a/Documentation/DemoApps/GRDBDemoiOS/README.md +++ /dev/null @@ -1,45 +0,0 @@ -UIKit Demo Application -====================== - - - -**This demo application is a storyboard-based UIKit application.** For a demo application that uses Combine + SwiftUI, see [GRDBCombineDemo](../GRDBCombineDemo/README.md), and for Async/Await + SwiftUI, see [GRDBAsyncDemo](../GRDBAsyncDemo/README.md). - -**Requirements**: iOS 15.0+ / Xcode 12+ - -> **Note**: This demo app is not a project template. Do not copy it as a starting point for your application. Instead, create a new project, choose a GRDB [installation method](../../../README.md#installation), and use the demo as an inspiration. - -The topics covered in this demo are: - -- How to setup a database in an iOS app. -- How to define a simple [Codable Record](../../../README.md#codable-records). -- How to track database changes and animate a table view with [ValueObservation](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation). -- How to apply the recommendations of [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices). - -**Files of interest:** - -- [AppDatabase.swift](GRDBDemoiOS/AppDatabase.swift) - - `AppDatabase` is the type that grants database access. It uses [DatabaseMigrator](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasemigrator) in order to setup the database schema, and [ValueObservation](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/valueobservation) in order to let the application observe database changes. - -- [Persistence.swift](GRDBDemoiOS/Persistence.swift) - - This file defines the `AppDatabase` instance used by the application. - -- [Player.swift](GRDBDemoiOS/Player.swift) - - `Player` is a [Record](../../../README.md#records) type, able to read and write in the database. It conforms to the standard Codable protocol in order to gain all advantages of [Codable Records](../../../README.md#codable-records). It defines the database requests used by the application. - -- [PlayerListViewController.swift](GRDBDemoiOS/ViewControllers/PlayerListViewController.swift) - - `PlayerListViewController` displays the list of players. - -- [PlayerEditionViewController.swift](GRDBDemoiOS/ViewControllers/PlayerEditionViewController.swift) - - `PlayerEditionViewController` can create or edit a player, and save it in the database. - -- [GRDBDemoiOSTests](GRDBDemoiOSTests) - - - Test the database schema - - Test the `Player` record and its requests - - Test the `AppDatabase` methods that let the app access the database. diff --git a/Documentation/DemoApps/GRDBDemoiOS/Screenshot.png b/Documentation/DemoApps/GRDBDemoiOS/Screenshot.png deleted file mode 100644 index 7a0fccd710..0000000000 Binary files a/Documentation/DemoApps/GRDBDemoiOS/Screenshot.png and /dev/null differ diff --git a/Documentation/DemoApps/README.md b/Documentation/DemoApps/README.md index 136665c684..8c6ce177f6 100644 --- a/Documentation/DemoApps/README.md +++ b/Documentation/DemoApps/README.md @@ -1,13 +1,9 @@ Demo Applications ================= -- [GRDBDemoiOS]: a storyboard-based UIKit application. -- [GRDBCombineDemo]: a Combine + SwiftUI application. -- [GRDBAsyncDemo]: a Async/Await + SwiftUI application. +[GRDBDemo] demonstrates how GRDB can fuel a SwiftUI application. -[GRDBCombineDemo] and [GRDBAsyncDemo] use the same `@Query` property wrapper, that lets SwiftUI views automatically update their content when the database changes. It is defined in the [GRDBQuery] package. +See also the demo apps of the [GRDBQuery] package: they use the `@Query` property wrapper that helps SwiftUI views automatically update their content when the database changes. -[GRDBDemoiOS]: GRDBDemoiOS -[GRDBCombineDemo]: GRDBCombineDemo -[GRDBAsyncDemo]: GRDBAsyncDemo +[GRDBDemo]: GRDBDemo [GRDBQuery]: https://github.com/groue/GRDBQuery diff --git a/Documentation/GRDB7MigrationGuide.md b/Documentation/GRDB7MigrationGuide.md new file mode 100644 index 0000000000..4fbf5df773 --- /dev/null +++ b/Documentation/GRDB7MigrationGuide.md @@ -0,0 +1,188 @@ +Migrating From GRDB 6 to GRDB 7 +=============================== + +**This guide helps you upgrade your applications from GRDB 6 to GRDB 7.** + +- [Preparing the Migration to GRDB 7](#preparing-the-migration-to-grdb-7) +- [New requirements](#new-requirements) +- [The Record Base Class is Discouraged](#the-record-base-class-is-discouraged) +- [Column Coding Strategies](#column-coding-strategies) +- [Cancellable Async Database Accesses](#cancellable-async-database-accesses) +- [Default Transaction Kind](#default-transaction-kind) +- [Access to SQLite C functions](#access-to-sqlite-c-functions) +- [Recommendations Regarding Swift Concurrency](#recommendations-regarding-swift-concurrency) +- [Other Changes](#other-changes) + +## Preparing the Migration to GRDB 7 + +Before upgrading, ensure you are using the [latest GRDB 6 release](https://github.com/groue/GRDB.swift/tags) and address any deprecation warnings. Once this is done, proceed with upgrading to GRDB 7. Due to breaking changes, your application may no longer compile. Follow the fix-it suggestions for simple syntax updates, and review the specific modifications described below. + +## New requirements + +GRDB requirements have been bumped: + +- **Swift Compiler 6+** (was Swift 5.7+). Both Swift 5 and Swift 6 language modes are supported. For more information, see the [Migrating to Swift 6] Apple guide. +- **Xcode 16.0+** (was Xcode 14.0+) +- **iOS 13+** (was iOS 11+) +- **macOS 10.15+** (was macOS 10.13+) +- **tvOS 13+** (was tvOS 11+) +- **watchOS 7.0+** (was watchOS 4+) +- **SQLite 3.20.0+** (was SQLite 3.19.3+) + +## The Record Base Class is Discouraged + +The usage of the [Record] base class is **discouraged** in GRDB 7. Present in GRDB 1.0, in 2017, it has served its purpose. + +It is not recommended to define any new type that subclass `Record`. + +It is recommended to refactor `Record` subclasses into Swift structs, before you enable the strict concurrency checkings or the Swift 6 language mode. See [Migrating to Swift 6] for more information about Swift 6 language modes. + +For example: + +```swift +// GRDB 6 +class Player: Record { + var id: UUID + var name: String + var score: Int + + override class var databaseTableName: String { "player" } + + init(id: UUID, name: String, score: Int) { ... } + required init(row: Row) throws { ... } + override func encode(to container: inout PersistenceContainer) throws { ...} +} + +// GRDB 7 +struct Player: Codable { + var id: UUID + var name: String + var score: Int +} + +extension Player: FetchableRecord, PersistableRecord { } +``` + +Do not miss [Swift Concurrency and GRDB], for more recommendations regarding non-Sendable record types in GRDB. + +## Column Coding Strategies + +In GRDB 6, Codable record types can specify how `Data`, `Date`, and `UUID` properties are stored in the database: + +```swift +// GRDB 6 +struct Player { + static let databaseDataDecodingStrategy = ... + static let databaseDateDecodingStrategy = ... + static let databaseDataEncodingStrategy = ... + static let databaseDateEncodingStrategy = ... + static let databaseUUIDEncodingStrategy = ... +} +``` + +These properties have been removed in GRDB 7. You must now define methods that accept a column argument: + +```swift +// GRDB 7 +struct Player { + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { ... } + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { ...} + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { ... } + static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy { ... } + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { ... } +} +``` + +## Cancellable Async Database Accesses + +In GRDB 6, asynchronous database accesses such as `try await read { ... }` or `try await write { ... }` complete even if the wrapper Task is cancelled. + +In GRDB 7, asynchronous database accesses respect Task cancellation. If a Task is cancelled, reads and writes throw a `CancellationError`, pending transactions are rolled back, and the database is not modified. The only SQL statement that can execute in a cancelled database access is `ROLLBACK`. + +The effect of this change on your application depends on how it uses tasks. For example, take care of database jobs initiated frop the [`task`](https://developer.apple.com/documentation/swiftui/view/task(priority:_:)) SwiftUI modifier. + +If you want an asynchronous database access to always complete, regardless of Task cancellation, wrap it in an unstructured Task: + +```swift +// Create a new Task in order to ignore +// cancellation of the current task, and +// make sure database changes are always +// committed to disk. +let task = Task { + try await writer.write { ... } +} +// If needed, wait for the database job to complete: +try await task.value +``` + +Other asynchronous database accesses, such as methods accepting a completion blocks (`asyncRead`, etc.), Combine publishers, RxSwift observables, do not handle cancellation and will proceed to completion by default. + +## Default Transaction Kind + +Some applications specify a default transaction kind, which was previously recommended in the [Sharing a Database] guide: + +```swift +// GRDB 6 +var config = Configuration() +config.defaultTransactionKind = .immediate +``` + +In GRDB 7, `Configuration` no longer has a `defaultTransactionKind` property, because transactions are automatically managed. Reads use DEFERRED transactions, and writes use IMMEDIATE transactions. + +You can still specify a transaction kind explicitly when necessary. See [Transaction Kinds] for details. + +## Access to SQLite C functions + +In GRDB 6, the underlying C SQLite library is implicitly available: + +```swift +// GRDB 6 +import GRDB + +let sqliteVersion = sqlite3_libversion_number() +``` + +In GRDB 7, you may need an additional import, depending on how GRDB is integrated: + +- If your app uses the GRDB Swift Package Manager (SPM) package: + + ```swift + import SQLite3 + + let sqliteVersion = sqlite3_libversion_number() + ``` + + The GRDB 6 SPM package included a product named "CSQLite." In GRDB 7, this product has been renamed "GRDBSQLite." Update your dependencies accordingly. It is unclear at the time of writing whether some projects can remove this dependency. + +- If your app uses SQLCipher: + + ```swift + import SQLCipher + + let sqliteVersion = sqlite3_libversion_number() + ``` + +- In other cases, no additional import is needed. + +## Recommendations Regarding Swift Concurrency + +GRDB 7 requires Xcode 16+ and a Swift 6 compiler. + +Depending of the language mode and level of concurrency checkings used by your application (see [Migrating to Swift 6]), you may see warnings or errors. We address those issues, and provide general guidance, in [Swift Concurrency and GRDB]. + + +## Other Changes + +- `ValueObservation` must be started from the Main Actor by default. Use an explicit `async(onQueue: .main)` scheduling in order to remove this constraint. + +- `DatabasePool.concurrentRead` has been removed. Use [`asyncConcurrentRead`](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasepool/asyncconcurrentread(_:)) instead. + +- The `PersistenceContainer` subscript no longer guarantees that the value returned is the same as what was previously set. It only guarantees that both values are encoded identically in the database. + +- The async sequence returned by [`ValueObservation.values`](https://swiftpackageindex.com/groue/grdb.swiftdocumentation/grdb/valueobservation/values(in:scheduling:bufferingpolicy:)) now iterates on the cooperative thread pool by default. Use .mainActor as the scheduler if you need the previous behavior. + +[Migrating to Swift 6]: https://www.swift.org/migration/documentation/migrationguide +[Sharing a Database]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databasesharing +[Transaction Kinds]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/transactions#Transaction-Kinds +[Swift Concurrency and GRDB]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/swiftconcurrency +[Record]: https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/record diff --git a/Documentation/ReleaseProcess.md b/Documentation/ReleaseProcess.md index 08210d7404..07749a5b3a 100644 --- a/Documentation/ReleaseProcess.md +++ b/Documentation/ReleaseProcess.md @@ -7,8 +7,7 @@ To release a new GRDB version: - Tests - `make distclean test` - - Build and run GRDBDemoiOS in Release configuration on a device - - Archive GRDBDemoiOS + - Build and run GRDBDemo - Check for performance regression with GRDBOSXPerformanceTests - On https://github.com/groue/sqlcipher.git upgrade, update SQLCipher version in README.md - On https://github.com/swiftlyfalling/SQLiteLib upgrade, update SQLite version in Documentation/CustomSQLiteBuilds.md diff --git a/Documentation/SQLInterpolation.md b/Documentation/SQLInterpolation.md index 2af59d2fb4..38666a8b9a 100644 --- a/Documentation/SQLInterpolation.md +++ b/Documentation/SQLInterpolation.md @@ -395,7 +395,9 @@ This chapter lists all kinds of supported interpolations. struct AltPlayer: TableRecord { static let databaseTableName = "player" - static let databaseSelection: [any SQLSelectable] = [Column("id"), Column("name")] + static var databaseSelection: [any SQLSelectable] { + [Column("id"), Column("name")] + } } // SELECT player.id, player.name FROM player diff --git a/GRDB.swift.podspec b/GRDB.swift.podspec index 9ebb0c099d..dcfc881e78 100644 --- a/GRDB.swift.podspec +++ b/GRDB.swift.podspec @@ -9,11 +9,11 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/groue/GRDB.swift.git', :tag => "v#{s.version}" } s.module_name = 'GRDB' - s.swift_versions = ['5.7'] - s.ios.deployment_target = '11.0' - s.osx.deployment_target = '10.13' - s.watchos.deployment_target = '4.0' - s.tvos.deployment_target = '11.0' + s.swift_versions = ['5.10'] + s.ios.deployment_target = '13.0' + s.osx.deployment_target = '10.15' + s.watchos.deployment_target = '7.0' + s.tvos.deployment_target = '13.0' s.default_subspec = 'standard' s.subspec 'standard' do |ss| diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 9c13f9eb07..bf103ccf18 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ 563363C01C942C04000BE133 /* DatabaseReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563363BF1C942C04000BE133 /* DatabaseReader.swift */; }; 563363C41C942C37000BE133 /* DatabaseWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563363C31C942C37000BE133 /* DatabaseWriter.swift */; }; 5636E9BC1D22574100B9B05F /* FetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5636E9BB1D22574100B9B05F /* FetchRequest.swift */; }; + 563866CB2C847659004C515A /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563866CA2C847654004C515A /* AsyncSemaphore.swift */; }; 563B06AB217EF0CC00B38F35 /* ValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06AA217EF0CC00B38F35 /* ValueObservation.swift */; }; 563B06BD2185CCD300B38F35 /* ValueObservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06BC2185CCD300B38F35 /* ValueObservationTests.swift */; }; 563B06C72185D29F00B38F35 /* ValueObservationReadonlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563B06C22185D29F00B38F35 /* ValueObservationReadonlyTests.swift */; }; @@ -105,6 +106,7 @@ 563C67B324628BEA00E94EDC /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563C67B224628BEA00E94EDC /* DatabasePoolTests.swift */; }; 563CBBE12A595131008905CE /* SQLIndexGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563CBBE02A595131008905CE /* SQLIndexGenerator.swift */; }; 563DE4F3231A91E2005081B7 /* DatabaseConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563DE4EC231A91E2005081B7 /* DatabaseConfigurationTests.swift */; }; + 563EA3E12C7B3A22001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E02C7B3A22001BE0D4 /* Mutex.swift */; }; 563EF415215F87EB007DAACD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF414215F87EB007DAACD /* OrderedDictionary.swift */; }; 563EF42D2161180D007DAACD /* AssociationAggregate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF42C2161180D007DAACD /* AssociationAggregate.swift */; }; 563EF43F216131D1007DAACD /* AssociationAggregateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF43E216131D1007DAACD /* AssociationAggregateTests.swift */; }; @@ -165,7 +167,7 @@ 5657AAB91D107001006283EF /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AAB81D107001006283EF /* NSData.swift */; }; 5657AB0F1D10899D006283EF /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB0E1D10899D006283EF /* URL.swift */; }; 5659F4881EA8D94E004A4992 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4871EA8D94E004A4992 /* Utils.swift */; }; - 5659F4901EA8D964004A4992 /* ReadWriteBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */; }; + 5659F4901EA8D964004A4992 /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */; }; 5659F4981EA8D989004A4992 /* Pool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4971EA8D989004A4992 /* Pool.swift */; }; 5664759A1D97D8A000FF74B8 /* SQLCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566475991D97D8A000FF74B8 /* SQLCollection.swift */; }; 566475CC1D981D5E00FF74B8 /* SQLFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566475CA1D981D5E00FF74B8 /* SQLFunctions.swift */; }; @@ -186,7 +188,7 @@ 566B912B1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */; }; 566B91331FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; 566B9C2025C6CC24004542CF /* RowDecodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B9C1F25C6CC24004542CF /* RowDecodingError.swift */; }; - 566BE71E2342542F00A8254B /* LockedBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BE7172342542F00A8254B /* LockedBox.swift */; }; + 566BE71E2342542F00A8254B /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BE7172342542F00A8254B /* Mutex.swift */; }; 566DDE0D288D763C0000DCFB /* Fixits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566DDE0C288D763C0000DCFB /* Fixits.swift */; }; 56703297212B5450007D270F /* DatabaseUUIDEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56703290212B544F007D270F /* DatabaseUUIDEncodingStrategyTests.swift */; }; 56713FDD2691F409006153C3 /* JSONRequiredEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56713FDC2691F409006153C3 /* JSONRequiredEncoder.swift */; }; @@ -267,7 +269,6 @@ 56A2388B1B9C75030082EB20 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238781B9C75030082EB20 /* Statement.swift */; }; 56A238931B9C750B0082EB20 /* DatabaseMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238921B9C750B0082EB20 /* DatabaseMigrator.swift */; }; 56A238A41B9C753B0082EB20 /* Record.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238A11B9C753B0082EB20 /* Record.swift */; }; - 56A2FA3624424D2A00E97D23 /* Export.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2FA3524424D2A00E97D23 /* Export.swift */; }; 56A5EF0F1EF7F20B00F03071 /* ForeignKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A5EF0E1EF7F20B00F03071 /* ForeignKeyInfoTests.swift */; }; 56A8C2301D1914540096E9D4 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A8C22F1D1914540096E9D4 /* UUID.swift */; }; 56AACAA822ACED7100A40F2A /* Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AACAA722ACED7100A40F2A /* Fetch.swift */; }; @@ -518,6 +519,7 @@ 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 = ""; }; + 563866CA2C847654004C515A /* AsyncSemaphore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSemaphore.swift; sourceTree = ""; }; 563B06AA217EF0CC00B38F35 /* ValueObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueObservation.swift; sourceTree = ""; }; 563B06BC2185CCD300B38F35 /* ValueObservationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueObservationTests.swift; sourceTree = ""; }; 563B06C22185D29F00B38F35 /* ValueObservationReadonlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationReadonlyTests.swift; sourceTree = ""; }; @@ -535,6 +537,7 @@ 563C67B224628BEA00E94EDC /* DatabasePoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolTests.swift; sourceTree = ""; }; 563CBBE02A595131008905CE /* SQLIndexGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLIndexGenerator.swift; sourceTree = ""; }; 563DE4EC231A91E2005081B7 /* DatabaseConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseConfigurationTests.swift; sourceTree = ""; }; + 563EA3E02C7B3A22001BE0D4 /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 563EF414215F87EB007DAACD /* OrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; 563EF42C2161180D007DAACD /* AssociationAggregate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociationAggregate.swift; sourceTree = ""; }; 563EF43E216131D1007DAACD /* AssociationAggregateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociationAggregateTests.swift; sourceTree = ""; }; @@ -602,7 +605,7 @@ 5657AB341D108BA9006283EF /* FoundationNSURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSURLTests.swift; sourceTree = ""; }; 5657AB351D108BA9006283EF /* FoundationURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationURLTests.swift; sourceTree = ""; }; 5659F4871EA8D94E004A4992 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteBox.swift; sourceTree = ""; }; + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; 5659F4971EA8D989004A4992 /* Pool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pool.swift; sourceTree = ""; }; 565B0FEE1BBC7D980098DE03 /* FetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableRecordTests.swift; sourceTree = ""; }; 565D5D701BBC694D00DC9BD4 /* Row+FoundationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Row+FoundationTests.swift"; sourceTree = ""; }; @@ -629,7 +632,7 @@ 566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatementAuthorizer.swift; sourceTree = ""; }; 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionObserver.swift; sourceTree = ""; }; 566B9C1F25C6CC24004542CF /* RowDecodingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowDecodingError.swift; sourceTree = ""; }; - 566BE7172342542F00A8254B /* LockedBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockedBox.swift; sourceTree = ""; }; + 566BE7172342542F00A8254B /* Mutex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 566DDE0C288D763C0000DCFB /* Fixits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fixits.swift; sourceTree = ""; }; 56703290212B544F007D270F /* DatabaseUUIDEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseUUIDEncodingStrategyTests.swift; sourceTree = ""; }; 56713FDC2691F409006153C3 /* JSONRequiredEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRequiredEncoder.swift; sourceTree = ""; }; @@ -717,6 +720,7 @@ 5698AD151DAAD16F0056AF8C /* FTS5Tokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5Tokenizer.swift; sourceTree = ""; }; 5698AD201DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5WrapperTokenizer.swift; sourceTree = ""; }; 5698AD341DABAF4A0056AF8C /* FTS5CustomTokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5CustomTokenizer.swift; sourceTree = ""; }; + 5698C1C32CA844F4001C0EB0 /* GRDBTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = GRDBTests.xcconfig; sourceTree = ""; }; 569BBA20228DE51800478429 /* AssociationPrefetchingFetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingFetchableRecordTests.swift; sourceTree = ""; }; 569BBA3522905FFA00478429 /* InflectionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InflectionsTests.swift; sourceTree = ""; }; 569BBA4522906A8200478429 /* InflectionsTests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InflectionsTests.json; sourceTree = ""; }; @@ -756,7 +760,6 @@ 56A238921B9C750B0082EB20 /* DatabaseMigrator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseMigrator.swift; sourceTree = ""; }; 56A238A11B9C753B0082EB20 /* Record.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Record.swift; sourceTree = ""; }; 56A238B51B9CA2590082EB20 /* DatabaseTimestampTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseTimestampTests.swift; sourceTree = ""; }; - 56A2FA3524424D2A00E97D23 /* Export.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Export.swift; sourceTree = ""; }; 56A4CDAF1D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLExpressionLiteralTests.swift; sourceTree = ""; }; 56A5E4081BA2BCF900707640 /* RecordWithColumnNameManglingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordWithColumnNameManglingTests.swift; sourceTree = ""; }; 56A5EF0E1EF7F20B00F03071 /* ForeignKeyInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForeignKeyInfoTests.swift; sourceTree = ""; }; @@ -1010,8 +1013,10 @@ 56176C581EACC2D8000F3F2B /* GRDBTests */ = { isa = PBXGroup; children = ( + 563866CA2C847654004C515A /* AsyncSemaphore.swift */, 56677C14241D14450050755D /* FailureTestCase.swift */, 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, + 563EA3E02C7B3A22001BE0D4 /* Mutex.swift */, 562EA81E1F17B26F00FA528C /* Compilation */, 56A238111B9C74A90082EB20 /* Core */, 567B5BDA2AD3281B00629622 /* Dump */, @@ -1320,11 +1325,11 @@ 56717270261C68E900423B6F /* CaseInsensitiveIdentifier.swift */, 563EF4492161F179007DAACD /* Inflections.swift */, 569BBA482291707D00478429 /* Inflections+English.swift */, - 566BE7172342542F00A8254B /* LockedBox.swift */, + 566BE7172342542F00A8254B /* Mutex.swift */, 563B8FC424A1D3B9007A48C9 /* OnDemandFuture.swift */, 563EF414215F87EB007DAACD /* OrderedDictionary.swift */, 5659F4971EA8D989004A4992 /* Pool.swift */, - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */, + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */, 563B8FB424A1D029007A48C9 /* ReceiveValuesOn.swift */, 56781B0A243F86E600650A83 /* Refinable.swift */, 5659F4871EA8D94E004A4992 /* Utils.swift */, @@ -1740,7 +1745,6 @@ DC37742D19C8CC90004FCF85 /* GRDB */ = { isa = PBXGroup; children = ( - 56A2FA3524424D2A00E97D23 /* Export.swift */, 566DDE0C288D763C0000DCFB /* Fixits.swift */, 648704AD2B7E66390036480B /* PrivacyInfo.xcprivacy */, 56A2386F1B9C75030082EB20 /* Core */, @@ -1766,6 +1770,7 @@ 56C48E731C9A9923005DF1D9 /* module.modulemap */, DC3773F719C8CBB3004FCF85 /* Info.plist */, 56B8F49A1B4E2F3600C24296 /* GRDB.xcconfig */, + 5698C1C32CA844F4001C0EB0 /* GRDBTests.xcconfig */, 56C494401ED7255500CC72AF /* GRDBDeploymentTarget.xcconfig */, ); name = "Supporting Files"; @@ -2061,6 +2066,7 @@ 5615B275222B107900061C1C /* AssociationHasOneThroughFetchableRecordTests.swift in Sources */, 56D4966F1D81309E008276D7 /* RecordPrimaryKeyNoneTests.swift in Sources */, 56D496621D81304E008276D7 /* StatementArguments+FoundationTests.swift in Sources */, + 563EA3E12C7B3A22001BE0D4 /* Mutex.swift in Sources */, 56176C5D1EACCCC7000F3F2B /* FTS5TokenizerTests.swift in Sources */, 56D496B21D8133CE008276D7 /* DatabaseQueueInMemoryTests.swift in Sources */, 562393601DEE06D300A6B01F /* CursorTests.swift in Sources */, @@ -2114,6 +2120,7 @@ 56D496681D813086008276D7 /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */, 5623934E1DEDFEFB00A6B01F /* EnumeratedCursorTests.swift in Sources */, 568068311EBBA26100EFB8AA /* SQLRequestTests.swift in Sources */, + 563866CB2C847659004C515A /* AsyncSemaphore.swift in Sources */, 56703297212B5450007D270F /* DatabaseUUIDEncodingStrategyTests.swift in Sources */, 5676FBA622F5CAD9004717D9 /* ValueObservationRegionRecordingTests.swift in Sources */, 56D496B41D8133F8008276D7 /* DatabaseTests.swift in Sources */, @@ -2180,7 +2187,7 @@ 563B06AB217EF0CC00B38F35 /* ValueObservation.swift in Sources */, 56D110FA28AFC97E00E64463 /* MutablePersistableRecord+DAO.swift in Sources */, 56CEB5111EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */, - 5659F4901EA8D964004A4992 /* ReadWriteBox.swift in Sources */, + 5659F4901EA8D964004A4992 /* ReadWriteLock.swift in Sources */, 566A841A2041146100E50BFD /* DatabaseSnapshot.swift in Sources */, 569EF0E2200D2D8400A9FA45 /* DatabaseRegion.swift in Sources */, 56CEB4F11EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */, @@ -2226,7 +2233,6 @@ 5617294E223533F40006E219 /* EncodableRecord.swift in Sources */, 56B7EE832863781300C0525F /* WALSnapshot.swift in Sources */, 5698AD181DAAD17A0056AF8C /* FTS5Tokenizer.swift in Sources */, - 56A2FA3624424D2A00E97D23 /* Export.swift in Sources */, 56B964B11DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */, 4E13D2F32769B87F0037588C /* DatabaseBackupProgress.swift in Sources */, 560A37A71C8FF6E500949E71 /* SerializedDatabase.swift in Sources */, @@ -2269,7 +2275,7 @@ 5690C3401D23E82A00E59934 /* Data.swift in Sources */, 5659F4881EA8D94E004A4992 /* Utils.swift in Sources */, 567B5BE82AD3284100629622 /* DumpFormat.swift in Sources */, - 566BE71E2342542F00A8254B /* LockedBox.swift in Sources */, + 566BE71E2342542F00A8254B /* Mutex.swift in Sources */, 56A238931B9C750B0082EB20 /* DatabaseMigrator.swift in Sources */, 5603CEBB2AC862EC00CF097D /* SQLJSONExpressible.swift in Sources */, 56F89DF72A57EAA9002FE2AA /* ColumnDefinition.swift in Sources */, @@ -2340,7 +2346,7 @@ /* Begin XCBuildConfiguration section */ 56E5D8021B4D422E00430942 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 56C494401ED7255500CC72AF /* GRDBDeploymentTarget.xcconfig */; + baseConfigurationReference = 5698C1C32CA844F4001C0EB0 /* GRDBTests.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -2370,7 +2376,7 @@ }; 56E5D8031B4D422E00430942 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 56C494401ED7255500CC72AF /* GRDBDeploymentTarget.xcconfig */; + baseConfigurationReference = 5698C1C32CA844F4001C0EB0 /* GRDBTests.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; diff --git a/GRDB.xcworkspace/contents.xcworkspacedata b/GRDB.xcworkspace/contents.xcworkspacedata index 3258ed7a7b..26f0eae470 100644 --- a/GRDB.xcworkspace/contents.xcworkspacedata +++ b/GRDB.xcworkspace/contents.xcworkspacedata @@ -45,12 +45,6 @@ location = "group:Tests/Performance/GRDBProfiling/GRDBProfiling.xcodeproj"> - - - - + location = "group:Documentation/DemoApps/GRDBDemo/GRDBDemo.xcodeproj"> diff --git a/GRDB/Core/Configuration.swift b/GRDB/Core/Configuration.swift index 6b261902b6..059bbb86f9 100644 --- a/GRDB/Core/Configuration.swift +++ b/GRDB/Core/Configuration.swift @@ -1,7 +1,16 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Dispatch import Foundation -public struct Configuration { +public struct Configuration: Sendable { // MARK: - Misc options @@ -186,7 +195,7 @@ public struct Configuration { // MARK: - Managing SQLite Connections - private var setups: [(Database) throws -> Void] = [] + private var setups: [@Sendable (Database) throws -> Void] = [] /// Defines a function to run whenever an SQLite connection is opened. /// @@ -223,34 +232,12 @@ public struct Configuration { /// /// On newly created databases files, ``DatabasePool`` activates the WAL /// mode after the preparation functions have run. - public mutating func prepareDatabase(_ setup: @escaping (Database) throws -> Void) { + public mutating func prepareDatabase(_ setup: @escaping @Sendable (Database) throws -> Void) { setups.append(setup) } // MARK: - Transactions - /// The default kind of write transactions. - /// - /// The default is ``Database/TransactionKind/deferred``. - /// - /// You can change the default transaction kind. For example, you can force - /// all write transactions to be `IMMEDIATE`: - /// - /// ```swift - /// var config = Configuration() - /// config.defaultTransactionKind = .immediate - /// let dbQueue = try DatabaseQueue(configuration: config) - /// - /// // BEGIN IMMEDIATE TRANSACTION; ...; COMMIT TRANSACTION; - /// try dbQueue.write { db in ... } - /// ``` - /// - /// This property is ignored for read-only transactions. Those always open - /// `DEFERRED` SQLite transactions. - /// - /// Related SQLite documentation: - public var defaultTransactionKind: Database.TransactionKind = .deferred - /// A boolean value indicating whether it is valid to leave a transaction /// opened at the end of a database access method. /// @@ -445,9 +432,25 @@ public struct Configuration { /// through a `SerializedDatabase`. var threadingMode = Database.ThreadingMode.default - var SQLiteConnectionDidOpen: (() -> Void)? - var SQLiteConnectionWillClose: ((SQLiteConnection) -> Void)? - var SQLiteConnectionDidClose: (() -> Void)? + private(set) var SQLiteConnectionDidOpen: (@Sendable () -> Void)? + private(set) var SQLiteConnectionWillClose: (@Sendable (SQLiteConnection) -> Void)? + private(set) var SQLiteConnectionDidClose: (@Sendable () -> Void)? + + // Workaround https://github.com/apple/swift/issues/72727 + mutating func onConnectionDidOpen(_ callback: @escaping @Sendable () -> Void) { + SQLiteConnectionDidOpen = callback + } + + // Workaround https://github.com/apple/swift/issues/72727 + mutating func onConnectionWillClose(_ callback: @escaping @Sendable (SQLiteConnection) -> Void) { + SQLiteConnectionWillClose = callback + } + + // Workaround https://github.com/apple/swift/issues/72727 + mutating func onConnectionDidClose(_ callback: @escaping @Sendable () -> Void) { + SQLiteConnectionDidClose = callback + } + var SQLiteOpenFlags: CInt { var flags = readonly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE) if sqlite3_libversion_number() >= 3037000 { diff --git a/GRDB/Core/Cursor.swift b/GRDB/Core/Cursor.swift index af874297c1..68baeab44d 100644 --- a/GRDB/Core/Cursor.swift +++ b/GRDB/Core/Cursor.swift @@ -670,9 +670,6 @@ extension Cursor where Element: Equatable { extension Cursor where Element: Comparable { /// Returns the maximum element in the cursor. /// - /// - Parameter areInIncreasingOrder: A predicate that returns `true` - /// if its first argument should be ordered before its second - /// argument; otherwise, `false`. /// - Returns: The cursor's maximum element, according to /// `areInIncreasingOrder`. If the cursor has no elements, returns /// `nil`. @@ -682,9 +679,6 @@ extension Cursor where Element: Comparable { /// Returns the minimum element in the cursor. /// - /// - Parameter areInIncreasingOrder: A predicate that returns `true` - /// if its first argument should be ordered before its second - /// argument; otherwise, `false`. /// - Returns: The cursor's minimum element, according to /// `areInIncreasingOrder`. If the cursor has no elements, returns /// `nil`. @@ -754,17 +748,13 @@ public final class AnyCursor: Cursor { } /// Creates a new cursor whose elements are elements of `iterator`. - public convenience init(iterator: I) - where I: IteratorProtocol, I.Element == Element - { + public convenience init(iterator: some IteratorProtocol) { var iterator = iterator self.init { iterator.next() } } /// Creates a new cursor whose elements are elements of `sequence`. - public convenience init(_ sequence: S) - where S: Sequence, S.Element == Element - { + public convenience init(_ sequence: some Sequence) { self.init(iterator: sequence.makeIterator()) } @@ -915,6 +905,10 @@ public final class FilterCursor { } } +// Explicit non-conformance to Sendable. +@available(*, unavailable) +extension FilterCursor: Sendable { } + extension FilterCursor: Cursor { public func next() throws -> Base.Element? { while let element = try base.next() { diff --git a/GRDB/Core/Database+Schema.swift b/GRDB/Core/Database+Schema.swift index 878cbe72bf..beb6712771 100644 --- a/GRDB/Core/Database+Schema.swift +++ b/GRDB/Core/Database+Schema.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + extension Database { /// A cache for the available database schemas. struct SchemaCache { @@ -589,13 +598,11 @@ extension Database { /// try db.table("t", hasUniqueKey: ["id", "a"]) // true /// try db.table("t", hasUniqueKey: ["id", "a", "b", "c"]) // true /// ``` - public func table( + public func table( _ tableName: String, - hasUniqueKey columns: Columns) - throws -> Bool - where Columns: Sequence, Columns.Element == String - { - try columnsForUniqueKey(Array(columns), in: tableName) != nil + hasUniqueKey columns: some Collection + ) throws -> Bool { + try columnsForUniqueKey(columns, in: tableName) != nil } /// Returns the foreign keys defined on table named `tableName`. @@ -920,12 +927,10 @@ extension Database { /// returns the columns of the unique key, ordered as the matching index (or /// primary key). The case of returned columns is not guaranteed to match /// the case of input columns. - func columnsForUniqueKey( - _ columns: Columns, - in tableName: String) - throws -> [String]? - where Columns: Sequence, Columns.Element == String - { + func columnsForUniqueKey( + _ columns: some Collection, + in tableName: String + ) throws -> [String]? { let lowercasedColumns = Set(columns.map { $0.lowercased() }) if lowercasedColumns.isEmpty { // Don't hit the database for trivial case diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index 061c5b3646..46038a83b5 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation extension Database { @@ -430,6 +439,7 @@ extension Database { // documentation of this method for more information). try checkForAbortedTransaction(sql: statement.sql, arguments: statement.arguments) + // Cancelled database accesses must not execute. // Suspended databases must not execute statements that create the risk // of `0xdead10cc` exception (see the documentation of this method for // more information). @@ -482,6 +492,17 @@ extension Database { // and throws the user-provided cancelled commit error. try observationBroker?.statementDidFail(statement) + switch ResultCode(rawValue: resultCode) { + case .SQLITE_INTERRUPT, .SQLITE_ABORT: + if suspensionMutex.load().isCancelled { + // The only error that a user sees when a Task is cancelled + // is CancellationError. + throw CancellationError() + } + default: + break + } + // Throw statement failure throw DatabaseError( resultCode: resultCode, @@ -531,19 +552,7 @@ struct StatementCache { // > time and probably reused many times. // // This looks like a perfect match for cached statements. - // - // However SQLITE_PREPARE_PERSISTENT was only introduced in - // SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20 - #if GRDBCUSTOMSQLITE || GRDBCIPHER let statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT)) - #else - let statement: Statement - if #available(iOS 12, macOS 10.14, watchOS 5, *) { // SQLite 3.20+ - statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT)) - } else { - statement = try db.makeStatement(sql: sql) - } - #endif statements[sql] = statement return statement } diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 9d5a13b7bd..61354847b1 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A raw SQLite connection, suitable for the SQLite C API. @@ -113,8 +122,14 @@ let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_ /// - ``logError`` /// - ``releaseMemory()`` /// - ``trace(options:_:)`` +/// +/// ### Supporting Types +/// +/// - ``BusyCallback`` +/// - ``BusyMode`` /// - ``CheckpointMode`` /// - ``DatabaseBackupProgress`` +/// - ``LogErrorFunction`` /// - ``StorageClass`` /// - ``TraceEvent`` /// - ``TracingOptions`` @@ -134,8 +149,26 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// The error logging function. /// + /// SQLite can be configured to invoke a callback function containing + /// an error code and a terse error message whenever anomalies occur. + /// + /// This global error callback must be configured early in the lifetime + /// of your application: + /// + /// ```swift + /// Database.logError = { (resultCode, message) in + /// NSLog("%@", "SQLite error \(resultCode): \(message)") + /// } + /// ``` + /// + /// - warning: Database.logError must be set before any database + /// connection is opened. This includes the connections that your + /// application opens with GRDB, but also connections opened by + /// other tools, such as third-party libraries. Setting it after a + /// connection has been opened is an SQLite misuse, and has no effect. + /// /// Related SQLite documentation: - public static var logError: LogErrorFunction? = nil { + nonisolated(unsafe) public static var logError: LogErrorFunction? = nil { didSet { if logError != nil { _registerErrorLogCallback { (_, code, message) in @@ -279,18 +312,29 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// `isRecordingSelectedRegion` is true. var selectedRegion = DatabaseRegion() - /// Support for `checkForAbortedTransaction()` - var isInsideTransactionBlock = false - - /// Support for `checkForSuspensionViolation(from:)` - @LockedBox var isSuspended = false - /// Support for `checkForSuspensionViolation(from:)` /// This cache is never cleared: we assume journal mode never changes. var journalModeCache: String? + // MARK: - Suspension + + struct Suspension { + /// If true, the database is suspended and should not acquire any + /// write lock in order to avoid the 0xDEAD10CC exception. + var isSuspended: Bool + + /// If true, the database access has been cancelled. + var isCancelled: Bool + } + + /// Support for `checkForSuspensionViolation(from:)` + let suspensionMutex = Mutex(Suspension(isSuspended: false, isCancelled: false)) + // MARK: - Transaction Date + /// Support for `checkForAbortedTransaction()` + var isInsideTransactionBlock = false + enum AutocommitState { case off case on @@ -352,10 +396,10 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib private var trace: ((TraceEvent) -> Void)? /// The registered custom SQL functions. - private var functions = Set() + private var functions: [DatabaseFunction.ID: DatabaseFunction] = [:] /// The registered custom SQL collations. - private var collations = Set() + private var collations: [DatabaseCollation.ID: DatabaseCollation] = [:] /// Support for `beginReadOnly()` and `endReadOnly()`. private var readOnlyDepth = 0 @@ -602,7 +646,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib guard code == SQLITE_OK else { // So there remain some unfinalized prepared statement somewhere. if let log = Self.logError { - if code == SQLITE_BUSY { + if ResultCode(rawValue: code).primaryResultCode == .SQLITE_BUSY { // Let the user know about unfinalized statements that did // prevent the connection from closing properly. var stmt: SQLiteStatement? = sqlite3_next_stmt(sqliteConnection, nil) @@ -698,13 +742,13 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// let dbPool = try DatabasePool(path: ..., configuration: config) /// ``` public func add(function: DatabaseFunction) { - functions.update(with: function) + functions[function.id] = function function.install(in: self) } /// Removes a custom SQL function. public func remove(function: DatabaseFunction) { - functions.remove(function) + functions.removeValue(forKey: function.id) function.uninstall(in: self) } @@ -725,7 +769,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// let dbPool = try DatabasePool(path: ..., configuration: config) /// ``` public func add(collation: DatabaseCollation) { - collations.update(with: collation) + collations[collation.id] = collation let collationPointer = Unmanaged.passUnretained(collation).toOpaque() let code = sqlite3_create_collation_v2( sqliteConnection, @@ -744,7 +788,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// Removes a collation. public func remove(collation: DatabaseCollation) { - collations.remove(collation) + collations.removeValue(forKey: collation.id) sqlite3_create_collation_v2( sqliteConnection, collation.name, @@ -1116,22 +1160,25 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// /// Suspension ends with `resume()`. func suspend() { - $isSuspended.update { isSuspended in - if isSuspended { - return + let needsInterrupt = suspensionMutex.withLock { suspension in + if suspension.isSuspended { + return false } - // Prevent future lock acquisition - isSuspended = true - - // Interrupt the database because this may trigger an - // SQLITE_INTERRUPT error which may itself abort a transaction and - // release a lock. See + suspension.isSuspended = true + return true + } + + if needsInterrupt { + // Interrupting the database can trigger an SQLITE_INTERRUPT + // error which may itself abort a transaction and + // release a database lock, which is our goal. + // See + // + // Maybe interrupt will not release any lock. To address this, + // we'll issue a rollback on next database access which requires + // a lock. See `checkForSuspensionViolation(from:).` interrupt() - - // Now what about the eventual remaining lock? We'll issue a - // rollback on next database access which requires a lock, in - // checkForSuspensionViolation(from:). } } @@ -1143,7 +1190,35 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// /// See suspend(). func resume() { - isSuspended = false + suspensionMutex.withLock { + $0.isSuspended = false + } + } + + /// Cancels the current database access. All statements but ROLLBACK + /// will throw `CancellationError`, until `uncancel()` is called. + /// + /// This method can be called from any thread. + func cancel() { + let needsInterrupt = suspensionMutex.withLock { suspension in + if suspension.isCancelled { + return false + } + + suspension.isCancelled = true + return true + } + + if needsInterrupt { + interrupt() + } + } + + /// Undo `cancel()`. + func uncancel() { + suspensionMutex.withLock { + $0.isCancelled = false + } } /// Support for `checkForSuspensionViolation(from:)` @@ -1167,15 +1242,51 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib return journalMode } - /// If the database is suspended, and executing the statement would lock the - /// database in a way that may trigger the [`0xdead10cc` exception](https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report), - /// this method rollbacks the current transaction and throws `SQLITE_ABORT`. + /// Prevents a statement from running, if the database is suspended, or + /// if the current database access is cancelled by Task cancellation. + /// + /// Transaction rollbacks are always allowed. For other statements: + /// + /// - When database access is cancelled, this method + /// throws `CancellationError`. /// - /// See `suspend()` and ``Configuration/observesSuspensionNotifications``. + /// - When database is suspensed, and if the statement would lock the + /// database in a way that may trigger the 0xDEAD10CC exception, this + /// method rollbacks the current transaction and throws `SQLITE_ABORT`. + /// + /// See `cancel()`, `suspend()` and + /// ``Configuration/observesSuspensionNotifications``. func checkForSuspensionViolation(from statement: Statement) throws { - try $isSuspended.read { isSuspended in - guard isSuspended else { - return + // No reason for suspension should prevent rollbacks: + // + // - A rollback releases the write lock when the database + // is interrupted, when preventing 0xDEAD10CC. + // + // - A rollback properly closes a transaction that fails because + // it runs in a Task that was cancelled. + // + // Finally, a rollback must be run by GRDB, not by a direct call + // to `sqlite3_exec`, so that transaction observers are + // properly notified. + if statement.transactionEffect == .rollbackTransaction { + return + } + + // How should we interrupt the statement? + enum Interrupt { + case abort // Rollback and throw SQLITE_ABORT + case cancel // Throw CancellationError + } + + let interrupt: Interrupt? = try suspensionMutex.withLock { suspension in + // Check for cancellation first, so that the only error that + // a user sees when a Task is cancelled is CancellationError. + if suspension.isCancelled { + return .cancel + } + + guard suspension.isSuspended else { + return nil } if try journalMode() == "wal" && statement.isReadonly { @@ -1186,7 +1297,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib // Those are not read-only: // - INSERT ... // - BEGIN IMMEDIATE TRANSACTION - return + return nil } if statement.releasesDatabaseLock { @@ -1195,15 +1306,24 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib // - ROLLBACK // - ROLLBACK TRANSACTION TO SAVEPOINT // - RELEASE SAVEPOINT - return + return nil } + // Assume statement can acquire a write lock: abort. + return .abort + } + + switch interrupt { + case nil: + break + + case .cancel: + throw CancellationError() + + case .abort: // Attempt at releasing an eventual lock with ROLLBACk, // as explained in Database.suspend(). - // - // Use sqlite3_exec instead of `try? rollback()` in order to avoid - // an infinite loop in checkForSuspensionViolation(from:) - _ = sqlite3_exec(sqliteConnection, "ROLLBACK", nil, nil, nil) + try? rollback() throw DatabaseError( resultCode: .SQLITE_ABORT, @@ -1273,14 +1393,10 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// Use ``inSavepoint(_:)`` instead. /// /// - parameters: - /// - kind: The transaction type (default nil). - /// - /// If nil, and the database connection is read-only, the transaction - /// kind is ``TransactionKind/deferred``. + /// - kind: The transaction type. /// - /// If nil, and the database connection is not read-only, the - /// transaction kind is the ``Configuration/defaultTransactionKind`` - /// of the ``configuration``. + /// If nil, the transaction kind is DEFERRED when the current + /// database access is read-only, and IMMEDIATE otherwise. /// - operations: A function that executes SQL statements and returns /// either ``TransactionCompletion/commit`` or ``TransactionCompletion/rollback``. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or the @@ -1404,8 +1520,7 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib // By default, top level SQLite savepoints open a // deferred transaction. // - // But GRDB database configuration mandates a default transaction - // kind that we have to honor. + // But GRDB prefers immediate transactions for writes. // // Besides, starting some (?) SQLCipher/SQLite version, SQLite has a // bug. Returning 1 from `sqlite3_commit_hook` does not leave the @@ -1493,18 +1608,22 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib /// Related SQLite documentation: /// /// - parameters: - /// - kind: The transaction type (default nil). + /// - kind: The transaction type. /// - /// If nil, and the database connection is read-only, the transaction - /// kind is ``TransactionKind/deferred``. - /// - /// If nil, and the database connection is not read-only, the - /// transaction kind is the ``Configuration/defaultTransactionKind`` - /// of the ``configuration``. + /// If nil, the transaction kind is DEFERRED when the current + /// database access is read-only, and IMMEDIATE otherwise. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public func beginTransaction(_ kind: TransactionKind? = nil) throws { // SQLite throws an error for non-deferred transactions when read-only. - let kind = kind ?? (isReadOnly ? .deferred : configuration.defaultTransactionKind) + // We prefer immediate transactions for writes, so that write + // transactions can not overlap. This reduces the opportunity for + // SQLITE_BUSY, which is immediately thrown whenever a transaction + // is upgraded after an initial read and a concurrent processes + // has acquired the write lock beforehand. This SQLITE_BUSY error + // can not be avoided with a busy timeout. + // + // See . + let kind = kind ?? (isReadOnly ? .deferred : .immediate) try execute(sql: "BEGIN \(kind.rawValue) TRANSACTION") assert(sqlite3_get_autocommit(sqliteConnection) == 0) } @@ -1816,8 +1935,8 @@ extension Database { // MARK: - Database-Related Types - /// See BusyMode and - public typealias BusyCallback = (_ numberOfTries: Int) -> Bool + /// See ``BusyMode`` and + public typealias BusyCallback = @Sendable (_ numberOfTries: Int) -> Bool /// When there are several connections to a database, a connection may try /// to access the database while it is locked by another connection. @@ -1845,7 +1964,7 @@ extension Database { /// - /// - /// - - public enum BusyMode { + public enum BusyMode: Sendable { /// The `SQLITE_BUSY` error is immediately returned to the connection /// that tries to access the locked database. case immediateError @@ -2005,7 +2124,7 @@ extension Database { } /// An error log function that takes an error code and message. - public typealias LogErrorFunction = (_ resultCode: ResultCode, _ message: String) -> Void + public typealias LogErrorFunction = @Sendable (_ resultCode: ResultCode, _ message: String) -> Void /// An SQLite storage class. /// diff --git a/GRDB/Core/DatabaseCollation.swift b/GRDB/Core/DatabaseCollation.swift index 2f76223c56..6748874c8b 100644 --- a/GRDB/Core/DatabaseCollation.swift +++ b/GRDB/Core/DatabaseCollation.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// `DatabaseCollation` is a custom string comparison function used by SQLite. @@ -20,10 +29,36 @@ import Foundation /// - ``localizedCompare`` /// - ``localizedStandardCompare`` /// - ``unicodeCompare`` -public final class DatabaseCollation { +public final class DatabaseCollation: Identifiable, Sendable { + /// The identifier of an SQLite collation. + /// + /// SQLite identifies collations by their name (case insensitive). + public struct ID: Hashable { + var name: String + + // Collation equality is based on the sqlite3_strnicmp SQLite function. + // (see https://www.sqlite.org/c3ref/create_collation.html). Computing + // a hash value that honors the Swift Hashable contract (value equality + // implies hash equality) is thus non trivial. But it's not that + // important, since this hashValue is only used when one adds + // or removes a collation from a database connection. + public func hash(into hasher: inout Hasher) { + hasher.combine(0) + } + + /// Two collations are equal if they share the same name (case insensitive) + public static func == (lhs: Self, rhs: Self) -> Bool { + // See + return sqlite3_stricmp(lhs.name, rhs.name) == 0 + } + } + + /// The identifier of the collation. + public var id: ID { ID(name: name) } + /// The name of the collation. public let name: String - let function: (CInt, UnsafeRawPointer?, CInt, UnsafeRawPointer?) -> ComparisonResult + let function: @Sendable (CInt, UnsafeRawPointer?, CInt, UnsafeRawPointer?) -> ComparisonResult /// Creates a collation. /// @@ -40,7 +75,7 @@ public final class DatabaseCollation { /// - parameters: /// - name: The collation name. /// - function: A function that compares two strings. - public init(_ name: String, function: @escaping (String, String) -> ComparisonResult) { + public init(_ name: String, function: @escaping @Sendable (String, String) -> ComparisonResult) { self.name = name self.function = { (length1, buffer1, length2, buffer2) in // Buffers are not C strings: they do not end with \0. @@ -58,21 +93,3 @@ public final class DatabaseCollation { } } } - -extension DatabaseCollation: Hashable { - // Collation equality is based on the sqlite3_strnicmp SQLite function. - // (see https://www.sqlite.org/c3ref/create_collation.html). Computing - // a hash value that honors the Swift Hashable contract (value equality - // implies hash equality) is thus non trivial. But it's not that - // important, since this hashValue is only used when one adds - // or removes a collation from a database connection. - public func hash(into hasher: inout Hasher) { - hasher.combine(0) - } - - /// Two collations are equal if they share the same name (case insensitive) - public static func == (lhs: DatabaseCollation, rhs: DatabaseCollation) -> Bool { - // See - return sqlite3_stricmp(lhs.name, rhs.name) == 0 - } -} diff --git a/GRDB/Core/DatabaseError.swift b/GRDB/Core/DatabaseError.swift index 53eb009b37..bf808dbaa5 100644 --- a/GRDB/Core/DatabaseError.swift +++ b/GRDB/Core/DatabaseError.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// An SQLite result code. @@ -406,6 +415,10 @@ extension DatabaseError { static func connectionIsClosed() -> Self { DatabaseError(resultCode: .SQLITE_MISUSE, message: "Connection is closed") } + + static func snapshotIsLost() -> Self { + DatabaseError(resultCode: .SQLITE_ABORT, message: "Snapshot is lost.") + } } // Support for `catch DatabaseError.SQLITE_XXX` diff --git a/GRDB/Core/DatabaseFunction.swift b/GRDB/Core/DatabaseFunction.swift index 81fe825748..9e2c03ed2f 100644 --- a/GRDB/Core/DatabaseFunction.swift +++ b/GRDB/Core/DatabaseFunction.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A custom SQL function or aggregate. /// /// ## Topics @@ -20,16 +29,20 @@ /// - ``localizedUppercase`` /// - ``lowercase`` /// - ``uppercase`` -public final class DatabaseFunction: Hashable { - // SQLite identifies functions by (name + argument count) - private struct Identity: Hashable { +public final class DatabaseFunction: Identifiable, Sendable { + /// The identifier of an SQLite function. + /// + /// SQLite identifies functions by their name and argument count. + public struct ID: Hashable, Sendable { let name: String let nArg: CInt // -1 for variadic functions } - /// The name of the SQL function - public var name: String { identity.name } - private let identity: Identity + /// The name of the SQL function. + public var name: String { id.name } + + /// The identifier of the SQL function. + public let id: ID let isPure: Bool private let kind: Kind private var eTextRep: CInt { (SQLITE_UTF8 | (isPure ? SQLITE_DETERMINISTIC : 0)) } @@ -73,11 +86,11 @@ public final class DatabaseFunction: Hashable { _ name: String, argumentCount: Int? = nil, pure: Bool = false, - function: @escaping ([DatabaseValue]) throws -> (any DatabaseValueConvertible)?) + function: @escaping @Sendable ([DatabaseValue]) throws -> (any DatabaseValueConvertible)?) { - self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1) + self.id = ID(name: name, nArg: argumentCount.map(CInt.init) ?? -1) self.isPure = pure - self.kind = .function{ (argc, argv) in + self.kind = .function { (argc, argv) in let arguments = (0.. - private enum Kind { + private enum Kind: Sendable { /// A regular function: SELECT f(1) - case function((CInt, UnsafeMutablePointer?) throws -> (any DatabaseValueConvertible)?) + case function(@Sendable (CInt, UnsafeMutablePointer?) throws -> (any DatabaseValueConvertible)?) /// An aggregate: SELECT f(foo) FROM bar GROUP BY baz - case aggregate(() -> any DatabaseAggregate) + case aggregate(@Sendable () -> any DatabaseAggregate) /// Feeds the `pApp` parameter of sqlite3_create_function_v2 /// @@ -415,17 +428,6 @@ public final class DatabaseFunction: Hashable { } } -extension DatabaseFunction { - public func hash(into hasher: inout Hasher) { - hasher.combine(identity) - } - - /// Two functions are equal if they share the same name and arity. - public static func == (lhs: DatabaseFunction, rhs: DatabaseFunction) -> Bool { - lhs.identity == rhs.identity - } -} - /// The protocol for custom SQLite aggregates. /// /// For example: diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index 7dd0525fc2..ce1bae833e 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -11,7 +11,7 @@ public final class DatabasePool { /// It is constant, until close() sets it to nil. private var readerPool: Pool? - @LockedBox var databaseSnapshotCount = 0 + let databaseSnapshotCountMutex = Mutex(0) /// If Database Suspension is enabled, this array contains the necessary `NotificationCenter` observers. private var suspensionObservers: [NSObjectProtocol] = [] @@ -62,17 +62,15 @@ public final class DatabasePool { // an opened transaction. readerConfiguration.allowsUnsafeTransactions = false - var readerCount = 0 readerPool = Pool( maximumCount: configuration.maximumReaderCount, qos: configuration.readQoS, - makeElement: { - readerCount += 1 // protected by Pool (TODO: document this protection behavior) + makeElement: { [readerConfiguration] index in return try SerializedDatabase( path: path, configuration: readerConfiguration, defaultLabel: "GRDB.DatabasePool", - purpose: "reader.\(readerCount)") + purpose: "reader.\(index)") }) // Set up journal mode unless readonly @@ -118,10 +116,6 @@ public final class DatabasePool { configuration.readonly = true - // Readers use deferred transactions by default. - // Other transaction kinds are forbidden by SQLite in read-only connections. - configuration.defaultTransactionKind = .deferred - // // > But there are some obscure cases where a query against a WAL-mode // > database can return SQLITE_BUSY, so applications should be prepared @@ -148,7 +142,7 @@ public final class DatabasePool { } } -// @unchecked because of databaseSnapshotCount, readerPool and suspensionObservers +// @unchecked because of readerPool and suspensionObservers extension DatabasePool: @unchecked Sendable { } extension DatabasePool { @@ -357,7 +351,47 @@ extension DatabasePool: DatabaseReader { } } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func read( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.") + guard let readerPool else { + throw DatabaseError.connectionIsClosed() + } + + let dbAccess = CancellableDatabaseAccess() + return try await dbAccess.withCancellableContinuation { continuation in + readerPool.asyncGet { result in + do { + let (reader, releaseReader) = try result.get() + // Second async jump because that's how `Pool.async` has to be used. + reader.async { db in + defer { + try? db.commit() // Ignore commit error + releaseReader(.reuse) + } + do { + let result = try dbAccess.inDatabase(db) { + // The block isolation comes from the DEFERRED transaction. + try db.beginTransaction(.deferred) + try db.clearSchemaCacheIfNeeded() + return try value(db) + } + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } catch { + continuation.resume(throwing: error) + } + } + } + } + + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) return @@ -401,7 +435,43 @@ extension DatabasePool: DatabaseReader { } } - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func unsafeRead( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + guard let readerPool else { + throw DatabaseError.connectionIsClosed() + } + + let dbAccess = CancellableDatabaseAccess() + return try await dbAccess.withCancellableContinuation { continuation in + readerPool.asyncGet { result in + do { + let (reader, releaseReader) = try result.get() + // Second async jump because that's how `Pool.async` has to be used. + reader.async { db in + defer { + releaseReader(.reuse) + } + do { + let result = try dbAccess.inDatabase(db) { + try db.clearSchemaCacheIfNeeded() + return try value(db) + } + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } catch { + continuation.resume(throwing: error) + } + } + } + } + + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) return @@ -446,25 +516,9 @@ extension DatabasePool: DatabaseReader { } } - public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { - // The semaphore that blocks until futureResult is defined: - let futureSemaphore = DispatchSemaphore(value: 0) - var futureResult: Result? = nil - - asyncConcurrentRead { dbResult in - // Fetch and release the future - futureResult = dbResult.flatMap { db in Result { try value(db) } } - futureSemaphore.signal() - } - - return DatabaseFuture { - // Block the future until results are fetched - _ = futureSemaphore.wait(timeout: .distantFuture) - return try futureResult!.get() - } - } - - public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { + public func spawnConcurrentRead( + _ value: @escaping @Sendable (Result) -> Void + ) { asyncConcurrentRead(value) } @@ -505,7 +559,9 @@ extension DatabasePool: DatabaseReader { /// ``` /// /// - parameter value: A function that accesses the database. - public func asyncConcurrentRead(_ value: @escaping (Result) -> Void) { + public func asyncConcurrentRead( + _ value: @escaping @Sendable (Result) -> Void + ) { // Check that we're on the writer queue... writer.execute { db in // ... and that no transaction is opened. @@ -644,8 +700,7 @@ extension DatabasePool: DatabaseReader { // MARK: - WAL Snapshot Transactions - // swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) /// Returns a long-lived WAL snapshot transaction on a reader connection. func walSnapshotTransaction() throws -> WALSnapshotTransaction { guard let readerPool else { @@ -665,7 +720,9 @@ extension DatabasePool: DatabaseReader { /// /// - important: The `completion` argument is executed in a serial /// dispatch queue, so make sure you use the transaction asynchronously. - func asyncWALSnapshotTransaction(_ completion: @escaping (Result) -> Void) { + func asyncWALSnapshotTransaction( + _ completion: @escaping @Sendable (Result) -> Void + ) { guard let readerPool else { completion(.failure(DatabaseError.connectionIsClosed())) return @@ -691,9 +748,8 @@ extension DatabasePool: DatabaseReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { if configuration.readonly { // The easy case: the database does not change return _addReadOnly( @@ -722,9 +778,8 @@ extension DatabasePool: DatabaseReader { private func _addConcurrent( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { assert(!configuration.readonly, "Use _addReadOnly(observation:) instead") assert(!observation.requiresWriteAccess, "Use _addWriteOnly(observation:) instead") let observer = ValueConcurrentObserver( @@ -746,6 +801,12 @@ extension DatabasePool: DatabaseWriter { try writer.sync(updates) } + public func writeWithoutTransaction( + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await writer.execute(updates) + } + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T { guard let readerPool else { @@ -756,7 +817,29 @@ extension DatabasePool: DatabaseWriter { } } - public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { + public func barrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + let dbAccess = CancellableDatabaseAccess() + return try await dbAccess.withCancellableContinuation { continuation in + asyncBarrierWriteWithoutTransaction { dbResult in + do { + try dbAccess.checkCancellation() + let db = try dbResult.get() + let result = try dbAccess.inDatabase(db) { + try updates(db) + } + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + public func asyncBarrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Result) -> Void + ) { guard let readerPool else { updates(.failure(DatabaseError.connectionIsClosed())) return @@ -788,9 +871,10 @@ extension DatabasePool: DatabaseWriter { /// /// - precondition: This method is not reentrant. /// - parameters: - /// - kind: The transaction type (default nil). If nil, the transaction - /// type is the ``Configuration/defaultTransactionKind`` of the - /// the ``configuration``. + /// - kind: The transaction type. + /// + /// If nil, the transaction kind is DEFERRED when the database + /// connection is read-only, and IMMEDIATE otherwise. /// - updates: A function that updates the database. /// - throws: The error thrown by `updates`, or by the wrapping transaction. public func writeInTransaction( @@ -809,7 +893,9 @@ extension DatabasePool: DatabaseWriter { try writer.reentrantSync(updates) } - public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) { + public func asyncWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) -> Void + ) { writer.async(updates) } } @@ -867,11 +953,10 @@ extension DatabasePool { path: path, configuration: DatabasePool.readerConfiguration(writer.configuration), defaultLabel: "GRDB.DatabasePool", - purpose: "snapshot.\($databaseSnapshotCount.increment())") + purpose: "snapshot.\(databaseSnapshotCountMutex.increment())") } - // swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) /// Creates a database snapshot that allows concurrent accesses to an /// unchanging database content, as it exists at the moment the snapshot /// is created. diff --git a/GRDB/Core/DatabasePublishers.swift b/GRDB/Core/DatabasePublishers.swift index 8f8054953b..c7e7a603f1 100644 --- a/GRDB/Core/DatabasePublishers.swift +++ b/GRDB/Core/DatabasePublishers.swift @@ -1,5 +1,4 @@ #if canImport(Combine) /// A namespace for database Combine publishers. -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public enum DatabasePublishers { } #endif diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index ae763f694c..37b62fde5b 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -233,7 +233,19 @@ extension DatabaseQueue: DatabaseReader { } } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func read( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await writer.execute { db in + try db.isolated(readOnly: true) { + try value(db) + } + } + } + + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { writer.async { db in defer { // Ignore error because we can not notify it. @@ -254,11 +266,20 @@ extension DatabaseQueue: DatabaseReader { } } + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func unsafeRead(_ value: (Database) throws -> T) rethrows -> T { try writer.sync(value) } - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func unsafeRead( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await writer.execute(value) + } + + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { writer.async { value(.success($0)) } } @@ -266,20 +287,9 @@ extension DatabaseQueue: DatabaseReader { try writer.reentrantSync(value) } - public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { - // DatabaseQueue can't perform parallel reads. - // Perform a blocking read instead. - return DatabaseFuture(Result { - // Check that we're on the writer queue, as documented - try writer.execute { db in - try db.isolated(readOnly: true) { - try value(db) - } - } - }) - } - - public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { + public func spawnConcurrentRead( + _ value: @escaping @Sendable (Result) -> Void + ) { // Check that we're on the writer queue... writer.execute { db in // ... and that no transaction is opened. @@ -309,9 +319,8 @@ extension DatabaseQueue: DatabaseReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { if configuration.readonly { // The easy case: the database does not change return _addReadOnly( @@ -352,9 +361,10 @@ extension DatabaseQueue: DatabaseWriter { /// ``` /// /// - parameters: - /// - kind: The transaction type (default nil). If nil, the transaction - /// type is the ``Configuration/defaultTransactionKind`` of the - /// the ``configuration``. + /// - kind: The transaction type. + /// + /// If nil, the transaction kind is DEFERRED when the database + /// connection is read-only, and IMMEDIATE otherwise. /// - updates: A function that updates the database. /// - throws: The error thrown by `updates`, or by the wrapping transaction. public func inTransaction( @@ -374,12 +384,26 @@ extension DatabaseQueue: DatabaseWriter { try writer.sync(updates) } + public func writeWithoutTransaction( + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await writer.execute(updates) + } + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T { try writer.sync(updates) } - public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { + public func barrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await writer.execute(updates) + } + + public func asyncBarrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Result) -> Void + ) { writer.async { updates(.success($0)) } } @@ -425,7 +449,9 @@ extension DatabaseQueue: DatabaseWriter { try writer.reentrantSync(updates) } - public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) { + public func asyncWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) -> Void + ) { writer.async(updates) } } diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 5fac0df44e..2c9d1cfdd3 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -22,14 +22,14 @@ import Dispatch /// ### Reading from the Database /// /// - ``read(_:)-3806d`` -/// - ``read(_:)-4w6gy`` +/// - ``read(_:)-4d1da`` /// - ``readPublisher(receiveOn:value:)`` /// - ``asyncRead(_:)`` /// /// ### Unsafe Methods /// /// - ``unsafeRead(_:)-5i7tf`` -/// - ``unsafeRead(_:)-11mk0`` +/// - ``unsafeRead(_:)-5gsav`` /// - ``unsafeReentrantRead(_:)`` /// - ``asyncUnsafeRead(_:)`` /// @@ -188,6 +188,36 @@ public protocol DatabaseReader: AnyObject, Sendable { @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails func read(_ value: (Database) throws -> T) throws -> T + /// Executes read-only database operations, and returns their result after + /// they have finished executing. + /// + /// For example: + /// + /// ```swift + /// let count = try await reader.read { db in + /// try Player.fetchCount(db) + /// } + /// ``` + /// + /// Database operations are isolated in a transaction: they do not see + /// changes performed by eventual concurrent writes (even writes performed + /// by other processes). + /// + /// The database connection is read-only: attempts to write throw a + /// ``DatabaseError`` with resultCode `SQLITE_READONLY`. + /// + /// The ``Database`` argument to `value` is valid only during the execution + /// of the closure. Do not store or return the database connection for + /// later use. + /// + /// - parameter value: A closure which accesses the database. + /// - throws: Any ``DatabaseError`` that happens while establishing the + /// database access, or the error thrown by `value`, or + /// `CancellationError` if the task is cancelled. + func read( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T + /// Schedules read-only database operations for execution, and /// returns immediately. /// @@ -214,7 +244,9 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - parameter value: A closure which accesses the database. Its argument /// is a `Result` that provides the database connection, or the failure /// that would prevent establishing the read access to the database. - func asyncRead(_ value: @escaping (Result) -> Void) + func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) /// Executes database operations, and returns their result after they have /// finished executing. @@ -254,6 +286,42 @@ public protocol DatabaseReader: AnyObject, Sendable { @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails func unsafeRead(_ value: (Database) throws -> T) throws -> T + /// Executes database operations, and returns their result after they have + /// finished executing. + /// + /// This method is "unsafe" because the database reader does nothing more + /// than providing a database connection. When you use this method, you + /// become responsible for the thread-safety of your application, and + /// responsible for database accesses performed by other processes. See + /// for + /// more information. + /// + /// For example: + /// + /// ```swift + /// let count = try await reader.unsafeRead { db in + /// try Player.fetchCount(db) + /// } + /// ``` + /// + /// The ``Database`` argument to `value` is valid only during the execution + /// of the closure. Do not store or return the database connection for + /// later use. + /// + /// - warning: Database operations may not be wrapped in a transaction. They + /// may see changes performed by concurrent writes or writes performed by + /// other processes: two identical requests performed by the `value` + /// closure may not return the same value. + /// - warning: Attempts to write in the database may succeed. + /// + /// - parameter value: A closure which accesses the database. + /// - throws: Any ``DatabaseError`` that happens while establishing the + /// database access, or the error thrown by `value`, or + /// `CancellationError` if the task is cancelled. + func unsafeRead( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T + /// Schedules database operations for execution, and returns immediately. /// /// This method is "unsafe" because the database reader does nothing more @@ -285,7 +353,9 @@ public protocol DatabaseReader: AnyObject, Sendable { /// - parameter value: A closure which accesses the database. Its argument /// is a `Result` that provides the database connection, or the failure /// that would prevent establishing the read access to the database. - func asyncUnsafeRead(_ value: @escaping (Result) -> Void) + func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) /// Executes database operations, and returns their result after they have /// finished executing. @@ -343,8 +413,8 @@ public protocol DatabaseReader: AnyObject, Sendable { func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable } extension DatabaseReader { @@ -398,7 +468,7 @@ extension DatabaseReader { /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or the /// error thrown by `progress`. public func backup( - to writer: some DatabaseWriter, + to writer: any DatabaseWriter, pagesPerStep: CInt = -1, progress: ((DatabaseBackupProgress) throws -> Void)? = nil) throws @@ -428,96 +498,6 @@ extension DatabaseReader { } } -extension DatabaseReader { - // MARK: - Asynchronous Database Access - - /// Executes read-only database operations, and returns their result after - /// they have finished executing. - /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// - /// For example: - /// - /// ```swift - /// let count = try await reader.read { db in - /// try Player.fetchCount(db) - /// } - /// ``` - /// - /// Database operations are isolated in a transaction: they do not see - /// changes performed by eventual concurrent writes (even writes performed - /// by other processes). - /// - /// The database connection is read-only: attempts to write throw a - /// ``DatabaseError`` with resultCode `SQLITE_READONLY`. - /// - /// The ``Database`` argument to `value` is valid only during the execution - /// of the closure. Do not store or return the database connection for - /// later use. - /// - /// - parameter value: A closure which accesses the database. - /// - throws: The error thrown by `value`, or any ``DatabaseError`` that - /// would happen while establishing the database access. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - public func read(_ value: @Sendable @escaping (Database) throws -> T) async throws -> T { - try await withUnsafeThrowingContinuation { continuation in - asyncRead { result in - do { - try continuation.resume(returning: value(result.get())) - } catch { - continuation.resume(throwing: error) - } - } - } - } - - /// Executes database operations, and returns their result after they have - /// finished executing. - /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// - /// This method is "unsafe" because the database reader does nothing more - /// than providing a database connection. When you use this method, you - /// become responsible for the thread-safety of your application, and - /// responsible for database accesses performed by other processes. See - /// for - /// more information. - /// - /// For example: - /// - /// ```swift - /// let count = try await reader.unsafeRead { db in - /// try Player.fetchCount(db) - /// } - /// ``` - /// - /// The ``Database`` argument to `value` is valid only during the execution - /// of the closure. Do not store or return the database connection for - /// later use. - /// - /// - warning: Database operations may not be wrapped in a transaction. They - /// may see changes performed by concurrent writes or writes performed by - /// other processes: two identical requests performed by the `value` - /// closure may not return the same value. - /// - warning: Attempts to write in the database may succeed. - /// - /// - parameter value: A closure which accesses the database. - /// - throws: The error thrown by `value`, or any ``DatabaseError`` that - /// would happen while establishing the database access. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - public func unsafeRead(_ value: @Sendable @escaping (Database) throws -> T) async throws -> T { - try await withUnsafeThrowingContinuation { continuation in - asyncUnsafeRead { result in - do { - try continuation.resume(returning: value(result.get())) - } catch { - continuation.resume(throwing: error) - } - } - } - } -} - #if canImport(Combine) extension DatabaseReader { // MARK: - Publishing Database Values @@ -549,17 +529,13 @@ extension DatabaseReader { /// /// - parameter scheduler: A Combine Scheduler. /// - parameter value: A closure which accesses the database. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func readPublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, - value: @escaping (Database) throws -> Output) - -> DatabasePublishers.Read - { - Deferred { - Future { fulfill in - self.asyncRead { dbResult in - fulfill(dbResult.flatMap { db in Result { try value(db) } }) - } + value: @escaping @Sendable (Database) throws -> Output + ) -> DatabasePublishers.Read { + OnDemandFuture { fulfill in + self.asyncRead { dbResult in + fulfill(dbResult.flatMap { db in Result { try value(db) } }) } } .receiveValues(on: scheduler) @@ -567,7 +543,6 @@ extension DatabaseReader { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension DatabasePublishers { /// A publisher that reads from the database. /// @@ -586,7 +561,6 @@ extension DatabasePublishers { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Publisher where Failure == Error { fileprivate func eraseToReadPublisher() -> DatabasePublishers.Read { .init(upstream: eraseToAnyPublisher()) @@ -602,9 +576,8 @@ extension DatabaseReader { func _addReadOnly( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { if scheduler.immediateInitialValue() { do { // Perform a reentrant read, in case the observation would be @@ -620,16 +593,16 @@ extension DatabaseReader { } return AnyDatabaseCancellable(cancel: { /* nothing to cancel */ }) } else { - var isCancelled = false + let cancellable = AnyDatabaseCancellable() asyncRead { dbResult in - guard !isCancelled else { return } + if cancellable.isCancelled { return } let result = dbResult.flatMap { db in Result { try observation.fetchInitialValue(db) } } scheduler.schedule { - guard !isCancelled else { return } + if cancellable.isCancelled { return } do { try onChange(result.get()) } catch { @@ -637,7 +610,7 @@ extension DatabaseReader { } } } - return AnyDatabaseCancellable(cancel: { isCancelled = true }) + return cancellable } } } @@ -651,7 +624,7 @@ public final class AnyDatabaseReader { /// Creates a new database reader that wraps and forwards operations /// to `base`. - public init(_ base: some DatabaseReader) { + public init(_ base: any DatabaseReader) { self.base = base } } @@ -678,7 +651,15 @@ extension AnyDatabaseReader: DatabaseReader { try base.read(value) } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func read( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await base.read(value) + } + + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { base.asyncRead(value) } @@ -687,7 +668,15 @@ extension AnyDatabaseReader: DatabaseReader { try base.unsafeRead(value) } - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func unsafeRead( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await base.unsafeRead(value) + } + + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { base.asyncUnsafeRead(value) } @@ -698,9 +687,8 @@ extension AnyDatabaseReader: DatabaseReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { base._add( observation: observation, scheduling: scheduler, @@ -753,12 +741,15 @@ extension DatabaseSnapshotReader { } // There is no such thing as an unsafe access to a snapshot. + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func unsafeRead(_ value: (Database) throws -> T) throws -> T { try read(value) } // There is no such thing as an unsafe access to a snapshot. - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { asyncRead(value) } } diff --git a/GRDB/Core/DatabaseRegion.swift b/GRDB/Core/DatabaseRegion.swift index fddeb77d37..271b53f604 100644 --- a/GRDB/Core/DatabaseRegion.swift +++ b/GRDB/Core/DatabaseRegion.swift @@ -416,7 +416,7 @@ private struct TableRegion: Equatable { /// ### Supporting Types /// /// - ``AnyDatabaseRegionConvertible`` -public protocol DatabaseRegionConvertible { +public protocol DatabaseRegionConvertible: Sendable { /// Returns a database region. /// /// - parameter db: A database connection. @@ -437,14 +437,14 @@ extension DatabaseRegion: DatabaseRegionConvertible { /// A type-erased DatabaseRegionConvertible public struct AnyDatabaseRegionConvertible: DatabaseRegionConvertible { - let _region: (Database) throws -> DatabaseRegion + let _region: @Sendable (Database) throws -> DatabaseRegion - public init(_ region: @escaping (Database) throws -> DatabaseRegion) { + public init(_ region: @escaping @Sendable (Database) throws -> DatabaseRegion) { _region = region } public init(_ region: some DatabaseRegionConvertible) { - _region = region.databaseRegion + _region = { try region.databaseRegion($0) } } public func databaseRegion(_ db: Database) throws -> DatabaseRegion { @@ -461,7 +461,7 @@ extension DatabaseRegion { } } - static func union(_ regions: [any DatabaseRegionConvertible]) -> (Database) throws -> DatabaseRegion { + static func union(_ regions: [any DatabaseRegionConvertible]) -> @Sendable (Database) throws -> DatabaseRegion { return { db in try regions.reduce(into: DatabaseRegion()) { union, region in try union.formUnion(region.databaseRegion(db)) diff --git a/GRDB/Core/DatabaseRegionObservation.swift b/GRDB/Core/DatabaseRegionObservation.swift index 4f1e1473c0..8d9cd97b03 100644 --- a/GRDB/Core/DatabaseRegionObservation.swift +++ b/GRDB/Core/DatabaseRegionObservation.swift @@ -3,10 +3,10 @@ import Combine #endif import Foundation -public struct DatabaseRegionObservation { +public struct DatabaseRegionObservation: Sendable { /// A closure that is evaluated when the observation starts, and returns /// the observed database region. - var observedRegion: (Database) throws -> DatabaseRegion + var observedRegion: @Sendable (Database) throws -> DatabaseRegion } extension DatabaseRegionObservation { @@ -43,10 +43,10 @@ extension DatabaseRegionObservation { extension DatabaseRegionObservation { /// The state of a started DatabaseRegionObservation - private enum ObservationState { + private enum ObservationState: Sendable { case cancelled case pending - case started(DatabaseRegionObserver) + case started(StrongReference) } /// Starts observing the database. @@ -84,21 +84,21 @@ extension DatabaseRegionObservation { /// modified the observed region. /// - returns: A DatabaseCancellable that can stop the observation. public func start( - in writer: some DatabaseWriter, - onError: @escaping (Error) -> Void, - onChange: @escaping (Database) -> Void) + in writer: any DatabaseWriter, + onError: @escaping @Sendable (Error) -> Void, + onChange: @escaping @Sendable (Database) -> Void) -> AnyDatabaseCancellable { - @LockedBox var state = ObservationState.pending + let stateMutex = Mutex(ObservationState.pending) // Use unsafeReentrantWrite so that observation can start from any // dispatch queue. writer.unsafeReentrantWrite { db in do { let region = try observedRegion(db).observableRegion(db) - $state.update { + stateMutex.withLock { state in let observer = DatabaseRegionObserver(region: region, onChange: { - if case .cancelled = state { + if case .cancelled = stateMutex.load() { return } onChange($0) @@ -111,7 +111,7 @@ extension DatabaseRegionObservation { // the observer. db.add(transactionObserver: observer, extent: .observerLifetime) - $0 = .started(observer) + state = .started(StrongReference(observer)) } } catch { onError(error) @@ -122,13 +122,12 @@ extension DatabaseRegionObservation { // Deallocates the transaction observer. This makes sure that the // `onChange` callback will never be called again, because the // observation was started with the `.observerLifetime` extent. - state = .cancelled + stateMutex.store(.cancelled) } } } #if canImport(Combine) -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension DatabaseRegionObservation { // MARK: - Publishing Impactful Transactions @@ -140,8 +139,7 @@ extension DatabaseRegionObservation { /// /// Do not reschedule the publisher with `receive(on:options:)` or any /// `Publisher` method that schedules publisher elements. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - public func publisher(in writer: some DatabaseWriter) -> DatabasePublishers.DatabaseRegion { + public func publisher(in writer: any DatabaseWriter) -> DatabasePublishers.DatabaseRegion { DatabasePublishers.DatabaseRegion(self, in: writer) } } @@ -149,10 +147,10 @@ extension DatabaseRegionObservation { private class DatabaseRegionObserver: TransactionObserver { let region: DatabaseRegion - let onChange: (Database) -> Void + let onChange: @Sendable (Database) -> Void var isChanged = false - init(region: DatabaseRegion, onChange: @escaping (Database) -> Void) { + init(region: DatabaseRegion, onChange: @escaping @Sendable (Database) -> Void) { self.region = region self.onChange = onChange } @@ -186,7 +184,6 @@ private class DatabaseRegionObserver: TransactionObserver { } #if canImport(Combine) -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension DatabasePublishers { /// A publisher that tracks transactions that modify a database region. /// @@ -198,7 +195,7 @@ extension DatabasePublishers { let writer: any DatabaseWriter let observation: DatabaseRegionObservation - init(_ observation: DatabaseRegionObservation, in writer: some DatabaseWriter) { + init(_ observation: DatabaseRegionObservation, in writer: any DatabaseWriter) { self.writer = writer self.observation = observation } @@ -212,9 +209,14 @@ extension DatabasePublishers { } } - private class DatabaseRegionSubscription: Subscription - where Downstream.Failure == Error, Downstream.Input == Database + private class DatabaseRegionSubscription: + Subscription, @unchecked Sendable + where Downstream: Subscriber, + Downstream.Failure == Error, + Downstream.Input == Database { + // @unchecked Sendable because `cancellable` and `state` are + // protected by `lock`. private struct WaitingForDemand { let downstream: Downstream let writer: any DatabaseWriter @@ -245,7 +247,7 @@ extension DatabasePublishers { private var lock = NSRecursiveLock() // Allow re-entrancy init( - writer: some DatabaseWriter, + writer: any DatabaseWriter, observation: DatabaseRegionObservation, downstream: Downstream) { diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 1381c1871c..7609a3433f 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -126,10 +126,6 @@ public final class DatabaseSnapshot { // DatabaseSnapshot is read-only. configuration.readonly = true - // DatabaseSnapshot uses deferred transactions by default. - // Other transaction kinds are forbidden by SQLite in read-only connections. - configuration.defaultTransactionKind = .deferred - // DatabaseSnapshot keeps a long-lived transaction. configuration.allowsUnsafeTransactions = true @@ -150,19 +146,41 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { // MARK: - Reading from Database + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func read(_ block: (Database) throws -> T) rethrows -> T { try reader.sync(block) } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func read( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await reader.execute(value) + } + + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { reader.async { value(.success($0)) } } + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func unsafeRead(_ value: (Database) throws -> T) rethrows -> T { try reader.sync(value) } - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + // There is no such thing as an unsafe access to a snapshot. + // We can't provide this as a default implementation in + // `DatabaseSnapshotReader`, because of + // . + public func unsafeRead( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await read(value) + } + + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { reader.async { value(.success($0)) } } @@ -175,9 +193,8 @@ extension DatabaseSnapshot: DatabaseSnapshotReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { _addReadOnly( observation: observation, scheduling: scheduler, diff --git a/GRDB/Core/DatabaseSnapshotPool.swift b/GRDB/Core/DatabaseSnapshotPool.swift index c32b0b1a26..20398c5562 100644 --- a/GRDB/Core/DatabaseSnapshotPool.swift +++ b/GRDB/Core/DatabaseSnapshotPool.swift @@ -1,5 +1,13 @@ -// swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A database connection that allows concurrent accesses to an unchanging /// database content, as it existed at the moment the snapshot was created. /// @@ -122,6 +130,7 @@ public final class DatabaseSnapshotPool { /// `db` is used. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public init(_ db: Database, configuration: Configuration? = nil) throws { + let path = db.path var configuration = Self.configure(configuration ?? db.configuration) // Acquire and hold WAL snapshot @@ -130,7 +139,7 @@ public final class DatabaseSnapshotPool { } var holderConfig = Configuration() holderConfig.allowsUnsafeTransactions = true - snapshotHolder = try DatabaseQueue(path: db.path, configuration: holderConfig) + snapshotHolder = try DatabaseQueue(path: path, configuration: holderConfig) try snapshotHolder.inDatabase { db in try db.beginTransaction(.deferred) try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1") @@ -150,20 +159,18 @@ public final class DatabaseSnapshotPool { } self.configuration = configuration - self.path = db.path + self.path = path self.walSnapshot = walSnapshot - var readerCount = 0 readerPool = Pool( maximumCount: configuration.maximumReaderCount, qos: configuration.readQoS, - makeElement: { - readerCount += 1 // protected by Pool (TODO: document this protection behavior) + makeElement: { [configuration] index in return try SerializedDatabase( - path: db.path, + path: path, configuration: configuration, defaultLabel: "GRDB.DatabaseSnapshotPool", - purpose: "snapshot.\(readerCount)") + purpose: "snapshot.\(index)") }) } @@ -211,17 +218,15 @@ public final class DatabaseSnapshotPool { self.path = path self.walSnapshot = walSnapshot - var readerCount = 0 readerPool = Pool( maximumCount: configuration.maximumReaderCount, qos: configuration.readQoS, - makeElement: { - readerCount += 1 // protected by Pool (TODO: document this protection behavior) + makeElement: { [configuration] index in return try SerializedDatabase( path: path, configuration: configuration, defaultLabel: "GRDB.DatabaseSnapshotPool", - purpose: "snapshot.\(readerCount)") + purpose: "snapshot.\(index)") }) } @@ -234,10 +239,6 @@ public final class DatabaseSnapshotPool { // DatabaseSnapshotPool is read-only. configuration.readonly = true - // DatabaseSnapshotPool uses deferred transactions by default. - // Other transaction kinds are forbidden by SQLite in read-only connections. - configuration.defaultTransactionKind = .deferred - // DatabaseSnapshotPool keeps a long-lived transaction. configuration.allowsUnsafeTransactions = true @@ -292,7 +293,42 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { } } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func read( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + guard let readerPool else { + throw DatabaseError.connectionIsClosed() + } + + let dbAccess = CancellableDatabaseAccess() + return try await dbAccess.withCancellableContinuation { continuation in + readerPool.asyncGet { result in + do { + let (reader, releaseReader) = try result.get() + // Second async jump because that's how `Pool.async` has to be used. + reader.async { db in + defer { + releaseReader(self.poolCompletion(db)) + } + do { + let result = try dbAccess.inDatabase(db) { + try value(db) + } + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } catch { + continuation.resume(throwing: error) + } + } + } + } + + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) return @@ -312,12 +348,22 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { } } + // There is no such thing as an unsafe access to a snapshot. + // We can't provide this as a default implementation in + // `DatabaseSnapshotReader`, because of + // . + public func unsafeRead( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await read(value) + } + public func unsafeReentrantRead(_ value: (Database) throws -> T) throws -> T { if let reader = currentReader { return try reader.reentrantSync { db in let result = try value(db) if snapshotIsLost(db) { - throw DatabaseError(resultCode: .SQLITE_ABORT, message: "Snapshot is lost.") + throw DatabaseError.snapshotIsLost() } return result } @@ -330,9 +376,8 @@ extension DatabaseSnapshotPool: DatabaseSnapshotReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable where Reducer: ValueReducer - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable where Reducer: ValueReducer { _addReadOnly(observation: observation, scheduling: scheduler, onChange: onChange) } diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index 574ea97916..1c8e721f98 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A value stored in a database table. diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index df9b1b6939..7ce4fb4307 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -571,7 +571,7 @@ extension DatabaseValueConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A ``DatabaseValueCursor`` over fetched values. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchCursor(_ db: Database, _ request: some FetchRequest) throws -> DatabaseValueCursor { @@ -605,7 +605,7 @@ extension DatabaseValueConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An array. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Self] { @@ -642,7 +642,7 @@ extension DatabaseValueConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An optional value. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Self? { @@ -678,7 +678,7 @@ extension DatabaseValueConvertible where Self: Hashable { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A set. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set { diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index b4de8f6fd8..cbc2041d44 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -22,25 +22,23 @@ import Dispatch /// ### Writing into the Database /// /// - ``write(_:)-76inz`` -/// - ``write(_:)-88g7e`` +/// - ``write(_:)-3db50`` /// - ``writePublisher(receiveOn:updates:)`` /// - ``writePublisher(receiveOn:updates:thenRead:)`` /// - ``writeWithoutTransaction(_:)-4qh1w`` -/// - ``writeWithoutTransaction(_:)-tckw`` +/// - ``writeWithoutTransaction(_:)-67mri`` /// - ``asyncWrite(_:completion:)`` /// - ``asyncWriteWithoutTransaction(_:)`` /// /// ### Exclusive Access to the Database /// /// - ``barrierWriteWithoutTransaction(_:)-280j1`` -/// - ``barrierWriteWithoutTransaction(_:)-7u4xw`` +/// - ``barrierWriteWithoutTransaction(_:)-48d63`` /// - ``asyncBarrierWriteWithoutTransaction(_:)`` /// /// ### Reading from the Latest Committed Database State /// -/// - ``concurrentRead(_:)`` /// - ``spawnConcurrentRead(_:)`` -/// - ``DatabaseFuture`` /// /// ### Unsafe Methods /// @@ -102,6 +100,39 @@ public protocol DatabaseWriter: DatabaseReader { @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails func writeWithoutTransaction(_ updates: (Database) throws -> T) rethrows -> T + /// Executes database operations, and returns their result after they have + /// finished executing. + /// + /// For example: + /// + /// ```swift + /// let newPlayerCount = try await writer.writeWithoutTransaction { db in + /// try Player(name: "Arthur").insert(db) + /// return try Player.fetchCount(db) + /// } + /// ``` + /// + /// Database operations run in the writer dispatch queue, serialized + /// with all database updates performed by this `DatabaseWriter`. + /// + /// The ``Database`` argument to `updates` is valid only during the + /// execution of the closure. Do not store or return the database connection + /// for later use. + /// + /// - warning: Database operations are not wrapped in a transaction. They + /// can see changes performed by concurrent writes or writes performed by + /// other processes: two identical requests performed by the `updates` + /// closure may not return the same value. Concurrent database accesses + /// can see partial updates performed by the `updates` closure. + /// + /// - parameter updates: A closure which accesses the database. + /// - throws: Any ``DatabaseError`` that happens while establishing the + /// database access, or the error thrown by `updates`, or + /// `CancellationError` if the task is cancelled. + func writeWithoutTransaction( + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T + /// Executes database operations, and returns their result after they have /// finished executing. /// @@ -142,6 +173,49 @@ public protocol DatabaseWriter: DatabaseReader { @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T + /// Executes database operations, and returns their result after they have + /// finished executing. + /// + /// Database operations are not executed until all currently executing + /// database accesses performed by the database writer finish executing + /// (both reads and writes). At that point, database operations are + /// executed. Once they finish, the database writer can proceed with other + /// database accesses. + /// + /// For example: + /// + /// ```swift + /// let newPlayerCount = try await writer.barrierWriteWithoutTransaction { db in + /// try Player(name: "Arthur").insert(db) + /// return try Player.fetchCount(db) + /// } + /// ``` + /// + /// Database operations run in the writer dispatch queue, serialized + /// with all database updates performed by this `DatabaseWriter`. + /// + /// The ``Database`` argument to `updates` is valid only during the + /// execution of the closure. Do not store or return the database connection + /// for later use. + /// + /// It is a programmer error to call this method from another database + /// access method. Doing so raises a "Database methods are not reentrant" + /// fatal error at runtime. + /// + /// - warning: Database operations are not wrapped in a transaction. They + /// can see changes performed by concurrent writes or writes performed by + /// other processes: two identical requests performed by the `updates` + /// closure may not return the same value. Concurrent database accesses + /// can see partial updates performed by the `updates` closure. + /// + /// - parameter updates: A closure which accesses the database. + /// - throws: Any ``DatabaseError`` that happens while establishing the + /// database access, or the error thrown by `updates`, or + /// `CancellationError` if the task is cancelled. + func barrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T + /// Schedules database operations for execution, and returns immediately. /// /// Database operations are not executed until all currently executing @@ -181,7 +255,9 @@ public protocol DatabaseWriter: DatabaseReader { /// - parameter updates: A closure which accesses the database. Its argument /// is a `Result` that provides the database connection, or the failure /// that would prevent establishing the barrier access to the database. - func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) + func asyncBarrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Result) -> Void + ) /// Schedules database operations for execution, and returns immediately. /// @@ -213,7 +289,9 @@ public protocol DatabaseWriter: DatabaseReader { /// for more information. /// /// - parameter updates: A closure which accesses the database. - func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) + func asyncWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) -> Void + ) /// Executes database operations, and returns their result after they have /// finished executing. @@ -251,54 +329,9 @@ public protocol DatabaseWriter: DatabaseReader { // MARK: - Reading from Database - /// Schedules read-only database operations for execution, and returns a - /// future value. - /// - /// This method must be called from the writer dispatch queue, outside of - /// any transaction. You'll get a fatal error otherwise. - /// - /// Database operations performed by the `value` closure are isolated in a - /// transaction: they do not see changes performed by eventual concurrent - /// writes (even writes performed by other processes). - /// - /// They see the database in the state left by the last updates performed - /// by the database writer. - /// - /// To access the fetched results, you call the ``DatabaseFuture/wait()`` - /// method of the returned future, on any dispatch queue. - /// - /// In the example below, the number of players is fetched concurrently with - /// the player insertion. Yet the future is guaranteed to return zero: - /// - /// ```swift - /// try writer.writeWithoutTransaction { db in - /// // Delete all players - /// try Player.deleteAll() - /// - /// // Count players concurrently - /// let future = writer.concurrentRead { db in - /// return try Player.fetchCount() - /// } - /// - /// // Insert a player - /// try Player(...).insert(db) - /// - /// // Guaranteed to be zero - /// let count = try future.wait() - /// } - /// ``` - /// - /// - note: Usage of this method is discouraged, because waiting on the - /// returned ``DatabaseFuture`` blocks a thread. You may prefer - /// ``spawnConcurrentRead(_:)`` instead. - /// - parameter value: A closure which accesses the database. - func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture - // Exposed for RxGRDB and GRBCombine. Naming is not stabilized. /// Schedules read-only database operations for execution. /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// /// This method must be called from the writer dispatch queue, outside of /// any transaction. You'll get a fatal error otherwise. /// @@ -340,7 +373,9 @@ public protocol DatabaseWriter: DatabaseReader { /// - parameter value: A closure which accesses the database. Its argument /// is a `Result` that provides the database connection, or the failure /// that would prevent establishing the read access to the database. - func spawnConcurrentRead(_ value: @escaping (Result) -> Void) + func spawnConcurrentRead( + _ value: @escaping @Sendable (Result) -> Void + ) } extension DatabaseWriter { @@ -426,9 +461,9 @@ extension DatabaseWriter { /// - parameter updates: A closure which accesses the database. /// - parameter completion: A closure called with the transaction result. public func asyncWrite( - _ updates: @escaping (Database) throws -> T, - completion: @escaping (Database, Result) -> Void) - { + _ updates: @escaping @Sendable (Database) throws -> T, + completion: @escaping @Sendable (Database, Result) -> Void + ) { asyncWriteWithoutTransaction { db in do { var result: T? @@ -541,7 +576,7 @@ extension DatabaseWriter { /// /// - Parameter filePath: file path for new database @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) + @available(iOS 14, macOS 10.16, tvOS 14, *) public func vacuum(into filePath: String) throws { try writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -557,9 +592,8 @@ extension DatabaseWriter { func _addWriteOnly( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { assert(!configuration.readonly, "Use _addReadOnly(observation:) instead") let observer = ValueWriteOnlyObserver( writer: self, @@ -579,8 +613,6 @@ extension DatabaseWriter { /// Executes database operations, and returns their result after they have /// finished executing. /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// /// For example: /// /// ```swift @@ -604,115 +636,23 @@ extension DatabaseWriter { /// for later use. /// /// - parameter updates: A closure which accesses the database. - /// - throws: The error thrown by `updates`, or any ``DatabaseError`` that - /// would happen while establishing the database access or committing - /// the transaction. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - public func write(_ updates: @Sendable @escaping (Database) throws -> T) async throws -> T { - try await withUnsafeThrowingContinuation { continuation in - asyncWrite(updates, completion: { _, result in - continuation.resume(with: result) - }) - } - } - - /// Executes database operations, and returns their result after they have - /// finished executing. - /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// - /// For example: - /// - /// ```swift - /// let newPlayerCount = try await writer.writeWithoutTransaction { db in - /// try Player(name: "Arthur").insert(db) - /// return try Player.fetchCount(db) - /// } - /// ``` - /// - /// Database operations run in the writer dispatch queue, serialized - /// with all database updates performed by this `DatabaseWriter`. - /// - /// The ``Database`` argument to `updates` is valid only during the - /// execution of the closure. Do not store or return the database connection - /// for later use. - /// - /// - warning: Database operations are not wrapped in a transaction. They - /// can see changes performed by concurrent writes or writes performed by - /// other processes: two identical requests performed by the `updates` - /// closure may not return the same value. Concurrent database accesses - /// can see partial updates performed by the `updates` closure. - /// - /// - parameter updates: A closure which accesses the database. - /// - throws: The error thrown by `updates`. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - public func writeWithoutTransaction(_ updates: @Sendable @escaping (Database) throws -> T) async throws -> T { - try await withUnsafeThrowingContinuation { continuation in - asyncWriteWithoutTransaction { db in - do { - try continuation.resume(returning: updates(db)) - } catch { - continuation.resume(throwing: error) - } - } - } - } - - /// Executes database operations, and returns their result after they have - /// finished executing. - /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// - /// Database operations are not executed until all currently executing - /// database accesses performed by the database writer finish executing - /// (both reads and writes). At that point, database operations are - /// executed. Once they finish, the database writer can proceed with other - /// database accesses. - /// - /// For example: - /// - /// ```swift - /// let newPlayerCount = try await writer.barrierWriteWithoutTransaction { db in - /// try Player(name: "Arthur").insert(db) - /// return try Player.fetchCount(db) - /// } - /// ``` - /// - /// Database operations run in the writer dispatch queue, serialized - /// with all database updates performed by this `DatabaseWriter`. - /// - /// The ``Database`` argument to `updates` is valid only during the - /// execution of the closure. Do not store or return the database connection - /// for later use. - /// - /// It is a programmer error to call this method from another database - /// access method. Doing so raises a "Database methods are not reentrant" - /// fatal error at runtime. - /// - /// - warning: Database operations are not wrapped in a transaction. They - /// can see changes performed by concurrent writes or writes performed by - /// other processes: two identical requests performed by the `updates` - /// closure may not return the same value. Concurrent database accesses - /// can see partial updates performed by the `updates` closure. - /// - /// - parameter updates: A closure which accesses the database. - /// - throws: The error thrown by `updates`. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - public func barrierWriteWithoutTransaction( - _ updates: @Sendable @escaping (Database) throws -> T) - async throws -> T - { - try await withUnsafeThrowingContinuation { continuation in - asyncBarrierWriteWithoutTransaction { dbResult in - continuation.resume(with: dbResult.flatMap { db in Result { try updates(db) } }) + /// - throws: Any ``DatabaseError`` that happens while establishing the + /// database access, or the error thrown by `updates`, or + /// `CancellationError` if the task is cancelled. + public func write( + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await writeWithoutTransaction { db in + var result: T? + try db.inTransaction { + result = try updates(db) + return .commit } + return result! } } /// Erase the database: delete all content, drop all tables, etc. - /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func erase() async throws { try await writeWithoutTransaction { try $0.erase() } } @@ -720,10 +660,7 @@ extension DatabaseWriter { /// Rebuilds the database file, repacking it into a minimal amount of /// disk space. /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// /// Related SQLite documentation: - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func vacuum() async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM") } } @@ -732,15 +669,12 @@ extension DatabaseWriter { /// Creates a new database file at the specified path with a minimum /// amount of disk space. /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// /// Databases encrypted with SQLCipher are copied with the same password /// and configuration as the original database. /// /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -750,12 +684,9 @@ extension DatabaseWriter { /// Creates a new database file at the specified path with a minimum /// amount of disk space. /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) @@ -800,12 +731,10 @@ extension DatabaseWriter { /// /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which accesses the database. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, - updates: @escaping (Database) throws -> Output) - -> DatabasePublishers.Write - { + updates: @escaping @Sendable (Database) throws -> Output + ) -> DatabasePublishers.Write { OnDemandFuture { fulfill in self.asyncWrite(updates, completion: { _, result in fulfill(result) @@ -865,14 +794,11 @@ extension DatabaseWriter { /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which writes in the database. /// - parameter value: A closure which reads from the database. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - public func writePublisher( - receiveOn scheduler: S = DispatchQueue.main, - updates: @escaping (Database) throws -> T, - thenRead value: @escaping (Database, T) throws -> Output) - -> DatabasePublishers.Write - where S: Scheduler - { + public func writePublisher( + receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, + updates: @escaping @Sendable (Database) throws -> T, + thenRead value: @escaping @Sendable (Database, T) throws -> Output + ) -> DatabasePublishers.Write { OnDemandFuture { fulfill in self.asyncWriteWithoutTransaction { db in var updatesValue: T? @@ -885,7 +811,7 @@ extension DatabaseWriter { fulfill(.failure(error)) return } - self.spawnConcurrentRead { dbResult in + self.spawnConcurrentRead { [updatesValue] dbResult in fulfill(dbResult.flatMap { db in Result { try value(db, updatesValue!) } }) } } @@ -897,7 +823,6 @@ extension DatabaseWriter { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension DatabasePublishers { /// A publisher that writes into the database. /// @@ -916,7 +841,6 @@ extension DatabasePublishers { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Publisher where Failure == Error { fileprivate func eraseToWritePublisher() -> DatabasePublishers.Write { .init(upstream: self.eraseToAnyPublisher()) @@ -924,49 +848,6 @@ extension Publisher where Failure == Error { } #endif -/// A future database value. -/// -/// You get instances of `DatabaseFuture` from the `DatabaseWriter` -/// ``DatabaseWriter/concurrentRead(_:)`` method. For example: -/// -/// ```swift -/// let futureCount: Future = try writer.writeWithoutTransaction { db in -/// try Player(...).insert() -/// -/// // Count players concurrently -/// return writer.concurrentRead { db in -/// return try Player.fetchCount() -/// } -/// } -/// -/// let count: Int = try futureCount.wait() -/// ``` -public class DatabaseFuture { - private var consumed = false - private let _wait: () throws -> Value - - init(_ wait: @escaping () throws -> Value) { - _wait = wait - } - - init(_ result: Result) { - _wait = result.get - } - - /// Blocks the current thread until the value is available, and returns it. - /// - /// It is a programmer error to call this method several times. - /// - /// - throws: Any error that prevented the value from becoming available. - public func wait() throws -> Value { - // Not thread-safe and quick and dirty. - // Goal is that users learn not to call this method twice. - GRDBPrecondition(consumed == false, "DatabaseFuture.wait() must be called only once") - consumed = true - return try _wait() - } -} - /// A type-erased database writer. /// /// An instance of `AnyDatabaseWriter` forwards its operations to an underlying @@ -976,7 +857,7 @@ public final class AnyDatabaseWriter { /// Creates a new database reader that wraps and forwards operations /// to `base`. - public init(_ base: some DatabaseWriter) { + public init(_ base: any DatabaseWriter) { self.base = base } } @@ -1003,7 +884,15 @@ extension AnyDatabaseWriter: DatabaseReader { try base.read(value) } - public func asyncRead(_ value: @escaping (Result) -> Void) { + public func read( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await base.read(value) + } + + public func asyncRead( + _ value: @escaping @Sendable (Result) -> Void + ) { base.asyncRead(value) } @@ -1012,7 +901,15 @@ extension AnyDatabaseWriter: DatabaseReader { try base.unsafeRead(value) } - public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { + public func unsafeRead( + _ value: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await base.unsafeRead(value) + } + + public func asyncUnsafeRead( + _ value: @escaping @Sendable (Result) -> Void + ) { base.asyncUnsafeRead(value) } @@ -1023,9 +920,8 @@ extension AnyDatabaseWriter: DatabaseReader { public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, - onChange: @escaping (Reducer.Value) -> Void) - -> AnyDatabaseCancellable - { + onChange: @escaping @Sendable (Reducer.Value) -> Void + ) -> AnyDatabaseCancellable { base._add( observation: observation, scheduling: scheduler, @@ -1039,16 +935,32 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.writeWithoutTransaction(updates) } + public func writeWithoutTransaction( + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await base.writeWithoutTransaction(updates) + } + @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T { try base.barrierWriteWithoutTransaction(updates) } - public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { + public func barrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + try await base.barrierWriteWithoutTransaction(updates) + } + + public func asyncBarrierWriteWithoutTransaction( + _ updates: @escaping @Sendable (Result) -> Void + ) { base.asyncBarrierWriteWithoutTransaction(updates) } - public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) { + public func asyncWriteWithoutTransaction( + _ updates: @escaping @Sendable (Database) -> Void + ) { base.asyncWriteWithoutTransaction(updates) } @@ -1056,11 +968,9 @@ extension AnyDatabaseWriter: DatabaseWriter { try base.unsafeReentrantWrite(updates) } - public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { - base.concurrentRead(value) - } - - public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { + public func spawnConcurrentRead( + _ value: @escaping @Sendable (Result) -> Void + ) { base.spawnConcurrentRead(value) } } diff --git a/GRDB/Core/FetchRequest.swift b/GRDB/Core/FetchRequest.swift index 43115cab70..c3e24d8ba2 100644 --- a/GRDB/Core/FetchRequest.swift +++ b/GRDB/Core/FetchRequest.swift @@ -171,7 +171,9 @@ extension FetchRequest { /// /// - parameter adapter: A closure that accepts a database connection and /// returns a row adapter. - public func adapted(_ adapter: @escaping (Database) throws -> any RowAdapter) -> AdaptedFetchRequest { + public func adapted( + _ adapter: @escaping @Sendable (Database) throws -> any RowAdapter + ) -> AdaptedFetchRequest { AdaptedFetchRequest(self, adapter) } } @@ -181,11 +183,11 @@ extension FetchRequest { /// See ``FetchRequest/adapted(_:)``. public struct AdaptedFetchRequest { let base: Base - let adapter: (Database) throws -> any RowAdapter + let adapter: @Sendable (Database) throws -> any RowAdapter /// Creates an adapted request from a base request and a closure that builds /// a row adapter from a database connection. - init(_ base: Base, _ adapter: @escaping (Database) throws -> any RowAdapter) { + init(_ base: Base, _ adapter: @escaping @Sendable (Database) throws -> any RowAdapter) { self.base = base self.adapter = adapter } @@ -274,7 +276,7 @@ extension AnyFetchRequest: FetchRequest { } // Class-based type erasure, so that we preserve full type information. -private class FetchRequestEraser: FetchRequest { +private class FetchRequestEraser: FetchRequest, @unchecked Sendable { typealias RowDecoder = Void var sqlSubquery: SQLSubquery { @@ -290,7 +292,7 @@ private class FetchRequestEraser: FetchRequest { } } -private final class ConcreteFetchRequestEraser: FetchRequestEraser { +private final class ConcreteFetchRequestEraser: FetchRequestEraser, @unchecked Sendable { let request: Request init(request: Request) { diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index e11f35d1ff..f1e49e5f67 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A database row. @@ -1424,8 +1433,7 @@ extension Row { /// elements are undefined. /// /// - parameters: - /// - db: A database connection. - /// - sql: An SQL string. + /// - statement: The statement to iterate. /// - arguments: Optional statement arguments. /// - adapter: Optional RowAdapter /// - returns: A ``RowCursor`` over fetched rows. @@ -1713,7 +1721,7 @@ extension Row { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A ``RowCursor`` over fetched rows. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchCursor(_ db: Database, _ request: some FetchRequest) throws -> RowCursor { @@ -1744,7 +1752,7 @@ extension Row { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An array of rows. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Row] { @@ -1776,7 +1784,7 @@ extension Row { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A set of rows. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set { @@ -1812,7 +1820,7 @@ extension Row { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An optional row. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Row? { @@ -2191,7 +2199,7 @@ extension Row { /// /// See ``Row/scopesTree`` for more information. /// - /// - parameter key: An association key. + /// - parameter name: The scope name. public subscript(_ name: String) -> Row? { var fifo = Array(scopes) while !fifo.isEmpty { @@ -2387,9 +2395,7 @@ extension RowImpl { struct ArrayRowImpl: RowImpl { let columns: [(String, DatabaseValue)] - init(columns: Columns) - where Columns: Collection, Columns.Element == (String, DatabaseValue) - { + init(columns: some Collection<(String, DatabaseValue)>) { self.columns = Array(columns) } diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index a077e58152..2c4b760f1e 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -183,9 +183,7 @@ public struct _LayoutedColumnMapping { /// /// // [foo:"foo" bar: "bar"] /// try Row.fetchOne(db, sql: "SELECT NULL, 'foo', 'bar'", adapter: FooBarAdapter()) - init(layoutColumns: S) - where S: Sequence, S.Element == (Int, String) - { + init(layoutColumns: some Collection<(Int, String)>) { self._layoutColumns = Array(layoutColumns) self.lowercaseColumnIndexes = Dictionary( layoutColumns @@ -318,7 +316,7 @@ extension Statement: _RowLayout { /// - ``RenameColumnAdapter`` /// - ``ScopeAdapter`` /// - ``SuffixRowAdapter`` -public protocol RowAdapter { +public protocol RowAdapter: Sendable { /// You never call this method directly. It is called for you whenever an /// adapter has to be applied. /// @@ -656,11 +654,11 @@ struct ChainedAdapter: RowAdapter { /// print(Array(adaptedRow.columnNames)) /// ``` public struct RenameColumnAdapter: RowAdapter { - let transform: (String) -> String + let transform: @Sendable (String) -> String /// Creates a `RenameColumnAdapter` adapter that renames columns according to the /// provided transform function. - public init(_ transform: @escaping (String) -> String) { + public init(_ transform: @escaping @Sendable (String) -> String) { self.transform = transform } diff --git a/GRDB/Core/RowDecodingError.swift b/GRDB/Core/RowDecodingError.swift index e2ca849d4d..ed82c1f891 100644 --- a/GRDB/Core/RowDecodingError.swift +++ b/GRDB/Core/RowDecodingError.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A key that is used to decode a value in a row @usableFromInline enum RowKey: Hashable, Sendable { diff --git a/GRDB/Core/SQL.swift b/GRDB/Core/SQL.swift index 4246cf202e..a6d2c439f4 100644 --- a/GRDB/Core/SQL.swift +++ b/GRDB/Core/SQL.swift @@ -39,7 +39,7 @@ /// /// - ``append(literal:)`` /// - ``append(sql:arguments:)`` -public struct SQL { +public struct SQL: Sendable { /// `SQL.Element` is a component of an `SQL` literal. /// /// Elements can be qualified with table aliases, and this is how `SQL` diff --git a/GRDB/Core/SchedulingWatchdog.swift b/GRDB/Core/SchedulingWatchdog.swift index 83cb6d17af..3c2ba702b0 100644 --- a/GRDB/Core/SchedulingWatchdog.swift +++ b/GRDB/Core/SchedulingWatchdog.swift @@ -21,9 +21,17 @@ import Dispatch /// /// - preconditionValidQueue() crashes whenever a database is used in an invalid /// dispatch queue. -final class SchedulingWatchdog { +final class SchedulingWatchdog: @unchecked Sendable { + // @unchecked Sendable because mutable `allowedDatabases` is only + // accessed from the serial dispatch queue the instance is attached to. + private static let watchDogKey = DispatchSpecificKey() + + /// The databases allowed in the current dispatch queue. + /// + /// MUST be accessed from the serial dispatch queue the instance is attached to. private(set) var allowedDatabases: [Database] + var databaseObservationBroker: DatabaseObservationBroker? private init(allowedDatabase database: Database) { @@ -36,10 +44,14 @@ final class SchedulingWatchdog { queue.setSpecific(key: watchDogKey, value: watchdog) } - func inheritingAllowedDatabases(from other: SchedulingWatchdog, execute body: () throws -> T) rethrows -> T { - let backup = allowedDatabases - allowedDatabases.append(contentsOf: other.allowedDatabases) - defer { allowedDatabases = backup } + /// Must be called from a DispatchQueue with an attached SchedulingWatchdog. + static func inheritingAllowedDatabases( + _ allowedDatabases: [Database], execute body: () throws -> T + ) rethrows -> T { + let watchdog = current! + let backup = watchdog.allowedDatabases + watchdog.allowedDatabases.append(contentsOf: allowedDatabases) + defer { watchdog.allowedDatabases = backup } return try body() } @@ -58,11 +70,11 @@ final class SchedulingWatchdog { current?.allows(db) ?? false } - static var current: SchedulingWatchdog? { - DispatchQueue.getSpecific(key: watchDogKey) - } - func allows(_ db: Database) -> Bool { allowedDatabases.contains { $0 === db } } + + static var current: SchedulingWatchdog? { + DispatchQueue.getSpecific(key: watchDogKey) + } } diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index f1dfe05721..b794fa63ea 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -133,7 +133,7 @@ final class SerializedDatabase { // Case 3 return try queue.sync { - try SchedulingWatchdog.current!.inheritingAllowedDatabases(from: watchdog) { + try SchedulingWatchdog.inheritingAllowedDatabases(watchdog.allowedDatabases) { defer { preconditionNoUnsafeTransactionLeft(db) } return try block(db) } @@ -209,7 +209,7 @@ final class SerializedDatabase { // Case 3 return try queue.sync { - try SchedulingWatchdog.current!.inheritingAllowedDatabases(from: watchdog) { + try SchedulingWatchdog.inheritingAllowedDatabases(watchdog.allowedDatabases) { // Since we are reentrant, a transaction may already be opened. // In this case, don't check for unsafe transaction at the end. if db.isInsideTransaction { @@ -223,7 +223,7 @@ final class SerializedDatabase { } /// Schedules database operations for execution, and returns immediately. - func async(_ block: @escaping (Database) -> Void) { + func async(_ block: @escaping @Sendable (Database) -> Void) { queue.async { block(self.db) self.preconditionNoUnsafeTransactionLeft(self.db) @@ -243,6 +243,25 @@ final class SerializedDatabase { return try block(db) } + /// Asynchrously executes the block. + func execute( + _ block: @escaping @Sendable (Database) throws -> T + ) async throws -> T { + let dbAccess = CancellableDatabaseAccess() + return try await dbAccess.withCancellableContinuation { continuation in + self.async { db in + do { + let result = try dbAccess.inDatabase(db) { + try block(db) + } + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } + } + func interrupt() { // Intentionally not scheduled in our serial queue db.interrupt() @@ -286,3 +305,108 @@ final class SerializedDatabase { // It happens the job of SerializedDatabase is precisely to provide thread-safe // access to `Database`. extension SerializedDatabase: @unchecked Sendable { } + +// MARK: - Task Cancellation Support + +enum DatabaseAccessCancellationState: @unchecked Sendable { + // @unchecked Sendable because database is only accessed from its + // dispatch queue. + case notConnected + case connected(Database) + case cancelled + case expired +} + +typealias CancellableDatabaseAccess = Mutex + +/// Supports Task cancellation in async database accesses. +/// +/// Usage: +/// +/// ```swift +/// let dbAccess = CancellableDatabaseAccess() +/// return try dbAccess.withCancellableContinuation { continuation in +/// asyncDatabaseAccess { db in +/// do { +/// let result = try dbAccess.inDatabase(db) { +/// // Perform database operations +/// } +/// continuation.resume(returning: result) +/// } catch { +/// continuation.resume(throwing: error) +/// } +/// } +/// } +/// ``` +extension CancellableDatabaseAccess: DatabaseCancellable { + convenience init() { + self.init(.notConnected) + } + + func cancel() { + withLock { state in + switch state { + case let .connected(db): + db.cancel() + state = .cancelled + case .notConnected: + state = .cancelled + case .cancelled, .expired: + break + } + } + } + + func withCancellableContinuation( + _ fn: (UnsafeContinuation) -> Void + ) async throws -> Value { + try await withTaskCancellationHandler { + try checkCancellation() + return try await withUnsafeThrowingContinuation { continuation in + fn(continuation) + } + } onCancel: { + cancel() + } + } + + func checkCancellation() throws { + try withLock { state in + if case .cancelled = state { + throw CancellationError() + } + } + } + + /// Wraps a full database access with cancellation support. When this + /// method returns, the database is NOT cancelled. + func inDatabase(_ db: Database, execute work: () throws -> Value) throws -> Value { + try withLock { state in + switch state { + case .connected, .expired: + fatalError("Can't use a CancellableDatabaseAccess twice") + case .notConnected: + state = .connected(db) + case .cancelled: + throw CancellationError() + } + } + + return try throwingFirstError { + try work() + } finally: { + let cancelled = withLock { state in + if case .cancelled = state { + db.uncancel() + return true + } else { + state = .expired + return false + } + } + if cancelled { + throw CancellationError() + } + } + } +} diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index e1a90a1082..f2792af315 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A raw SQLite statement, suitable for the SQLite C API. @@ -26,7 +35,7 @@ extension String { } public final class Statement { - enum TransactionEffect { + enum TransactionEffect: Equatable { case beginTransaction case commitTransaction case rollbackTransaction @@ -130,21 +139,9 @@ public final class Statement { authorizer.reset() var sqliteStatement: SQLiteStatement? = nil - let code: CInt - // sqlite3_prepare_v3 was introduced in SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20 -#if GRDBCUSTOMSQLITE || GRDBCIPHER - code = sqlite3_prepare_v3( + let code = sqlite3_prepare_v3( database.sqliteConnection, statementStart, -1, prepFlags, &sqliteStatement, statementEnd) -#else - if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) { // SQLite 3.20+ - code = sqlite3_prepare_v3( - database.sqliteConnection, statementStart, -1, prepFlags, - &sqliteStatement, statementEnd) - } else { - code = sqlite3_prepare_v2(database.sqliteConnection, statementStart, -1, &sqliteStatement, statementEnd) - } -#endif guard code == SQLITE_OK else { throw DatabaseError( @@ -710,7 +707,7 @@ extension Statement { /// [`sqlite3_reset`](https://www.sqlite.org/c3ref/reset.html) when the cursor /// is created, and when it is deallocated. Don't share the same prepared /// statement between two cursors! -public protocol DatabaseCursor: Cursor { +public protocol DatabaseCursor: Cursor { /// Must be initialized to false. var _isDone: Bool { get set } @@ -856,14 +853,12 @@ func checkBindingSuccess(code: CInt, sqliteStatement: SQLiteStatement) throws { /// - parameter index: The index of the first binding. /// - parameter body: The closure to execute when arguments are bound. @usableFromInline -func withBindings( - _ bindings: C, +func withBindings( + _ bindings: some Collection, to sqliteStatement: SQLiteStatement, from index: CInt = 1, - do body: () throws -> T) -throws -> T -where C: Collection, C.Element == DatabaseValue -{ + do body: () throws -> T +) throws -> T { guard let binding = bindings.first else { return try body() } @@ -929,7 +924,8 @@ where C: Collection, C.Element == DatabaseValue /// ## Concatenating Arguments /// /// Several arguments can be concatenated and mixed with the -/// ``append(contentsOf:)`` method and the `+`, `&+`, `+=` operators: +/// ``StatementArguments/append(contentsOf:)`` method and the `+`, `&+`, +/// `+=` operators: /// /// ```swift /// var arguments: StatementArguments = ["Arthur"] @@ -949,8 +945,8 @@ where C: Collection, C.Element == DatabaseValue /// arguments += ["name": "Barbara"] /// ``` /// -/// On the other side, `&+` and ``append(contentsOf:)`` allow overriding -/// named arguments: +/// On the other side, `&+` and ``StatementArguments/append(contentsOf:)`` +/// allow overriding named arguments: /// /// ```swift /// var arguments: StatementArguments = ["name": "Arthur"] @@ -1009,10 +1005,8 @@ public struct StatementArguments: Hashable { /// let values: [(any DatabaseValueConvertible)?] = ["foo", 1, nil] /// db.execute(sql: "INSERT ... (?,?,?)", arguments: StatementArguments(values)) /// ``` - public init(_ sequence: S) - where S: Sequence, S.Element == (any DatabaseValueConvertible)? - { - values = sequence.map { $0?.databaseValue ?? .null } + public init(_ values: some Sequence<(any DatabaseValueConvertible)?>) { + self.values = values.map { $0?.databaseValue ?? .null } namedValues = .init() } @@ -1024,10 +1018,8 @@ public struct StatementArguments: Hashable { /// let values: [String] = ["foo", "bar"] /// db.execute(sql: "INSERT ... (?,?)", arguments: StatementArguments(values)) /// ``` - public init(_ sequence: S) - where S: Sequence, S.Element: DatabaseValueConvertible - { - values = sequence.map(\.databaseValue) + public init(_ values: some Sequence) { + self.values = values.map(\.databaseValue) namedValues = .init() } @@ -1068,11 +1060,11 @@ public struct StatementArguments: Hashable { /// Creates a `StatementArguments` of named arguments from a sequence of /// (key, value) pairs. - public init(_ sequence: S) - where S: Sequence, S.Element == (String, (any DatabaseValueConvertible)?) - { - namedValues = .init(minimumCapacity: sequence.underestimatedCount) - for (key, value) in sequence { + public init( + _ keysAndValues: some Sequence<(String, (any DatabaseValueConvertible)?)> + ) { + namedValues = .init(minimumCapacity: keysAndValues.underestimatedCount) + for (key, value) in keysAndValues { namedValues[key] = value?.databaseValue ?? .null } values = .init() diff --git a/GRDB/Core/StatementAuthorizer.swift b/GRDB/Core/StatementAuthorizer.swift index 54d9057781..d2427ac30f 100644 --- a/GRDB/Core/StatementAuthorizer.swift +++ b/GRDB/Core/StatementAuthorizer.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + #if canImport(string_h) import string_h #elseif os(Linux) diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index cb081ec63d..d16c038429 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A type that can decode itself from the low-level C interface to /// SQLite results. /// @@ -108,7 +117,6 @@ public protocol StatementColumnConvertible { /// - parameters: /// - sqliteStatement: A pointer to an SQLite statement. /// - index: The column index. - /// - returns: A decoded value, or, if decoding is impossible, nil. init?(sqliteStatement: SQLiteStatement, index: CInt) } @@ -585,7 +593,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A ``FastDatabaseValueCursor`` over fetched values. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchCursor(_ db: Database, _ request: some FetchRequest) @@ -621,7 +629,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An array of values. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Self] { @@ -658,7 +666,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: An optional value. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Self? { @@ -694,7 +702,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible & Hash /// /// - parameters: /// - db: A database connection. - /// - request: A FetchRequest. + /// - request: A fetch request. /// - returns: A set of values. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set { diff --git a/GRDB/Core/Support/Foundation/Data.swift b/GRDB/Core/Support/Foundation/Data.swift index 6827b71d9c..55d6bdb20f 100644 --- a/GRDB/Core/Support/Foundation/Data.swift +++ b/GRDB/Core/Support/Foundation/Data.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// Data is convertible to and from DatabaseValue. diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index d4ebba478a..fb861feb8a 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A database value that holds date components. diff --git a/GRDB/Core/Support/Foundation/Date.swift b/GRDB/Core/Support/Foundation/Date.swift index cc056bea45..6676c065ae 100644 --- a/GRDB/Core/Support/Foundation/Date.swift +++ b/GRDB/Core/Support/Foundation/Date.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation #if !os(Linux) diff --git a/GRDB/Core/Support/Foundation/Decimal.swift b/GRDB/Core/Support/Foundation/Decimal.swift index 44fe2eaeb1..e200c6c596 100644 --- a/GRDB/Core/Support/Foundation/Decimal.swift +++ b/GRDB/Core/Support/Foundation/Decimal.swift @@ -1,4 +1,13 @@ #if !os(Linux) +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// Decimal adopts DatabaseValueConvertible diff --git a/GRDB/Core/Support/Foundation/UUID.swift b/GRDB/Core/Support/Foundation/UUID.swift index 8aa88d0901..1ebca9d847 100644 --- a/GRDB/Core/Support/Foundation/UUID.swift +++ b/GRDB/Core/Support/Foundation/UUID.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation #if !os(Linux) @@ -8,7 +17,7 @@ extension NSUUID: DatabaseValueConvertible { var uuidBytes = ContiguousArray(repeating: UInt8(0), count: 16) return uuidBytes.withUnsafeMutableBufferPointer { buffer in getBytes(buffer.baseAddress!) - return NSData(bytes: buffer.baseAddress, length: 16).databaseValue + return Data(bytes: buffer.baseAddress!, count: 16).databaseValue } } diff --git a/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Encodable.swift b/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Encodable.swift index 7735a8fb68..2076df72d5 100644 --- a/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Encodable.swift +++ b/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Encodable.swift @@ -129,7 +129,7 @@ private class DatabaseValueEncoder: Encoder { // eventually perform JSON decoding. // TODO: possible optimization: avoid this conversion to string, // and store raw data bytes as an SQLite string - let jsonString = String(data: jsonData, encoding: .utf8)! + let jsonString = String(decoding: jsonData, as: UTF8.self) try jsonString.encode(to: self) } } diff --git a/GRDB/Core/Support/StandardLibrary/Optional.swift b/GRDB/Core/Support/StandardLibrary/Optional.swift index 4c9e3a3b9b..0902097d9a 100644 --- a/GRDB/Core/Support/StandardLibrary/Optional.swift +++ b/GRDB/Core/Support/StandardLibrary/Optional.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + extension Optional: StatementBinding where Wrapped: StatementBinding { public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt { switch self { diff --git a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift index 021beb4926..8a4c5a7d83 100644 --- a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift +++ b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + // MARK: - Value Types /// Bool adopts DatabaseValueConvertible and StatementColumnConvertible. diff --git a/GRDB/Core/TransactionClock.swift b/GRDB/Core/TransactionClock.swift index d878cd4504..b467ce7240 100644 --- a/GRDB/Core/TransactionClock.swift +++ b/GRDB/Core/TransactionClock.swift @@ -10,7 +10,7 @@ import Foundation /// /// - ``DefaultTransactionClock`` /// - ``CustomTransactionClock`` -public protocol TransactionClock { +public protocol TransactionClock: Sendable { /// Returns the date of the current transaction. /// /// This function is called whenever a transaction starts - precisely @@ -36,7 +36,7 @@ extension TransactionClock where Self == CustomTransactionClock { /// /// It is also called when the ``Database/transactionDate`` property is /// called, and the database connection is not in a transaction. - public static func custom(_ now: @escaping (Database) throws -> Date) -> Self { + public static func custom(_ now: @escaping @Sendable (Database) throws -> Date) -> Self { CustomTransactionClock(now) } } @@ -53,9 +53,9 @@ public struct DefaultTransactionClock: TransactionClock { /// A custom transaction clock. public struct CustomTransactionClock: TransactionClock { - let _now: (Database) throws -> Date + let _now: @Sendable (Database) throws -> Date - public init(_ now: @escaping (Database) throws -> Date) { + public init(_ now: @escaping @Sendable (Database) throws -> Date) { self._now = now } diff --git a/GRDB/Core/TransactionObserver.swift b/GRDB/Core/TransactionObserver.swift index d45b3b1206..5c7593916c 100644 --- a/GRDB/Core/TransactionObserver.swift +++ b/GRDB/Core/TransactionObserver.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + extension Database { // MARK: - Database Observation @@ -101,14 +110,17 @@ extension Database { /// - parameter onCommit: A closure executed on transaction commit. /// - parameter onRollback: A closure executed on transaction rollback. public func afterNextTransaction( - onCommit: @escaping (Database) -> Void, - onRollback: @escaping (Database) -> Void = { _ in }) + onCommit: @escaping @Sendable (Database) -> Void, + onRollback: @escaping @Sendable (Database) -> Void = { _ in }) { class TransactionHandler: TransactionObserver { - let onCommit: (Database) -> Void - let onRollback: (Database) -> Void + let onCommit: @Sendable (Database) -> Void + let onRollback: @Sendable (Database) -> Void - init(onCommit: @escaping (Database) -> Void, onRollback: @escaping (Database) -> Void) { + init( + onCommit: @escaping @Sendable (Database) -> Void, + onRollback: @escaping @Sendable (Database) -> Void + ) { self.onCommit = onCommit self.onRollback = onRollback } diff --git a/GRDB/Core/WALSnapshot.swift b/GRDB/Core/WALSnapshot.swift index 4d50c67e08..a8c0168a73 100644 --- a/GRDB/Core/WALSnapshot.swift +++ b/GRDB/Core/WALSnapshot.swift @@ -1,5 +1,13 @@ -// swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// An instance of WALSnapshot records the state of a WAL mode database for some /// specific point in history. /// @@ -15,8 +23,8 @@ /// With custom SQLite builds, it only works if `SQLITE_ENABLE_SNAPSHOT` /// is defined. /// -/// With system SQLite, it can only work when the SDK exposes the C apis and -/// their availability, which means XCode 14 (identified with Swift 5.7). +/// With system SQLite, it works because the SDK exposes the C apis and +/// since XCode 14. /// /// Yes, this is an awfully complex logic. /// diff --git a/GRDB/Core/WALSnapshotTransaction.swift b/GRDB/Core/WALSnapshotTransaction.swift index 5f56f1d2fc..eb6597fe1a 100644 --- a/GRDB/Core/WALSnapshotTransaction.swift +++ b/GRDB/Core/WALSnapshotTransaction.swift @@ -1,12 +1,30 @@ -// swiftlint:disable:next line_length -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) /// A long-live read-only WAL transaction. /// /// `WALSnapshotTransaction` **takes ownership** of its reader /// `SerializedDatabase` (TODO: make it a move-only type eventually). -class WALSnapshotTransaction { - private let reader: SerializedDatabase - private let release: (_ isInsideTransaction: Bool) -> Void +final class WALSnapshotTransaction: @unchecked Sendable { + // @unchecked because `databaseAccess` is protected by a mutex. + + private struct DatabaseAccess { + let reader: SerializedDatabase + let release: @Sendable (_ isInsideTransaction: Bool) -> Void + + // MUST be called only once + func commitAndRelease() { + // WALSnapshotTransaction may be deinitialized in the dispatch + // queue of its reader: allow reentrancy. + let isInsideTransaction = reader.reentrantSync(allowingLongLivedTransaction: false) { db in + try? db.commit() + return db.isInsideTransaction + } + release(isInsideTransaction) + } + } + + // TODO: consider using the serialized DispatchQueue of reader instead of a lock. + /// nil when closed + private let databaseAccessMutex: Mutex /// The state of the database at the beginning of the transaction. let walSnapshot: WALSnapshot @@ -37,10 +55,11 @@ class WALSnapshotTransaction { /// is no longer used. init( onReader reader: SerializedDatabase, - release: @escaping (_ isInsideTransaction: Bool) -> Void) + release: @escaping @Sendable (_ isInsideTransaction: Bool) -> Void) throws { assert(reader.configuration.readonly) + let databaseAccess = DatabaseAccess(reader: reader, release: release) do { // Open a long-lived transaction, and enter snapshot isolation @@ -51,44 +70,56 @@ class WALSnapshotTransaction { try db.clearSchemaCacheIfNeeded() return try WALSnapshot(db) } - self.reader = reader - self.release = release + self.databaseAccessMutex = Mutex(databaseAccess) } catch { // self is not initialized, so deinit will not run. - Self.commitAndRelease(reader: reader, release: release) + databaseAccess.commitAndRelease() throw error } } deinit { - Self.commitAndRelease(reader: reader, release: release) + close() } /// Executes database operations in the snapshot transaction, and /// returns their result after they have finished executing. - func read(_ value: (Database) throws -> T) rethrows -> T { - // We should check the validity of the snapshot, as DatabaseSnapshotPool does. - try reader.sync(value) + func read(_ value: (Database) throws -> T) throws -> T { + try databaseAccessMutex.withLock { databaseAccess in + guard let databaseAccess else { + throw DatabaseError.snapshotIsLost() + } + + // We should check the validity of the snapshot, as DatabaseSnapshotPool does. + return try databaseAccess.reader.sync(value) + } } /// Schedules database operations for execution, and /// returns immediately. - func asyncRead(_ value: @escaping (Database) -> Void) { - // We should check the validity of the snapshot, as DatabaseSnapshotPool does. - reader.async(value) + func asyncRead(_ value: @escaping @Sendable (Result) -> Void) { + databaseAccessMutex.withLock { databaseAccess in + guard let databaseAccess else { + value(.failure(DatabaseError.snapshotIsLost())) + return + } + + databaseAccess.reader.async { db in + // We should check the validity of the snapshot, as DatabaseSnapshotPool does. + // At least check if self was closed: + if self.databaseAccessMutex.load() == nil { + value(.failure(DatabaseError.snapshotIsLost())) + } + value(.success(db)) + } + } } - private static func commitAndRelease( - reader: SerializedDatabase, - release: (_ isInsideTransaction: Bool) -> Void) - { - // WALSnapshotTransaction may be deinitialized in the dispatch - // queue of its reader: allow reentrancy. - let isInsideTransaction = reader.reentrantSync(allowingLongLivedTransaction: false) { db in - try? db.commit() - return db.isInsideTransaction + func close() { + databaseAccessMutex.withLock { databaseAccess in + databaseAccess?.commitAndRelease() + databaseAccess = nil } - release(isInsideTransaction) } } #endif diff --git a/GRDB/Documentation.docc/Concurrency.md b/GRDB/Documentation.docc/Concurrency.md index 4c5f7e908a..439ce4fa54 100644 --- a/GRDB/Documentation.docc/Concurrency.md +++ b/GRDB/Documentation.docc/Concurrency.md @@ -1,6 +1,6 @@ # Concurrency -GRDB helps your app deal with SQLite concurrency. +GRDB helps your app deal with Swift and SQLite concurrency. ## Overview @@ -84,9 +84,7 @@ try dbQueue.write { db in 🔀 **An async access does not block the current thread.** Instead, it notifies you when the database operations are completed. There are four ways to access the database asynchronously: -- **Swift concurrency** (async/await) - - [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) +- **Swift concurrency** (async/await) ```swift let playerCount = try await dbQueue.read { db in @@ -99,9 +97,13 @@ try dbQueue.write { db in } ``` - See ``DatabaseReader/read(_:)-4w6gy`` and ``DatabaseWriter/write(_:)-88g7e``. + See ``DatabaseReader/read(_:)-4d1da`` and ``DatabaseWriter/write(_:)-3db50``. Note the identical method names: `read`, `write`. The async version is only available in async Swift functions. + + The async database access methods honor task cancellation. Once an async Task is cancelled, reads and writes throw `CancellationError`, and any transaction is rollbacked. + + See for more information about GRDB and Swift 6. - **Combine publishers** @@ -277,31 +279,7 @@ let newPlayerCount = try dbPool.write { db in } ``` -➡️ The synchronous solution is the ``DatabaseWriter/concurrentRead(_:)`` method. It must be called from within a write access, outside of any transaction. It returns a ``DatabaseFuture`` which you consume any time later, with the ``DatabaseFuture/wait()`` method: - -```swift -let future: DatabaseFuture = try dbPool.writeWithoutTransaction { db in - // Increment the number of players - try db.inTransaction { - try Player(...).insert(db) - return .commit - } - - // <- Not in a transaction here - return dbPool.concurrentRead { db - try Player.fetchCount(db) - } -} - -do { - // Handle the new player count - guaranteed greater than zero - let newPlayerCount = try future.wait() -} catch { - // Handle error -} -``` - -🔀 The asynchronous version of `concurrentRead` is ``DatabasePool/asyncConcurrentRead(_:)``: +🔀 The solution is ``DatabasePool/asyncConcurrentRead(_:)``. It must be called from within a write access, outside of any transaction: ```swift try dbPool.writeWithoutTransaction { db in @@ -324,11 +302,10 @@ try dbPool.writeWithoutTransaction { db in } ``` -Both ``DatabaseWriter/concurrentRead(_:)`` and ``DatabasePool/asyncConcurrentRead(_:)`` block until they can guarantee their closure argument an isolated access to the database, in the exact state left by the last transaction. It then asynchronously executes this closure. +The ``DatabasePool/asyncConcurrentRead(_:)`` method blocks until it can guarantee its closure argument an isolated access to the database, in the exact state left by the last transaction. It then asynchronously executes the closure. In the illustration below, the striped band shows the delay needed for the reading thread to acquire isolation. Until then, no other thread can write: - ![DatabasePool Concurrent Read](DatabasePoolConcurrentRead.png) Types that conform to ``TransactionObserver`` can also use those methods in their ``TransactionObserver/databaseDidCommit(_:)`` method, in order to process database changes without blocking other threads that want to write into the database. @@ -341,8 +318,9 @@ Types that conform to ``TransactionObserver`` can also use those methods in thei - ``DatabaseReader`` - ``DatabaseSnapshotReader`` -### Advanced Concurrency +### Going Further +- - diff --git a/GRDB/Documentation.docc/DatabaseSchema.md b/GRDB/Documentation.docc/DatabaseSchema.md index abfd53021b..b81073adc0 100644 --- a/GRDB/Documentation.docc/DatabaseSchema.md +++ b/GRDB/Documentation.docc/DatabaseSchema.md @@ -170,9 +170,9 @@ struct Player: Codable { extension Player: FetchableRecord, MutablePersistableRecord { // Required because the primary key // is the hidden rowid column. - static let databaseSelection: [any SQLSelectable] = [ - AllColumns(), - Column.rowID] + static var databaseSelection: [any SQLSelectable] { + [AllColumns(), Column.rowID] + } // Update id upon successful insertion mutating func didInsert(_ inserted: InsertionSuccess) { diff --git a/GRDB/Documentation.docc/DatabaseSharing.md b/GRDB/Documentation.docc/DatabaseSharing.md index 50b9569f4c..29dd6b9b68 100644 --- a/GRDB/Documentation.docc/DatabaseSharing.md +++ b/GRDB/Documentation.docc/DatabaseSharing.md @@ -152,12 +152,11 @@ If several processes want to write in the database, configure the database pool ```swift var configuration = Configuration() -configuration.defaultTransactionKind = .immediate configuration.busyMode = .timeout(/* a TimeInterval */) let dbPool = try DatabasePool(path: ..., configuration: configuration) ``` -Both the `defaultTransactionKind` and `busyMode` are important for preventing `SQLITE_BUSY`. The `immediate` transaction kind prevents write transactions from overlapping, and the busy timeout has write transactions wait, instead of throwing `SQLITE_BUSY`, whenever another process is writing. +The busy timeout has write transactions wait, instead of throwing `SQLITE_BUSY`, whenever another process is writing. GRDB automatically opens all write transactions with the IMMEDIATE kind, preventing write transactions from overlapping. With such a setup, you will still get `SQLITE_BUSY` errors if the database remains locked by another process for longer than the specified timeout. You can catch those errors: diff --git a/GRDB/Documentation.docc/Extension/Configuration.md b/GRDB/Documentation.docc/Extension/Configuration.md index efb77b8a62..55f0d770d8 100644 --- a/GRDB/Documentation.docc/Extension/Configuration.md +++ b/GRDB/Documentation.docc/Extension/Configuration.md @@ -90,7 +90,6 @@ do { ### Configuring GRDB Connections - ``allowsUnsafeTransactions`` -- ``defaultTransactionKind`` - ``label`` - ``maximumReaderCount`` - ``observesSuspensionNotifications`` diff --git a/GRDB/Documentation.docc/Extension/TransactionObserver.md b/GRDB/Documentation.docc/Extension/TransactionObserver.md index 9815e08520..72acd5e512 100644 --- a/GRDB/Documentation.docc/Extension/TransactionObserver.md +++ b/GRDB/Documentation.docc/Extension/TransactionObserver.md @@ -205,11 +205,8 @@ This extra API can be activated in two ways: 1. Use the GRDB.swift CocoaPod with a custom compilation option, as below. - It uses the system SQLite, which is compiled with `SQLITE_ENABLE_PREUPDATE_HOOK` support, but only on iOS 11.0+ (we don't know the minimum version of macOS, tvOS, watchOS): - ```ruby pod 'GRDB.swift' - platform :ios, '11.0' # or above post_install do |installer| installer.pods_project.targets.select { |target| target.name == "GRDB.swift" }.each do |target| diff --git a/GRDB/Documentation.docc/Extension/ValueObservation.md b/GRDB/Documentation.docc/Extension/ValueObservation.md index 2fbf4f9e1a..7358a2ac2d 100644 --- a/GRDB/Documentation.docc/Extension/ValueObservation.md +++ b/GRDB/Documentation.docc/Extension/ValueObservation.md @@ -78,7 +78,7 @@ See below for the li By default, `ValueObservation` notifies a fresh value whenever any component of its fetched value is modified (any fetched column, row, etc.). This can be configured: see . -By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main dispatch queue, asynchronously. This can be configured: see . +By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main actor, asynchronously. This can be configured: see . By default, `ValueObservation` fetches a fresh value immediately after a change is committed in the database. In particular, modifying the database on the main thread triggers a fetch on the main thread as well. This behavior can be configured: see . @@ -98,40 +98,38 @@ The database observation stops when the cancellable returned by the `start` meth ## ValueObservation Scheduling -By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main dispatch queue, asynchronously: +By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main actor, asynchronously: ```swift // The default scheduling let cancellable = observation.start(in: dbQueue) { error in - // Called asynchronously on the main dispatch queue + // Called asynchronously on the main actor } onChange: { value in - // Called asynchronously on the main dispatch queue + // Called asynchronously on the main actor print("Fresh value", value) } ``` You can change this behavior by adding a `scheduling` argument to the `start()` method. -For example, the ``ValueObservationScheduler/immediate`` scheduler notifies all values on the main dispatch queue, and notifies the first one immediately when the observation starts. +For example, the ``ValueObservationMainActorScheduler/immediate`` scheduler notifies all values on the main actor, and notifies the first one immediately when the observation starts. It is very useful in graphic applications, because you can configure views right away, without waiting for the initial value to be fetched eventually. You don't have to implement any empty or loading screen, or to prevent some undesired initial animation. Take care that the user interface is not responsive during the fetch of the first value, so only use the `immediate` scheduling for very fast database requests! -The `immediate` scheduling requires that the observation starts from the main dispatch queue (a fatal error is raised otherwise): - ```swift // Immediate scheduling notifies // the initial value right on subscription. let cancellable = observation .start(in: dbQueue, scheduling: .immediate) { error in - // Called on the main dispatch queue + // Called on the main actor } onChange: { value in - // Called on the main dispatch queue + // Called on the main actor print("Fresh value", value) } // <- Here "Fresh value" has already been printed. ``` -The other built-in scheduler ``ValueObservationScheduler/async(onQueue:)`` asynchronously schedules values and errors on the dispatch queue of your choice. Make sure you provide a serial queue, because a concurrent one such as `DispachQueue.global(qos: .default)` would mess with the ordering of fresh value notifications: +The ``ValueObservationScheduler/async(onQueue:)`` scheduler asynchronously schedules values and errors on the dispatch queue of your choice. Make sure you provide a serial dispatch queue, because a concurrent one such as `DispachQueue.global(qos: .default)` would mess with the ordering of fresh value notifications: ```swift // Async scheduling notifies all values @@ -146,9 +144,33 @@ let cancellable = observation } ``` +The ``ValueObservationScheduler/task`` scheduler asynchronously schedules values and errors on the cooperative thread pool. It is implicitly used when you turn a ValueObservation into an async sequence. You can specify it explicitly when you intend to consume a shared observation as an async sequence: + +```swift +do { + for try await players in observation.values(in: dbQueue) { + // Called on the cooperative thread pool + print("Fresh players", players) + } +} catch { + // Handle error +} + +let sharedObservation = observation.shared(in: dbQueue, scheduling: .task) +do { + for try await players in sharedObservation.values() { + // Called on the cooperative thread pool + print("Fresh players", players) + } +} catch { + // Handle error +} + +``` + As described above, the `scheduling` argument controls the execution of the change and error callbacks. You also have some control on the execution of the database fetch: -- With the `.immediate` scheduling, the initial fetch is always performed synchronously, on the main thread, when the observation starts, so that the initial value can be notified immediately. +- With the `.immediate` scheduling, the initial fetch is always performed synchronously, on the main actor, when the observation starts, so that the initial value can be notified immediately. - With the default `.async` scheduling, the initial fetch is always performed asynchronouly. It never blocks the main thread. @@ -289,11 +311,13 @@ When needed, you can help GRDB optimize observations and reduce database content ### Accessing Observed Values +- ``start(in:scheduling:onError:onChange:)-t62r`` +- ``start(in:scheduling:onError:onChange:)-4mqbs`` - ``publisher(in:scheduling:)`` -- ``start(in:scheduling:onError:onChange:)`` - ``values(in:scheduling:bufferingPolicy:)`` - ``DatabaseCancellable`` - ``ValueObservationScheduler`` +- ``ValueObservationMainActorScheduler`` ### Mapping Values @@ -313,6 +337,6 @@ When needed, you can help GRDB optimize observations and reduce database content - ``handleEvents(willStart:willFetch:willTrackRegion:databaseDidChange:didReceiveValue:didFail:didCancel:)`` - ``print(_:to:)`` -### Support +### Supporting Types - ``ValueReducer`` diff --git a/GRDB/Documentation.docc/JSON.md b/GRDB/Documentation.docc/JSON.md index 34fdba1257..ad4acad302 100644 --- a/GRDB/Documentation.docc/JSON.md +++ b/GRDB/Documentation.docc/JSON.md @@ -92,7 +92,9 @@ struct Team: Codable { extension Team: FetchableRecord, PersistableRecord { // Support SQLite JSON functions and operators // by storing JSON data as database text: - static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { + .text + } } ``` @@ -129,7 +131,7 @@ The `->` and `->>` SQL operators are available on the ``SQLJSONExpressible`` pro ### Build new JSON values at the SQL level - ``Database/json(_:)`` -- ``Database/jsonArray(_:)-8xxe3`` +- ``Database/jsonArray(_:)-8p2p8`` - ``Database/jsonArray(_:)-469db`` - ``Database/jsonObject(_:)`` - ``Database/jsonQuote(_:)`` diff --git a/GRDB/Documentation.docc/QueryInterface.md b/GRDB/Documentation.docc/QueryInterface.md index f09b20f473..e169324418 100644 --- a/GRDB/Documentation.docc/QueryInterface.md +++ b/GRDB/Documentation.docc/QueryInterface.md @@ -8,9 +8,8 @@ For an overview, see [Records](https://github.com/groue/GRDB.swift/blob/master/R ## Topics -### Records +### Records Protocols -- ``Record`` - ``EncodableRecord`` - ``FetchableRecord`` - ``MutablePersistableRecord`` @@ -48,3 +47,7 @@ For an overview, see [Records](https://github.com/groue/GRDB.swift/blob/master/R - ``SQLSubqueryable`` - ``SQLOrderingTerm`` - ``SQLSelectable`` + +### Legacy Types + +- ``Record`` diff --git a/GRDB/Documentation.docc/SwiftConcurrency.md b/GRDB/Documentation.docc/SwiftConcurrency.md new file mode 100644 index 0000000000..d81e145b99 --- /dev/null +++ b/GRDB/Documentation.docc/SwiftConcurrency.md @@ -0,0 +1,256 @@ +# Swift Concurrency and GRDB + +How to best integrate GRDB and Swift Concurrency + +## Overview + +GRDB’s primary goal is to leverage SQLite’s concurrency features for the benefit of application developers. Swift 6 makes it possible to achieve this goal while ensuring data-race safety. + +For example, the ``DatabasePool`` connection allows applications to fetch and display database values on screen, even while a background task is writing the results of a network request to disk. + +Application previews and tests prefer to use an in-memory ``DatabaseQueue`` connection. + +Both connection types provide the same database access methods: + +```swift +// Read +let playerCount = try await writer.read { db in + try Player.fetchCount(db) +} + +// Write +let newPlayerCount = try await writer.write { db in + try Player(name: "Arthur").insert(db) + return try Player.fetchCount(db) +} + +// Observe database changes +let observation = ValueObservation.tracking { db in + try Player.fetchAll(db) +} +for try await players in observation.values(in: writer) { + print("Fresh players", players) +} +``` + +`DatabaseQueue` serializes all database accesses, when `DatabasePool` allows parallel reads and writes. The common ``DatabaseWriter`` protocol provides the [SQLite isolation guarantees](https://www.sqlite.org/isolation.html) that abstract away the differences between the two connection types, without sacrificing data integrity. See the guide for more information. + +All safety guarantees of Swift 6 are enforced during database accesses. They are controlled by the language mode and level of concurrency checkings used by your application, as described in [Migrating to Swift 6] on swift.org. + +The following sections describe, with more details, how GRDB interacts with Swift Concurrency. + +- +- +- +- + +### Non-Sendable Record Types + +In the Swift 6 language mode, and in the Swift 5 language mode with strict concurrency checkings, the compiler emits an error or a warning when the application reads, writes, or observes a non-[`Sendable`](https://developer.apple.com/documentation/swift/sendable) type. + +By default, Swift classes are not Sendable. They are not thread-safe. With GRDB, record classes will typically trigger compiler diagnostics: + +```swift +// A non-Sendable record type +final class Player: Codable, Identifiable { + var id: Int64 + var name: String + var score: Int +} + +extension Player: FetchableRecord, PersistableRecord { } + +// ❌ Type 'Player' does not conform to the 'Sendable' protocol +let player = try await writer.read { db in + try Player.fetchOne(db, id: 42) +} + +// ❌ Capture of 'player' with non-sendable type 'Player' in a `@Sendable` closure +let player: Player +try await writer.read { db in + try player.insert(db) +} + +// ❌ Type 'Player' does not conform to the 'Sendable' protocol +let observation = ValueObservation.tracking { db in + try Player.fetchAll(db) +} +``` + +#### The solution + +The solution is to have the record type conform to `Sendable`. + +Since classes are difficult to make `Sendable`, the easiest way to is to replace classes with structs composed of `Sendable` properties: + +```swift +// This struct is Sendable +struct Player: Codable, Identifiable { + var id: Int64 + var name: String + var score: Int +} + +extension Player: FetchableRecord, PersistableRecord { } +``` + +You do not need to perform this refactoring right away: you can compile your application in the Swift 5 language mode, with minimal concurrency checkings. Take your time, and only when your application is ready, enable strict concurrency checkings or the Swift 6 language mode. + +#### FAQ: My application defines record classes, because… + +- **Question: My record types are subclasses of the built-in GRDB `Record` class.** + + Consider refactoring them as structs. The ``Record`` class was present in GRDB 1.0, in 2017. It has served its purpose. It is not `Sendable`, and its use is actively discouraged since GRDB 7. + +- **Question: I need a hierarchy of record classes because I use inheritance.** + + It should be possible to refactor the class hiearchy with Swift protocols. See for a practical example. Protocols make it possible to define records as structs. + +- **Question: I use the `@Observable` macro for my record types, and this macro requires a class.** + + A possible solution is to define two types: an `@Observable` class that drives your SwiftUI views, and a plain record struct for database work. An indirect advantage is that you will be able to make them evolve independently. + +- **Question: I use classes instead of structs because I monitored my application and classes have a lower CPU/memory footprint.** + + Now that's tricky. Please do not think the `Sendable` requirement is a whim: see the following questions. + +#### FAQ: How to make classes Sendable? + +- **Question: Can I mark my record classes as `@unchecked Sendable`?** + + Take care that all humans and machines who will read your code will think that the class is thread-safe, so make sure it really is. See the following questions. + +- **Question: I can use locks to make my class safely Sendable.** + + You can indeed put a lock on the whole instance, or on each individual property, or on multiple subgroups of properties, as needed by your application. Remember that structs are simpler, because they do not need locks and the compiler does all the hard work for you. + +- **Question: Can I make my record classes immutable?** + + Yes. Classes that can not be modified, made of constant `let` properties, are Sendable. Those immutable classes will not make it easy to modify the database, though. + +#### FAQ: Why this Sendable requirement? + +**GRDB needs new features in the Swift language and the SDKs in order to deal with non-Sendable types.** + +[SE-0430: `sending` parameter and result values](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0430-transferring-parameters-and-results.md) looks like the language feature we need, but: + +- `DispatchQueue.async` does not accept a `sending` closure. GRDB needs this in order to accept non-Sendable records to be sent to the database, as below: + + ```swift + let nonSendableRecord: Player + try await writer.write { db in + try nonSendableRecord.insert(db) + } + ``` + + Please [file a feedback](http://feedbackassistant.apple.com) for requesting this DispatchQueue improvement. The more the merrier. I personally filed FB15270949. + +- Database access methods taint the values they fetch. In the code below, the rules of [SE-0414: Region based Isolation](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md) have the compiler refuse that the fetched player is sent back to the caller: + + ```swift + let player = try await writer.read { db in + try Player.fetchOne(db, id: 42) + } + ``` + + Strictly speaking, the compiler diagnostic is correct: one could copy the non-Sendable `db` argument into the fetched `Player` instance, making it unsuitable for later use. In practice, nobody does that. Copying `db` is a programmer error, and GRDB promptly raises a fatal error whenever a `db` copy would be improperly used. But there is no way to tell the compiler about this practice. + +For all those reasons, GRDB has to require values that are asynchronously written and read from the database to be `Sendable`. + +### Shorthand Closure Notation + +In the Swift 5 language mode, the compiler emits a warning when a database access is written with the shorthand closure notation: + +```swift +// Standard closure: +let count = try await writer.read { db in + try Player.fetchCount(db) +} + +// Shorthand notation: +// ⚠️ Converting non-sendable function value to '@Sendable (Database) +// throws -> Int' may introduce data races. +let count = try await writer.read(Player.fetchCount) +``` + +**You can remove this warning** by enabling [SE-0418: Inferring `Sendable` for methods and key path literals](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0418-inferring-sendable-for-methods.md), as below: + +- **Using Xcode** + + Set `SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES` to `YES` in the build settings of your target. + +- **In a SwiftPM package manifest** + + Enable the `InferSendableFromCaptures` upcoming feature: + + ```swift + .target( + name: "MyTarget", + swiftSettings: [ + .enableUpcomingFeature("InferSendableFromCaptures") + ] + ) + ``` + +This language feature is not enabled by default, because it can potentially [affect source compatibility](https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/sourcecompatibility#Inferring-Sendable-for-methods-and-key-path-literals). + +### Non-Sendable Configuration of Record Types + +In the Swift 6 language mode, and in the Swift 5 language mode with strict concurrency checkings, the compiler emits an error or a warning when a record type specifies which columns it fetches from the database, with the ``TableRecord/databaseSelection-7iphs`` static property: + +```swift +extension Player: FetchableRecord, MutablePersistableRecord { + // ❌ Static property 'databaseSelection' is not concurrency-safe + // because non-'Sendable' type '[any SQLSelectable]' + // may have shared mutable state + static let databaseSelection: [any SQLSelectable] = [ + Column("id"), Column("name"), Column("score") + ] +} +``` + +**To fix this error**, replace the stored property with a computed property: + +```swift +extension Player: FetchableRecord, MutablePersistableRecord { + static var databaseSelection: [any SQLSelectable] { + [Column("id"), Column("name"), Column("score")] + } +} +``` + +### Choosing between Synchronous and Asynchronous Database Accesses + +GRDB connections provide two versions of `read` and `write`, one that is synchronous, and one that is asynchronous. It might not be clear how to choose one or the other. + +```swift +// Synchronous database access +try writer.write { ... } + +// Asynchronous database access +await try writer.write { ... } +``` + +Synchronous database accesses are handy. They avoid undesired delays, flashes of missing content in the user interface, or `async` functions. Many apps access the database synchronously, even from the main thread, because SQLite is very fast. Of course, it is still possible to run slow queries: in this case, asynchronous accesses should be preferred. They are guaranteed to never block the main thread. + +Performing synchronous accesses from Swift Concurrency tasks is not incorrect. + +Some people recommend to avoid performing long blocking jobs on the cooperative thread pool, so you might want to follow this advice, and prefer to always `await` for the database in Swift tasks. In many occasions, the compiler will help you. For example, in the sample code below, the compiler requires the `await` keyword: + +```swift +func fetchPlayers() async throws -> [Player] { + try await writer.read(Player.fetchAll) +} +``` + +But there are some scenarios where the compiler misses opportunities to use `await`, such as inside closures ([swiftlang/swift#74459](https://github.com/swiftlang/swift/issues/74459)): + +```swift +Task { + // The compiler does not spot the missing `await` + let players = try writer.read(Player.fetchAll) +} +``` + +[demo apps]: https://github.com/groue/GRDB.swift/tree/master/Documentation/DemoApps +[Migrating to Swift 6]: https://www.swift.org/migration/documentation/migrationguide/ diff --git a/GRDB/Documentation.docc/Transactions.md b/GRDB/Documentation.docc/Transactions.md index 56dedce47b..469ffd9538 100644 --- a/GRDB/Documentation.docc/Transactions.md +++ b/GRDB/Documentation.docc/Transactions.md @@ -142,16 +142,21 @@ try dbQueue.writeWithoutTransaction { db } ``` -Transactions can't be left opened unless the ``Configuration/allowsUnsafeTransactions`` configuration flag is set: +Make sure all transactions opened from a database access are committed or rollbacked from that same database access, because it is a programmer error to leave an opened transaction: ```swift -// fatal error: A transaction has been left opened at the end of a database access +// fatal error: A transaction has been left +// opened at the end of a database access. try dbQueue.writeWithoutTransaction { db in try db.execute(sql: "BEGIN TRANSACTION") // <- no commit or rollback } ``` +In particular, since commits may throw an error, make sure you perform a rollback when a commit fails. + +This restriction can be left with the ``Configuration/allowsUnsafeTransactions`` configuration flag. + It is possible to ask if a transaction is currently opened: ```swift @@ -214,6 +219,8 @@ SQLite savepoints are more than nested transactions, though. For advanced uses, SQLite supports [three kinds of transactions](https://www.sqlite.org/lang_transaction.html): deferred (the default), immediate, and exclusive. +By default, GRDB opens DEFERRED transaction for reads, and IMMEDIATE transactions for writes. + The transaction kind can be chosen for individual transaction: ```swift @@ -222,20 +229,3 @@ let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite") // BEGIN EXCLUSIVE TRANSACTION ... try dbQueue.inTransaction(.exclusive) { db in ... } ``` - -It is also possible to configure the ``Configuration/defaultTransactionKind``: - -```swift -var config = Configuration() -config.defaultTransactionKind = .immediate - -let dbQueue = try DatabaseQueue( - path: "/path/to/database.sqlite", - configuration: config) - -// BEGIN IMMEDIATE TRANSACTION ... -try dbQueue.write { db in ... } - -// BEGIN IMMEDIATE TRANSACTION ... -try dbQueue.inTransaction { db in ... } -``` diff --git a/GRDB/Dump/Database+Dump.swift b/GRDB/Dump/Database+Dump.swift index d4abfb653a..bb5aed2641 100644 --- a/GRDB/Dump/Database+Dump.swift +++ b/GRDB/Dump/Database+Dump.swift @@ -45,7 +45,7 @@ extension Database { /// ``` /// /// - Parameters: - /// - request : The executed request. + /// - request: The executed request. /// - format: The output format. /// - stream: A stream for text output, which directs output to the /// console by default. diff --git a/GRDB/Dump/DatabaseReader+dump.swift b/GRDB/Dump/DatabaseReader+dump.swift index 55e829c89d..cbe3e922de 100644 --- a/GRDB/Dump/DatabaseReader+dump.swift +++ b/GRDB/Dump/DatabaseReader+dump.swift @@ -38,7 +38,7 @@ extension DatabaseReader { /// ``` /// /// - Parameters: - /// - request : The executed request. + /// - request: The executed request. /// - format: The output format. /// - stream: A stream for text output, which directs output to the /// console by default. diff --git a/GRDB/Dump/DumpFormats/DebugDumpFormat.swift b/GRDB/Dump/DumpFormats/DebugDumpFormat.swift index 02f2f11304..b3ae312485 100644 --- a/GRDB/Dump/DumpFormats/DebugDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/DebugDumpFormat.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A format that prints one line per database row, suitable diff --git a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift index 7e3bc61d5b..d5bc8cb10b 100644 --- a/GRDB/Dump/DumpFormats/JSONDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/JSONDumpFormat.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A format that prints database rows as a JSON array. @@ -43,9 +52,7 @@ public struct JSONDumpFormat: Sendable { public static var defaultEncoder: JSONEncoder { // This encoder MUST NOT CHANGE, because some people rely on this format. let encoder = JSONEncoder() - if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) { - encoder.outputFormatting = .withoutEscapingSlashes - } + encoder.outputFormatting = .withoutEscapingSlashes encoder.nonConformingFloatEncodingStrategy = .convertToString( positiveInfinity: "inf", negativeInfinity: "-inf", @@ -159,10 +166,7 @@ extension JSONDumpFormat: DumpFormat { private func formattedValue(_ value: some Encodable) throws -> String { let data = try encoder.encode(value) - guard let string = String(data: data, encoding: .utf8) else { - throw EncodingError.invalidValue(data, .init(codingPath: [], debugDescription: "Invalid JSON data")) - } - return string + return String(decoding: data, as: UTF8.self) } } diff --git a/GRDB/Dump/DumpFormats/LineDumpFormat.swift b/GRDB/Dump/DumpFormats/LineDumpFormat.swift index a0c9f9a1fd..a71b05ca69 100644 --- a/GRDB/Dump/DumpFormats/LineDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/LineDumpFormat.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A format that prints one line per database value. All blob values diff --git a/GRDB/Dump/DumpFormats/ListDumpFormat.swift b/GRDB/Dump/DumpFormats/ListDumpFormat.swift index c256af4c82..dc7bfdb6c3 100644 --- a/GRDB/Dump/DumpFormats/ListDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/ListDumpFormat.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A format that prints one line per database row. All blob values diff --git a/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift b/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift index 3c6ae5c17a..3818c7c17e 100644 --- a/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift +++ b/GRDB/Dump/DumpFormats/QuoteDumpFormat.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A format that prints one line per database row, formatting values /// as SQL literals. /// diff --git a/GRDB/Export.swift b/GRDB/Export.swift deleted file mode 100644 index f5aaed5728..0000000000 --- a/GRDB/Export.swift +++ /dev/null @@ -1,8 +0,0 @@ -// Export the underlying SQLite library -#if SWIFT_PACKAGE -@_exported import CSQLite -#elseif GRDBCIPHER -@_exported import SQLCipher -#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER -@_exported import SQLite3 -#endif diff --git a/GRDB/FTS/FTS3.swift b/GRDB/FTS/FTS3.swift index 80e677ae18..26f0643d11 100644 --- a/GRDB/FTS/FTS3.swift +++ b/GRDB/FTS/FTS3.swift @@ -47,7 +47,7 @@ public struct FTS3 { #elseif !GRDBCIPHER /// Remove diacritics from Latin script characters. This option matches /// the `remove_diacritics=2` tokenizer argument. - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.27+ + @available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.27+ case remove #endif } diff --git a/GRDB/FTS/FTS5.swift b/GRDB/FTS/FTS5.swift index a18da5a3f0..a87b184b1d 100644 --- a/GRDB/FTS/FTS5.swift +++ b/GRDB/FTS/FTS5.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_FTS5 +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// The virtual table module for the FTS5 full-text engine. @@ -60,7 +69,7 @@ public struct FTS5 { /// Remove diacritics from Latin script characters. This /// option matches the raw "remove_diacritics=2" tokenizer argument, /// available from SQLite 3.27.0 - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.27+ + @available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.27+ case remove #endif } @@ -108,46 +117,6 @@ public struct FTS5 { /// /// Related SQLite documentation: public static func api(_ db: Database) -> UnsafePointer { - // Access to FTS5 is one of the rare SQLite api which was broken in - // SQLite 3.20.0+, for security reasons: - // - // Starting SQLite 3.20.0+, we need to use the new sqlite3_bind_pointer api. - // The previous way to access FTS5 does not work any longer. - // - // So let's see which SQLite version we are linked against: - - #if GRDBCUSTOMSQLITE || GRDBCIPHER - // GRDB is linked against SQLCipher or a custom SQLite build: SQLite 3.20.0 or more. - return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer) - #else - // GRDB is linked against the system SQLite. - if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) { // SQLite 3.20+ - return api_v2(db, sqlite3_prepare_v3, sqlite3_bind_pointer) - } else { - return api_v1(db) - } - #endif - } - - private static func api_v1(_ db: Database) -> UnsafePointer { - guard let data = try! Data.fetchOne(db, sql: "SELECT fts5()") else { - fatalError("FTS5 is not available") - } - return data.withUnsafeBytes { - $0.bindMemory(to: UnsafePointer.self).first! - } - } - - // Technique given by Jordan Rose: - // https://forums.swift.org/t/c-interoperability-combinations-of-library-and-os-versions/14029/4 - private static func api_v2( - _ db: Database, - // swiftlint:disable:next line_length - _ sqlite3_prepare_v3: @convention(c) (OpaquePointer?, UnsafePointer?, CInt, CUnsignedInt, UnsafeMutablePointer?, UnsafeMutablePointer?>?) -> CInt, - // swiftlint:disable:next line_length - _ sqlite3_bind_pointer: @convention(c) (OpaquePointer?, CInt, UnsafeMutableRawPointer?, UnsafePointer?, (@convention(c) (UnsafeMutableRawPointer?) -> Void)?) -> CInt) - -> UnsafePointer - { var statement: SQLiteStatement? = nil var api: UnsafePointer? = nil let type: StaticString = "fts5_api_ptr" diff --git a/GRDB/FTS/FTS5CustomTokenizer.swift b/GRDB/FTS/FTS5CustomTokenizer.swift index a294c35b3e..370f1256c5 100644 --- a/GRDB/FTS/FTS5CustomTokenizer.swift +++ b/GRDB/FTS/FTS5CustomTokenizer.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_FTS5 +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + /// A type that implements a custom tokenizer for the ``FTS5`` full-text engine. /// /// See [FTS5 Tokenizers](https://github.com/groue/GRDB.swift/blob/master/Documentation/FTS5Tokenizers.md) diff --git a/GRDB/FTS/FTS5Tokenizer.swift b/GRDB/FTS/FTS5Tokenizer.swift index 8609a5ad72..e4aecc36f5 100644 --- a/GRDB/FTS/FTS5Tokenizer.swift +++ b/GRDB/FTS/FTS5Tokenizer.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_FTS5 +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// A low-level SQLite function that lets FTS5Tokenizer notify tokens. diff --git a/GRDB/FTS/FTS5WrapperTokenizer.swift b/GRDB/FTS/FTS5WrapperTokenizer.swift index 16ac1e1f96..5851866be7 100644 --- a/GRDB/FTS/FTS5WrapperTokenizer.swift +++ b/GRDB/FTS/FTS5WrapperTokenizer.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_FTS5 +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation /// Flags that tell SQLite how to register a token. diff --git a/GRDB/Fixits.swift b/GRDB/Fixits.swift index ed82d514de..1092b1164c 100644 --- a/GRDB/Fixits.swift +++ b/GRDB/Fixits.swift @@ -1,171 +1,17 @@ -// Fixits for changes introduced by GRDB 6.0.0 -// swiftlint:disable all +// Fixits for changes introduced by GRDB 7.0.0 -extension AggregatingRequest { - @available(*, unavailable, renamed: "groupWhenConnected(_:)") - func group(_ expressions: @escaping (Database) throws -> [any SQLExpressible]) -> Self { preconditionFailure() } - - @available(*, unavailable, renamed: "havingWhenConnected(_:)") - func having(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self { preconditionFailure() } -} - -extension Association { - @available(*, unavailable, message: "limit(_:offset:) was not working properly, and was removed.") - public func limit(_ limit: Int, offset: Int? = nil) -> Self { preconditionFailure() } -} - -extension Database { - @available(*, unavailable, renamed: "cachedStatement(sql:)") - public func cachedSelectStatement(sql: String) throws -> Statement { preconditionFailure() } - - @available(*, unavailable, renamed: "cachedStatement(literal:)") - public func cachedSelectStatement(literal sqlLiteral: SQL) throws -> Statement { preconditionFailure() } - - @available(*, unavailable, renamed: "cachedStatement(sql:)") - public func cachedUpdateStatement(sql: String) throws -> Statement { preconditionFailure() } - - @available(*, unavailable, renamed: "cachedStatement(sql:)") - public func cachedUpdateStatement(literal sqlLiteral: SQL) throws -> Statement { preconditionFailure() } - - @available(*, unavailable, message: "Use Database.isSQLiteInternalTable(_:) static method instead.") - public func isSQLiteInternalTable(_ tableName: String) -> Bool { preconditionFailure() } - - @available(*, unavailable, message: "Use Database.isGRDBInternalTable(_:) static method instead.") - public func isGRDBInternalTable(_ tableName: String) -> Bool { preconditionFailure() } - - @available(*, unavailable, renamed: "makeStatement(sql:)") - public func makeSelectStatement(sql: String) throws -> Statement { preconditionFailure() } - - @available(*, unavailable, renamed: "makeStatement(literal:)") - public func makeSelectStatement(literal sqlLiteral: SQL) throws -> Statement { preconditionFailure() } - - @available(*, unavailable, renamed: "makeStatement(sql:)") - public func makeUpdateStatement(sql: String) throws -> Statement { preconditionFailure() } - - @available(*, unavailable, renamed: "makeStatement(literal:)") - public func makeUpdateStatement(literal sqlLiteral: SQL) throws -> Statement { preconditionFailure() } - - @available(*, unavailable, renamed: "afterNextTransaction(onCommit:)") - public func afterNextTransactionCommit(_ closure: @escaping (Database) -> Void) { preconditionFailure() } -} - -extension DatabaseCursor { - @available(*, unavailable, message: "statement has been removed. You may use other cursor properties instead.") - public var statement: Statement { preconditionFailure() } -} - -extension DatabaseMigrator { - @available(*, unavailable, message: "The completion function now accepts one Result argument") - public func asyncMigrate( - _ writer: any DatabaseWriter, - completion: @escaping (Database, Error?) -> Void) - { preconditionFailure() } -} - -extension DatabaseRegionObservation { - @available(*, unavailable, message: "The extent of the observation is now controlled by the cancellable returned by DatabaseRegionObservation.start().") - public var extent: Database.TransactionObservationExtent { +extension Configuration { + @available(*, unavailable, message: "The default transaction kind is now automatically managed.") + public var defaultTransactionKind: Database.TransactionKind { get { preconditionFailure() } set { preconditionFailure() } } } -extension DatabaseUUIDEncodingStrategy { - @available(*, unavailable, renamed: "uppercaseString") - public static var string: Self { preconditionFailure() } -} - -@available(*, unavailable, message: "FastNullableDatabaseValueCursor has been replaced with FastDatabaseValueCursor") -typealias FastNullableDatabaseValueCursor = FastDatabaseValueCursor - -extension FilteredRequest { - @available(*, unavailable, renamed: "filterWhenConnected(with:)") - func filter(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self { preconditionFailure() } -} +@available(*, unavailable, message: "DatabaseFuture has been removed.") +public class DatabaseFuture { } -extension MutablePersistableRecord { - @available(*, unavailable, message: "Use persistence callbacks instead.") - public mutating func performInsert(_ db: Database) throws { preconditionFailure() } - - @available(*, unavailable, message: "Use persistence callbacks instead.") - public func performUpdate(_ db: Database, columns: Set) throws { preconditionFailure() } - - @available(*, unavailable, message: "Use persistence callbacks instead.") - public mutating func performSave(_ db: Database) throws { preconditionFailure() } - - @available(*, unavailable, message: "Use persistence callbacks instead.") - public func performDelete(_ db: Database) throws -> Bool { preconditionFailure() } - - @available(*, unavailable, message: "performExists(_:) was removed without any replacement.") - public func performExists(_ db: Database) throws -> Bool { preconditionFailure() } - - @available(*, unavailable, renamed: "updateChanges(_:modify:)") - public mutating func updateChanges(_ db: Database, with change: (inout Self) throws -> Void) throws -> Bool { preconditionFailure() } +extension DatabasePool { + @available(*, unavailable, message: "concurrentRead has been removed. Use `asyncConcurrentRead` instead.") + public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { preconditionFailure() } } - -@available(*, unavailable, message: "NullableDatabaseValueCursor has been replaced with DatabaseValueCursor") -typealias NullableDatabaseValueCursor = DatabaseValueCursor - -extension OrderedRequest { - @available(*, unavailable, renamed: "orderWhenConnected(_:)") - func order(_ orderings: @escaping (Database) throws -> [any SQLOrderingTerm]) -> Self { preconditionFailure() } -} - -extension PersistableRecord { - @available(*, unavailable, message: "Use persistence callbacks instead.") - public func performInsert(_ db: Database) throws { preconditionFailure() } - - @available(*, unavailable, message: "Use persistence callbacks instead.") - public func performSave(_ db: Database) throws { preconditionFailure() } -} - -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) -extension QueryInterfaceRequest where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { - @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") - public func selectID() -> QueryInterfaceRequest { preconditionFailure() } -} - -extension Record { - @available(*, unavailable, message: "Record.copy() was removed without any replacement.") - final func copy() -> Self { preconditionFailure() } -} - -@available(*, unavailable, renamed: "Statement") -public typealias SelectStatement = Statement - -extension SelectionRequest { - @available(*, unavailable, renamed: "annotatedWhenConnected(with:)") - func annotated(with selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self { preconditionFailure() } - - @available(*, unavailable, renamed: "selectWhenConnected(_:)") - func select(_ selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self { preconditionFailure() } -} - -@available(*, unavailable, renamed: "SQLExpression.AssociativeBinaryOperator") -public typealias SQLAssociativeBinaryOperator = SQLExpression.AssociativeBinaryOperator - -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) -extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { - @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") - public func selectID() -> QueryInterfaceRequest { preconditionFailure() } -} - -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) -extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { - @available(*, unavailable, message: "selectID() has been removed. You may use selectPrimaryKey(as:) instead.") - public static func selectID() -> QueryInterfaceRequest { preconditionFailure() } -} - -@available(*, unavailable, renamed: "Statement") -public typealias UpdateStatement = Statement - -extension ValueObservation { - @available(*, unavailable, renamed: "tracking(_:)") - public static func trackingVaryingRegion( - _ fetch: @escaping (Database) throws -> Value) - -> Self - where Reducer == ValueReducers.Fetch - { preconditionFailure() } -} - -// swiftlint:enable all diff --git a/GRDB/JSON/SQLJSONExpressible.swift b/GRDB/JSON/SQLJSONExpressible.swift index 991a5dd7d5..e2c402283e 100644 --- a/GRDB/JSON/SQLJSONExpressible.swift +++ b/GRDB/JSON/SQLJSONExpressible.swift @@ -8,7 +8,7 @@ /// the SQL level. /// /// - When used in a JSON-building function such as -/// ``Database/jsonArray(_:)-8xxe3`` or ``Database/jsonObject(_:)``, +/// ``Database/jsonArray(_:)-8p2p8`` or ``Database/jsonObject(_:)``, /// they are parsed and interpreted as JSON, not as plain strings. /// /// To build a JSON value, create a ``JSONColumn``, or call the @@ -66,7 +66,7 @@ /// ## Build JSON objects and arrays from JSON values /// /// When used in a JSON-building function such as -/// ``Database/jsonArray(_:)-8xxe3`` or ``Database/jsonObject(_:)-5iswr``, +/// ``Database/jsonArray(_:)-8p2p8`` or ``Database/jsonObject(_:)-5iswr``, /// JSON values are parsed and interpreted as JSON, not as plain strings. /// /// In the example below, we can see how the `JSONColumn` is interpreted as @@ -224,9 +224,9 @@ extension SQLJSONExpressible { /// Related SQL documentation: /// /// - parameter paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public func jsonExtract(atPaths paths: C) -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public func jsonExtract( + atPaths paths: some Collection + ) -> SQLExpression { Database.jsonExtract(self, atPaths: paths) } @@ -312,7 +312,7 @@ extension SQLJSONExpressible { /// Related SQL documentation: /// /// - parameter path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public func jsonExtract(atPath path: some SQLExpressible) -> SQLExpression { Database.jsonExtract(self, atPath: path) } @@ -334,10 +334,10 @@ extension SQLJSONExpressible { /// Related SQL documentation: /// /// - parameter paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public func jsonExtract(atPaths paths: C) -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + public func jsonExtract( + atPaths paths: some Collection + ) -> SQLExpression { Database.jsonExtract(self, atPaths: paths) } @@ -385,7 +385,7 @@ extension SQLJSONExpressible { // /// ``` // /// // /// Related SQLite documentation: -// @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS +// @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS // public func jsonPatch( // with patch: some SQLExpressible) // -> ColumnAssignment @@ -408,7 +408,7 @@ extension SQLJSONExpressible { // /// // /// - Parameters: // /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). -// @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS +// @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS // public func jsonRemove(atPath path: some SQLExpressible) -> ColumnAssignment { // .init(columnName: name, value: Database.jsonRemove(self, atPath: path)) // } @@ -428,11 +428,10 @@ extension SQLJSONExpressible { // /// // /// - Parameters: // /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). -// @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS -// public func jsonRemove(atPaths paths: C) -// -> ColumnAssignment -// where C: Collection, C.Element: SQLExpressible -// { +// @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS +// public func jsonRemove( +// atPaths paths: some Collection +// ) -> ColumnAssignment { // .init(columnName: name, value: Database.jsonRemove(self, atPaths: paths)) // } // diff --git a/GRDB/JSON/SQLJSONFunctions.swift b/GRDB/JSON/SQLJSONFunctions.swift index 85122f2895..7b3d3d16ef 100644 --- a/GRDB/JSON/SQLJSONFunctions.swift +++ b/GRDB/JSON/SQLJSONFunctions.swift @@ -24,9 +24,9 @@ extension Database { /// ``` /// /// Related SQLite documentation: - public static func jsonArray(_ values: C) -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public static func jsonArray( + _ values: some Collection + ) -> SQLExpression { .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) } @@ -40,9 +40,9 @@ extension Database { /// ``` /// /// Related SQLite documentation: - public static func jsonArray(_ values: C) -> SQLExpression - where C: Collection, C.Element == any SQLExpressible - { + public static func jsonArray( + _ values: some Collection + ) -> SQLExpression { .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) } @@ -130,10 +130,10 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public static func jsonExtract(_ value: some SQLExpressible, atPaths paths: C) - -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public static func jsonExtract( + _ value: some SQLExpressible, + atPaths paths: some Collection + ) -> SQLExpression { .function("JSON_EXTRACT", [value.sqlExpression] + paths.map(\.sqlExpression)) } @@ -152,13 +152,10 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public static func jsonInsert( + public static func jsonInsert( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_INSERT", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -179,13 +176,10 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public static func jsonReplace( + public static func jsonReplace( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_REPLACE", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -206,13 +200,10 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public static func jsonSet( + public static func jsonSet( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_SET", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -241,11 +232,9 @@ extension Database { /// ``` /// /// Related SQLite documentation: - public static func jsonObject(_ elements: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + public static func jsonObject( + _ elements: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_OBJECT", elements.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -301,10 +290,10 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - public static func jsonRemove(_ value: some SQLExpressible, atPaths paths: C) - -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + public static func jsonRemove( + _ value: some SQLExpressible, + atPaths paths: some Collection + ) -> SQLExpression { .function("JSON_REMOVE", [value.sqlExpression] + paths.map(\.sqlExpression)) } @@ -442,7 +431,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func json(_ value: some SQLExpressible) -> SQLExpression { .function("JSON", [value.sqlExpression]) } @@ -457,10 +446,10 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonArray(_ values: C) -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + public static func jsonArray( + _ values: some Collection + ) -> SQLExpression { .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) } @@ -474,10 +463,10 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonArray(_ values: C) -> SQLExpression - where C: Collection, C.Element == any SQLExpressible - { + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + public static func jsonArray( + _ values: some Collection + ) -> SQLExpression { .function("JSON_ARRAY", values.map(\.sqlExpression.jsonBuilderExpression)) } @@ -492,7 +481,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonArrayLength(_ value: some SQLExpressible) -> SQLExpression { .function("JSON_ARRAY_LENGTH", [value.sqlExpression]) } @@ -512,7 +501,7 @@ extension Database { /// - Parameters: /// - value: A JSON array. /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonArrayLength( _ value: some SQLExpressible, atPath path: some SQLExpressible) @@ -535,7 +524,7 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonExtract(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { .function("JSON_EXTRACT", [value.sqlExpression, path.sqlExpression]) } @@ -554,11 +543,11 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonExtract(_ value: some SQLExpressible, atPaths paths: C) - -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + public static func jsonExtract( + _ value: some SQLExpressible, + atPaths paths: some Collection + ) -> SQLExpression { .function("JSON_EXTRACT", [value.sqlExpression] + paths.map(\.sqlExpression)) } @@ -577,14 +566,11 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonInsert( + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + public static func jsonInsert( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_INSERT", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -605,14 +591,11 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonReplace( + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + public static func jsonReplace( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_REPLACE", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -633,14 +616,11 @@ extension Database { /// - value: A JSON value. /// - assignments: A collection of key/value pairs, where keys are /// [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonSet( + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + public static func jsonSet( _ value: some SQLExpressible, - _ assignments: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + _ assignments: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_SET", [value.sqlExpression] + assignments.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -669,12 +649,10 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonObject(_ elements: C) - -> SQLExpression - where C: Collection, - C.Element == (key: String, value: any SQLExpressible) - { + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + public static func jsonObject( + _ elements: some Collection<(key: String, value: any SQLExpressible)> + ) -> SQLExpression { .function("JSON_OBJECT", elements.flatMap { [$0.key.sqlExpression, $0.value.sqlExpression.jsonBuilderExpression] }) @@ -690,7 +668,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonPatch( _ value: some SQLExpressible, with patch: some SQLExpressible) @@ -712,8 +690,8 @@ extension Database { /// /// - Parameters: /// - value: A JSON value. - /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonRemove(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { .function("JSON_REMOVE", [value.sqlExpression, path.sqlExpression]) } @@ -732,11 +710,11 @@ extension Database { /// - Parameters: /// - value: A JSON value. /// - paths: A collection of [JSON paths](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS - public static func jsonRemove(_ value: some SQLExpressible, atPaths paths: C) - -> SQLExpression - where C: Collection, C.Element: SQLExpressible - { + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + public static func jsonRemove( + _ value: some SQLExpressible, + atPaths paths: some Collection + ) -> SQLExpression { .function("JSON_REMOVE", [value.sqlExpression] + paths.map(\.sqlExpression)) } @@ -750,7 +728,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonType(_ value: some SQLExpressible) -> SQLExpression { .function("JSON_TYPE", [value.sqlExpression]) } @@ -768,8 +746,8 @@ extension Database { /// /// - Parameters: /// - value: A JSON value. - /// - paths: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + /// - path: A [JSON path](https://www.sqlite.org/json1.html#path_arguments). + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonType(_ value: some SQLExpressible, atPath path: some SQLExpressible) -> SQLExpression { .function("JSON_TYPE", [value.sqlExpression, path.sqlExpression]) } @@ -784,7 +762,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonIsValid(_ value: some SQLExpressible) -> SQLExpression { .function("JSON_VALID", [value.sqlExpression]) } @@ -802,7 +780,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonQuote(_ value: some SQLExpressible) -> SQLExpression { .function("JSON_QUOTE", [value.sqlExpression.jsonBuilderExpression]) } @@ -820,7 +798,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonGroupArray( _ value: some SQLExpressible, filter: (any SQLSpecificExpressible)? = nil) @@ -850,7 +828,7 @@ extension Database { /// ``` /// /// Related SQLite documentation: - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS public static func jsonGroupObject( key: some SQLExpressible, value: some SQLExpressible, diff --git a/GRDB/Migration/DatabaseMigrator.swift b/GRDB/Migration/DatabaseMigrator.swift index f4de96c69e..739945a270 100644 --- a/GRDB/Migration/DatabaseMigrator.swift +++ b/GRDB/Migration/DatabaseMigrator.swift @@ -40,7 +40,7 @@ import Foundation /// - ``completedMigrations(_:)`` /// - ``hasBeenSuperseded(_:)`` /// - ``hasCompletedMigrations(_:)`` -public struct DatabaseMigrator { +public struct DatabaseMigrator: Sendable { /// Controls how a migration handle foreign keys constraints. public enum ForeignKeyChecks: Sendable { /// The migration runs with disabled foreign keys. @@ -207,7 +207,7 @@ public struct DatabaseMigrator { public mutating func registerMigration( _ identifier: String, foreignKeyChecks: ForeignKeyChecks = .deferred, - migrate: @escaping (Database) throws -> Void) + migrate: @escaping @Sendable (Database) throws -> Void) { let migrationChecks: Migration.ForeignKeyChecks switch foreignKeyChecks { @@ -230,7 +230,7 @@ public struct DatabaseMigrator { /// /// - parameter writer: A DatabaseWriter. /// - throws: The error thrown by the first failed migration. - public func migrate(_ writer: some DatabaseWriter) throws { + public func migrate(_ writer: any DatabaseWriter) throws { guard let lastMigration = _migrations.last else { return } @@ -249,7 +249,7 @@ public struct DatabaseMigrator { /// - parameter writer: A DatabaseWriter. /// - parameter targetIdentifier: A migration identifier. /// - throws: The error thrown by the first failed migration. - public func migrate(_ writer: some DatabaseWriter, upTo targetIdentifier: String) throws { + public func migrate(_ writer: any DatabaseWriter, upTo targetIdentifier: String) throws { try writer.barrierWriteWithoutTransaction { db in try migrate(db, upTo: targetIdentifier) } @@ -263,8 +263,8 @@ public struct DatabaseMigrator { /// database, or the failure that prevented the migrations /// from succeeding. public func asyncMigrate( - _ writer: some DatabaseWriter, - completion: @escaping (Result) -> Void) + _ writer: any DatabaseWriter, + completion: @escaping @Sendable (Result) -> Void) { writer.asyncBarrierWriteWithoutTransaction { dbResult in do { @@ -496,10 +496,9 @@ extension DatabaseMigrator { /// - parameter writer: A DatabaseWriter. /// where migrations should apply. /// - parameter scheduler: A Combine Scheduler. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func migratePublisher( - _ writer: some DatabaseWriter, - receiveOn scheduler: some Scheduler = DispatchQueue.main) + _ writer: any DatabaseWriter, + receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main) -> DatabasePublishers.Migrate { DatabasePublishers.Migrate( @@ -514,7 +513,6 @@ extension DatabaseMigrator { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension DatabasePublishers { /// A publisher that migrates a database. /// diff --git a/GRDB/Migration/Migration.swift b/GRDB/Migration/Migration.swift index 36676d017f..5a100bdec7 100644 --- a/GRDB/Migration/Migration.swift +++ b/GRDB/Migration/Migration.swift @@ -1,5 +1,5 @@ /// An internal struct that defines a migration. -struct Migration { +struct Migration: Sendable { enum ForeignKeyChecks { case deferred case immediate @@ -8,7 +8,7 @@ struct Migration { let identifier: String var foreignKeyChecks: ForeignKeyChecks - let migrate: (Database) throws -> Void + let migrate: @Sendable (Database) throws -> Void func run(_ db: Database) throws { if try Bool.fetchOne(db, sql: "PRAGMA foreign_keys") ?? false { diff --git a/GRDB/QueryInterface/Request/Association/Association.swift b/GRDB/QueryInterface/Request/Association/Association.swift index debe9590a3..7e6d7d0ac9 100644 --- a/GRDB/QueryInterface/Request/Association/Association.swift +++ b/GRDB/QueryInterface/Request/Association/Association.swift @@ -33,7 +33,7 @@ import Foundation /// /// - ``ForeignKey`` /// - ``Inflections`` -public protocol Association: DerivableRequest { +public protocol Association: DerivableRequest, Sendable { // OriginRowDecoder and RowDecoder inherited from DerivableRequest provide // type safety: // @@ -173,7 +173,9 @@ extension Association { // SelectionRequest conformance extension Association { - public func selectWhenConnected(_ selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self { + public func selectWhenConnected( + _ selection: @escaping @Sendable (Database) throws -> [any SQLSelectable] + ) -> Self { withDestinationRelation { relation in relation = relation.selectWhenConnected { db in try selection(db).map(\.sqlSelection) @@ -181,7 +183,9 @@ extension Association { } } - public func annotatedWhenConnected(with selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self { + public func annotatedWhenConnected( + with selection: @escaping @Sendable (Database) throws -> [any SQLSelectable] + ) -> Self { withDestinationRelation { relation in relation = relation.annotatedWhenConnected { db in try selection(db).map(\.sqlSelection) @@ -192,7 +196,9 @@ extension Association { // FilteredRequest conformance extension Association { - public func filterWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self { + public func filterWhenConnected( + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible + ) -> Self { withDestinationRelation { relation in relation = relation.filterWhenConnected { db in try predicate(db).sqlExpression @@ -203,7 +209,9 @@ extension Association { // OrderedRequest conformance extension Association { - public func orderWhenConnected(_ orderings: @escaping (Database) throws -> [any SQLOrderingTerm]) -> Self { + public func orderWhenConnected( + _ orderings: @escaping @Sendable (Database) throws -> [any SQLOrderingTerm] + ) -> Self { withDestinationRelation { relation in relation = relation.orderWhenConnected { db in try orderings(db).map(\.sqlOrdering) @@ -239,7 +247,9 @@ extension Association { // AggregatingRequest conformance extension Association { - public func groupWhenConnected(_ expressions: @escaping (Database) throws -> [any SQLExpressible]) -> Self { + public func groupWhenConnected( + _ expressions: @escaping @Sendable (Database) throws -> [any SQLExpressible] + ) -> Self { withDestinationRelation { relation in relation = relation.groupWhenConnected { db in try expressions(db).map(\.sqlExpression) @@ -247,7 +257,9 @@ extension Association { } } - public func havingWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self { + public func havingWhenConnected( + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible + ) -> Self { withDestinationRelation { relation in relation = relation.havingWhenConnected { db in try predicate(db).sqlExpression diff --git a/GRDB/QueryInterface/Request/CommonTableExpression.swift b/GRDB/QueryInterface/Request/CommonTableExpression.swift index 17d192fb2a..9cbd4d2bb6 100644 --- a/GRDB/QueryInterface/Request/CommonTableExpression.swift +++ b/GRDB/QueryInterface/Request/CommonTableExpression.swift @@ -380,12 +380,12 @@ extension CommonTableExpression { /// /// - parameter cte: A common table expression. /// - parameter condition: A function that returns the joining clause. - /// - parameter left: A `TableAlias` for the left table. - /// - parameter right: A `TableAlias` for the right table. + /// First argument is a ``TableAlias`` for the left table, second + /// argument an alias for the right table. /// - returns: An association to the common table expression. public func association( to cte: CommonTableExpression, - on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) + on condition: @escaping @Sendable (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation { JoinAssociation( @@ -416,12 +416,12 @@ extension CommonTableExpression { /// - parameter destination: The record type at the other side of /// the association. /// - parameter condition: A function that returns the joining clause. - /// - parameter left: A `TableAlias` for the left table. - /// - parameter right: A `TableAlias` for the right table. + /// First argument is a ``TableAlias`` for the left table, second + /// argument an alias for the right table. /// - returns: An association to the common table expression. public func association( to destination: Destination.Type, - on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) + on condition: @escaping @Sendable (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation where Destination: TableRecord { @@ -453,12 +453,12 @@ extension CommonTableExpression { /// /// - parameter destination: The table at the other side of the association. /// - parameter condition: A function that returns the joining clause. - /// - parameter left: A `TableAlias` for the left table. - /// - parameter right: A `TableAlias` for the right table. + /// First argument is a ``TableAlias`` for the left table, second + /// argument an alias for the right table. /// - returns: An association to the common table expression. public func association( to destination: Table, - on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) + on condition: @escaping @Sendable (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation { JoinAssociation( diff --git a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift index 1d0d7cdadd..56451b4384 100644 --- a/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/Request/QueryInterfaceRequest.swift @@ -118,7 +118,7 @@ extension QueryInterfaceRequest: FetchRequest { extension QueryInterfaceRequest: SelectionRequest { public func selectWhenConnected( - _ selection: @escaping (Database) throws -> [any SQLSelectable]) + _ selection: @escaping @Sendable (Database) throws -> [any SQLSelectable]) -> Self { with { @@ -282,7 +282,7 @@ extension QueryInterfaceRequest: SelectionRequest { } public func annotatedWhenConnected( - with selection: @escaping (Database) throws -> [any SQLSelectable]) + with selection: @escaping @Sendable (Database) throws -> [any SQLSelectable]) -> Self { with { @@ -295,7 +295,7 @@ extension QueryInterfaceRequest: SelectionRequest { extension QueryInterfaceRequest: FilteredRequest { public func filterWhenConnected( - _ predicate: @escaping (Database) throws -> any SQLExpressible) + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible) -> Self { with { @@ -308,7 +308,7 @@ extension QueryInterfaceRequest: FilteredRequest { extension QueryInterfaceRequest: OrderedRequest { public func orderWhenConnected( - _ orderings: @escaping (Database) throws -> [any SQLOrderingTerm]) + _ orderings: @escaping @Sendable (Database) throws -> [any SQLOrderingTerm]) -> Self { with { @@ -355,7 +355,7 @@ extension QueryInterfaceRequest: OrderedRequest { extension QueryInterfaceRequest: AggregatingRequest { public func groupWhenConnected( - _ expressions: @escaping (Database) throws -> [any SQLExpressible]) + _ expressions: @escaping @Sendable (Database) throws -> [any SQLExpressible]) -> Self { with { @@ -366,7 +366,7 @@ extension QueryInterfaceRequest: AggregatingRequest { } public func havingWhenConnected( - _ predicate: @escaping (Database) throws -> any SQLExpressible) + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible) -> Self { with { @@ -646,7 +646,6 @@ extension QueryInterfaceRequest { /// - parameter db: A database connection. /// - returns: A set of deleted ids. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) // Identifiable public func deleteAndFetchIds(_ db: Database) throws -> Set where RowDecoder: TableRecord & Identifiable, diff --git a/GRDB/QueryInterface/Request/RequestProtocols.swift b/GRDB/QueryInterface/Request/RequestProtocols.swift index def5c2f52c..b725b8b4bc 100644 --- a/GRDB/QueryInterface/Request/RequestProtocols.swift +++ b/GRDB/QueryInterface/Request/RequestProtocols.swift @@ -58,7 +58,9 @@ public protocol SelectionRequest { /// /// - parameter selection: A closure that accepts a database connection and /// returns an array of result columns. - func selectWhenConnected(_ selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self + func selectWhenConnected( + _ selection: @escaping @Sendable (Database) throws -> [any SQLSelectable] + ) -> Self /// Appends result columns to the selected columns. /// @@ -78,7 +80,9 @@ public protocol SelectionRequest { /// /// - parameter selection: A closure that accepts a database connection and /// returns an array of result columns. - func annotatedWhenConnected(with selection: @escaping (Database) throws -> [any SQLSelectable]) -> Self + func annotatedWhenConnected( + with selection: @escaping @Sendable (Database) throws -> [any SQLSelectable] + ) -> Self } extension SelectionRequest { @@ -100,7 +104,8 @@ extension SelectionRequest { /// .select([Column("score")]) /// ``` public func select(_ selection: [any SQLSelectable]) -> Self { - selectWhenConnected { _ in selection } + let selection = selection.map(\.sqlSelection) + return selectWhenConnected { _ in selection } } /// Defines the result columns. @@ -186,7 +191,8 @@ extension SelectionRequest { /// let request = Player.all().annotated(with: [totalScore]) /// ``` public func annotated(with selection: [any SQLSelectable]) -> Self { - annotatedWhenConnected(with: { _ in selection }) + let selection = selection.map(\.sqlSelection) + return annotatedWhenConnected(with: { _ in selection }) } /// Appends result columns to the selected columns. @@ -240,7 +246,9 @@ public protocol FilteredRequest { /// /// - parameter predicate: A closure that accepts a database connection and /// returns a boolean SQL expression. - func filterWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self + func filterWhenConnected( + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible + ) -> Self } extension FilteredRequest { @@ -257,7 +265,8 @@ extension FilteredRequest { /// let request = Player.all().filter(Column("name") == name) /// ``` public func filter(_ predicate: some SQLSpecificExpressible) -> Self { - filterWhenConnected { _ in predicate } + let predicate = predicate.sqlExpression + return filterWhenConnected { _ in predicate } } /// Filters the fetched rows with an SQL string. @@ -322,7 +331,7 @@ extension FilteredRequest { /// - ``filter(ids:)`` /// - ``filter(key:)-1p9sq`` /// - ``filter(key:)-2te6v`` -/// - ``filter(keys:)-6ggt1`` +/// - ``filter(keys:)-9p9i5`` /// - ``filter(keys:)-8fbn9`` /// - ``matching(_:)-3s3zr`` /// - ``matching(_:)-7c1e8`` @@ -431,9 +440,8 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { /// ``` /// /// - parameter keys: A collection of primary keys - public func filter(keys: Sequence) - -> Self - where Sequence.Element: DatabaseValueConvertible + public func filter(keys: Keys) -> Self + where Keys: Collection, Keys.Element: DatabaseValueConvertible { // In order to encode keys in the database, we perform a runtime check // for EncodableRecord, and look for a customized encoding strategy. @@ -442,46 +450,81 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { // make it impractical to define `filter(id:)`, `fetchOne(_:key:)`, // `deleteAll(_:ids:)` etc. if let recordType = RowDecoder.self as? any EncodableRecord.Type { - if Sequence.Element.self == Data.self || Sequence.Element.self == Optional.self { - let strategy = recordType.databaseDataEncodingStrategy - let keys = keys.compactMap { ($0 as! Data?).flatMap(strategy.encode)?.databaseValue } - return filter(rawKeys: keys) - } else if Sequence.Element.self == Date.self || Sequence.Element.self == Optional.self { - let strategy = recordType.databaseDateEncodingStrategy - let keys = keys.compactMap { ($0 as! Date?).flatMap(strategy.encode)?.databaseValue } - return filter(rawKeys: keys) - } else if Sequence.Element.self == UUID.self || Sequence.Element.self == Optional.self { - let strategy = recordType.databaseUUIDEncodingStrategy - let keys = keys.map { ($0 as! UUID?).map(strategy.encode)?.databaseValue } - return filter(rawKeys: keys) + if Keys.Element.self == Data.self || Keys.Element.self == Optional.self { + let datas = keys.compactMap { ($0 as! Data?) } + if datas.isEmpty { + // Don't hit the database + return none() + } + + return filterWhenConnected(keys: { [databaseTableName] db in + let primaryKey = try db.primaryKey(databaseTableName) + GRDBPrecondition( + primaryKey.columns.count == 1, + "Requesting by key requires a single-column primary key in the table \(databaseTableName)") + let column = primaryKey.columns[0] + let strategy = recordType.databaseDataEncodingStrategy(for: column) + let expressions = try datas.map { try strategy.encode($0).sqlExpression } + return expressions + }) + } else if Keys.Element.self == Date.self || Keys.Element.self == Optional.self { + let dates = keys.compactMap { ($0 as! Date?) } + if dates.isEmpty { + // Don't hit the database + return none() + } + + return filterWhenConnected(keys: { [databaseTableName] db in + let primaryKey = try db.primaryKey(databaseTableName) + GRDBPrecondition( + primaryKey.columns.count == 1, + "Requesting by key requires a single-column primary key in the table \(databaseTableName)") + let column = primaryKey.columns[0] + let strategy = recordType.databaseDateEncodingStrategy(for: column) + let expressions = dates.map { strategy.encode($0).sqlExpression } + return expressions + }) + } else if Keys.Element.self == UUID.self || Keys.Element.self == Optional.self { + let uuids = keys.compactMap { ($0 as! UUID?) } + if uuids.isEmpty { + // Don't hit the database + return none() + } + + return filterWhenConnected(keys: { [databaseTableName] db in + let primaryKey = try db.primaryKey(databaseTableName) + GRDBPrecondition( + primaryKey.columns.count == 1, + "Requesting by key requires a single-column primary key in the table \(databaseTableName)") + let column = primaryKey.columns[0] + let strategy = recordType.databaseUUIDEncodingStrategy(for: column) + let expressions = uuids.map { strategy.encode($0).sqlExpression } + return expressions + }) } } - return filter(rawKeys: keys) + let expressions = keys.map { $0.sqlExpression } + if expressions.isEmpty { + // Don't hit the database + return none() + } + return filterWhenConnected(keys: { _ in expressions }) } /// Creates a request filtered by primary key. /// /// // SELECT * FROM player WHERE ... id IN (1, 2, 3) - /// let request = try Player...filter(rawKeys: [1, 2, 3]) + /// let request = try Player...filterWhenConnected(keys: { db in [1, 2, 3] }) /// /// - parameter keys: A collection of primary keys - func filter(rawKeys: Keys) -> Self - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { - // Don't bother removing NULLs. We'd lose CPU cycles, and this does not - // change the SQLite results anyway. - let expressions = rawKeys.map { - $0.databaseValue.sqlExpression - } - - if expressions.isEmpty { - // Don't hit the database - return none() - } - + fileprivate func filterWhenConnected(keys: @escaping @Sendable (Database) throws -> [SQLExpression]) -> Self { let databaseTableName = self.databaseTableName return filterWhenConnected { db in + // Don't bother removing NULLs. We'd lose CPU cycles, and this does not + // change the SQLite results anyway. + let expressions = try keys(db) + let primaryKey = try db.primaryKey(databaseTableName) GRDBPrecondition( primaryKey.columns.count == 1, @@ -545,6 +588,11 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { return none() } + // Turn key values into sendable DatabaseValue + let keys = keys.map { key in + key.mapValues { $0?.databaseValue ?? .null } + } + let databaseTableName = self.databaseTableName return filterWhenConnected { db in try keys @@ -583,7 +631,6 @@ extension TableRequest where Self: FilteredRequest, Self: TypedRequest { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension TableRequest where Self: FilteredRequest, Self: TypedRequest, @@ -620,9 +667,7 @@ where Self: FilteredRequest, /// ``` /// /// - parameter ids: A collection of primary keys - public func filter(ids: IDS) -> Self - where IDS: Collection, IDS.Element == RowDecoder.ID - { + public func filter(ids: some Collection) -> Self { filter(keys: ids) } } @@ -729,7 +774,9 @@ public protocol AggregatingRequest { /// /// - parameter expressions: A closure that accepts a database connection /// and returns an array of SQL expressions. - func groupWhenConnected(_ expressions: @escaping (Database) throws -> [any SQLExpressible]) -> Self + func groupWhenConnected( + _ expressions: @escaping @Sendable (Database) throws -> [any SQLExpressible] + ) -> Self /// Filters the aggregated groups with a boolean SQL expression. /// @@ -753,7 +800,9 @@ public protocol AggregatingRequest { /// /// - parameter predicate: A closure that accepts a database connection and /// returns a boolean SQL expression. - func havingWhenConnected(_ predicate: @escaping (Database) throws -> any SQLExpressible) -> Self + func havingWhenConnected( + _ predicate: @escaping @Sendable (Database) throws -> any SQLExpressible + ) -> Self } extension AggregatingRequest { @@ -774,7 +823,8 @@ extension AggregatingRequest { /// /// - parameter expressions: An array of SQL expressions. public func group(_ expressions: [any SQLExpressible]) -> Self { - groupWhenConnected { _ in expressions } + let expressions = expressions.map(\.sqlExpression) + return groupWhenConnected { _ in expressions } } /// Returns an aggregate request grouped on the given SQL expressions. @@ -849,7 +899,8 @@ extension AggregatingRequest { /// .having(max(Column("score")) > 1000) /// ``` public func having(_ predicate: some SQLExpressible) -> Self { - havingWhenConnected { _ in predicate } + let predicate = predicate.sqlExpression + return havingWhenConnected { _ in predicate } } /// Filters the aggregated groups with an SQL string. @@ -934,7 +985,9 @@ public protocol OrderedRequest { /// /// - parameter orderings: A closure that accepts a database connection and /// returns an array of SQL ordering terms. - func orderWhenConnected(_ orderings: @escaping (Database) throws -> [any SQLOrderingTerm]) -> Self + func orderWhenConnected( + _ orderings: @escaping @Sendable (Database) throws -> [any SQLOrderingTerm] + ) -> Self /// Returns a request with reversed ordering. /// @@ -996,7 +1049,8 @@ extension OrderedRequest { /// .order(Column("name")) /// ``` public func order(_ orderings: any SQLOrderingTerm...) -> Self { - orderWhenConnected { _ in orderings } + let orderings = orderings.map(\.sqlOrdering) + return orderWhenConnected { _ in orderings } } /// Sorts the fetched rows according to the given SQL ordering terms. @@ -1018,7 +1072,8 @@ extension OrderedRequest { /// .order([Column("name")]) /// ``` public func order(_ orderings: [any SQLOrderingTerm]) -> Self { - orderWhenConnected { _ in orderings } + let orderings = orderings.map(\.sqlOrdering) + return orderWhenConnected { _ in orderings } } /// Sorts the fetched rows according to the given SQL string. @@ -1355,7 +1410,7 @@ extension JoinableRequest where Self: SelectionRequest { /// - ``TableRequest/filter(ids:)`` /// - ``TableRequest/filter(key:)-1p9sq`` /// - ``TableRequest/filter(key:)-2te6v`` -/// - ``TableRequest/filter(keys:)-6ggt1`` +/// - ``TableRequest/filter(keys:)-9p9i5`` /// - ``TableRequest/filter(keys:)-8fbn9`` /// - ``FilteredRequest/filter(literal:)`` /// - ``FilteredRequest/filter(sql:arguments:)`` diff --git a/GRDB/QueryInterface/SQL/DatabasePromise.swift b/GRDB/QueryInterface/SQL/DatabasePromise.swift index 9d497b5846..7fb7c3edeb 100644 --- a/GRDB/QueryInterface/SQL/DatabasePromise.swift +++ b/GRDB/QueryInterface/SQL/DatabasePromise.swift @@ -23,20 +23,20 @@ /// see SQLRelation.filterPromise. struct DatabasePromise { /// Returns the resolved value. - let resolve: (Database) throws -> T + let resolve: @Sendable (Database) throws -> T /// Creates a promise that resolves to a value. - init(value: T) { + init(value: T) where T: Sendable { self.resolve = { _ in value } } /// Creates a promise from a closure. - init(_ resolve: @escaping (Database) throws -> T) { + init(_ resolve: @escaping @Sendable (Database) throws -> T) { self.resolve = resolve } /// Returns a promise whose value is transformed by the given closure. - func map(_ transform: @escaping (T) throws -> U) -> DatabasePromise { + func map(_ transform: @escaping @Sendable (T) throws -> U) -> DatabasePromise { DatabasePromise { db in try transform(resolve(db)) } diff --git a/GRDB/QueryInterface/SQL/SQLAssociation.swift b/GRDB/QueryInterface/SQL/SQLAssociation.swift index 4d9a8d30c7..ea31e2f8db 100644 --- a/GRDB/QueryInterface/SQL/SQLAssociation.swift +++ b/GRDB/QueryInterface/SQL/SQLAssociation.swift @@ -49,7 +49,7 @@ /// through: Pivot1.hasMany(Pivot2.self), /// via: Pivot2.belongsTo(Destination.self))) /// Origin.including(required: association) -public struct _SQLAssociation { +public struct _SQLAssociation: Sendable { // All steps, from pivot to destination. Never empty. private(set) var steps: [SQLAssociationStep] var keyPath: [String] { steps.map(\.keyName) } diff --git a/GRDB/QueryInterface/SQL/SQLExpression.swift b/GRDB/QueryInterface/SQL/SQLExpression.swift index eb63e59c40..58acaf7c41 100644 --- a/GRDB/QueryInterface/SQL/SQLExpression.swift +++ b/GRDB/QueryInterface/SQL/SQLExpression.swift @@ -34,7 +34,7 @@ /// ``` /// /// Related SQLite documentation: -public struct SQLExpression { +public struct SQLExpression: Sendable { private var impl: Impl /// The preferred interpretation of the expression in JSON @@ -1989,7 +1989,7 @@ struct SQLAggregateFunctionInvocation { var arguments: [SQLExpression] var isDistinct = false var ordering: SQLOrdering? = nil // SQLite 3.44.0+ - var filter: SQLExpression? = nil // @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) SQLite 3.30+ + var filter: SQLExpression? = nil // @available(iOS 14, macOS 10.16, tvOS 14, *) SQLite 3.30+ /// A boolean value indicating if a function is known to return a /// JSON value. @@ -2106,7 +2106,7 @@ extension SQLExpression { } } #else - @available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS + @available(iOS 16, tvOS 17, watchOS 9, *) // SQLite 3.38+ with exceptions for macOS /// Returns an expression suitable in JSON building contexts. var jsonBuilderExpression: SQLExpression { switch preferredJSONInterpretation { @@ -2329,13 +2329,13 @@ extension SQLSpecificExpressible { } #elseif !GRDBCIPHER /// An ordering term for ascending order (nulls last). - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ + @available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public var ascNullsLast: SQLOrdering { .ascNullsLast(sqlExpression) } /// An ordering term for descending order (nulls first). - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ + @available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public var descNullsFirst: SQLOrdering { .descNullsFirst(sqlExpression) } diff --git a/GRDB/QueryInterface/SQL/SQLFunctions.swift b/GRDB/QueryInterface/SQL/SQLFunctions.swift index 08a1a39ea8..fab42b8e42 100644 --- a/GRDB/QueryInterface/SQL/SQLFunctions.swift +++ b/GRDB/QueryInterface/SQL/SQLFunctions.swift @@ -34,7 +34,7 @@ public func average( /// // AVG(length) FILTER (WHERE length > 0) /// average(Column("length"), filter: Column("length") > 0) /// ``` -@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +@available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public func average( _ value: some SQLSpecificExpressible, filter: some SQLSpecificExpressible) @@ -145,7 +145,7 @@ public func max( /// // MAX(score) FILTER (WHERE score < 0) /// max(Column("score"), filter: Column("score") < 0) /// ``` -@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +@available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public func max( _ value: some SQLSpecificExpressible, filter: some SQLSpecificExpressible) @@ -190,7 +190,7 @@ public func min( /// // MIN(score) FILTER (WHERE score > 0) /// min(Column("score"), filter: Column("score") > 0) /// ``` -@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +@available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public func min( _ value: some SQLSpecificExpressible, filter: some SQLSpecificExpressible) @@ -248,7 +248,7 @@ public func sum( /// See also ``total(_:)``. /// /// Related SQLite documentation: . -@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +@available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public func sum( _ value: some SQLSpecificExpressible, filter: some SQLSpecificExpressible) @@ -312,7 +312,7 @@ public func total( /// See also ``total(_:)``. /// /// Related SQLite documentation: . -@available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // SQLite 3.30+ +@available(iOS 14, macOS 10.16, tvOS 14, *) // SQLite 3.30+ public func total( _ value: some SQLSpecificExpressible, filter: some SQLSpecificExpressible) diff --git a/GRDB/QueryInterface/SQL/SQLOrdering.swift b/GRDB/QueryInterface/SQL/SQLOrdering.swift index e7b18f9748..0badbcca25 100644 --- a/GRDB/QueryInterface/SQL/SQLOrdering.swift +++ b/GRDB/QueryInterface/SQL/SQLOrdering.swift @@ -12,7 +12,7 @@ /// function arguments, prefer the ``SQLOrderingTerm`` protocol. /// /// Related SQLite documentation: -public struct SQLOrdering { +public struct SQLOrdering: Sendable { private var impl: Impl private enum Impl { diff --git a/GRDB/QueryInterface/SQL/SQLRelation.swift b/GRDB/QueryInterface/SQL/SQLRelation.swift index 4488cc96f4..469c845bf9 100644 --- a/GRDB/QueryInterface/SQL/SQLRelation.swift +++ b/GRDB/QueryInterface/SQL/SQLRelation.swift @@ -90,7 +90,7 @@ /// // JOIN passport ON passport.citizenId = citizens.id /// // AND passport.countryCode IN ('BE', 'DE', 'FR', ...); /// Country.including(all: Country.citizens) -struct SQLRelation { +struct SQLRelation: Sendable { struct Child: Refinable { enum Kind { // Record.including(optional: association) @@ -187,7 +187,7 @@ extension SQLRelation { /// Convenience factory methods which selects all rows from a table. static func all( fromTable tableName: String, - selection: @escaping (Database) -> [SQLSelection] = { _ in [.allColumns] }) + selection: @escaping @Sendable (Database) -> [SQLSelection] = { _ in [.allColumns] }) -> Self { SQLRelation( @@ -197,7 +197,7 @@ extension SQLRelation { } extension SQLRelation: Refinable { - func selectWhenConnected(_ selection: @escaping (Database) throws -> [SQLSelection]) -> Self { + func selectWhenConnected(_ selection: @escaping @Sendable (Database) throws -> [SQLSelection]) -> Self { with { $0.selectionPromise = DatabasePromise(selection) } @@ -228,7 +228,7 @@ extension SQLRelation: Refinable { } } - func annotatedWhenConnected(with selection: @escaping (Database) throws -> [SQLSelection]) -> Self { + func annotatedWhenConnected(with selection: @escaping @Sendable (Database) throws -> [SQLSelection]) -> Self { with { let old = $0.selectionPromise $0.selectionPromise = DatabasePromise { db in @@ -242,7 +242,7 @@ extension SQLRelation: Refinable { annotatedWhenConnected(with: { _ in selection }) } - func filterWhenConnected(_ predicate: @escaping (Database) throws -> SQLExpression) -> Self { + func filterWhenConnected(_ predicate: @escaping @Sendable (Database) throws -> SQLExpression) -> Self { with { if let old = $0.filterPromise { $0.filterPromise = DatabasePromise { db in @@ -259,7 +259,7 @@ extension SQLRelation: Refinable { filterWhenConnected { _ in predicate } } - func orderWhenConnected(_ orderings: @escaping (Database) throws -> [SQLOrdering]) -> Self { + func orderWhenConnected(_ orderings: @escaping @Sendable (Database) throws -> [SQLOrdering]) -> Self { with { $0.ordering = SQLRelation.Ordering(orderings: orderings) } @@ -313,13 +313,13 @@ extension SQLRelation: Refinable { } } - func groupWhenConnected(_ expressions: @escaping (Database) throws -> [SQLExpression]) -> Self { + func groupWhenConnected(_ expressions: @escaping @Sendable (Database) throws -> [SQLExpression]) -> Self { with { $0.groupPromise = DatabasePromise(expressions) } } - func havingWhenConnected(_ predicate: @escaping (Database) throws -> SQLExpression) -> Self { + func havingWhenConnected(_ predicate: @escaping @Sendable (Database) throws -> SQLExpression) -> Self { with { if let old = $0.havingExpressionPromise { $0.havingExpressionPromise = DatabasePromise { db in @@ -672,7 +672,7 @@ struct SQLLimit { // MARK: - SQLSource -struct SQLSource { +struct SQLSource: Sendable { var tableName: String var alias: TableAlias? @@ -691,7 +691,7 @@ struct SQLSource { extension SQLRelation { /// SQLRelation.Ordering provides the order clause to SQLRelation. - struct Ordering { + struct Ordering: Sendable { private enum Element { case terms(DatabasePromise<[SQLOrdering]>) case ordering(SQLRelation.Ordering) @@ -740,7 +740,7 @@ extension SQLRelation { isReversed: false) } - init(orderings: @escaping (Database) throws -> [SQLOrdering]) { + init(orderings: @escaping @Sendable (Database) throws -> [SQLOrdering]) { self.init( elements: [.terms(DatabasePromise(orderings))], isReversed: false) @@ -796,7 +796,7 @@ extension SQLRelation { /// // SELECT * FROM book WHERE author.id = 1 /// // ~~~~~~~~~~~~~ /// author.request(for: Author.books) -enum SQLAssociationCondition { +enum SQLAssociationCondition: Sendable { /// A condition based on a foreign key. case foreignKey(SQLForeignKeyCondition) @@ -814,7 +814,7 @@ enum SQLAssociationCondition { /// player[Column("id")] == bonus[Column("playerID")] /// }) /// Player.with(bonus).joining(required: association) - case expression((_ left: TableAlias, _ right: TableAlias) -> SQLExpression?) + case expression(@Sendable (_ left: TableAlias, _ right: TableAlias) -> SQLExpression?) /// The condition that does not constrain the two associated tables /// in any way. @@ -923,10 +923,9 @@ extension JoinMapping { /// - precondition: leftRows contains all mapping left columns. /// - precondition: All rows have the same layout: a column index returned /// by `index(forColumn:)` refers to the same column in all rows. - func joinExpression(leftRows: Rows) - -> SQLExpression - where Rows: Collection, Rows.Element: ColumnAddressable - { + func joinExpression( + leftRows: some Collection + ) -> SQLExpression { guard let firstLeftRow = leftRows.first else { // We could return `false.sqlExpression`. // @@ -1027,9 +1026,6 @@ extension Row: ColumnAddressable { /// PersistenceContainer has columns extension PersistenceContainer: ColumnAddressable { func index(forColumn column: String) -> String? { column } - func databaseValue(at column: String) -> DatabaseValue { - self[caseInsensitive: column]?.databaseValue ?? .null - } } // MARK: - Merging diff --git a/GRDB/QueryInterface/SQL/SQLSelection.swift b/GRDB/QueryInterface/SQL/SQLSelection.swift index c4ff88045b..a0f29120d5 100644 --- a/GRDB/QueryInterface/SQL/SQLSelection.swift +++ b/GRDB/QueryInterface/SQL/SQLSelection.swift @@ -15,7 +15,7 @@ /// function arguments, prefer the ``SQLSelectable`` protocol. /// /// Related SQLite documentation: -public struct SQLSelection { +public struct SQLSelection: Sendable { private var impl: Impl /// The private implementation of the public `SQLSelection`. diff --git a/GRDB/QueryInterface/SQL/SQLSubquery.swift b/GRDB/QueryInterface/SQL/SQLSubquery.swift index fb3d595849..fdc9c3a3ac 100644 --- a/GRDB/QueryInterface/SQL/SQLSubquery.swift +++ b/GRDB/QueryInterface/SQL/SQLSubquery.swift @@ -1,7 +1,7 @@ /// An SQL subquery. /// /// `SQLSubquery` is an opaque representation of an SQL subquery. -public struct SQLSubquery { +public struct SQLSubquery: Sendable { private var impl: Impl private enum Impl { diff --git a/GRDB/QueryInterface/SQL/Table.swift b/GRDB/QueryInterface/SQL/Table.swift index c6de851c74..a96714bc15 100644 --- a/GRDB/QueryInterface/SQL/Table.swift +++ b/GRDB/QueryInterface/SQL/Table.swift @@ -40,7 +40,7 @@ /// /// - ``deleteAll(_:)`` /// - ``deleteAll(_:ids:)`` -/// - ``deleteAll(_:keys:)-5t865`` +/// - ``deleteAll(_:keys:)-594uc`` /// - ``deleteAll(_:keys:)-28sff`` /// - ``deleteOne(_:id:)`` /// - ``deleteOne(_:key:)-404su`` @@ -69,7 +69,7 @@ /// - ``filter(ids:)`` /// - ``filter(key:)-tw3i`` /// - ``filter(key:)-4sun7`` -/// - ``filter(keys:)-85e0v`` +/// - ``filter(keys:)-5ws7f`` /// - ``filter(keys:)-qqgf`` /// - ``filter(literal:)`` /// - ``filter(sql:arguments:)`` @@ -507,10 +507,9 @@ extension Table { /// ``` /// /// - parameter keys: A collection of primary keys - public func filter(keys: Keys) - -> QueryInterfaceRequest - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { + public func filter( + keys: some Collection + ) -> QueryInterfaceRequest { all().filter(keys: keys) } @@ -723,7 +722,6 @@ extension Table { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible { /// Returns a request filtered by primary key. /// @@ -775,9 +773,9 @@ extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConv /// ``` /// /// - parameter ids: A collection of primary keys - public func filter(ids: IDS) -> QueryInterfaceRequest - where IDS: Collection, IDS.Element == RowDecoder.ID - { + public func filter( + ids: some Collection + ) -> QueryInterfaceRequest { all().filter(ids: ids) } } @@ -1292,12 +1290,12 @@ extension Table { /// /// - parameter cte: A common table expression. /// - parameter condition: A function that returns the joining clause. - /// - parameter left: A `TableAlias` for the left table. - /// - parameter right: A `TableAlias` for the right table. + /// First argument is a ``TableAlias`` for the left table, second + /// argument an alias for the right table. /// - returns: An association to the common table expression. public func association( to cte: CommonTableExpression, - on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) + on condition: @escaping @Sendable (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation { JoinAssociation( @@ -1546,7 +1544,6 @@ extension Table { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible @@ -1644,11 +1641,10 @@ extension Table { /// - keys: A sequence of primary keys. /// - returns: The number of deleted rows. @discardableResult - public func deleteAll(_ db: Database, keys: Keys) - throws -> Int - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { - let keys = Array(keys) + public func deleteAll( + _ db: Database, + keys: some Collection + ) throws -> Int { if keys.isEmpty { // Avoid hitting the database return 0 @@ -1688,7 +1684,6 @@ extension Table { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Table where RowDecoder: Identifiable, RowDecoder.ID: DatabaseValueConvertible @@ -1723,9 +1718,10 @@ where RowDecoder: Identifiable, /// - ids: A collection of primary keys. /// - returns: The number of deleted rows. @discardableResult - public func deleteAll(_ db: Database, ids: IDS) throws -> Int - where IDS: Collection, IDS.Element == RowDecoder.ID - { + public func deleteAll( + _ db: Database, + ids: some Collection + ) throws -> Int { if ids.isEmpty { // Avoid hitting the database return 0 diff --git a/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift b/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift index d364a6bbba..ed1470ecdd 100644 --- a/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift +++ b/GRDB/QueryInterface/SQLGeneration/SQLGenerationContext.swift @@ -172,11 +172,14 @@ class StatementArgumentsSink { private(set) var arguments: StatementArguments private let rawSQL: Bool + // This non-Sendable instance can be used from multiple threads + // concurrently, because it never modifies its `arguments` + // mutable state. /// A sink which turns all argument values into SQL literals. /// /// The `"WHERE name = \("O'Brien")"` SQL literal is turned into the /// `WHERE name = 'O''Brien'` SQL. - static let literalValues = StatementArgumentsSink(rawSQL: true) + nonisolated(unsafe) static let literalValues = StatementArgumentsSink(rawSQL: true) private init(rawSQL: Bool) { self.arguments = [] @@ -213,7 +216,9 @@ class StatementArgumentsSink { /// See ``TableRequest/aliased(_:)`` for more information and examples. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) -public class TableAlias { +public class TableAlias: @unchecked Sendable { + // This Sendable conformance is transient. TableAlias IS NOT really Sendable. + // TODO: GRDB7 Make TableAlias really Sendable private enum Impl { /// A TableAlias is undefined when it is created by the GRDB user: /// @@ -479,8 +484,6 @@ public class TableAlias { /// A boolean SQL expression indicating whether this alias refers to some /// rows, or not. /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// /// In the example below, we only fetch books that are not associated to /// any author: /// diff --git a/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift b/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift index 17551514bd..5b37ead626 100644 --- a/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift +++ b/GRDB/QueryInterface/SQLGeneration/SQLQueryGenerator.swift @@ -513,6 +513,9 @@ struct SQLQueryGenerator: Refinable { } } +// This type is marked as `Sendable` in order to avoid compiler warnings, +// but it should not need to be: instances are synchronously created, used, +// and discarded. /// To generate SQL, we need a "qualified" relation, where all tables, /// expressions, etc, are qualified with table aliases. /// @@ -546,8 +549,7 @@ struct SQLQueryGenerator: Refinable { /// HAVING ... -- havingExpressionPromise /// ORDER BY ... -- ordering /// LIMIT ... -- limit - -private struct SQLQualifiedRelation { +private struct SQLQualifiedRelation: Sendable { /// All aliases, including aliases of joined relations var allAliases: [TableAlias] { joins.reduce(into: [source.alias].compactMap { $0 }) { diff --git a/GRDB/QueryInterface/SQLInterpolation+QueryInterface.swift b/GRDB/QueryInterface/SQLInterpolation+QueryInterface.swift index eb161ab8bd..35a9912b12 100644 --- a/GRDB/QueryInterface/SQLInterpolation+QueryInterface.swift +++ b/GRDB/QueryInterface/SQLInterpolation+QueryInterface.swift @@ -227,9 +227,9 @@ extension SQLInterpolation { /// let request: SQLRequest = """ /// SELECT * FROM player WHERE id IN \(ids) /// """ - public mutating func appendInterpolation(_ sequence: S) - where S: Sequence, S.Element: SQLExpressible - { + public mutating func appendInterpolation( + _ sequence: some Sequence + ) { let e: [SQL.Element] = sequence.map { .expression($0.sqlExpression) } if e.isEmpty { appendLiteral("(SELECT NULL WHERE NULL)") @@ -255,9 +255,9 @@ extension SQLInterpolation { /// let request: SQLRequest = """ /// SELECT * FROM player WHERE a IN \(expressions) /// """ - public mutating func appendInterpolation(_ sequence: S) - where S: Sequence, S.Element == any SQLExpressible - { + public mutating func appendInterpolation( + _ sequence: some Sequence + ) { appendInterpolation(sequence.lazy.map(\.sqlExpression)) } diff --git a/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift b/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift index 10b95ebc3a..fb40efa738 100644 --- a/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift +++ b/GRDB/QueryInterface/Schema/Database+SchemaDefinition.swift @@ -356,7 +356,7 @@ extension Database { /// Related SQLite documentation: /// /// - parameters: - /// - view: The view name. + /// - name: The view name. /// - options: View creation options. /// - columns: The columns of the view. If nil, the columns are the /// columns of the request. @@ -410,7 +410,7 @@ extension Database { /// Related SQLite documentation: /// /// - parameters: - /// - view: The view name. + /// - name: The view name. /// - options: View creation options. /// - columns: The columns of the view. If nil, the columns are the /// columns of the request. diff --git a/GRDB/QueryInterface/Schema/TableAlteration.swift b/GRDB/QueryInterface/Schema/TableAlteration.swift index c731e7ea54..9b40a2d62b 100644 --- a/GRDB/QueryInterface/Schema/TableAlteration.swift +++ b/GRDB/QueryInterface/Schema/TableAlteration.swift @@ -129,7 +129,6 @@ public final class TableAlteration { /// /// - parameter name: the old name of the column. /// - parameter newName: the new name of the column. - @available(iOS 13, tvOS 13, watchOS 6, *) // SQLite 3.25+ public func rename(column name: String, to newName: String) { _rename(column: name, to: newName) } diff --git a/GRDB/QueryInterface/Schema/TableDefinition.swift b/GRDB/QueryInterface/Schema/TableDefinition.swift index 235c54bf88..2395d62b08 100644 --- a/GRDB/QueryInterface/Schema/TableDefinition.swift +++ b/GRDB/QueryInterface/Schema/TableDefinition.swift @@ -136,6 +136,7 @@ public final class TableDefinition { /// - /// - /// + /// - parameter name: the name of the primary key. /// - parameter conflictResolution: An optional conflict resolution /// (see ). /// - returns: `self` so that you can further refine the column definition. @@ -163,6 +164,8 @@ public final class TableDefinition { /// /// - parameter name: the column name. /// - parameter type: the column type. + /// - parameter conflictResolution: An optional conflict resolution + /// (see ). /// - returns: A ``ColumnDefinition`` that allows you to refine the /// column definition. @discardableResult diff --git a/GRDB/QueryInterface/Schema/VirtualTableModule.swift b/GRDB/QueryInterface/Schema/VirtualTableModule.swift index 07bdf7bdd8..6194abe7ba 100644 --- a/GRDB/QueryInterface/Schema/VirtualTableModule.swift +++ b/GRDB/QueryInterface/Schema/VirtualTableModule.swift @@ -113,7 +113,7 @@ extension Database { /// Related SQLite documentation: /// /// - parameters: - /// - name: The table name. + /// - tableName: The table name. /// - ifNotExists: If false (the default), an error is thrown if the /// table already exists. Otherwise, the table is created unless it /// already exists. diff --git a/GRDB/QueryInterface/TableRecord+Association.swift b/GRDB/QueryInterface/TableRecord+Association.swift index e86a548141..e59a634a38 100644 --- a/GRDB/QueryInterface/TableRecord+Association.swift +++ b/GRDB/QueryInterface/TableRecord+Association.swift @@ -334,12 +334,12 @@ extension TableRecord { /// /// - parameter cte: A common table expression. /// - parameter condition: A function that returns the joining clause. - /// - parameter left: A `TableAlias` for the left table. - /// - parameter right: A `TableAlias` for the right table. + /// First argument is a ``TableAlias`` for the left table, second + /// argument an alias for the right table. /// - returns: An association to the common table expression. public static func association( to cte: CommonTableExpression, - on condition: @escaping (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) + on condition: @escaping @Sendable (_ left: TableAlias, _ right: TableAlias) -> any SQLExpressible) -> JoinAssociation { JoinAssociation( @@ -567,6 +567,19 @@ extension TableRecord where Self: EncodableRecord { fatalError("Not implemented: request association without any foreign key") case let .foreignKey(foreignKey): + // Build the sendable persistence container before building the + // request, and catch the eventual error in a Result, so that it + // is thrown later, when the request is executed. This allows + // this method to not throw: + // + // extension Player { + // // We don't want this property to have a throwing getter: + // var team: QueryInterfaceRequest { + // request(for: Player.team) + // } + // } + let persistenceContainer = Result { try PersistenceContainer(self) } + let destinationRelation = association ._sqlAssociation .with { @@ -574,7 +587,7 @@ extension TableRecord where Self: EncodableRecord { // Filter the pivot on self try foreignKey .joinMapping(db, from: Self.databaseTableName) - .joinExpression(leftRows: [PersistenceContainer(db, self)]) + .joinExpression(leftRows: [persistenceContainer.get()]) } } .destinationRelation() diff --git a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift index 8044373f30..e86156f24e 100644 --- a/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift +++ b/GRDB/QueryInterface/TableRecord+QueryInterfaceRequest.swift @@ -343,10 +343,9 @@ extension TableRecord { /// ``` /// /// - parameter keys: A collection of primary keys - public static func filter(keys: Keys) - -> QueryInterfaceRequest - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { + public static func filter( + keys: some Collection + ) -> QueryInterfaceRequest { all().filter(keys: keys) } @@ -605,7 +604,6 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns a request filtered by primary key. /// @@ -651,9 +649,9 @@ extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// ``` /// /// - parameter ids: A collection of primary keys - public static func filter(ids: IDS) -> QueryInterfaceRequest - where IDS: Collection, IDS.Element == ID - { + public static func filter( + ids: some Collection + ) -> QueryInterfaceRequest { all().filter(ids: ids) } } diff --git a/GRDB/Record/EncodableRecord+Encodable.swift b/GRDB/Record/EncodableRecord+Encodable.swift index 8ead9397bc..788f4b1c96 100644 --- a/GRDB/Record/EncodableRecord+Encodable.swift +++ b/GRDB/Record/EncodableRecord+Encodable.swift @@ -118,11 +118,17 @@ private class RecordEncoder: Encoder { fileprivate func encode(_ value: T, forKey key: any CodingKey) throws where T: Encodable { if let data = value as? Data { - persist(Record.databaseDataEncodingStrategy.encode(data), forKey: key) + let column = keyEncodingStrategy.column(forKey: key) + let dbValue = try Record.databaseDataEncodingStrategy(for: column).encode(data) + _persistenceContainer[column] = dbValue } else if let date = value as? Date { - persist(Record.databaseDateEncodingStrategy.encode(date), forKey: key) + let column = keyEncodingStrategy.column(forKey: key) + let dbValue = Record.databaseDateEncodingStrategy(for: column).encode(date) + _persistenceContainer[column] = dbValue } else if let uuid = value as? UUID { - persist(Record.databaseUUIDEncodingStrategy.encode(uuid), forKey: key) + let column = keyEncodingStrategy.column(forKey: key) + let dbValue = Record.databaseUUIDEncodingStrategy(for: column).encode(uuid) + _persistenceContainer[column] = dbValue } else if let value = value as? any DatabaseValueConvertible { // Prefer DatabaseValueConvertible encoding over Decodable. persist(value.databaseValue, forKey: key) @@ -150,7 +156,7 @@ private class RecordEncoder: Encoder { // eventually perform JSON decoding. // TODO: possible optimization: avoid this conversion to string, // and store raw data bytes as an SQLite string - let jsonString = String(data: jsonData, encoding: .utf8)! + let jsonString = String(decoding: jsonData, as: UTF8.self) persist(jsonString, forKey: key) } } diff --git a/GRDB/Record/EncodableRecord.swift b/GRDB/Record/EncodableRecord.swift index b1015c751f..4f52ad4468 100644 --- a/GRDB/Record/EncodableRecord.swift +++ b/GRDB/Record/EncodableRecord.swift @@ -20,11 +20,11 @@ import Foundation // For JSONEncoder /// ### Configuring Persistence for the Standard Encodable Protocol /// /// - ``databaseColumnEncodingStrategy-5sx4v`` -/// - ``databaseDataEncodingStrategy-9y0c7`` -/// - ``databaseDateEncodingStrategy-2gtc1`` -/// - ``databaseEncodingUserInfo-8upii`` +/// - ``databaseDataEncodingStrategy(for:)`` +/// - ``databaseDateEncodingStrategy(for:)`` /// - ``databaseJSONEncoder(for:)-6x62c`` -/// - ``databaseUUIDEncodingStrategy-2t96q`` +/// - ``databaseUUIDEncodingStrategy(for:)`` +/// - ``databaseEncodingUserInfo-8upii`` /// - ``DatabaseColumnEncodingStrategy`` /// - ``DatabaseDataEncodingStrategy`` /// - ``DatabaseDateEncodingStrategy`` @@ -89,7 +89,9 @@ public protocol EncodableRecord { /// /// struct Player: PersistableRecord, Encodable { /// // Customize the encoder name when encoding a database row - /// static let databaseEncodingUserInfo: [CodingUserInfoKey: Any] = [encoderName: "Database"] + /// static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { + /// [encoderName: "Database"] + /// } /// /// func encode(to encoder: Encoder) throws { /// // Print the encoder name @@ -108,6 +110,23 @@ public protocol EncodableRecord { /// encoder.userInfo = [encoderName: "JSON"] /// let data = try encoder.encode(player) /// ``` + /// + /// > Important: Make sure the `databaseEncodingUserInfo` property is + /// > explicitly declared as `[CodingUserInfoKey: Any]`. If it is not, + /// > the Swift compiler may silently miss the protocol requirement. + /// + /// > Important: Make sure the property is declared as a computed + /// > property (`static var`), instead of a stored property + /// > (`static let`). Computed properties avoid a compiler diagnostic + /// > with stored properties: + /// > + /// > ```swift + /// > // static property 'databaseEncodingUserInfo' is not + /// > // concurrency-safe because non-'Sendable' type + /// > // '[CodingUserInfoKey: Any]' may have shared + /// > // mutable state. + /// > static let databaseEncodingUserInfo: [CodingUserInfoKey: Any] = [encoderName: "Database"] + /// > ``` static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get } /// Returns the `JSONEncoder` that encodes the value for a given column. @@ -127,13 +146,15 @@ public protocol EncodableRecord { /// /// ```swift /// struct Player: EncodableRecord, Encodable { - /// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text + /// static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { + /// .text + /// } /// /// // Encoded as SQL text. Data must contain valid UTF8 bytes. /// var jsonData: Data /// } /// ``` - static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { get } + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy /// The strategy for encoding `Date` columns. /// @@ -145,13 +166,15 @@ public protocol EncodableRecord { /// /// ```swift /// struct Player: EncodableRecord, Encodable { - /// static let databaseDateEncodingStrategy = DatabaseDateEncodingStrategy.timeIntervalSince1970 + /// static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy { + /// .timeIntervalSince1970 + /// } /// /// // Encoded as an epoch timestamp /// var creationDate: Date /// } /// ``` - static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get } + static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy /// The strategy for encoding `UUID` columns. /// @@ -163,13 +186,15 @@ public protocol EncodableRecord { /// /// ```swift /// struct Player: EncodableRecord, Encodable { - /// static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString + /// static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { + /// .uppercaseString + /// } /// /// // Encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" /// var uuid: UUID /// } /// ``` - static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get } + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy /// The strategy for converting coding keys to column names. /// @@ -221,19 +246,19 @@ extension EncodableRecord { /// Returns the default strategy for encoding `Data` columns: /// ``DatabaseDataEncodingStrategy/deferredToData``. - public static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { + public static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { .deferredToData } /// Returns the default strategy for encoding `Date` columns: /// ``DatabaseDateEncodingStrategy/deferredToDate``. - public static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { + public static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy { .deferredToDate } /// Returns the default strategy for encoding `UUID` columns: /// ``DatabaseUUIDEncodingStrategy/deferredToUUID``. - public static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { + public static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { .deferredToUUID } @@ -252,7 +277,7 @@ extension EncodableRecord { /// database representation. public var databaseDictionary: [String: DatabaseValue] { get throws { - try Dictionary(PersistenceContainer(self).storage).mapValues { $0?.databaseValue ?? .null } + try Dictionary(uniqueKeysWithValues: PersistenceContainer(self)) } } } @@ -330,19 +355,32 @@ extension EncodableRecord { /// /// `PersistenceContainer` is the argument of the /// ``EncodableRecord/encode(to:)-k9pf`` method. -public struct PersistenceContainer { - // fileprivate for Row(_:PersistenceContainer) +public struct PersistenceContainer: Sendable { // The ordering of the OrderedDictionary helps generating always the same // SQL queries, and hit the statement cache. - fileprivate var storage: OrderedDictionary + private var storage: OrderedDictionary /// The value associated with the given column. + /// + /// The getter may not return the exact same value that has been + /// previously set. The only guarantee is that both are encoded + /// identically in the database. public subscript(_ column: String) -> (any DatabaseValueConvertible)? { - get { self[caseInsensitive: column] } - set { storage.updateValue(newValue, forKey: column) } + get { + storage[CaseInsensitiveIdentifier(rawValue: column)] + } + set { + storage.updateValue( + newValue?.databaseValue ?? .null, + forKey: CaseInsensitiveIdentifier(rawValue: column)) + } } /// The value associated with the given column. + /// + /// The getter may not return the exact same value that has been + /// previously set. The only guarantee is that both are encoded + /// identically in the database. public subscript(_ column: some ColumnExpression) -> (any DatabaseValueConvertible)? { get { self[column.name] } set { self[column.name] = newValue } @@ -372,66 +410,25 @@ public struct PersistenceContainer { } /// Columns stored in the container, ordered like values. - var columns: [String] { Array(storage.keys) } + var columns: [String] { storage.keys.map(\.rawValue) } /// Values stored in the container, ordered like columns. - var values: [(any DatabaseValueConvertible)?] { Array(storage.values) } + var values: [DatabaseValue] { storage.values } - /// Accesses the value associated with the given column, in a - /// case-insensitive fashion. - subscript(caseInsensitive column: String) -> (any DatabaseValueConvertible)? { - get { - if let value = storage[column] { - return value - } - let lowercaseColumn = column.lowercased() - for (key, value) in storage where key.lowercased() == lowercaseColumn { - return value - } - return nil - } - set { - if storage[column] != nil { - storage[column] = newValue - return - } - let lowercaseColumn = column.lowercased() - for key in storage.keys where key.lowercased() == lowercaseColumn { - storage[key] = newValue - return - } - - storage[column] = newValue - } - } - - // Returns nil if column is not defined - func value(forCaseInsensitiveColumn column: String) -> DatabaseValue? { - let lowercaseColumn = column.lowercased() - for (key, value) in storage where key.lowercased() == lowercaseColumn { - return value?.databaseValue ?? .null - } - return nil - } - - var isEmpty: Bool { storage.isEmpty } - - /// An iterator over the (column, value) pairs - func makeIterator() -> IndexingIterator> { - storage.makeIterator() + /// Returns ``DatabaseValue/null`` if column is not defined + func databaseValue(at column: String) -> DatabaseValue { + storage[CaseInsensitiveIdentifier(rawValue: column)] ?? .null } @usableFromInline func changesIterator(from container: PersistenceContainer) -> AnyIterator<(String, DatabaseValue)> { - var newValueIterator = makeIterator() + var newValueIterator = storage.makeIterator() return AnyIterator { // Loop until we find a change, or exhaust columns: - while let (column, newValue) = newValueIterator.next() { - let oldValue = container[caseInsensitive: column] - let oldDbValue = oldValue?.databaseValue ?? .null - let newDbValue = newValue?.databaseValue ?? .null + while let (column, newDbValue) = newValueIterator.next() { + let oldDbValue = container.storage[column] ?? .null if newDbValue != oldDbValue { - return (column, oldDbValue) + return (column.rawValue, oldDbValue) } } return nil @@ -439,13 +436,26 @@ public struct PersistenceContainer { } } +extension PersistenceContainer: RandomAccessCollection { + public typealias Index = Int + + public var startIndex: Int { storage.startIndex } + public var endIndex: Int { storage.endIndex } + + /// Returns the (column, value) pair at given index. + public subscript(position: Int) -> (String, DatabaseValue) { + let element = storage[position] + return (element.key.rawValue, element.value) + } +} + extension Row { convenience init(_ record: Record) throws { try self.init(PersistenceContainer(record)) } convenience init(_ container: PersistenceContainer) { - self.init(Dictionary(container.storage)) + self.init(impl: ArrayRowImpl(columns: container.lazy.map { ($0, $1) })) } } @@ -460,13 +470,15 @@ extension Row { /// /// ```swift /// struct Player: EncodableRecord, Encodable { -/// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text +/// static func databaseDataEncodingStrategy(for column: Column) -> DatabaseDataEncodingStrategy { +/// .text +/// } /// /// // Encoded as SQL text. Data must contain valid UTF8 bytes. /// var jsonData: Data /// } /// ``` -public enum DatabaseDataEncodingStrategy { +public enum DatabaseDataEncodingStrategy: Sendable { /// Encodes `Data` columns as SQL blob. case deferredToData @@ -474,15 +486,17 @@ public enum DatabaseDataEncodingStrategy { case text /// Encodes `Data` column as the result of the user-provided function. - case custom((Data) -> (any DatabaseValueConvertible)?) + case custom(@Sendable (Data) -> (any DatabaseValueConvertible)?) - func encode(_ data: Data) -> DatabaseValue { + func encode(_ data: Data) throws -> DatabaseValue { switch self { case .deferredToData: return data.databaseValue case .text: guard let string = String(data: data, encoding: .utf8) else { - fatalError("Invalid UTF8 data") + throw EncodingError.invalidValue(data, EncodingError.Context( + codingPath: [], + debugDescription: "Non-UTF8 data can't be encoded as text in the database")) } return string.databaseValue case .custom(let format): @@ -502,13 +516,15 @@ public enum DatabaseDataEncodingStrategy { /// /// ```swift /// struct Player: EncodableRecord, Encodable { -/// static let databaseDateEncodingStrategy = DatabaseDateEncodingStrategy.timeIntervalSince1970 +/// static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy {` +/// .timeIntervalSince1970 +/// } /// /// // Encoded as an epoch timestamp /// var creationDate: Date /// } /// ``` -public enum DatabaseDateEncodingStrategy { +public enum DatabaseDateEncodingStrategy: Sendable { /// The strategy that uses formatting from the Date structure. /// /// It encodes dates using the format "YYYY-MM-DD HH:MM:SS.SSS" in the @@ -538,9 +554,10 @@ public enum DatabaseDateEncodingStrategy { case formatted(DateFormatter) /// Encodes the result of the user-provided function - case custom((Date) -> (any DatabaseValueConvertible)?) + case custom(@Sendable (Date) -> (any DatabaseValueConvertible)?) - private static let iso8601Formatter: ISO8601DateFormatter = { + // Assume this non-Sendable instance can be used from multiple threads concurrently. + nonisolated(unsafe) private static let iso8601Formatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime return formatter @@ -579,7 +596,9 @@ public enum DatabaseDateEncodingStrategy { /// /// ```swift /// struct Player: EncodableRecord, Encodable { -/// static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString +/// static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy {` +/// .uppercaseString +/// } /// /// // Encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" /// var uuid: UUID @@ -626,7 +645,7 @@ public enum DatabaseUUIDEncodingStrategy: Sendable { /// var playerID: String /// } /// ``` -public enum DatabaseColumnEncodingStrategy { +public enum DatabaseColumnEncodingStrategy: Sendable { /// A key encoding strategy that doesn’t change key names during encoding. case useDefaultKeys @@ -634,7 +653,7 @@ public enum DatabaseColumnEncodingStrategy { case convertToSnakeCase /// A key encoding strategy defined by the closure you supply. - case custom((any CodingKey) -> String) + case custom(@Sendable (any CodingKey) -> String) func column(forKey key: some CodingKey) -> String { switch self { diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index f89a12a5c2..a4c6e423b2 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation extension FetchableRecord where Self: Decodable { @@ -52,8 +61,8 @@ extension FetchableRecord where Self: Decodable { /// The behavior of the decoder depends on the decoded type. See: /// /// - ``FetchableRecord/databaseColumnDecodingStrategy-6uefz`` -/// - ``FetchableRecord/databaseDataDecodingStrategy-71bh1`` -/// - ``FetchableRecord/databaseDateDecodingStrategy-78y03`` +/// - ``FetchableRecord/databaseDataDecodingStrategy(for:)`` +/// - ``FetchableRecord/databaseDateDecodingStrategy(for:)`` /// - ``FetchableRecord/databaseDecodingUserInfo-77jim`` /// - ``FetchableRecord/databaseJSONDecoder(for:)-7lmxd`` public class FetchableRecordDecoder { @@ -276,13 +285,13 @@ private struct _RowDecoder: Decoder { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. if type == Data.self { - return try R.databaseDataDecodingStrategy.decodeIfPresent( - fromRow: row, - atUncheckedIndex: index) as! T? + return try R + .databaseDataDecodingStrategy(for: column) + .decodeIfPresent(fromRow: row, atUncheckedIndex: index) as! T? } else if type == Date.self { - return try R.databaseDateDecodingStrategy.decodeIfPresent( - fromRow: row, - atUncheckedIndex: index) as! T? + return try R + .databaseDateDecodingStrategy(for: column) + .decodeIfPresent(fromRow: row, atUncheckedIndex: index) as! T? } else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type { return try type.fastDecodeIfPresent(fromRow: row, atUncheckedIndex: index) as! T? } else if let type = T.self as? any DatabaseValueConvertible.Type { @@ -325,9 +334,13 @@ private struct _RowDecoder: Decoder { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. if type == Data.self { - return try R.databaseDataDecodingStrategy.decode(fromRow: row, atUncheckedIndex: index) as! T + return try R + .databaseDataDecodingStrategy(for: column) + .decode(fromRow: row, atUncheckedIndex: index) as! T } else if type == Date.self { - return try R.databaseDateDecodingStrategy.decode(fromRow: row, atUncheckedIndex: index) as! T + return try R + .databaseDateDecodingStrategy(for: column) + .decode(fromRow: row, atUncheckedIndex: index) as! T } else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type { return try type.fastDecode(fromRow: row, atUncheckedIndex: index) as! T } else if let type = T.self as? any DatabaseValueConvertible.Type { @@ -643,11 +656,16 @@ extension ColumnDecoder: SingleValueDecodingContainer { func decode(_ type: String.Type) throws -> String { try row.decode(atIndex: columnIndex) } func decode(_ type: T.Type) throws -> T where T: Decodable { - // TODO: not tested if type == Data.self { - return try R.databaseDataDecodingStrategy.decode(fromRow: row, atUncheckedIndex: columnIndex) as! T + let columnName = row.impl.columnName(atUncheckedIndex: columnIndex) + return try R + .databaseDataDecodingStrategy(for: columnName) + .decode(fromRow: row, atUncheckedIndex: columnIndex) as! T } else if type == Date.self { - return try R.databaseDateDecodingStrategy.decode(fromRow: row, atUncheckedIndex: columnIndex) as! T + let columnName = row.impl.columnName(atUncheckedIndex: columnIndex) + return try R + .databaseDateDecodingStrategy(for: columnName) + .decode(fromRow: row, atUncheckedIndex: columnIndex) as! T } else if let type = T.self as? any (DatabaseValueConvertible & StatementColumnConvertible).Type { return try type.fastDecode(fromRow: row, atUncheckedIndex: columnIndex) as! T } else if let type = T.self as? any DatabaseValueConvertible.Type { @@ -658,7 +676,8 @@ extension ColumnDecoder: SingleValueDecodingContainer { } } -private let iso8601Formatter: ISO8601DateFormatter = { +// Assume this non-Sendable instance can be used from multiple threads concurrently. +nonisolated(unsafe) private let iso8601Formatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime return formatter diff --git a/GRDB/Record/FetchableRecord+TableRecord.swift b/GRDB/Record/FetchableRecord+TableRecord.swift index 97178adebe..b755110640 100644 --- a/GRDB/Record/FetchableRecord+TableRecord.swift +++ b/GRDB/Record/FetchableRecord+TableRecord.swift @@ -128,10 +128,10 @@ extension FetchableRecord where Self: TableRecord { /// - keys: A sequence of primary keys. /// - returns: A ``RecordCursor`` over fetched records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchCursor(_ db: Database, keys: Keys) - throws -> RecordCursor - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { + public static func fetchCursor( + _ db: Database, + keys: some Collection + ) throws -> RecordCursor { try filter(keys: keys).fetchCursor(db) } @@ -154,11 +154,10 @@ extension FetchableRecord where Self: TableRecord { /// - keys: A sequence of primary keys. /// - returns: An array of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchAll(_ db: Database, keys: Keys) - throws -> [Self] - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { - let keys = Array(keys) + public static func fetchAll( + _ db: Database, + keys: some Collection + ) throws -> [Self] { if keys.isEmpty { // Avoid hitting the database return [] @@ -217,7 +216,6 @@ extension FetchableRecord where Self: TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseValueConvertible { // MARK: Fetching by Single-Column Primary Key @@ -249,10 +247,10 @@ extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseVa /// - ids: A collection of primary keys. /// - returns: A ``RecordCursor`` over fetched records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchCursor(_ db: Database, ids: IDS) - throws -> RecordCursor - where IDS: Collection, IDS.Element == ID - { + public static func fetchCursor( + _ db: Database, + ids: some Collection + ) throws -> RecordCursor { try filter(ids: ids).fetchCursor(db) } @@ -275,9 +273,10 @@ extension FetchableRecord where Self: TableRecord & Identifiable, ID: DatabaseVa /// - ids: A collection of primary keys. /// - returns: An array of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchAll(_ db: Database, ids: IDS) throws -> [Self] - where IDS: Collection, IDS.Element == ID - { + public static func fetchAll( + _ db: Database, + ids: some Collection + ) throws -> [Self] { if ids.isEmpty { // Avoid hitting the database return [] @@ -346,11 +345,10 @@ extension FetchableRecord where Self: TableRecord & Hashable { /// - keys: A sequence of primary keys. /// - returns: A set of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchSet(_ db: Database, keys: Keys) - throws -> Set - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { - let keys = Array(keys) + public static func fetchSet( + _ db: Database, + keys: some Collection + ) throws -> Set { if keys.isEmpty { // Avoid hitting the database return [] @@ -359,7 +357,6 @@ extension FetchableRecord where Self: TableRecord & Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension FetchableRecord where Self: TableRecord & Hashable & Identifiable, ID: DatabaseValueConvertible { /// Returns a set of records identified by their primary keys. /// @@ -377,9 +374,10 @@ extension FetchableRecord where Self: TableRecord & Hashable & Identifiable, ID: /// - ids: A collection of primary keys. /// - returns: A set of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. - public static func fetchSet(_ db: Database, ids: IDS) throws -> Set - where IDS: Collection, IDS.Element == ID - { + public static func fetchSet( + _ db: Database, + ids: some Collection + ) throws -> Set { if ids.isEmpty { // Avoid hitting the database return [] diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index 609279560f..61130a4dbe 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -71,9 +71,9 @@ import Foundation /// - ``fetchAll(_:ids:)`` /// - ``fetchSet(_:ids:)`` /// - ``fetchOne(_:id:)`` -/// - ``fetchCursor(_:keys:)-2jrm1`` -/// - ``fetchAll(_:keys:)-4c8no`` -/// - ``fetchSet(_:keys:)-e6uy`` +/// - ``fetchCursor(_:keys:)-1x4ja`` +/// - ``fetchAll(_:keys:)-60fah`` +/// - ``fetchSet(_:keys:)-7lhcn`` /// - ``fetchOne(_:key:)-3f3hc`` /// - ``find(_:id:)`` /// - ``find(_:key:)-4kry5`` @@ -89,10 +89,10 @@ import Foundation /// ### Configuring Row Decoding for the Standard Decodable Protocol /// /// - ``databaseColumnDecodingStrategy-6uefz`` -/// - ``databaseDataDecodingStrategy-71bh1`` -/// - ``databaseDateDecodingStrategy-78y03`` -/// - ``databaseDecodingUserInfo-77jim`` +/// - ``databaseDataDecodingStrategy(for:)`` +/// - ``databaseDateDecodingStrategy(for:)`` /// - ``databaseJSONDecoder(for:)-7lmxd`` +/// - ``databaseDecodingUserInfo-77jim`` /// - ``DatabaseColumnDecodingStrategy`` /// - ``DatabaseDataDecodingStrategy`` /// - ``DatabaseDateDecodingStrategy`` @@ -129,7 +129,9 @@ public protocol FetchableRecord { /// // A FetchableRecord + Decodable record /// struct Player: FetchableRecord, Decodable { /// // Customize the decoder name when decoding a database row - /// static let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [decoderName: "Database"] + /// static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { + /// [decoderName: "Database"] + /// } /// /// init(from decoder: Decoder) throws { /// // Print the decoder name @@ -146,6 +148,23 @@ public protocol FetchableRecord { /// decoder.userInfo = [decoderName: "JSON"] /// let player = try decoder.decode(Player.self, from: ...) /// ``` + /// + /// > Important: Make sure the `databaseDecodingUserInfo` property is + /// > explicitly declared as `[CodingUserInfoKey: Any]`. If it is not, + /// > the Swift compiler may silently miss the protocol requirement. + /// + /// > Important: Make sure the property is declared as a computed + /// > property (`static var`), instead of a stored property + /// > (`static let`). Computed properties avoid a compiler diagnostic + /// > with stored properties: + /// > + /// > ```swift + /// > // static property 'databaseDecodingUserInfo' is not + /// > // concurrency-safe because non-'Sendable' type + /// > // '[CodingUserInfoKey: Any]' may have shared + /// > // mutable state. + /// > static let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [decoderName: "Database"] + /// > ``` static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { get } /// Returns the `JSONDecoder` that decodes the value for a given column. @@ -165,18 +184,20 @@ public protocol FetchableRecord { /// /// ```swift /// struct Player: FetchableRecord, Decodable { - /// static let databaseDataDecodingStrategy = DatabaseDataDecodingStrategy.custom { dbValue - /// guard let base64Data = Data.fromDatabaseValue(dbValue) else { - /// return nil + /// static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { + /// .custom { dbValue + /// guard let base64Data = Data.fromDatabaseValue(dbValue) else { + /// return nil + /// } + /// return Data(base64Encoded: base64Data) /// } - /// return Data(base64Encoded: base64Data) /// } /// /// // Decoded from both database base64 strings and blobs /// var myData: Data /// } /// ``` - static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { get } + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy /// The strategy for decoding `Date` columns. /// @@ -188,13 +209,15 @@ public protocol FetchableRecord { /// /// ```swift /// struct Player: FetchableRecord, Decodable { - /// static let databaseDateDecodingStrategy = DatabaseDateDecodingStrategy.timeIntervalSince1970 + /// static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { + /// .timeIntervalSince1970 + /// } /// /// // Decoded from an epoch timestamp /// var creationDate: Date /// } /// ``` - static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { get } + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy /// The strategy for converting column names to coding keys. /// @@ -243,13 +266,13 @@ extension FetchableRecord { /// The default strategy for decoding `Data` columns is /// ``DatabaseDataDecodingStrategy/deferredToData``. - public static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { + public static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { .deferredToData } /// The default strategy for decoding `Date` columns is /// ``DatabaseDateDecodingStrategy/deferredToDate``. - public static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { + public static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { .deferredToDate } @@ -555,7 +578,7 @@ extension FetchableRecord { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. + /// - request: a fetch request. /// - returns: A ``RecordCursor`` over fetched records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchCursor(_ db: Database, _ request: some FetchRequest) throws -> RecordCursor { @@ -586,7 +609,7 @@ extension FetchableRecord { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. + /// - request: a fetch request. /// - returns: An array of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Self] { @@ -621,7 +644,7 @@ extension FetchableRecord { /// ``` /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. + /// - request: a fetch request. /// - returns: An optional record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Self? { @@ -661,7 +684,7 @@ extension FetchableRecord where Self: Hashable { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. + /// - request: a fetch request. /// - returns: A set of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set { @@ -714,7 +737,6 @@ extension FetchRequest where RowDecoder: FetchableRecord { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. /// - returns: A ``RecordCursor`` over fetched records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public func fetchCursor(_ db: Database) throws -> RecordCursor { @@ -743,7 +765,6 @@ extension FetchRequest where RowDecoder: FetchableRecord { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. /// - returns: An array of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public func fetchAll(_ db: Database) throws -> [RowDecoder] { @@ -771,7 +792,6 @@ extension FetchRequest where RowDecoder: FetchableRecord { /// ``` /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. /// - returns: An optional record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public func fetchOne(_ db: Database) throws -> RowDecoder? { @@ -802,7 +822,6 @@ extension FetchRequest where RowDecoder: FetchableRecord & Hashable { /// /// - parameters: /// - db: A database connection. - /// - sql: a FetchRequest. /// - returns: A set of records. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public func fetchSet(_ db: Database) throws -> Set { @@ -869,18 +888,20 @@ extension RecordCursor: Sendable { } /// /// ```swift /// struct Player: FetchableRecord, Decodable { -/// static let databaseDataDecodingStrategy = DatabaseDataDecodingStrategy.custom { dbValue -/// guard let base64Data = Data.fromDatabaseValue(dbValue) else { -/// return nil +/// static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { +/// .custom { dbValue +/// guard let base64Data = Data.fromDatabaseValue(dbValue) else { +/// return nil +/// } +/// return Data(base64Encoded: base64Data) /// } -/// return Data(base64Encoded: base64Data) /// } /// /// // Decoded from both database base64 strings and blobs /// var myData: Data /// } /// ``` -public enum DatabaseDataDecodingStrategy { +public enum DatabaseDataDecodingStrategy: Sendable { /// Decodes `Data` columns from SQL blobs and UTF8 text. case deferredToData @@ -889,7 +910,7 @@ public enum DatabaseDataDecodingStrategy { /// If the database value does not contain a suitable value, the function /// must return nil (GRDB will interpret this nil result as a conversion /// error, and react accordingly). - case custom((DatabaseValue) -> Data?) + case custom(@Sendable (DatabaseValue) -> Data?) } // MARK: - DatabaseDateDecodingStrategy @@ -901,12 +922,14 @@ public enum DatabaseDataDecodingStrategy { /// For example: /// /// struct Player: FetchableRecord, Decodable { -/// static let databaseDateDecodingStrategy = DatabaseDateDecodingStrategy.timeIntervalSince1970 +/// static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { +/// .timeIntervalSince1970 +/// } /// /// var name: String /// var registrationDate: Date // decoded from epoch timestamp /// } -public enum DatabaseDateDecodingStrategy { +public enum DatabaseDateDecodingStrategy: Sendable { /// The strategy that uses formatting from the Date structure. /// /// It decodes numeric values as a number of seconds since Epoch @@ -947,7 +970,7 @@ public enum DatabaseDateDecodingStrategy { /// If the database value does not contain a suitable value, the function /// must return nil (GRDB will interpret this nil result as a conversion /// error, and react accordingly). - case custom((DatabaseValue) -> Date?) + case custom(@Sendable (DatabaseValue) -> Date?) } // MARK: - DatabaseColumnDecodingStrategy @@ -964,7 +987,7 @@ public enum DatabaseDateDecodingStrategy { /// // Decoded from the player_id column /// var playerID: Int /// } -public enum DatabaseColumnDecodingStrategy { +public enum DatabaseColumnDecodingStrategy: Sendable { /// A key decoding strategy that doesn’t change key names during decoding. case useDefaultKeys @@ -972,7 +995,7 @@ public enum DatabaseColumnDecodingStrategy { case convertFromSnakeCase /// A key decoding strategy defined by the closure you supply. - case custom((String) -> CodingKey) + case custom(@Sendable (String) -> CodingKey) func key(forColumn column: String) -> K? { switch self { diff --git a/GRDB/Record/MutablePersistableRecord+DAO.swift b/GRDB/Record/MutablePersistableRecord+DAO.swift index 48b9025c82..89efd4cd7d 100644 --- a/GRDB/Record/MutablePersistableRecord+DAO.swift +++ b/GRDB/Record/MutablePersistableRecord+DAO.swift @@ -142,7 +142,7 @@ final class DAO { // Fail early if primary key does not resolve to a database row. let primaryKeyColumns = primaryKey.columns let primaryKeyValues = primaryKeyColumns.map { - persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + persistenceContainer.databaseValue(at: $0) } if primaryKeyValues.allSatisfy({ $0.isNull }) { return nil @@ -173,7 +173,7 @@ final class DAO { } let updatedValues = updatedColumns.map { - persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + persistenceContainer.databaseValue(at: $0) } let query = UpdateQuery( @@ -193,7 +193,7 @@ final class DAO { // Fail early if primary key does not resolve to a database row. let primaryKeyColumns = primaryKey.columns let primaryKeyValues = primaryKeyColumns.map { - persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + persistenceContainer.databaseValue(at: $0) } if primaryKeyValues.allSatisfy({ $0.isNull }) { return nil @@ -212,7 +212,7 @@ final class DAO { // Fail early if primary key does not resolve to a database row. let primaryKeyColumns = primaryKey.columns let primaryKeyValues = primaryKeyColumns.map { - persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null + persistenceContainer.databaseValue(at: $0) } if primaryKeyValues.allSatisfy({ $0.isNull }) { return nil @@ -229,7 +229,7 @@ final class DAO { /// Throws a RecordError.recordNotFound error func recordNotFound() throws -> Never { let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map { - ($0, persistenceContainer[caseInsensitive: $0]?.databaseValue ?? .null) + ($0, persistenceContainer.databaseValue(at: $0)) }) throw RecordError.recordNotFound( databaseTableName: databaseTableName, @@ -273,29 +273,33 @@ private struct InsertQuery: Hashable { } extension InsertQuery { - @ReadWriteBox private static var sqlCache: [InsertQuery: String] = [:] + private static let cacheLock: ReadWriteLock<[InsertQuery: String]> = ReadWriteLock([:]) var sql: String { - if let sql = Self.sqlCache[self] { + if let sql = Self.cacheLock.read({ $0[self] }) { return sql } - let columnsSQL = insertedColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", ") - let valuesSQL = databaseQuestionMarks(count: insertedColumns.count) - let sql: String - switch onConflict { - case .abort: - sql = """ - INSERT INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ - VALUES (\(valuesSQL)) - """ - default: - sql = """ - INSERT OR \(onConflict.rawValue) \ - INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ - VALUES (\(valuesSQL)) - """ + + return Self.cacheLock.withLock { cache in + let columnsSQL = insertedColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", ") + let valuesSQL = databaseQuestionMarks(count: insertedColumns.count) + let sql: String + switch onConflict { + case .abort: + sql = """ + INSERT INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ + VALUES (\(valuesSQL)) + """ + default: + sql = """ + INSERT OR \(onConflict.rawValue) \ + INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ + VALUES (\(valuesSQL)) + """ + } + + cache[self] = sql + return sql } - Self.sqlCache[self] = sql - return sql } } @@ -309,30 +313,34 @@ private struct UpdateQuery: Hashable { } extension UpdateQuery { - @ReadWriteBox private static var sqlCache: [UpdateQuery: String] = [:] + private static let cacheLock: ReadWriteLock<[UpdateQuery: String]> = ReadWriteLock([:]) var sql: String { - if let sql = Self.sqlCache[self] { + if let sql = Self.cacheLock.read({ $0[self] }) { return sql } - let updateSQL = updatedColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: ", ") - let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ") - let sql: String - switch onConflict { - case .abort: - sql = """ - UPDATE \(tableName.quotedDatabaseIdentifier) \ - SET \(updateSQL) \ - WHERE \(whereSQL) - """ - default: - sql = """ - UPDATE OR \(onConflict.rawValue) \(tableName.quotedDatabaseIdentifier) \ - SET \(updateSQL) \ - WHERE \(whereSQL) - """ + + return Self.cacheLock.withLock { cache in + let updateSQL = updatedColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: ", ") + let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ") + let sql: String + switch onConflict { + case .abort: + sql = """ + UPDATE \(tableName.quotedDatabaseIdentifier) \ + SET \(updateSQL) \ + WHERE \(whereSQL) + """ + default: + sql = """ + UPDATE OR \(onConflict.rawValue) \(tableName.quotedDatabaseIdentifier) \ + SET \(updateSQL) \ + WHERE \(whereSQL) + """ + } + + cache[self] = sql + return sql } - Self.sqlCache[self] = sql - return sql } } diff --git a/GRDB/Record/MutablePersistableRecord+Insert.swift b/GRDB/Record/MutablePersistableRecord+Insert.swift index 4b2b1e1d04..f63d9f938d 100644 --- a/GRDB/Record/MutablePersistableRecord+Insert.swift +++ b/GRDB/Record/MutablePersistableRecord+Insert.swift @@ -91,7 +91,6 @@ extension MutablePersistableRecord { extension MutablePersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -108,22 +107,22 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The inserted record, if any. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The inserted record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.insertAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -161,11 +160,10 @@ extension MutablePersistableRecord { /// var partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -174,19 +172,24 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { - try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + let record = self + return try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) } } @@ -266,7 +269,6 @@ extension MutablePersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -283,23 +285,23 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The inserted record, if any. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The inserted record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.insertAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -337,11 +339,10 @@ extension MutablePersistableRecord { /// var partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -350,20 +351,25 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { - try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + let record = self + return try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) } } @@ -519,7 +525,7 @@ extension MutablePersistableRecord { // to false in its `aroundInsert` callback. var persistenceContainer = dao.persistenceContainer if let rowIDColumn { - persistenceContainer[caseInsensitive: rowIDColumn] = rowid + persistenceContainer[rowIDColumn] = rowid } let inserted = InsertionSuccess( diff --git a/GRDB/Record/MutablePersistableRecord+Save.swift b/GRDB/Record/MutablePersistableRecord+Save.swift index a9776cc820..a579884330 100644 --- a/GRDB/Record/MutablePersistableRecord+Save.swift +++ b/GRDB/Record/MutablePersistableRecord+Save.swift @@ -87,7 +87,6 @@ extension MutablePersistableRecord { extension MutablePersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -107,22 +106,22 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The saved record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The saved record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.saveAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -135,26 +134,31 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { + let record = self success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) }) return success!.saved } @@ -211,7 +215,6 @@ extension MutablePersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -231,23 +234,23 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The saved record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The saved record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { var result = self return try result.saveAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -260,27 +263,32 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { + let record = self success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) }) return success!.saved } @@ -401,7 +409,7 @@ extension MutablePersistableRecord { let primaryKeyInfo = try db.primaryKey(databaseTableName) let container = try PersistenceContainer(db, self) let primaryKey = Dictionary(uniqueKeysWithValues: primaryKeyInfo.columns.map { - ($0, container[caseInsensitive: $0]?.databaseValue ?? .null) + ($0, container.databaseValue(at: $0)) }) if primaryKey.allSatisfy({ $0.value.isNull }) { return nil diff --git a/GRDB/Record/MutablePersistableRecord+Update.swift b/GRDB/Record/MutablePersistableRecord+Update.swift index 691d77d8d4..89d81d817c 100644 --- a/GRDB/Record/MutablePersistableRecord+Update.swift +++ b/GRDB/Record/MutablePersistableRecord+Update.swift @@ -41,13 +41,11 @@ extension MutablePersistableRecord { /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. @inlinable // allow specialization so that empty callbacks are removed - public func update( + public func update( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns) - throws - where Columns: Sequence, Columns.Element == String - { + columns: some Collection + ) throws { try willSave(db) var updated: PersistenceSuccess? @@ -84,13 +82,11 @@ extension MutablePersistableRecord { /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. @inlinable // allow specialization so that empty callbacks are removed - public func update( + public func update( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns) - throws - where Columns: Sequence, Columns.Element: ColumnExpression - { + columns: some Collection + ) throws { try update(db, onConflict: conflictResolution, columns: columns.map(\.name)) } @@ -218,7 +214,6 @@ extension MutablePersistableRecord { extension MutablePersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `UPDATE RETURNING` statement on all columns, and returns a /// new record built from the updated row. /// @@ -226,23 +221,23 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The updated record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The updated record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { try updateAndFetch(db, onConflict: conflictResolution, as: Self.self) } - // TODO: GRDB7 make it unable to return an optional /// Executes an `UPDATE RETURNING` statement on all columns, and returns a /// new record built from the updated row. /// @@ -251,25 +246,28 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try updateAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -280,12 +278,13 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter modify: A closure that modifies the record. - /// - returns: An updated record, or nil if the record has no change, or - /// in case of a failed update due to the `IGNORE` conflict policy. + /// - returns: An updated record, or nil if the record has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func updateChangesAndFetch( _ db: Database, @@ -297,7 +296,6 @@ extension MutablePersistableRecord { try updateChangesAndFetch(db, onConflict: conflictResolution, as: Self.self, modify: modify) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -310,12 +308,13 @@ extension MutablePersistableRecord { /// - parameter returnedType: The type of the returned record. /// - parameter modify: A closure that modifies the record. /// - returns: A record of type `returnedType`, or nil if the record has - /// no change, or in case of a failed update due to the `IGNORE` - /// conflict policy. + /// no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public mutating func updateChangesAndFetch( _ db: Database, @@ -324,10 +323,16 @@ extension MutablePersistableRecord { modify: (inout Self) throws -> Void) throws -> T? { - try updateChangesAndFetch( + let record = self + return try updateChangesAndFetch( db, onConflict: conflictResolution, selection: T.databaseSelection, - fetch: { try T.fetchOne($0) }, + fetch: { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) + }, modify: modify) } @@ -362,15 +367,13 @@ extension MutablePersistableRecord { /// primary key does not match any row in the database. /// - precondition: `selection` is not empty. @inlinable // allow specialization so that empty callbacks are removed - public func updateAndFetch( + public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns, + columns: some Collection, selection: [any SQLSelectable], - fetch: (Statement) throws -> T) - throws -> T - where Columns: Sequence, Columns.Element == String - { + fetch: (Statement) throws -> T + ) throws -> T { GRDBPrecondition(!selection.isEmpty, "Invalid empty selection") try willSave(db) @@ -423,15 +426,13 @@ extension MutablePersistableRecord { /// primary key does not match any row in the database. /// - precondition: `selection` is not empty. @inlinable // allow specialization so that empty callbacks are removed - public func updateAndFetch( + public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns, + columns: some Collection, selection: [any SQLSelectable], - fetch: (Statement) throws -> T) - throws -> T - where Columns: Sequence, Columns.Element: ColumnExpression - { + fetch: (Statement) throws -> T + ) throws -> T { try updateAndFetch( db, onConflict: conflictResolution, columns: columns.map(\.name), @@ -482,7 +483,6 @@ extension MutablePersistableRecord { fetch: fetch) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -495,7 +495,8 @@ extension MutablePersistableRecord { /// - parameter selection: The returned columns (must not be empty). /// - parameter fetch: A function that executes it ``Statement`` argument. /// - parameter modify: A closure that modifies the record. - /// - returns: The result of the `fetch` function. + /// - returns: The result of the `fetch` function, or nil if the record + /// has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the @@ -506,7 +507,7 @@ extension MutablePersistableRecord { _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?, + fetch: (Statement) throws -> T, modify: (inout Self) throws -> Void) throws -> T? { @@ -519,7 +520,6 @@ extension MutablePersistableRecord { fetch: fetch) } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `UPDATE RETURNING` statement on all columns, and returns a /// new record built from the updated row. /// @@ -527,18 +527,19 @@ extension MutablePersistableRecord { /// - parameter conflictResolution: A policy for conflict resolution. If /// nil, /// is used. - /// - returns: The updated record. The result can be nil when the - /// conflict policy is `IGNORE`. + /// - returns: The updated record. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil) - throws -> Self? + throws -> Self where Self: FetchableRecord { try updateAndFetch(db, onConflict: conflictResolution, as: Self.self) @@ -552,26 +553,29 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try updateAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -582,12 +586,13 @@ extension MutablePersistableRecord { /// nil, /// is used. /// - parameter modify: A closure that modifies the record. - /// - returns: An updated record, or nil if the record has no change, or - /// in case of a failed update due to the `IGNORE` conflict policy. + /// - returns: An updated record, or nil if the record has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func updateChangesAndFetch( @@ -600,7 +605,6 @@ extension MutablePersistableRecord { try updateChangesAndFetch(db, onConflict: conflictResolution, as: Self.self, modify: modify) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -613,12 +617,13 @@ extension MutablePersistableRecord { /// - parameter returnedType: The type of the returned record. /// - parameter modify: A closure that modifies the record. /// - returns: A record of type `returnedType`, or nil if the record has - /// no change, or in case of a failed update due to the `IGNORE` - /// conflict policy. + /// no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the /// primary key does not match any row in the database. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the update fails due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public mutating func updateChangesAndFetch( @@ -628,10 +633,16 @@ extension MutablePersistableRecord { modify: (inout Self) throws -> Void) throws -> T? { - try updateChangesAndFetch( + let record = self + return try updateChangesAndFetch( db, onConflict: conflictResolution, selection: T.databaseSelection, - fetch: { try T.fetchOne($0) }, + fetch: { + if let result = try T.fetchOne($0) { + return result + } + throw record.recordNotFound(db) + }, modify: modify) } @@ -667,15 +678,13 @@ extension MutablePersistableRecord { /// - precondition: `selection` is not empty. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ - public func updateAndFetch( + public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns, + columns: some Collection, selection: [any SQLSelectable], - fetch: (Statement) throws -> T) - throws -> T - where Columns: Sequence, Columns.Element == String - { + fetch: (Statement) throws -> T + ) throws -> T { GRDBPrecondition(!selection.isEmpty, "Invalid empty selection") try willSave(db) @@ -729,15 +738,13 @@ extension MutablePersistableRecord { /// - precondition: `selection` is not empty. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ - public func updateAndFetch( + public func updateAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, - columns: Columns, + columns: some Collection, selection: [any SQLSelectable], - fetch: (Statement) throws -> T) - throws -> T - where Columns: Sequence, Columns.Element: ColumnExpression - { + fetch: (Statement) throws -> T + ) throws -> T { try updateAndFetch( db, onConflict: conflictResolution, columns: columns.map(\.name), @@ -789,7 +796,6 @@ extension MutablePersistableRecord { fetch: fetch) } - // TODO: GRDB7 make it unable to return an optional /// Modifies the record according to the provided `modify` closure, and /// executes an `UPDATE RETURNING` statement that updates the modified /// columns, if and only if the record was modified. The method returns a @@ -802,7 +808,8 @@ extension MutablePersistableRecord { /// - parameter selection: The returned columns (must not be empty). /// - parameter fetch: A function that executes it ``Statement`` argument. /// - parameter modify: A closure that modifies the record. - /// - returns: The result of the `fetch` function. + /// - returns: The result of the `fetch` function, or nil if the record + /// has no change. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type, /// or ``RecordError/recordNotFound(databaseTableName:key:)`` if the @@ -814,7 +821,7 @@ extension MutablePersistableRecord { _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?, + fetch: (Statement) throws -> T, modify: (inout Self) throws -> Void) throws -> T? { @@ -839,7 +846,7 @@ extension MutablePersistableRecord { onConflict conflictResolution: Database.ConflictResolution?, from container: PersistenceContainer, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?) + fetch: (Statement) throws -> T) throws -> T? { let changes = try PersistenceContainer(db, self).changesIterator(from: container) @@ -861,7 +868,7 @@ extension MutablePersistableRecord { onConflict conflictResolution: Database.ConflictResolution?, from container: PersistenceContainer, selection: [any SQLSelectable], - fetch: (Statement) throws -> T?) + fetch: (Statement) throws -> T) throws -> T? { let changes = try PersistenceContainer(db, self).changesIterator(from: container) diff --git a/GRDB/Record/MutablePersistableRecord+Upsert.swift b/GRDB/Record/MutablePersistableRecord+Upsert.swift index 4374c19601..567feee500 100644 --- a/GRDB/Record/MutablePersistableRecord+Upsert.swift +++ b/GRDB/Record/MutablePersistableRecord+Upsert.swift @@ -452,7 +452,7 @@ extension MutablePersistableRecord { var persistenceContainer = dao.persistenceContainer let rowIDColumn = dao.primaryKey.rowIDColumn if let rowIDColumn { - persistenceContainer[caseInsensitive: rowIDColumn] = rowid + persistenceContainer[rowIDColumn] = rowid } let inserted = InsertionSuccess( diff --git a/GRDB/Record/MutablePersistableRecord.swift b/GRDB/Record/MutablePersistableRecord.swift index d26cb1cbcc..a3f8d8901f 100644 --- a/GRDB/Record/MutablePersistableRecord.swift +++ b/GRDB/Record/MutablePersistableRecord.swift @@ -69,8 +69,8 @@ /// See inherited ``TableRecord`` methods for batch updates. /// /// - ``update(_:onConflict:)`` -/// - ``update(_:onConflict:columns:)-4foo1`` -/// - ``update(_:onConflict:columns:)-5hxyx`` +/// - ``update(_:onConflict:columns:)-5qfk`` +/// - ``update(_:onConflict:columns:)-9fip4`` /// - ``updateChanges(_:onConflict:from:)`` /// - ``updateChanges(_:onConflict:modify:)`` /// @@ -78,8 +78,8 @@ /// /// - ``updateAndFetch(_:onConflict:)`` /// - ``updateAndFetch(_:onConflict:as:)`` -/// - ``updateAndFetch(_:onConflict:columns:selection:fetch:)-7s7y1`` -/// - ``updateAndFetch(_:onConflict:columns:selection:fetch:)-30d2v`` +/// - ``updateAndFetch(_:onConflict:columns:selection:fetch:)-98dtr`` +/// - ``updateAndFetch(_:onConflict:columns:selection:fetch:)-9npht`` /// - ``updateAndFetch(_:onConflict:selection:fetch:)`` /// - ``updateChangesAndFetch(_:onConflict:modify:)`` /// - ``updateChangesAndFetch(_:onConflict:as:modify:)`` @@ -193,6 +193,7 @@ public protocol MutablePersistableRecord: EncodableRecord, TableRecord { /// Default implementation does nothing. /// /// - parameter db: A database connection. + /// - parameter columns: The updated columns. func willUpdate(_ db: Database, columns: Set) throws /// Persistence callback called around the record update. @@ -253,7 +254,7 @@ public protocol MutablePersistableRecord: EncodableRecord, TableRecord { /// ``` /// /// - parameter db: A database connection. - /// - parameter update: A function that updates the record. Its result is + /// - parameter save: A function that saves the record. Its result is /// reserved for GRDB usage. func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws diff --git a/GRDB/Record/PersistableRecord+Insert.swift b/GRDB/Record/PersistableRecord+Insert.swift index b44f3f4328..6292e6a0e5 100644 --- a/GRDB/Record/PersistableRecord+Insert.swift +++ b/GRDB/Record/PersistableRecord+Insert.swift @@ -56,7 +56,6 @@ extension PersistableRecord { extension PersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -94,11 +93,10 @@ extension PersistableRecord { /// let partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -107,19 +105,23 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } @@ -199,7 +201,6 @@ extension PersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` statement, and returns a new record built /// from the inserted row. /// @@ -237,11 +238,10 @@ extension PersistableRecord { /// let partialPlayer = PartialPlayer(name: "Alice") /// /// // INSERT INTO player (name) VALUES ('Alice') RETURNING * - /// if let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) { - /// print(player.id) // The inserted id - /// print(player.name) // The inserted name - /// print(player.score) // The default score - /// } + /// let player = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) + /// print(player.id) // The inserted id + /// print(player.name) // The inserted name + /// print(player.score) // The default score /// } /// ``` /// @@ -250,20 +250,24 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`, if any. The result can be - /// nil when the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the insertion failed due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func insertAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try insertAndFetch(db, onConflict: conflictResolution, selection: T.databaseSelection) { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) } } diff --git a/GRDB/Record/PersistableRecord+Save.swift b/GRDB/Record/PersistableRecord+Save.swift index 9ba55346b9..80a9f3f815 100644 --- a/GRDB/Record/PersistableRecord+Save.swift +++ b/GRDB/Record/PersistableRecord+Save.swift @@ -39,7 +39,6 @@ extension PersistableRecord { extension PersistableRecord { #if GRDBCUSTOMSQLITE || GRDBCIPHER - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -52,26 +51,30 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) }) return success!.saved } @@ -128,7 +131,6 @@ extension PersistableRecord { return success.returned } #else - // TODO: GRDB7 make it unable to return an optional /// Executes an `INSERT RETURNING` or `UPDATE RETURNING` statement, and /// returns a new record built from the saved row. /// @@ -141,27 +143,31 @@ extension PersistableRecord { /// nil, /// is used. /// - parameter returnedType: The type of the returned record. - /// - returns: A record of type `returnedType`. The result can be nil when - /// the conflict policy is `IGNORE`. + /// - returns: A record of type `returnedType`. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or any /// error thrown by the persistence callbacks defined by the record type. + /// ``RecordError/recordNotFound(databaseTableName:key:)`` can be + /// thrown if the database changes fail due to the IGNORE conflict policy. @inlinable // allow specialization so that empty callbacks are removed @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ public func saveAndFetch( _ db: Database, onConflict conflictResolution: Database.ConflictResolution? = nil, as returnedType: T.Type) - throws -> T? + throws -> T { try willSave(db) - var success: (saved: PersistenceSuccess, returned: T?)? + var success: (saved: PersistenceSuccess, returned: T)? try aroundSave(db) { success = try updateOrInsertAndFetchWithCallbacks( db, onConflict: conflictResolution, selection: T.databaseSelection, fetch: { - try T.fetchOne($0) + if let result = try T.fetchOne($0) { + return result + } + throw recordNotFound(db) }) return success!.saved } diff --git a/GRDB/Record/Record.swift b/GRDB/Record/Record.swift index 47f37d7421..eed3a1292c 100644 --- a/GRDB/Record/Record.swift +++ b/GRDB/Record/Record.swift @@ -2,6 +2,12 @@ /// A base class for types that can be fetched and persisted in the database. /// +/// ## Overview +/// +/// - warning: `Record` is a legacy GRDB type. Since GRDB 7, it is not +/// recommended to define record types by subclassing the `Record` class. +/// See for more information. +/// /// ## Topics /// /// ### Creating Record Instances @@ -201,8 +207,7 @@ open class Record { var newValueIterator = try PersistenceContainer(self).makeIterator() return AnyIterator { // Loop until we find a change, or exhaust columns: - while let (column, newValue) = newValueIterator.next() { - let newDbValue = newValue?.databaseValue ?? .null + while let (column, newDbValue) = newValueIterator.next() { guard let oldRow, let oldDbValue: DatabaseValue = oldRow[column] else { return (column, nil) } @@ -281,6 +286,7 @@ open class Record { /// your implementation. /// /// - parameter db: A database connection. + /// - parameter columns: The updated columns. open func willUpdate(_ db: Database, columns: Set) throws { } /// Called around the record update. @@ -348,7 +354,7 @@ open class Record { /// ``` /// /// - parameter db: A database connection. - /// - parameter update: A function that updates the record. Its result is + /// - parameter save: A function that saves the recordsave: A function that saves the record. Its result is /// reserved for GRDB usage. open func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws { _ = try save() @@ -431,3 +437,9 @@ open class Record { extension Record: TableRecord { } extension Record: PersistableRecord { } extension Record: FetchableRecord { } + +// Explicit non-conformance to Sendable, because persistence methods mutate +// the instance (`referenceRow`). Nothing prevents a single Record instance +// from being concurrencly persisted in two distinct database connections. +@available(*, unavailable) +extension Record: Sendable { } diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index 4e8d8b1b4c..2f29ec8fbd 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -29,6 +29,7 @@ import Foundation /// - ``exists(_:id:)`` /// - ``exists(_:key:)-60hf2`` /// - ``exists(_:key:)-6ha6`` +/// - ``recordNotFound(_:)`` /// /// ### Throwing Record Not Found Errors /// @@ -40,7 +41,7 @@ import Foundation /// /// - ``deleteAll(_:)`` /// - ``deleteAll(_:ids:)`` -/// - ``deleteAll(_:keys:)-jbkm`` +/// - ``deleteAll(_:keys:)-5l3ih`` /// - ``deleteAll(_:keys:)-5s1jg`` /// - ``deleteOne(_:id:)`` /// - ``deleteOne(_:key:)-413u8`` @@ -70,7 +71,7 @@ import Foundation /// - ``filter(key:)-9ey53`` /// - ``filter(key:)-34lau`` /// - ``filter(keys:)-4hq8y`` -/// - ``filter(keys:)-7skw1`` +/// - ``filter(keys:)-s1q0`` /// - ``filter(literal:)`` /// - ``filter(sql:arguments:)`` /// - ``having(_:)`` @@ -133,15 +134,16 @@ public protocol TableRecord { /// /// ```swift /// struct Player: TableRecord { - /// static let databaseSelection: [any SQLSelectable] = [AllColumns()] + /// static var databaseSelection: [any SQLSelectable] { + /// [AllColumns()] + /// } /// } /// /// struct PartialPlayer: TableRecord { /// static let databaseTableName = "player" - /// static let databaseSelection: [any SQLSelectable] = [ - /// Column("id"), - /// Column("name"), - /// ] + /// static var databaseSelection: [any SQLSelectable] { + /// [Column("id"), Column("name")] + /// } /// } /// /// // SELECT * FROM player @@ -155,6 +157,19 @@ public protocol TableRecord { /// > explicitly declared as `[any SQLSelectable]`. If it is not, the /// > Swift compiler may silently miss the protocol requirement, /// > resulting in sticky `SELECT *` requests. + /// + /// > Important: Make sure the property is declared as a computed + /// > property (`static var`), instead of a stored property + /// > (`static let`). Computed properties avoid a compiler diagnostic + /// > with stored properties: + /// > + /// > ```swift + /// > // static property 'databaseSelection' is not + /// > // concurrency-safe because non-'Sendable' type + /// > // '[any SQLSelectable]' may have shared + /// > // mutable state. + /// > static let databaseSelection: [any SQLSelectable] = [AllColumns()] + /// > ``` static var databaseSelection: [any SQLSelectable] { get } } @@ -242,7 +257,9 @@ extension TableRecord { /// /// struct PartialPlayer: TableRecord { /// static let databaseTableName = "player" - /// static let databaseSelection = [Column("id"), Column("name")] + /// static var databaseSelection: [any SQLSelectable] { + /// [Column("id"), Column("name")] + /// } /// } /// /// try dbQueue.write { db in @@ -318,7 +335,6 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns whether a record exists for this primary key. /// @@ -410,11 +426,10 @@ extension TableRecord { /// - keys: A sequence of primary keys. /// - returns: The number of deleted records. @discardableResult - public static func deleteAll(_ db: Database, keys: Keys) - throws -> Int - where Keys: Sequence, Keys.Element: DatabaseValueConvertible - { - let keys = Array(keys) + public static func deleteAll( + _ db: Database, + keys: some Collection + ) throws -> Int { if keys.isEmpty { // Avoid hitting the database return 0 @@ -454,7 +469,6 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Deletes records identified by their primary keys, and returns the number /// of deleted records. @@ -483,9 +497,10 @@ extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// - ids: A collection of primary keys. /// - returns: The number of deleted records. @discardableResult - public static func deleteAll(_ db: Database, ids: IDS) throws -> Int - where IDS: Collection, IDS.Element == ID - { + public static func deleteAll( + _ db: Database, + ids: some Collection + ) throws -> Int { if ids.isEmpty { // Avoid hitting the database return 0 @@ -697,6 +712,15 @@ extension TableRecord { public enum RecordError: Error { /// A record does not exist in the database. /// + /// This error can be thrown from methods that update, such as + /// ``MutablePersistableRecord/update(_:onConflict:)``. In this case, + /// the error means that the database was not changed. + /// + /// It can also be thrown from methods that inserts or update with a + /// `RETURNING` clause, and the `IGNORE` conflict policy. In this case, + /// the error notifies that a conflict has prevented the change from + /// being applied. + /// /// - parameters: /// - databaseTableName: The table of the missing record. /// - key: The key of the missing record (column and values). @@ -740,7 +764,30 @@ extension TableRecord { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +extension TableRecord where Self: EncodableRecord { + /// Returns an error that tells that the record does not exist in + /// the database. + /// + /// - returns: ``RecordError/recordNotFound(databaseTableName:key:)``, or + /// any error that prevented the `RecordError` from being constructed. + public func recordNotFound(_ db: Database) -> any Error { + do { + let databaseTableName = type(of: self).databaseTableName + let primaryKey = try db.primaryKey(databaseTableName) + + let container = try PersistenceContainer(db, self) + let key = Dictionary(uniqueKeysWithValues: primaryKey.columns.map { + ($0, container.databaseValue(at: $0)) + }) + return RecordError.recordNotFound( + databaseTableName: databaseTableName, + key: key) + } catch { + return error + } + } +} + extension TableRecord where Self: Identifiable, ID: DatabaseValueConvertible { /// Returns an error for a record that does not exist in the database. /// @@ -757,4 +804,10 @@ public typealias PersistenceError = RecordError /// Calculating `defaultDatabaseTableName` is somewhat expensive due to the regular expression evaluation /// /// This cache mitigates the cost of the calculation by storing the name for later retrieval -private let defaultDatabaseTableNameCache = NSCache() +/// +/// Assume this non-Sendable cache of strings can be used from multiple +/// threads concurrently, because the NSCache documentation says: +/// +/// > You can add, remove, and query items in the cache from different +/// > threads without having to lock the cache yourself. +nonisolated(unsafe) private let defaultDatabaseTableNameCache = NSCache() diff --git a/GRDB/Utils/CaseInsensitiveIdentifier.swift b/GRDB/Utils/CaseInsensitiveIdentifier.swift index f1fd12c5de..9e80a36e2f 100644 --- a/GRDB/Utils/CaseInsensitiveIdentifier.swift +++ b/GRDB/Utils/CaseInsensitiveIdentifier.swift @@ -1,5 +1,5 @@ /// A case-preserving, case-insensitive identifier -/// that matches the ASCII version of sqlite3_stricmp +/// that intends to match the ASCII version of sqlite3_stricmp. struct CaseInsensitiveIdentifier: Hashable { private let lowercased: String let rawValue: String diff --git a/GRDB/Utils/Inflections+English.swift b/GRDB/Utils/Inflections+English.swift index 58338ec4f2..adea88eaec 100644 --- a/GRDB/Utils/Inflections+English.swift +++ b/GRDB/Utils/Inflections+English.swift @@ -43,8 +43,12 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. extension Inflections { - /// The default inflections - public static var `default`: Inflections = { + /// The default inflections. + /// + /// This global variable is not concurrency-safe. If you modify the + /// default inflections, do it once, early in the lifetime of your + /// application, before you access query interface methods. + nonisolated(unsafe) public static var `default`: Inflections = { // Defines the standard inflection rules. These are the starting point // for new projects and are not considered complete. The current set of // inflection rules is frozen. This means, we do not change them to diff --git a/GRDB/Utils/LockedBox.swift b/GRDB/Utils/LockedBox.swift deleted file mode 100644 index aef512604d..0000000000 --- a/GRDB/Utils/LockedBox.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation - -/// A LockedBox protects a value with an NSLock. -@propertyWrapper -final class LockedBox { - private var _wrappedValue: T - private var lock = NSLock() - - var wrappedValue: T { - get { read { $0 } } - set { update { $0 = newValue } } - } - - var projectedValue: LockedBox { self } - - init(wrappedValue: T) { - _wrappedValue = wrappedValue - } - - /// Runs the provided closure while holding a lock on the value. - /// - /// For example: - /// - /// // Prints "0" - /// @LockedBox var count = 0 - /// $count.read { print($0) } - /// - /// - parameter block: A closure that accepts the value. - @inline(__always) - @usableFromInline - func read(_ block: (T) throws -> U) rethrows -> U { - lock.lock() - defer { lock.unlock() } - return try block(_wrappedValue) - } - - /// Runs the provided closure while holding a lock on the value. - /// - /// For example: - /// - /// // Prints "1" - /// @LockedBox var count = 0 - /// $count.update { $0 += 1 } - /// print(count) - /// - /// - parameter block: A closure that can modify the value. - func update(_ block: (inout T) throws -> U) rethrows -> U { - lock.lock() - defer { lock.unlock() } - return try block(&_wrappedValue) - } -} - -extension LockedBox where T: Numeric { - @discardableResult - func increment() -> T { - update { n in - n += 1 - return n - } - } - - @discardableResult - func decrement() -> T { - update { n in - n -= 1 - return n - } - } -} - -extension LockedBox: @unchecked Sendable where T: Sendable { } diff --git a/GRDB/Utils/Mutex.swift b/GRDB/Utils/Mutex.swift new file mode 100644 index 0000000000..771a57a4a6 --- /dev/null +++ b/GRDB/Utils/Mutex.swift @@ -0,0 +1,83 @@ +import Foundation + +/// A Mutex protects a value with an NSLock. +/// +/// We'll replace it with the SE-0433 Mutex when it is available. +/// +final class Mutex { + private var _value: T + private var lock = NSLock() + + init(_ value: T) { + _value = value + } + + /// Runs the provided closure while holding a lock on the value. + /// + /// - parameter body: A closure that can modify the value. + func withLock(_ body: (inout T) throws -> U) rethrows -> U { + lock.lock() + defer { lock.unlock() } + return try body(&_value) + } +} + +// Inspired by +extension Mutex { + func load() -> T { + withLock { $0 } + } + + func store(_ value: T) { + withLock { $0 = value } + } +} + +extension Mutex where T: Numeric { + @discardableResult + func increment() -> T { + withLock { n in + n += 1 + return n + } + } + + @discardableResult + func decrement() -> T { + withLock { n in + n -= 1 + return n + } + } +} + +extension Mutex: @unchecked Sendable where T: Sendable { } + +// MARK: - UnsafeSendableMutex + +/// `UnsafeSendableMutex` is a Mutex that is always Sendable. It is unsafe, +/// because it does not guarantee that its value won't escape from the +/// critical section. +/// +/// We'll replace it with the SE-0433 Mutex when it is available. +/// +/// +/// For a longer discussion about Sendable and mutexes, see +/// +final class UnsafeSendableMutex: @unchecked Sendable { + private var _value: T + private var lock = NSLock() + + init(_ value: sending T) { + _value = value + } + + /// Runs the provided closure while holding a lock on the value. + /// + /// - parameter body: A closure that can modify the value. + func withLock(_ body: (inout T) throws -> U) rethrows -> U { + lock.lock() + defer { lock.unlock() } + return try body(&_value) + } +} diff --git a/GRDB/Utils/OnDemandFuture.swift b/GRDB/Utils/OnDemandFuture.swift index 6826014a47..938750d4ab 100644 --- a/GRDB/Utils/OnDemandFuture.swift +++ b/GRDB/Utils/OnDemandFuture.swift @@ -14,14 +14,16 @@ import Foundation /// Both two extra scheduling guarantees are used by GRDB in order to be /// able to spawn concurrent database reads right from the database writer /// queue, and fulfill GRDB preconditions. -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +/// +/// OnDemandFuture also adds Sendable requirements that avoid +/// compiler warnings. struct OnDemandFuture: Publisher { - typealias Promise = (Result) -> Void + typealias Promise = @Sendable (Result) -> Void typealias Output = Output typealias Failure = Failure - fileprivate let attemptToFulfill: (@escaping Promise) -> Void + fileprivate let attemptToFulfill: @Sendable (@escaping Promise) -> Void - init(_ attemptToFulfill: @escaping (@escaping Promise) -> Void) { + init(_ attemptToFulfill: @escaping @Sendable (@escaping Promise) -> Void) { self.attemptToFulfill = attemptToFulfill } @@ -33,9 +35,9 @@ struct OnDemandFuture: Publisher { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) -private class OnDemandFutureSubscription: Subscription { - typealias Promise = (Result) -> Void +private class OnDemandFutureSubscription: Subscription, @unchecked Sendable { + // @unchecked because `state` is protected with `lock`. + typealias Promise = @Sendable (Result) -> Void private enum State { case waitingForDemand(downstream: Downstream, attemptToFulfill: (@escaping Promise) -> Void) @@ -47,7 +49,7 @@ private class OnDemandFutureSubscription: Subscription { private let lock = NSRecursiveLock() // Allow re-entrancy init( - attemptToFulfill: @escaping (@escaping Promise) -> Void, + attemptToFulfill: @escaping @Sendable (@escaping Promise) -> Void, downstream: Downstream) { self.state = .waitingForDemand(downstream: downstream, attemptToFulfill: attemptToFulfill) diff --git a/GRDB/Utils/OrderedDictionary.swift b/GRDB/Utils/OrderedDictionary.swift index 4f7d5242b8..ad40174e6b 100644 --- a/GRDB/Utils/OrderedDictionary.swift +++ b/GRDB/Utils/OrderedDictionary.swift @@ -114,12 +114,10 @@ struct OrderedDictionary { return OrderedDictionary(keys: keys, dictionary: dictionary) } - mutating func merge( - _ other: S, - uniquingKeysWith combine: (Value, Value) throws -> Value) - rethrows - where S: Sequence, S.Element == (Key, Value) - { + mutating func merge( + _ other: some Sequence<(Key, Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows { for (key, value) in other { if let current = self[key] { self[key] = try combine(current, value) @@ -129,12 +127,10 @@ struct OrderedDictionary { } } - mutating func merge( - _ other: S, - uniquingKeysWith combine: (Value, Value) throws -> Value) - rethrows - where S: Sequence, S.Element == (key: Key, value: Value) - { + mutating func merge( + _ other: some Sequence<(key: Key, value: Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows { for (key, value) in other { if let current = self[key] { self[key] = try combine(current, value) @@ -144,23 +140,19 @@ struct OrderedDictionary { } } - func merging( - _ other: S, - uniquingKeysWith combine: (Value, Value) throws -> Value) - rethrows -> OrderedDictionary - where S: Sequence, S.Element == (Key, Value) - { + func merging( + _ other: some Sequence<(Key, Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows -> OrderedDictionary { var result = self try result.merge(other, uniquingKeysWith: combine) return result } - func merging( - _ other: S, - uniquingKeysWith combine: (Value, Value) throws -> Value) - rethrows -> OrderedDictionary - where S: Sequence, S.Element == (key: Key, value: Value) - { + func merging( + _ other: some Sequence<(key: Key, value: Value)>, + uniquingKeysWith combine: (Value, Value) throws -> Value + ) rethrows -> OrderedDictionary { var result = self try result.merge(other, uniquingKeysWith: combine) return result @@ -207,6 +199,8 @@ extension OrderedDictionary: CustomStringConvertible { } } +extension OrderedDictionary: Sendable where Key: Sendable, Value: Sendable { } + extension Dictionary { init(_ orderedDictionary: OrderedDictionary) { self = orderedDictionary.dictionary diff --git a/GRDB/Utils/Pool.swift b/GRDB/Utils/Pool.swift index 6e1014c75c..661c9c4d9d 100644 --- a/GRDB/Utils/Pool.swift +++ b/GRDB/Utils/Pool.swift @@ -35,8 +35,9 @@ import Dispatch /// got 2 /// got 1 /// got 3 -final class Pool { - private class Item { +final class Pool: Sendable { + private class Item: @unchecked Sendable { + // @unchecked Sendable because `isAvailable` is protected by `contentLock`. let element: T var isAvailable: Bool @@ -46,8 +47,19 @@ final class Pool { } } - private let makeElement: () throws -> T - @ReadWriteBox private var items: [Item] = [] + private struct Content { + var items: [Item] + + /// The number of created items. May become greater than the number + /// of elements in items, as some items are destroyed and other + /// are created. + var createdCount = 0 + } + + typealias ElementAndRelease = (element: T, release: @Sendable (PoolCompletion) -> Void) + + private let makeElement: @Sendable (Int) throws -> T + private let contentLock = ReadWriteLock(Content(items: [], createdCount: 0)) private let itemsSemaphore: DispatchSemaphore // limits the number of elements private let itemsGroup: DispatchGroup // knows when no element is used private let barrierQueue: DispatchQueue @@ -59,11 +71,12 @@ final class Pool { /// - maximumCount: The maximum number of elements. /// - qos: The quality of service of asynchronous accesses. /// - makeElement: A function that creates an element. It is called - /// on demand. + /// on demand. Its argument is the index of the created elements + /// (1, then 2, etc). init( maximumCount: Int, qos: DispatchQoS = .unspecified, - makeElement: @escaping () throws -> T) + makeElement: @escaping @Sendable (_ index: Int) throws -> T) { GRDBPrecondition(maximumCount > 0, "Pool size must be at least 1") self.makeElement = makeElement @@ -75,19 +88,20 @@ final class Pool { /// Returns a tuple (element, release) /// Client must call release(), only once, after the element has been used. - func get() throws -> (element: T, release: (PoolCompletion) -> Void) { + func get() throws -> ElementAndRelease { try barrierQueue.sync { itemsSemaphore.wait() itemsGroup.enter() do { - let item = try $items.update { items -> Item in - if let item = items.first(where: \.isAvailable) { + let item = try contentLock.withLock { content -> Item in + if let item = content.items.first(where: \.isAvailable) { item.isAvailable = false return item } else { - let element = try makeElement() + content.createdCount += 1 + let element = try makeElement(content.createdCount) let item = Item(element: element, isAvailable: false) - items.append(item) + content.items.append(item) return item } } @@ -107,7 +121,7 @@ final class Pool { /// /// - important: The `execute` argument is executed in a serial dispatch /// queue, so make sure you use the element asynchronously. - func asyncGet(_ execute: @escaping (Result<(element: T, release: (PoolCompletion) -> Void), Error>) -> Void) { + func asyncGet(_ execute: @escaping @Sendable (Result) -> Void) { // Inspired by https://khanlou.com/2016/04/the-GCD-handbook/ // > We wait on the semaphore in the serial queue, which means that // > we’ll have at most one blocked thread when we reach maximum @@ -128,7 +142,7 @@ final class Pool { } private func release(_ item: Item, completion: PoolCompletion) { - $items.update { items in + contentLock.withLock { content in switch completion { case .reuse: // This is why Item is a class, not a struct: so that we can @@ -136,8 +150,8 @@ final class Pool { item.isAvailable = true case .discard: // Discard should be rare: perform lookup. - if let index = items.firstIndex(where: { $0 === item }) { - items.remove(at: index) + if let index = content.items.firstIndex(where: { $0 === item }) { + content.items.remove(at: index) } } } @@ -148,8 +162,8 @@ final class Pool { /// Performs a block on each pool element, available or not. /// The block is run is some arbitrary dispatch queue. func forEach(_ body: (T) throws -> Void) rethrows { - try $items.read { items in - for item in items { + try contentLock.read { content in + for item in content.items { try body(item.element) } } @@ -158,7 +172,7 @@ final class Pool { /// Removes all elements from the pool. /// Currently used elements won't be reused. func removeAll() { - items = [] + contentLock.withLock { $0.items.removeAll() } } /// Blocks until no element is used, and runs the `barrier` function before @@ -172,7 +186,7 @@ final class Pool { /// Asynchronously runs the `barrier` function when no element is used, and /// before any other element is dequeued. - func asyncBarrier(execute barrier: @escaping () -> Void) { + func asyncBarrier(execute barrier: @escaping @Sendable () -> Void) { barrierQueue.async(flags: [.barrier]) { self.itemsGroup.wait() barrier() diff --git a/GRDB/Utils/ReadWriteBox.swift b/GRDB/Utils/ReadWriteBox.swift deleted file mode 100644 index 1037ef8b39..0000000000 --- a/GRDB/Utils/ReadWriteBox.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Dispatch - -/// A ReadWriteBox grants multiple readers and single-writer guarantees on a -/// value. It is backed by a concurrent DispatchQueue. -@propertyWrapper -final class ReadWriteBox { - private var _wrappedValue: T - private var queue: DispatchQueue - - var wrappedValue: T { - get { read { $0 } } - set { update { $0 = newValue } } - } - - var projectedValue: ReadWriteBox { self } - - init(wrappedValue: T) { - _wrappedValue = wrappedValue - queue = DispatchQueue(label: "GRDB.ReadWriteBox", attributes: [.concurrent]) - } - - func read(_ block: (T) throws -> U) rethrows -> U { - try queue.sync { - try block(_wrappedValue) - } - } - - func update(_ block: (inout T) throws -> U) rethrows -> U { - try queue.sync(flags: [.barrier]) { - try block(&_wrappedValue) - } - } -} - -extension ReadWriteBox where T: Numeric { - @discardableResult - func increment() -> T { - update { n in - n += 1 - return n - } - } - - @discardableResult - func decrement() -> T { - update { n in - n -= 1 - return n - } - } -} diff --git a/GRDB/Utils/ReadWriteLock.swift b/GRDB/Utils/ReadWriteLock.swift new file mode 100644 index 0000000000..85f191efb8 --- /dev/null +++ b/GRDB/Utils/ReadWriteLock.swift @@ -0,0 +1,32 @@ +import Dispatch + +/// A ReadWriteLock grants multiple readers and single-writer guarantees on +/// a value. It is backed by a concurrent DispatchQueue. +final class ReadWriteLock { + private var _value: T + private var queue: DispatchQueue + + init(_ value: T) { + _value = value + queue = DispatchQueue(label: "GRDB.ReadWriteLock", attributes: [.concurrent]) + } + + /// Reads the value. + func read(_ body: (T) throws -> U) rethrows -> U { + try queue.sync { + try body(_value) + } + } + + /// Runs the provided closure while holding a lock on the value. + /// + /// - parameter body: A closure that can modify the value. + func withLock(_ body: (inout T) throws -> U) rethrows -> U { + try queue.sync(flags: [.barrier]) { + try body(&_value) + } + } +} + +// @unchecked because `_value` is protected by `queue` +extension ReadWriteLock: @unchecked Sendable where T: Sendable { } diff --git a/GRDB/Utils/ReceiveValuesOn.swift b/GRDB/Utils/ReceiveValuesOn.swift index abc688f78a..2df87fc46a 100644 --- a/GRDB/Utils/ReceiveValuesOn.swift +++ b/GRDB/Utils/ReceiveValuesOn.swift @@ -11,7 +11,6 @@ import Foundation /// This scheduling guarantee is used by GRDB in order to be able /// to make promises on the scheduling of database values without surprising /// the users as in . -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) struct ReceiveValuesOn: Publisher { typealias Output = Upstream.Output typealias Failure = Upstream.Failure @@ -30,7 +29,6 @@ struct ReceiveValuesOn: Publisher { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) private class ReceiveValuesOnSubscription: Subscription, Subscriber where Upstream: Publisher, @@ -211,7 +209,6 @@ where } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Publisher { /// Specifies the scheduler on which to receive values from the publisher /// diff --git a/GRDB/Utils/Utils.swift b/GRDB/Utils/Utils.swift index 90e3ca5573..561c3b91f1 100644 --- a/GRDB/Utils/Utils.swift +++ b/GRDB/Utils/Utils.swift @@ -67,8 +67,8 @@ extension Dictionary { } extension DispatchQueue { - private static var mainKey: DispatchSpecificKey<()> = { - let key = DispatchSpecificKey<()>() + private static let mainKey: DispatchSpecificKey = { + let key = DispatchSpecificKey() DispatchQueue.main.setSpecific(key: key, value: ()) return key }() @@ -118,14 +118,29 @@ func throwingFirstError(execute: () throws -> T, finally: () throws -> Void) return result! } -struct PrintOutputStream: TextOutputStream { +struct PrintOutputStream: TextOutputStream, Sendable { func write(_ string: String) { Swift.print(string) } } +/// A Sendable strong reference to an object. +/// +/// This type hides its retained object in order to provide the +/// Sendable guarantee. +final class StrongReference: @unchecked Sendable { + private let value: Value + + init(_ value: Value) { + self.value = value + } +} + /// Concatenates two functions -func concat(_ rhs: (() -> Void)?, _ lhs: (() -> Void)?) -> (() -> Void)? { +func concat( + _ rhs: (@Sendable () -> Void)?, + _ lhs: (@Sendable () -> Void)? +) -> (@Sendable () -> Void)? { switch (rhs, lhs) { case let (rhs, nil): return rhs @@ -140,7 +155,10 @@ func concat(_ rhs: (() -> Void)?, _ lhs: (() -> Void)?) -> (() -> Void)? { } /// Concatenates two functions -func concat(_ rhs: ((T) -> Void)?, _ lhs: ((T) -> Void)?) -> ((T) -> Void)? { +func concat( + _ rhs: (@Sendable (T) -> Void)?, + _ lhs: (@Sendable (T) -> Void)? +) -> (@Sendable (T) -> Void)? { switch (rhs, lhs) { case let (rhs, nil): return rhs diff --git a/GRDB/ValueObservation/DatabaseCancellable.swift b/GRDB/ValueObservation/DatabaseCancellable.swift index de62987917..fc904892f0 100644 --- a/GRDB/ValueObservation/DatabaseCancellable.swift +++ b/GRDB/ValueObservation/DatabaseCancellable.swift @@ -5,7 +5,7 @@ /// ### Supporting Types /// /// - ``AnyDatabaseCancellable`` -public protocol DatabaseCancellable { +public protocol DatabaseCancellable: Sendable { /// Cancel the activity. func cancel() } @@ -15,31 +15,39 @@ public protocol DatabaseCancellable { /// /// An `AnyDatabaseCancellable` instance automatically calls ``cancel()`` /// when deinitialized. -public class AnyDatabaseCancellable: DatabaseCancellable { - private var _cancel: (() -> Void)? +public final class AnyDatabaseCancellable: DatabaseCancellable { + private let cancelMutex: Mutex<(@Sendable () -> Void)?> + + var isCancelled: Bool { + cancelMutex.withLock { $0 == nil } + } + + convenience init() { + self.init(cancel: { }) + } /// Initializes the cancellable object with the given cancel-time closure. - public init(cancel: @escaping () -> Void) { - _cancel = cancel + public init(cancel: @escaping @Sendable () -> Void) { + cancelMutex = Mutex(cancel) } /// Creates a cancellable object that forwards cancellation to `base`. public convenience init(_ base: some DatabaseCancellable) { - var cancellable = Optional.some(base) self.init { - cancellable?.cancel() - cancellable = nil // Release memory + base.cancel() } } deinit { - _cancel?() + cancel() } public func cancel() { - // Don't prevent multiple concurrent calls to _cancel, because it is - // pointless. But release memory! - _cancel?() - _cancel = nil + let cancel = cancelMutex.withLock { + let cancel = $0 + $0 = nil + return cancel + } + cancel?() } } diff --git a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift index a287a37675..9cc507ddfd 100644 --- a/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueConcurrentObserver.swift @@ -18,7 +18,10 @@ import Foundation /// reducing stage. /// /// **Notify** is calling user callbacks, in case of database change or error. -final class ValueConcurrentObserver { +final class ValueConcurrentObserver: @unchecked Sendable +where Reducer: ValueReducer, + Scheduler: ValueObservationScheduler +{ // MARK: - Configuration // // Configuration is not mutable. @@ -76,25 +79,25 @@ final class ValueConcurrentObserver Reducer.Fetched { + func fetch(_ db: Database) throws -> Reducer.Fetcher.Value { try db.isolated(readOnly: true) { - try reducer._fetch(db) + try fetcher.fetch(db) } } - func fetchRecordingObservedRegion(_ db: Database) throws -> (Reducer.Fetched, DatabaseRegion) { + func fetchRecordingObservedRegion(_ db: Database) throws -> (Reducer.Fetcher.Value, DatabaseRegion) { var region = DatabaseRegion() let fetchedValue = try db.isolated(readOnly: true) { try db.recordingSelection(®ion) { - try reducer._fetch(db) + try fetcher.fetch(db) } } return try (fetchedValue, region.observableRegion(db)) @@ -146,7 +149,7 @@ final class ValueConcurrentObserver Void) + onChange: @escaping @Sendable (Reducer.Value) -> Void) { // Configuration self.scheduler = scheduler @@ -170,9 +173,9 @@ final class ValueConcurrentObserver=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) extension ValueConcurrentObserver { /// Synchronously starts the observation, and returns the initial value. /// @@ -299,7 +301,9 @@ extension ValueConcurrentObserver { return try syncStartWithoutWALSnapshot(from: databaseAccess) } - let (fetchedValue, initialRegion): (Reducer.Fetched, DatabaseRegion) = try initialFetchTransaction.read { db in + let fetchedValue: Reducer.Fetcher.Value + let initialRegion: DatabaseRegion + (fetchedValue, initialRegion) = try initialFetchTransaction.read { db in switch trackingMode { case let .constantRegion(regions): let fetchedValue = try databaseAccess.fetch(db) @@ -353,10 +357,12 @@ extension ValueConcurrentObserver { let initialFetchTransaction = try result.get() // Second async jump because that's how // `DatabasePool.asyncWALSnapshotTransaction` has to be used. - initialFetchTransaction.asyncRead { db in + initialFetchTransaction.asyncRead { dbResult in do { - let fetchedValue: Reducer.Fetched + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue: Reducer.Fetcher.Value let initialRegion: DatabaseRegion + let db = try dbResult.get() switch self.trackingMode { case let .constantRegion(regions): @@ -430,11 +436,9 @@ extension ValueConcurrentObserver { // checkpointed. That's why we'll keep `initialFetchTransaction` // alive until the comparison is done. // - // However, we want to release `initialFetchTransaction` as soon as + // However, we want to close `initialFetchTransaction` as soon as // possible, so that the reader connection it holds becomes - // available for other reads. It will be released when this optional - // is set to nil: - var initialFetchTransaction: WALSnapshotTransaction? = initialFetchTransaction + // available for other reads. databaseAccess.dbPool.asyncWriteWithoutTransaction { writerDB in let events = self.lock.synchronized { self.notificationCallbacks?.events } @@ -447,7 +451,7 @@ extension ValueConcurrentObserver { // Was the database modified since the initial fetch? let isModified: Bool if let currentWALSnapshot = try? WALSnapshot(writerDB) { - let ordering = initialFetchTransaction!.walSnapshot.compare(currentWALSnapshot) + let ordering = initialFetchTransaction.walSnapshot.compare(currentWALSnapshot) assert(ordering <= 0, "Unexpected snapshot ordering") isModified = ordering < 0 } else { @@ -455,15 +459,16 @@ extension ValueConcurrentObserver { isModified = true } - // Comparison done: end the WAL snapshot transaction + // Comparison done: close the WAL snapshot transaction // and release its reader connection. - initialFetchTransaction = nil + initialFetchTransaction.close() if isModified { events.databaseDidChange?() // Fetch - let fetchedValue: Reducer.Fetched + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue: Reducer.Fetcher.Value switch self.trackingMode { case .constantRegion: @@ -540,7 +545,9 @@ extension ValueConcurrentObserver { // for observing the database is to be able to fetch the initial value // without having to wait for an eventual long-running write // transaction to complete. - let (fetchedValue, initialRegion) = try databaseAccess.dbPool.read { db -> (Reducer.Fetched, DatabaseRegion) in + let fetchedValue: Reducer.Fetcher.Value + let initialRegion: DatabaseRegion + (fetchedValue, initialRegion) = try databaseAccess.dbPool.read { db in switch trackingMode { case let .constantRegion(regions): let fetchedValue = try databaseAccess.fetch(db) @@ -585,7 +592,8 @@ extension ValueConcurrentObserver { do { // Fetch - let fetchedValue: Reducer.Fetched + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue: Reducer.Fetcher.Value let initialRegion: DatabaseRegion let db = try dbResult.get() switch self.trackingMode { @@ -645,7 +653,8 @@ extension ValueConcurrentObserver { do { try writerDB.isolated(readOnly: true) { // Fetch - let fetchedValue: Reducer.Fetched + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue: Reducer.Fetcher.Value let observedRegion: DatabaseRegion switch self.trackingMode { case .constantRegion: @@ -775,13 +784,12 @@ extension ValueConcurrentObserver: TransactionObserver { } catch { stopDatabaseObservation(writerDB) notifyError(error) - return } } } private func setNeedsFetching(databaseAccess: DatabaseAccess) { - $fetchingState.update { state in + fetchingStateMutex.withLock { state in switch state { case .idle: state = .fetching @@ -807,7 +815,7 @@ extension ValueConcurrentObserver: TransactionObserver { self.reduce(fetchResult) - $fetchingState.update { state in + fetchingStateMutex.withLock { state in switch state { case .idle: // GRDB bug @@ -824,7 +832,10 @@ extension ValueConcurrentObserver: TransactionObserver { } } - private func reduce(_ fetchResult: Result) { + private func reduce(_ fetchResult: Result) { + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchResult = fetchResult + reduceQueue.async { do { let fetchedValue = try fetchResult.get() diff --git a/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift b/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift index e7dc67bda9..67d915bf04 100644 --- a/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift +++ b/GRDB/ValueObservation/Observers/ValueWriteOnlyObserver.swift @@ -18,10 +18,10 @@ import Foundation /// reducing stage. /// /// **Notify** is calling user callbacks, in case of database change or error. -final class ValueWriteOnlyObserver< - Writer: DatabaseWriter, - Reducer: ValueReducer, - Scheduler: ValueObservationScheduler> +final class ValueWriteOnlyObserver: @unchecked Sendable +where Writer: DatabaseWriter, + Reducer: ValueReducer, + Scheduler: ValueObservationScheduler { // MARK: - Configuration // @@ -83,26 +83,26 @@ final class ValueWriteOnlyObserver< /// If true, database values are fetched from a read-only access. private let readOnly: Bool - /// A reducer that fetches database values. - private let reducer: Reducer + /// The fetcher that fetches database values. + private let fetcher: Reducer.Fetcher - init(writer: Writer, readOnly: Bool, reducer: Reducer) { + init(writer: Writer, readOnly: Bool, fetcher: Reducer.Fetcher) { self.writer = writer self.readOnly = readOnly - self.reducer = reducer + self.fetcher = fetcher } - func fetch(_ db: Database) throws -> Reducer.Fetched { + func fetch(_ db: Database) throws -> Reducer.Fetcher.Value { try db.isolated(readOnly: readOnly) { - try reducer._fetch(db) + try fetcher.fetch(db) } } - func fetchRecordingObservedRegion(_ db: Database) throws -> (Reducer.Fetched, DatabaseRegion) { + func fetchRecordingObservedRegion(_ db: Database) throws -> (Reducer.Fetcher.Value, DatabaseRegion) { var region = DatabaseRegion() let fetchedValue = try db.isolated(readOnly: readOnly) { try db.recordingSelection(®ion) { - try reducer._fetch(db) + try fetcher.fetch(db) } } return try (fetchedValue, region.observableRegion(db)) @@ -150,7 +150,7 @@ final class ValueWriteOnlyObserver< trackingMode: ValueObservationTrackingMode, reducer: Reducer, events: ValueObservationEvents, - onChange: @escaping (Reducer.Value) -> Void) + onChange: @escaping @Sendable (Reducer.Value) -> Void) { // Configuration self.scheduler = scheduler @@ -160,9 +160,9 @@ final class ValueWriteOnlyObserver< self.databaseAccess = DatabaseAccess( writer: writer, readOnly: readOnly, - // ValueReducer semantics guarantees that reducer._fetch + // ValueReducer semantics guarantees that the fetcher // is independent from the reducer state - reducer: reducer) + fetcher: reducer._makeFetcher()) self.notificationCallbacks = NotificationCallbacks(events: events, onChange: onChange) self.reducer = reducer self.reduceQueue = DispatchQueue( @@ -252,9 +252,11 @@ extension ValueWriteOnlyObserver { writer.asyncWriteWithoutTransaction { db in do { // Fetch & Start observing the database - guard let fetchedValue = try self.fetchAndStartObservation(db) else { + guard let _fetchedValue = try self.fetchAndStartObservation(db) else { return /* Cancelled */ } + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue = _fetchedValue // Reduce // @@ -301,7 +303,7 @@ extension ValueWriteOnlyObserver { /// By grouping the initial fetch and the beginning of observation in a /// single database access, we are sure that no concurrent write can happen /// during the initial fetch, and that we won't miss any future change. - private func fetchAndStartObservation(_ db: Database) throws -> Reducer.Fetched? { + private func fetchAndStartObservation(_ db: Database) throws -> Reducer.Fetcher.Value? { let (events, databaseAccess) = lock.synchronized { (notificationCallbacks?.events, self.databaseAccess) } @@ -380,7 +382,8 @@ extension ValueWriteOnlyObserver: TransactionObserver { do { // Fetch - let fetchedValue: Reducer.Fetched + // Assume this value can safely be sent to the reduce queue. + nonisolated(unsafe) let fetchedValue: Reducer.Fetcher.Value switch trackingMode { case .constantRegion, .constantRegionRecordedFromSelection: diff --git a/GRDB/ValueObservation/Reducers/Fetch.swift b/GRDB/ValueObservation/Reducers/Fetch.swift index 4f9b16c88f..693c934bbf 100644 --- a/GRDB/ValueObservation/Reducers/Fetch.swift +++ b/GRDB/ValueObservation/Reducers/Fetch.swift @@ -1,16 +1,24 @@ extension ValueReducers { /// A `ValueReducer` that perform database fetches. - public struct Fetch: ValueReducer { - private let __fetch: (Database) throws -> Value + public struct Fetch: ValueReducer { + public struct _Fetcher: _ValueReducerFetcher { + let _fetch: @Sendable (Database) throws -> Value + + public func fetch(_ db: Database) throws -> Value { + assert(db.isInsideTransaction, "Fetching in a non-isolated way is illegal") + return try _fetch(db) + } + } + + private let _fetch: @Sendable (Database) throws -> Value /// Creates a reducer which passes raw fetched values through. - init(fetch: @escaping (Database) throws -> Value) { - self.__fetch = fetch + init(fetch: @escaping @Sendable (Database) throws -> Value) { + self._fetch = fetch } - public func _fetch(_ db: Database) throws -> Value { - assert(db.isInsideTransaction, "Fetching in a non-isolated way is illegal") - return try __fetch(db) + public func _makeFetcher() -> _Fetcher { + _Fetcher(_fetch: _fetch) } public func _value(_ fetched: Value) -> Value? { diff --git a/GRDB/ValueObservation/Reducers/Map.swift b/GRDB/ValueObservation/Reducers/Map.swift index b69984b248..ba6f1ec674 100644 --- a/GRDB/ValueObservation/Reducers/Map.swift +++ b/GRDB/ValueObservation/Reducers/Map.swift @@ -18,7 +18,7 @@ extension ValueObservation { /// /// - parameter transform: A closure that takes one value as its parameter /// and returns a new value. - public func map(_ transform: @escaping (Reducer.Value) throws -> T) + public func map(_ transform: @escaping @Sendable (Reducer.Value) throws -> T) -> ValueObservation> { mapReducer { ValueReducers.Map($0, transform) } @@ -26,28 +26,26 @@ extension ValueObservation { } extension ValueReducers { - /// A `ValueReducer` whose values consist of those in a `Base` reduced + /// A `ValueReducer` whose values consist of those in a `Base` reducer /// passed through a transform function. /// /// See ``ValueObservation/map(_:)``. - public struct Map: _ValueReducer { + public struct Map: ValueReducer { private var base: Base private let transform: (Base.Value) throws -> Value - init(_ base: Base, _ transform: @escaping (Base.Value) throws -> Value) { + init(_ base: Base, _ transform: @escaping @Sendable (Base.Value) throws -> Value) { self.base = base self.transform = transform } - public mutating func _value(_ fetched: Base.Fetched) throws -> Value? { + public func _makeFetcher() -> Base.Fetcher { + base._makeFetcher() + } + + public mutating func _value(_ fetched: Base.Fetcher.Value) throws -> Value? { guard let value = try base._value(fetched) else { return nil } return try transform(value) } } } - -extension ValueReducers.Map: ValueReducer where Base: ValueReducer { - public func _fetch(_ db: Database) throws -> Base.Fetched { - try base._fetch(db) - } -} diff --git a/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift b/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift index 6d2e64e537..07ec82f593 100644 --- a/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift +++ b/GRDB/ValueObservation/Reducers/RemoveDuplicates.swift @@ -5,10 +5,22 @@ extension ValueObservation { /// - parameter predicate: A closure to evaluate whether two values are /// equivalent, for purposes of filtering. Return true from this closure /// to indicate that the second element is a duplicate of the first. - public func removeDuplicates(by predicate: @escaping (Reducer.Value, Reducer.Value) -> Bool) - -> ValueObservation> - { - mapReducer { ValueReducers.RemoveDuplicates($0, predicate: predicate) } + public func removeDuplicates( + by predicate: sending @escaping (Reducer.Value, Reducer.Value) -> Bool + ) -> ValueObservation> { + // The predicate is marked `sending`, which allows us to statically + // determine that it will have no other uses after this call. + // (according to ) + // + // And because `predicate` will only be used serially, in the + // reducer queue of `ValueObservation` observers, we can say that + // this is safe. + // + // Anyway if we would not accept non-sendable closures, we could + // not deal with `Equatable.==`... + nonisolated(unsafe) let predicate = predicate + + return mapReducer { ValueReducers.RemoveDuplicates($0, predicate: predicate) } } } @@ -67,7 +79,7 @@ extension ValueReducers { /// previously observed value. /// /// See ``ValueObservation/removeDuplicates()``. - public struct RemoveDuplicates: _ValueReducer { + public struct RemoveDuplicates: ValueReducer { private var base: Base private var previousValue: Base.Value? private var predicate: (Base.Value, Base.Value) -> Bool @@ -77,7 +89,11 @@ extension ValueReducers { self.predicate = predicate } - public mutating func _value(_ fetched: Base.Fetched) throws -> Base.Value? { + public func _makeFetcher() -> Base.Fetcher { + base._makeFetcher() + } + + public mutating func _value(_ fetched: Base.Fetcher.Value) throws -> Base.Value? { guard let value = try base._value(fetched) else { return nil } @@ -90,9 +106,3 @@ extension ValueReducers { } } } - -extension ValueReducers.RemoveDuplicates: ValueReducer where Base: ValueReducer { - public func _fetch(_ db: Database) throws -> Base.Fetched { - try base._fetch(db) - } -} diff --git a/GRDB/ValueObservation/Reducers/Trace.swift b/GRDB/ValueObservation/Reducers/Trace.swift index bb2db4f623..107d80de78 100644 --- a/GRDB/ValueObservation/Reducers/Trace.swift +++ b/GRDB/ValueObservation/Reducers/Trace.swift @@ -4,12 +4,26 @@ extension ValueReducers { /// /// See ``ValueObservation/handleEvents(willStart:willFetch:willTrackRegion:databaseDidChange:didReceiveValue:didFail:didCancel:)`` /// and ``ValueObservation/print(_:to:)``. - public struct Trace: _ValueReducer { + public struct Trace: ValueReducer { + public struct _Fetcher: _ValueReducerFetcher { + let base: Base.Fetcher + let willFetch: @Sendable () -> Void + + public func fetch(_ db: Database) throws -> Base.Fetcher.Value { + willFetch() + return try base.fetch(db) + } + } + var base: Base - let willFetch: () -> Void + let willFetch: @Sendable () -> Void let didReceiveValue: (Base.Value) -> Void - public mutating func _value(_ fetched: Base.Fetched) throws -> Base.Value? { + public func _makeFetcher() -> _Fetcher { + _Fetcher(base: base._makeFetcher(), willFetch: willFetch) + } + + public mutating func _value(_ fetched: Base.Fetcher.Value) throws -> Base.Value? { guard let value = try base._value(fetched) else { return nil } @@ -19,10 +33,3 @@ extension ValueReducers { } // swiftlint:enable line_length } - -extension ValueReducers.Trace: ValueReducer where Base: ValueReducer { - public func _fetch(_ db: Database) throws -> Base.Fetched { - willFetch() - return try base._fetch(db) - } -} diff --git a/GRDB/ValueObservation/Reducers/ValueReducer.swift b/GRDB/ValueObservation/Reducers/ValueReducer.swift index 0a4706057f..8c7386693a 100644 --- a/GRDB/ValueObservation/Reducers/ValueReducer.swift +++ b/GRDB/ValueObservation/Reducers/ValueReducer.swift @@ -1,10 +1,27 @@ +// A `ValueReducer` fetches and transforms the database values +// observed by a ``ValueObservation``. +// +// It is NOT Sendable, because we need `ValueReducers.RemoveDuplicates` to +// be able to call `Equatable.==`, which IS not a Sendable function. +// Thread-safety will be assured by `ValueObservation`, which will make sure +// it does not invoke the reducer concurrently. +// +// However, we need to be able to fetch from any database dispatch queue, +// and maybe concurrently. That's why a `ValueReducer` has a Sendable facet, +// which is its `Fetcher`. + /// Implementation details of `ValueReducer`. public protocol _ValueReducer { - /// The type of fetched database values - associatedtype Fetched + /// The Sendable type that fetches database values + associatedtype Fetcher: _ValueReducerFetcher /// The type of observed values - associatedtype Value + associatedtype Value: Sendable + + /// Returns a value that fetches database values upon changes in an + /// observed database region. The returned value method must not depend + /// on the state of the reducer. + func _makeFetcher() -> Fetcher /// Transforms a fetched value into an eventual observed value. Returns nil /// when observer should not be notified. @@ -18,7 +35,14 @@ public protocol _ValueReducer { /// reducer._value(...) // MUST NOT be nil /// reducer._value(...) // MAY be nil /// reducer._value(...) // MAY be nil - mutating func _value(_ fetched: Fetched) throws -> Value? + mutating func _value(_ fetched: Fetcher.Value) throws -> Value? +} + +public protocol _ValueReducerFetcher: Sendable { + /// The type of fetched database values + associatedtype Value + + func fetch(_ db: Database) throws -> Value } /// `ValueReducer` supports ``ValueObservation``. @@ -26,17 +50,15 @@ public protocol _ValueReducer { /// A `ValueReducer` fetches and transforms the database values /// observed by a ``ValueObservation``. /// +/// Do not declare new conformances to `ValueReducer`. Only the built-in +/// conforming types are valid. +/// /// ## Topics /// -/// ### Support +/// ### Supporting Types /// /// - ``ValueReducers`` -public protocol ValueReducer: _ValueReducer { - /// Fetches database values upon changes in an observed database region. - /// - /// This method must does not depend on the state of the reducer. - func _fetch(_ db: Database) throws -> Fetched -} +public protocol ValueReducer: _ValueReducer { } /// A namespace for concrete types that adopt the ``ValueReducer`` protocol. public enum ValueReducers { } diff --git a/GRDB/ValueObservation/SharedValueObservation.swift b/GRDB/ValueObservation/SharedValueObservation.swift index 0a0e742ae3..0bc78496c6 100644 --- a/GRDB/ValueObservation/SharedValueObservation.swift +++ b/GRDB/ValueObservation/SharedValueObservation.swift @@ -87,9 +87,9 @@ extension ValueObservation { /// main dispatch queue. You can change this behavior by providing a /// scheduler. /// - /// For example, the ``ValueObservationScheduler/immediate`` scheduler - /// notifies all values on the main dispatch queue, and notifies the first - /// one immediately when the + /// For example, the ``ValueObservationMainActorScheduler/immediate`` + /// scheduler notifies all values on the main dispatch queue, and + /// notifies the first one immediately when the /// ``SharedValueObservation/start(onError:onChange:)`` method is called. /// The `immediate` scheduling requires that the observation starts from the /// main thread (a fatal error is raised otherwise): @@ -111,16 +111,13 @@ extension ValueObservation { /// // <- here "Fresh players" is already printed. /// ``` /// - /// Note that the `.immediate` scheduler requires that the observation is - /// subscribed from the main thread. It raises a fatal error otherwise. - /// /// - parameter reader: A DatabaseReader. /// - parameter scheduler: A Scheduler. By default, fresh values are /// dispatched asynchronously on the main queue. /// - parameter extent: The extent of the shared database observation. /// - returns: A `SharedValueObservation` public func shared( - in reader: some DatabaseReader, + in reader: any DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main), extent: SharedValueObservationExtent = .whileObserved) -> SharedValueObservation @@ -167,7 +164,8 @@ extension ValueObservation { /// let cancellable1 = ValueObservation.tracking { db in ... }.shared(in: dbQueue).start(...) /// let cancellable2 = ValueObservation.tracking { db in ... }.shared(in: dbQueue).start(...) /// ``` -public final class SharedValueObservation { +public final class SharedValueObservation: @unchecked Sendable { + // @unchecked Sendable because state is protected by `lock`. private let scheduler: any ValueObservationScheduler private let extent: SharedValueObservationExtent private let startObservation: ValueObservationStart @@ -179,11 +177,14 @@ public final class SharedValueObservation { private var cancellable: AnyDatabaseCancellable? private var lastResult: Result? - private final class Client { - var onError: (Error) -> Void - var onChange: (Element) -> Void + private final class Client: Sendable { + let onError: @Sendable (Error) -> Void + let onChange: @Sendable (Element) -> Void - init(onError: @escaping (Error) -> Void, onChange: @escaping (Element) -> Void) { + init( + onError: @escaping @Sendable (Error) -> Void, + onChange: @escaping @Sendable (Element) -> Void + ) { self.onError = onError self.onChange = onChange } @@ -223,12 +224,12 @@ public final class SharedValueObservation { /// - parameter onChange: The closure to execute on receipt of a /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. - public func start( - onError: @escaping (Error) -> Void, - onChange: @escaping (Element) -> Void) + @preconcurrency public func start( + onError: @escaping @Sendable (Error) -> Void, + onChange: @escaping @Sendable (Element) -> Void) -> AnyDatabaseCancellable { - synchronized { + withLock { // Support for reentrancy: a shared immediate observation is // started from the first value notification of that same shared // immediate observation. Yeah, users are nasty. @@ -291,7 +292,6 @@ public final class SharedValueObservation { /// print("fresh players: \(players)") /// } /// ``` - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func publisher() -> DatabasePublishers.Value { DatabasePublishers.Value { onError, onChange in self.start(onError: onError, onChange: onChange) @@ -300,7 +300,7 @@ public final class SharedValueObservation { #endif private func handleError(_ error: Error) { - synchronized { + withLock { let notifiedClients = clients // State change @@ -321,7 +321,7 @@ public final class SharedValueObservation { } private func handleChange(_ value: Element) { - synchronized { + withLock { // State change lastResult = .success(value) @@ -333,7 +333,7 @@ public final class SharedValueObservation { } private func handleCancel(_ client: Client) { - synchronized { + withLock { // State change clients.removeFirst(where: { $0 === client }) if clients.isEmpty && extent == .whileObserved { @@ -344,7 +344,7 @@ public final class SharedValueObservation { } } - private func synchronized(_ execute: () throws -> T) rethrows -> T { + private func withLock(_ execute: () throws -> T) rethrows -> T { lock.lock() defer { lock.unlock() } return try execute() @@ -355,8 +355,6 @@ extension SharedValueObservation { // MARK: - Asynchronous Observation /// Returns an asynchronous sequence of observed values. /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// /// For example: /// /// ```swift @@ -368,7 +366,6 @@ extension SharedValueObservation { /// print("Fresh players: \(players)") /// } /// ``` - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func values(bufferingPolicy: AsyncValueObservation.BufferingPolicy = .unbounded) -> AsyncValueObservation { diff --git a/GRDB/ValueObservation/ValueObservation.swift b/GRDB/ValueObservation/ValueObservation.swift index 37c313b20a..16467c6dd1 100644 --- a/GRDB/ValueObservation/ValueObservation.swift +++ b/GRDB/ValueObservation/ValueObservation.swift @@ -4,7 +4,7 @@ import Combine import Dispatch import Foundation -public struct ValueObservation { +public struct ValueObservation: Sendable { var events = ValueObservationEvents() /// A boolean value indicating whether the observation requires write access @@ -30,10 +30,10 @@ public struct ValueObservation { /// The reducer is created when observation starts, and is triggered upon /// each database change. - var makeReducer: () -> Reducer + var makeReducer: @Sendable () -> Reducer /// Returns a ValueObservation with a transformed reducer. - func mapReducer(_ transform: @escaping (Reducer) -> R) -> ValueObservation { + func mapReducer(_ transform: @escaping @Sendable (Reducer) -> R) -> ValueObservation { let makeReducer = self.makeReducer return ValueObservation( events: events, @@ -74,16 +74,16 @@ enum ValueObservationTrackingMode { } struct ValueObservationEvents: Refinable { - var willStart: (() -> Void)? - var willTrackRegion: ((DatabaseRegion) -> Void)? - var databaseDidChange: (() -> Void)? - var didFail: ((Error) -> Void)? - var didCancel: (() -> Void)? + var willStart: (@Sendable () -> Void)? + var willTrackRegion: (@Sendable (DatabaseRegion) -> Void)? + var databaseDidChange: (@Sendable () -> Void)? + var didFail: (@Sendable (Error) -> Void)? + var didCancel: (@Sendable () -> Void)? } -typealias ValueObservationStart = ( - _ onError: @escaping (Error) -> Void, - _ onChange: @escaping (T) -> Void) +typealias ValueObservationStart = @Sendable ( + _ onError: @escaping @Sendable (Error) -> Void, + _ onChange: @escaping @Sendable (T) -> Void) -> AnyDatabaseCancellable extension ValueObservation: Refinable { @@ -102,6 +102,54 @@ extension ValueObservation: Refinable { /// try Player.fetchAll(db) /// } /// + /// let cancellable = try observation.start( + /// in: dbQueue, + /// scheduling: .async(onQueue: .main)) + /// { error in + /// // handle error + /// } onChange: { (players: [Player]) in + /// print("Fresh players: \(players)") + /// } + /// ``` + /// + /// - parameter reader: A DatabaseReader. + /// - parameter scheduler: A ValueObservationScheduler. + /// - parameter onError: The closure to execute when the + /// observation fails. + /// - parameter onChange: The closure to execute on receipt of a + /// fresh value. + /// - returns: A DatabaseCancellable that can stop the observation. + @preconcurrency public func start( + in reader: any DatabaseReader, + scheduling scheduler: some ValueObservationScheduler, + onError: @escaping @Sendable (Error) -> Void, + onChange: @escaping @Sendable (Reducer.Value) -> Void) + -> AnyDatabaseCancellable + where Reducer: ValueReducer + { + let observation = self.with { + $0.events.didFail = concat($0.events.didFail, onError) + } + observation.events.willStart?() + return reader._add( + observation: observation, + scheduling: scheduler, + onChange: onChange) + } + + /// Starts observing the database and notifies fresh values on the + /// main actor. + /// + /// The observation lasts until the returned cancellable is cancelled + /// or deallocated. + /// + /// For example: + /// + /// ```swift + /// let observation = ValueObservation.tracking { db in + /// try Player.fetchAll(db) + /// } + /// /// let cancellable = try observation.start(in: dbQueue) { error in /// // handle error /// } onChange: { (players: [Player]) in @@ -110,14 +158,8 @@ extension ValueObservation: Refinable { /// ``` /// /// By default, fresh values are dispatched asynchronously on the - /// main dispatch queue. You can change this behavior by providing a - /// scheduler. - /// - /// For example, the ``ValueObservationScheduler/immediate`` scheduler - /// notifies all values on the main dispatch queue, and notifies the first - /// one immediately when the observation starts. The `immediate` scheduling - /// requires that the observation starts from the main dispatch queue (a - /// fatal error is raised otherwise): + /// main actor. Pass `.immediate` if the first value shoud be notified + /// immediately when the observation starts: /// /// ```swift /// let cancellable = try observation.start(in: dbQueue, scheduling: .immediate) { error in @@ -129,28 +171,36 @@ extension ValueObservation: Refinable { /// ``` /// /// - parameter reader: A DatabaseReader. - /// - parameter scheduler: A ValueObservationScheduler. By default, fresh - /// values are dispatched asynchronously on the main queue. - /// - parameter onError: The closure to execute when the observation fails. + /// - parameter scheduler: A ValueObservationMainActorScheduler. + /// By default, fresh values are dispatched asynchronously on the + /// main actor. + /// - parameter onError: The closure to execute when the + /// observation fails. /// - parameter onChange: The closure to execute on receipt of a /// fresh value. /// - returns: A DatabaseCancellable that can stop the observation. - public func start( - in reader: some DatabaseReader, - scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main), - onError: @escaping (Error) -> Void, - onChange: @escaping (Reducer.Value) -> Void) + @preconcurrency @MainActor public func start( + in reader: any DatabaseReader, + scheduling scheduler: some ValueObservationMainActorScheduler = .mainActor, + onError: @escaping @MainActor (Error) -> Void, + onChange: @escaping @MainActor (Reducer.Value) -> Void) -> AnyDatabaseCancellable where Reducer: ValueReducer { - let observation = self.with { - $0.events.didFail = concat($0.events.didFail, onError) - } - observation.events.willStart?() - return reader._add( - observation: observation, - scheduling: scheduler, - onChange: onChange) + let regularScheduler: some ValueObservationScheduler = scheduler + return start( + in: reader, + scheduling: regularScheduler, + onError: { error in + MainActor.assumeIsolated { + onError(error) + } + }, + onChange: { value in + MainActor.assumeIsolated { + onChange(value) + } + }) } // MARK: - Debugging @@ -175,13 +225,13 @@ extension ValueObservation: Refinable { /// - returns: A `ValueObservation` that performs the specified closures /// when ValueObservation events occur. public func handleEvents( - willStart: (() -> Void)? = nil, - willFetch: (() -> Void)? = nil, - willTrackRegion: ((DatabaseRegion) -> Void)? = nil, - databaseDidChange: (() -> Void)? = nil, - didReceiveValue: ((Reducer.Value) -> Void)? = nil, - didFail: ((Error) -> Void)? = nil, - didCancel: (() -> Void)? = nil) + willStart: (@Sendable () -> Void)? = nil, + willFetch: (@Sendable () -> Void)? = nil, + willTrackRegion: (@Sendable (DatabaseRegion) -> Void)? = nil, + databaseDidChange: (@Sendable () -> Void)? = nil, + didReceiveValue: (@Sendable (Reducer.Value) -> Void)? = nil, + didFail: (@Sendable (Error) -> Void)? = nil, + didCancel: (@Sendable () -> Void)? = nil) -> ValueObservation> { self @@ -231,34 +281,33 @@ extension ValueObservation: Refinable { /// used to log messages to other destinations. public func print( _ prefix: String = "", - to stream: TextOutputStream? = nil) + to stream: sending TextOutputStream? = nil) -> ValueObservation> { - let lock = NSLock() + let streamMutex = UnsafeSendableMutex(stream ?? PrintOutputStream()) let prefix = prefix.isEmpty ? "" : "\(prefix): " - var stream = stream ?? PrintOutputStream() return handleEvents( willStart: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)start") }, + streamMutex.withLock { $0.write("\(prefix)start") } + }, willFetch: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)fetch") }, - willTrackRegion: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)tracked region: \($0)") }, + streamMutex.withLock { $0.write("\(prefix)fetch") } + }, + willTrackRegion: { region in + streamMutex.withLock { $0.write("\(prefix)tracked region: \(region)") } + }, databaseDidChange: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)database did change") }, - didReceiveValue: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)value: \($0)") }, - didFail: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)failure: \($0)") }, + streamMutex.withLock { $0.write("\(prefix)database did change") } + }, + didReceiveValue: { value in + streamMutex.withLock { $0.write("\(prefix)value: \(value)") } + }, + didFail: { error in + streamMutex.withLock { $0.write("\(prefix)failure: \(error)") } + }, didCancel: { - lock.lock(); defer { lock.unlock() } - stream.write("\(prefix)cancel") }) + streamMutex.withLock { $0.write("\(prefix)cancel") } + }) } // MARK: - Fetching Values @@ -268,7 +317,9 @@ extension ValueObservation: Refinable { where Reducer: ValueReducer { var reducer = makeReducer() - guard let value = try reducer._value(reducer._fetch(db)) else { + let fetcher = reducer._makeFetcher() + let fetchedValue = try fetcher.fetch(db) + guard let value = try reducer._value(fetchedValue) else { fatalError("Broken contract: reducer has no initial value") } return value @@ -279,8 +330,6 @@ extension ValueObservation { // MARK: - Asynchronous Observation /// Returns an asynchronous sequence of observed values. /// - /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) - /// /// For example: /// /// ```swift @@ -294,12 +343,13 @@ extension ValueObservation { /// ``` /// /// - parameter reader: A DatabaseReader. - /// - parameter scheduler: A ValueObservationScheduler. By default, fresh - /// values are dispatched asynchronously on the main dispatch queue. - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) + /// - parameter scheduler: A ValueObservationScheduler. By default, + /// fresh values are dispatched on the cooperative thread pool. + /// - parameter bufferingPolicy: see the documntation + /// of `AsyncThrowingStream`. public func values( - in reader: some DatabaseReader, - scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main), + in reader: any DatabaseReader, + scheduling scheduler: some ValueObservationScheduler = .task, bufferingPolicy: AsyncValueObservation.BufferingPolicy = .unbounded) -> AsyncValueObservation where Reducer: ValueReducer @@ -310,11 +360,8 @@ extension ValueObservation { } } -// TODO: [GRDB7] Make it Sendable for easier integration with AsyncAlgorithms /// An asynchronous sequence of values observed by a ``ValueObservation``. /// -/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) -/// /// An `AsyncValueObservation` sequence produces a fresh value whenever the /// results of database requests change. /// @@ -332,12 +379,13 @@ extension ValueObservation { /// /// You build an `AsyncValueObservation` from ``ValueObservation`` or /// ``SharedValueObservation``. -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) -public struct AsyncValueObservation: AsyncSequence { +public struct AsyncValueObservation: AsyncSequence, Sendable { public typealias BufferingPolicy = AsyncThrowingStream.Continuation.BufferingPolicy public typealias AsyncIterator = Iterator - var bufferingPolicy: BufferingPolicy + // AsyncThrowingStream.Continuation.BufferingPolicy is obviously + // Sendable, but lacks Sendable conformance. + nonisolated(unsafe) var bufferingPolicy: BufferingPolicy var start: ValueObservationStart public func makeAsyncIterator() -> Iterator { @@ -413,11 +461,11 @@ extension ValueObservation { /// main dispatch queue. You can change this behavior by providing a /// scheduler. /// - /// For example, the ``ValueObservationScheduler/immediate`` scheduler - /// notifies all values on the main dispatch queue, and notifies the first - /// one immediately when the observation starts. The `immediate` scheduling - /// requires that the observation starts from the main dispatch queue (a - /// fatal error is raised otherwise): + /// For example, the ``ValueObservationMainActorScheduler/immediate`` + /// scheduler notifies all values on the main dispatch queue, and + /// notifies the first one immediately when the observation starts. The + /// `immediate` scheduling requires that the observation starts from the + /// main dispatch queue (a fatal error is raised otherwise): /// /// ```swift /// let publisher = observation.publisher(in: dbQueue, scheduling: .immediate) @@ -434,9 +482,8 @@ extension ValueObservation { /// - parameter scheduler: A ValueObservationScheduler. By default, fresh /// values are dispatched asynchronously on the main dispatch queue. /// - returns: A Combine publisher - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func publisher( - in reader: some DatabaseReader, + in reader: any DatabaseReader, scheduling scheduler: some ValueObservationScheduler = .async(onQueue: .main)) -> DatabasePublishers.Value where Reducer: ValueReducer @@ -451,7 +498,6 @@ extension ValueObservation { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension DatabasePublishers { /// A publisher that publishes the values of a ``ValueObservation``. /// @@ -473,9 +519,13 @@ extension DatabasePublishers { } } - private class ValueSubscription: Subscription - where Downstream.Failure == Error + private class ValueSubscription: + Subscription, @unchecked Sendable + where Downstream: Subscriber, + Downstream.Failure == Error { + // @unchecked Sendable because `cancellable` and `state` are + // protected by `lock`. private struct WaitingForDemand { let downstream: Downstream let start: ValueObservationStart @@ -729,8 +779,8 @@ extension ValueObservation { /// ``` /// /// - parameter fetch: The closure that fetches the observed value. - public static func trackingConstantRegion( - _ fetch: @escaping (Database) throws -> Value) + @preconcurrency public static func trackingConstantRegion( + _ fetch: @escaping @Sendable (Database) throws -> Value) -> Self where Reducer == ValueReducers.Fetch { @@ -801,10 +851,10 @@ extension ValueObservation { /// - parameter otherRegions: A list of supplementary regions /// to observe. /// - parameter fetch: The closure that fetches the observed value. - public static func tracking( + @preconcurrency public static func tracking( region: any DatabaseRegionConvertible, _ otherRegions: any DatabaseRegionConvertible..., - fetch: @escaping (Database) throws -> Value) + fetch: @escaping @Sendable (Database) throws -> Value) -> Self where Reducer == ValueReducers.Fetch { @@ -871,9 +921,9 @@ extension ValueObservation { /// /// - parameter regions: An array of observed regions. /// - parameter fetch: The closure that fetches the observed value. - public static func tracking( + @preconcurrency public static func tracking( regions: [any DatabaseRegionConvertible], - fetch: @escaping (Database) throws -> Value) + fetch: @escaping @Sendable (Database) throws -> Value) -> Self where Reducer == ValueReducers.Fetch { @@ -929,8 +979,8 @@ extension ValueObservation { /// ``` /// /// - parameter fetch: The closure that fetches the observed value. - public static func tracking( - _ fetch: @escaping (Database) throws -> Value) + @preconcurrency public static func tracking( + _ fetch: @escaping @Sendable (Database) throws -> Value) -> Self where Reducer == ValueReducers.Fetch { diff --git a/GRDB/ValueObservation/ValueObservationScheduler.swift b/GRDB/ValueObservation/ValueObservationScheduler.swift index d43cad7dc3..168c7c7062 100644 --- a/GRDB/ValueObservation/ValueObservationScheduler.swift +++ b/GRDB/ValueObservation/ValueObservationScheduler.swift @@ -9,19 +9,22 @@ import Foundation /// /// - ``async(onQueue:)`` /// - ``immediate`` +/// - ``mainActor`` +/// - ``task`` +/// - ``task(priority:)`` /// - ``AsyncValueObservationScheduler`` -/// - ``ImmediateValueObservationScheduler`` -public protocol ValueObservationScheduler { +/// - ``TaskValueObservationScheduler`` +public protocol ValueObservationScheduler: Sendable { /// Returns whether the initial value should be immediately notified. /// /// If the result is true, then this method was called on the main thread. func immediateInitialValue() -> Bool - func schedule(_ action: @escaping () -> Void) + func schedule(_ action: @escaping @Sendable () -> Void) } extension ValueObservationScheduler { - func scheduleInitial(_ action: @escaping () -> Void) { + func scheduleInitial(_ action: @escaping @Sendable () -> Void) { if immediateInitialValue() { action() } else { @@ -30,6 +33,29 @@ extension ValueObservationScheduler { } } +// MARK: - ValueObservationMainActorScheduler + +/// A type that determines when `ValueObservation` notifies its fresh +/// values, on the main actor. +/// +/// ## Topics +/// +/// ### Built-In Schedulers +/// +/// - ``immediate`` +/// - ``ValueObservationScheduler/mainActor`` +/// - ``ImmediateValueObservationScheduler`` +/// - ``DelayedMainActorValueObservationScheduler`` +public protocol ValueObservationMainActorScheduler: ValueObservationScheduler { + func scheduleOnMainActor(_ action: @escaping @MainActor () -> Void) +} + +extension ValueObservationMainActorScheduler { + public func schedule(_ action: @escaping @Sendable () -> Void) { + scheduleOnMainActor(action) + } +} + // MARK: - AsyncValueObservationScheduler /// A scheduler that asynchronously notifies fresh value of a `DispatchQueue`. @@ -42,7 +68,7 @@ public struct AsyncValueObservationScheduler: ValueObservationScheduler { public func immediateInitialValue() -> Bool { false } - public func schedule(_ action: @escaping () -> Void) { + public func schedule(_ action: @escaping @Sendable () -> Void) { queue.async(execute: action) } } @@ -77,10 +103,10 @@ extension ValueObservationScheduler where Self == AsyncValueObservationScheduler // MARK: - ImmediateValueObservationScheduler -/// A scheduler that notifies all values on the main `DispatchQueue`. The +/// A scheduler that notifies all values on the main actor. The /// first value is immediately notified when the `ValueObservation` /// is started. -public struct ImmediateValueObservationScheduler: ValueObservationScheduler, Sendable { +public struct ImmediateValueObservationScheduler: ValueObservationMainActorScheduler { public init() { } public func immediateInitialValue() -> Bool { @@ -90,13 +116,13 @@ public struct ImmediateValueObservationScheduler: ValueObservationScheduler, Sen return true } - public func schedule(_ action: @escaping () -> Void) { + public func scheduleOnMainActor(_ action: @escaping @MainActor () -> Void) { DispatchQueue.main.async(execute: action) } } extension ValueObservationScheduler where Self == ImmediateValueObservationScheduler { - /// A scheduler that notifies all values on the main `DispatchQueue`. The + /// A scheduler that notifies all values on the main actor. The /// first value is immediately notified when the `ValueObservation` /// is started. /// @@ -118,8 +144,102 @@ extension ValueObservationScheduler where Self == ImmediateValueObservationSched /// ``` /// /// - important: this scheduler requires that the observation is started - /// from the main queue. A fatal error is raised otherwise. + /// from the main actor. A fatal error is raised otherwise. public static var immediate: ImmediateValueObservationScheduler { ImmediateValueObservationScheduler() } } + +extension ValueObservationMainActorScheduler where Self == ImmediateValueObservationScheduler { + /// A scheduler that notifies all values on the main actor. The + /// first value is immediately notified when the `ValueObservation` + /// is started. + /// + /// For example: + /// + /// ```swift + /// let observation = ValueObservation.tracking { db in + /// try Player.fetchAll(db) + /// } + /// + /// let cancellable = try observation.start( + /// in: dbQueue, + /// scheduling: .immediate, + /// onError: { error in ... }, + /// onChange: { (players: [Player]) in + /// print("fresh players: \(players)") + /// }) + /// // <- here "fresh players" is already printed. + /// ``` + /// + /// - important: this scheduler requires that the observation is started + /// from the main actor. A fatal error is raised otherwise. + public static var immediate: ImmediateValueObservationScheduler { + ImmediateValueObservationScheduler() + } +} + +// MARK: - TaskValueObservationScheduler + +/// A scheduler that notifies all values on the cooperative thread pool. +public final class TaskValueObservationScheduler: ValueObservationScheduler { + typealias Action = @Sendable () -> Void + let continuation: AsyncStream.Continuation + let task: Task + + init(priority: TaskPriority?) { + let (stream, continuation) = AsyncStream.makeStream(of: Action.self) + + self.continuation = continuation + self.task = Task(priority: priority) { + for await action in stream { + action() + } + } + } + + deinit { + task.cancel() + } + + public func immediateInitialValue() -> Bool { + false + } + + public func schedule(_ action: @escaping @Sendable () -> Void) { + continuation.yield(action) + } +} + +extension ValueObservationScheduler where Self == TaskValueObservationScheduler { + /// A scheduler that notifies all values from a new `Task`. + public static var task: TaskValueObservationScheduler { + TaskValueObservationScheduler(priority: nil) + } + + /// A scheduler that notifies all values from a new `Task` with the + /// given priority. + public static func task(priority: TaskPriority) -> TaskValueObservationScheduler { + TaskValueObservationScheduler(priority: priority) + } +} + +// MARK: - DelayedMainActorValueObservationScheduler + +/// A scheduler that notifies all values on the cooperative thread pool. +public final class DelayedMainActorValueObservationScheduler: ValueObservationMainActorScheduler { + public func immediateInitialValue() -> Bool { + false + } + + public func scheduleOnMainActor(_ action: @escaping @MainActor () -> Void) { + DispatchQueue.main.async(execute: action) + } +} + +extension ValueObservationScheduler where Self == DelayedMainActorValueObservationScheduler { + /// A scheduler that notifies all values on the main actor. + public static var mainActor: DelayedMainActorValueObservationScheduler { + DelayedMainActorValueObservationScheduler() + } +} diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 253e90b00e..a4ab361527 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ 563C67B824628C0C00E94EDC /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563C67B624628C0C00E94EDC /* DatabasePoolTests.swift */; }; 563CBBE42A595141008905CE /* SQLIndexGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563CBBE22A595141008905CE /* SQLIndexGenerator.swift */; }; 563DE4F8231A91F6005081B7 /* DatabaseConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563DE4F6231A91F6005081B7 /* DatabaseConfigurationTests.swift */; }; + 563EA3E32C7B3A3A001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E22C7B3A3A001BE0D4 /* Mutex.swift */; }; 563EF420215F8A76007DAACD /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF41E215F8A76007DAACD /* OrderedDictionary.swift */; }; 563EF442216131F5007DAACD /* AssociationAggregateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF441216131F5007DAACD /* AssociationAggregateTests.swift */; }; 563EF44D2161F196007DAACD /* Inflections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EF44C2161F196007DAACD /* Inflections.swift */; }; @@ -166,7 +167,7 @@ 5657AB611D108BA9006283EF /* FoundationNSURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB341D108BA9006283EF /* FoundationNSURLTests.swift */; }; 5657AB691D108BA9006283EF /* FoundationURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB351D108BA9006283EF /* FoundationURLTests.swift */; }; 5659F48A1EA8D94E004A4992 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4871EA8D94E004A4992 /* Utils.swift */; }; - 5659F4921EA8D964004A4992 /* ReadWriteBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */; }; + 5659F4921EA8D964004A4992 /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */; }; 5659F49A1EA8D989004A4992 /* Pool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4971EA8D989004A4992 /* Pool.swift */; }; 565EFAF11D0436CE00A8FA9D /* NumericOverflowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565EFAED1D0436CE00A8FA9D /* NumericOverflowTests.swift */; }; 5665F868203EF4640084C6C0 /* ColumnInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5665F865203EF4590084C6C0 /* ColumnInfoTests.swift */; }; @@ -185,7 +186,7 @@ 566B91351FA4D3810012D5B0 /* TransactionObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */; }; 566BD7332927AFD600595649 /* ValueConcurrentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BD7312927AFD600595649 /* ValueConcurrentObserver.swift */; }; 566BD7342927AFD600595649 /* ValueWriteOnlyObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BD7322927AFD600595649 /* ValueWriteOnlyObserver.swift */; }; - 566BE7152342541F00A8254B /* LockedBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BE7132342541F00A8254B /* LockedBox.swift */; }; + 566BE7152342541F00A8254B /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566BE7132342541F00A8254B /* Mutex.swift */; }; 566DDE12288D76400000DCFB /* Fixits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566DDE11288D76400000DCFB /* Fixits.swift */; }; 5670329B212B5462007D270F /* DatabaseUUIDEncodingStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56703299212B5461007D270F /* DatabaseUUIDEncodingStrategyTests.swift */; }; 567071F4208A00BE006AD95A /* SQLiteDateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567071F2208A00BE006AD95A /* SQLiteDateParser.swift */; }; @@ -254,7 +255,6 @@ 569BBA43229066CB00478429 /* InflectionsTests.json in Resources */ = {isa = PBXBuildFile; fileRef = 569BBA42229066CB00478429 /* InflectionsTests.json */; }; 569BBA4D229170B300478429 /* Inflections+English.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569BBA4B229170B300478429 /* Inflections+English.swift */; }; 569EF0E6200D37FD00A9FA45 /* DatabaseRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569EF0E5200D37FC00A9FA45 /* DatabaseRegion.swift */; }; - 56A2FA3924424F4200E97D23 /* Export.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2FA3724424F4200E97D23 /* Export.swift */; }; 56A4CDB31D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A4CDAF1D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift */; }; 56A5EF121EF7F20B00F03071 /* ForeignKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A5EF0E1EF7F20B00F03071 /* ForeignKeyInfoTests.swift */; }; 56A6EB2426076F6A00C27594 /* SQL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A6EB2226076F6A00C27594 /* SQL.swift */; }; @@ -309,6 +309,7 @@ 56DF0015228DDB8300D611F3 /* AssociationPrefetchingCodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0013228DDB8200D611F3 /* AssociationPrefetchingCodableRecordTests.swift */; }; 56DF0017228DDB8300D611F3 /* AssociationPrefetchingRowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0014228DDB8200D611F3 /* AssociationPrefetchingRowTests.swift */; }; 56DF37A723D77AA0009AAA05 /* Refinable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF37A623D77AA0009AAA05 /* Refinable.swift */; }; + 56DFC3AE2C84794400DFE5DC /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DFC3AD2C84794400DFE5DC /* AsyncSemaphore.swift */; }; 56E4F7F92392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7F72392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift */; }; 56E9FAC42210468500C703A8 /* SQLInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FAC32210468500C703A8 /* SQLInterpolation.swift */; }; 56EA63C9209C7F1E009715B8 /* DerivableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EA63C7209C7F1E009715B8 /* DerivableRequestTests.swift */; }; @@ -546,6 +547,7 @@ 563C67B624628C0C00E94EDC /* DatabasePoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolTests.swift; sourceTree = ""; }; 563CBBE22A595141008905CE /* SQLIndexGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLIndexGenerator.swift; sourceTree = ""; }; 563DE4F6231A91F6005081B7 /* DatabaseConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseConfigurationTests.swift; sourceTree = ""; }; + 563EA3E22C7B3A3A001BE0D4 /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 563EF41E215F8A76007DAACD /* OrderedDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; 563EF441216131F5007DAACD /* AssociationAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationAggregateTests.swift; sourceTree = ""; }; 563EF44C2161F196007DAACD /* Inflections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Inflections.swift; sourceTree = ""; }; @@ -633,7 +635,7 @@ 5657AB341D108BA9006283EF /* FoundationNSURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSURLTests.swift; sourceTree = ""; }; 5657AB351D108BA9006283EF /* FoundationURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationURLTests.swift; sourceTree = ""; }; 5659F4871EA8D94E004A4992 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteBox.swift; sourceTree = ""; }; + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; 5659F4971EA8D989004A4992 /* Pool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pool.swift; sourceTree = ""; }; 565B0FEE1BBC7D980098DE03 /* FetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableRecordTests.swift; sourceTree = ""; }; 565D5D701BBC694D00DC9BD4 /* Row+FoundationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Row+FoundationTests.swift"; sourceTree = ""; }; @@ -656,7 +658,7 @@ 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionObserver.swift; sourceTree = ""; }; 566BD7312927AFD600595649 /* ValueConcurrentObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueConcurrentObserver.swift; sourceTree = ""; }; 566BD7322927AFD600595649 /* ValueWriteOnlyObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueWriteOnlyObserver.swift; sourceTree = ""; }; - 566BE7132342541F00A8254B /* LockedBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockedBox.swift; sourceTree = ""; }; + 566BE7132342541F00A8254B /* Mutex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 566DDE11288D76400000DCFB /* Fixits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fixits.swift; sourceTree = ""; }; 56703299212B5461007D270F /* DatabaseUUIDEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseUUIDEncodingStrategyTests.swift; sourceTree = ""; }; 567071F2208A00BE006AD95A /* SQLiteDateParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteDateParser.swift; sourceTree = ""; }; @@ -770,7 +772,6 @@ 56A238921B9C750B0082EB20 /* DatabaseMigrator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseMigrator.swift; sourceTree = ""; }; 56A238A11B9C753B0082EB20 /* Record.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Record.swift; sourceTree = ""; }; 56A238B51B9CA2590082EB20 /* DatabaseTimestampTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseTimestampTests.swift; sourceTree = ""; }; - 56A2FA3724424F4200E97D23 /* Export.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Export.swift; sourceTree = ""; }; 56A4CDAF1D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLExpressionLiteralTests.swift; sourceTree = ""; }; 56A5E4081BA2BCF900707640 /* RecordWithColumnNameManglingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordWithColumnNameManglingTests.swift; sourceTree = ""; }; 56A5EF0E1EF7F20B00F03071 /* ForeignKeyInfoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForeignKeyInfoTests.swift; sourceTree = ""; }; @@ -830,6 +831,7 @@ 56DF0013228DDB8200D611F3 /* AssociationPrefetchingCodableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingCodableRecordTests.swift; sourceTree = ""; }; 56DF0014228DDB8200D611F3 /* AssociationPrefetchingRowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingRowTests.swift; sourceTree = ""; }; 56DF37A623D77AA0009AAA05 /* Refinable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Refinable.swift; sourceTree = ""; }; + 56DFC3AD2C84794400DFE5DC /* AsyncSemaphore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSemaphore.swift; sourceTree = ""; }; 56E4F7F72392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAbortedTransactionTests.swift; sourceTree = ""; }; 56E8CE0C1BB4FA5600828BEC /* DatabaseValueConvertibleFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleFetchTests.swift; sourceTree = ""; }; 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertibleFetchTests.swift; sourceTree = ""; }; @@ -1032,8 +1034,10 @@ 56176C581EACC2D8000F3F2B /* GRDBTests */ = { isa = PBXGroup; children = ( + 56DFC3AD2C84794400DFE5DC /* AsyncSemaphore.swift */, 567E4207242AB3CB00CAAD2C /* FailureTestCase.swift */, 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, + 563EA3E22C7B3A3A001BE0D4 /* Mutex.swift */, 562EA81E1F17B26F00FA528C /* Compilation */, 56A238111B9C74A90082EB20 /* Core */, 567B5BFC2AD3285C00629622 /* Dump */, @@ -1341,11 +1345,11 @@ 564D4F91261E1D3300F55856 /* CaseInsensitiveIdentifier.swift */, 563EF44C2161F196007DAACD /* Inflections.swift */, 569BBA4B229170B300478429 /* Inflections+English.swift */, - 566BE7132342541F00A8254B /* LockedBox.swift */, + 566BE7132342541F00A8254B /* Mutex.swift */, 563B8FBC24A1D388007A48C9 /* OnDemandFuture.swift */, 563EF41E215F8A76007DAACD /* OrderedDictionary.swift */, 5659F4971EA8D989004A4992 /* Pool.swift */, - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */, + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */, 563B8FB924A1D036007A48C9 /* ReceiveValuesOn.swift */, 56DF37A623D77AA0009AAA05 /* Refinable.swift */, 5659F4871EA8D94E004A4992 /* Utils.swift */, @@ -1746,7 +1750,6 @@ DC37742D19C8CC90004FCF85 /* GRDB */ = { isa = PBXGroup; children = ( - 56A2FA3724424F4200E97D23 /* Export.swift */, 566DDE11288D76400000DCFB /* Fixits.swift */, 648704B82B8261070036480B /* PrivacyInfo.xcprivacy */, 56A2386F1B9C75030082EB20 /* Core */, @@ -2006,7 +2009,7 @@ 56012B82257404A400B4925B /* CommonTableExpression.swift in Sources */, 5656A8972295BD56001FF3FF /* SQLRelation.swift in Sources */, 56D110FF28AFC9C600E64463 /* MutablePersistableRecord+DAO.swift in Sources */, - 5659F4921EA8D964004A4992 /* ReadWriteBox.swift in Sources */, + 5659F4921EA8D964004A4992 /* ReadWriteLock.swift in Sources */, 566A842D20413D9A00E50BFD /* DatabaseSnapshot.swift in Sources */, 56CEB4F31EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */, 5656A8512295BD56001FF3FF /* SQLInterpolation+QueryInterface.swift in Sources */, @@ -2055,7 +2058,6 @@ F3BA806F1CFB2E55003DC1BA /* DatabaseValue.swift in Sources */, 56C0539722ACEECD0029D27D /* ValueReducer.swift in Sources */, 4E13D2F82769BC230037588C /* DatabaseBackupProgress.swift in Sources */, - 56A2FA3924424F4200E97D23 /* Export.swift in Sources */, 56FA0C3728B1F2EB00B2DFF7 /* MutablePersistableRecord+Upsert.swift in Sources */, 56B964B31DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */, F3BA80731CFB2E55003DC1BA /* RowAdapter.swift in Sources */, @@ -2101,7 +2103,7 @@ 5656A8A72295BF44001FF3FF /* DatabasePromise.swift in Sources */, 5656A86D2295BD56001FF3FF /* HasManyThroughAssociation.swift in Sources */, 56894FE3260658A400268F4D /* Decimal.swift in Sources */, - 566BE7152342541F00A8254B /* LockedBox.swift in Sources */, + 566BE7152342541F00A8254B /* Mutex.swift in Sources */, F3BA806D1CFB2E55003DC1BA /* DatabaseSchemaCache.swift in Sources */, F3BA80841CFB2E67003DC1BA /* StandardLibrary.swift in Sources */, 5656A87F2295BD56001FF3FF /* SQLForeignKeyRequest.swift in Sources */, @@ -2305,6 +2307,7 @@ F3BA81321CFB3064003DC1BA /* RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift in Sources */, 5653EB7420961FB200F46237 /* AssociationRowScopeSearchTests.swift in Sources */, 5691578E231BF2BE00E1D237 /* PoolTests.swift in Sources */, + 563EA3E32C7B3A3A001BE0D4 /* Mutex.swift in Sources */, 5623935A1DEE013C00A6B01F /* FilterCursorTests.swift in Sources */, 56419C7E24A51D6E004967E1 /* DatabaseWriterWritePublisherTests.swift in Sources */, 5665FA3D2129EED8004D8612 /* DatabaseDateEncodingStrategyTests.swift in Sources */, @@ -2358,6 +2361,7 @@ 567DAF381EAB789800FC0928 /* DatabaseLogErrorTests.swift in Sources */, 569BBA2B228DE53200478429 /* AssociationPrefetchingFetchableRecordTests.swift in Sources */, 56F34FBF24B094D2007513FC /* SQLExpressionIsConstantTests.swift in Sources */, + 56DFC3AE2C84794400DFE5DC /* AsyncSemaphore.swift in Sources */, 5670329B212B5462007D270F /* DatabaseUUIDEncodingStrategyTests.swift in Sources */, 5616B50028B5F5490052017E /* SingletonRecordTest.swift in Sources */, 56F34FC624B0A0C9007513FC /* SQLIdentifyingColumnsTests.swift in Sources */, diff --git a/Makefile b/Makefile index 819e8dcae7..9c91521f3b 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,9 @@ XCRUN := $(shell command -v xcrun) XCODEBUILD := set -o pipefail && $(shell command -v xcodebuild) ifdef TOOLCHAIN + # Look for the toolchain identifier in the CFBundleIdentifier key of its Info.plist: + # TOOLCHAIN=org.swift.600202404221a make test + # If TOOLCHAIN is specified, add xcodebuild parameter XCODEBUILD += -toolchain $(TOOLCHAIN) @@ -78,7 +81,7 @@ test_framework_SQLCipher: test_framework_SQLCipher3 test_framework_SQLCipher3Enc test_archive: test_universal_xcframework test_install: test_install_manual test_install_SPM test_install_customSQLite test_install_GRDB_CocoaPods test_CocoaPodsLint: test_CocoaPodsLint_GRDB -test_demo_apps: test_GRDBDemoiOS test_GRDBCombineDemo test_GRDBAsyncDemo +test_demo_apps: test_GRDBDemo test_framework_GRDBOSX: $(XCODEBUILD) \ @@ -214,6 +217,7 @@ endif test_SPM: # Add sanitizers when available: https://twitter.com/simjp/status/929140877540278272 + rm -rf Tests/products $(SWIFT) package clean $(SWIFT) build $(SWIFT) build -c release @@ -267,21 +271,23 @@ test_universal_xcframework: test_install_manual: $(XCODEBUILD) \ - -project Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj \ - -scheme GRDBDemoiOS \ + -project Tests/GRDBManualInstall/GRDBManualInstall.xcodeproj \ + -scheme GRDBManualInstall \ -configuration Release \ - -destination $(MAX_IOS_DESTINATION) \ + -destination "platform=macOS" \ clean build \ $(XCPRETTY) test_install_SPM: test_install_SPM_Package test_install_SPM_Project test_install_SPM_Dynamic_Project test_install_SPM_macos_release test_install_SPM_ios_release test_install_SPM_Package: + rm -rf Tests/products cd Tests/SPM/PlainPackage && \ $(SWIFT) build && \ ./.build/debug/SPM test_install_SPM_Project: + rm -rf Tests/products $(XCODEBUILD) \ -project Tests/SPM/PlainProject/Plain.xcodeproj \ -scheme Plain \ @@ -291,6 +297,7 @@ test_install_SPM_Project: $(XCPRETTY) test_install_SPM_Dynamic_Project: + rm -rf Tests/products $(XCODEBUILD) \ -project Tests/SPM/ios-dynamic/ios-dynamic.xcodeproj \ -scheme ios-dynamic \ @@ -300,6 +307,7 @@ test_install_SPM_Dynamic_Project: $(XCPRETTY) test_install_SPM_macos_release: + rm -rf Tests/products $(XCODEBUILD) \ -project Tests/SPM/macos/macos.xcodeproj \ -scheme macos \ @@ -309,6 +317,7 @@ test_install_SPM_macos_release: $(XCPRETTY) test_install_SPM_ios_release: + rm -rf Tests/products $(XCODEBUILD) \ -project Tests/SPM/ios/ios.xcodeproj \ -scheme ios \ @@ -368,26 +377,10 @@ else @exit 1 endif -test_GRDBDemoiOS: - $(XCODEBUILD) \ - -project Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj \ - -scheme GRDBDemoiOS \ - -destination $(MAX_IOS_DESTINATION) \ - $(TEST_ACTIONS) \ - $(XCPRETTY) - -test_GRDBCombineDemo: - $(XCODEBUILD) \ - -project Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj \ - -scheme GRDBCombineDemo \ - -destination $(MAX_IOS_DESTINATION) \ - $(TEST_ACTIONS) \ - $(XCPRETTY) - -test_GRDBAsyncDemo: +test_GRDBDemo: $(XCODEBUILD) \ - -project Documentation/DemoApps/GRDBAsyncDemo/GRDBAsyncDemo.xcodeproj \ - -scheme GRDBAsyncDemo \ + -project Documentation/DemoApps/GRDBDemo/GRDBDemo.xcodeproj \ + -scheme GRDBDemo \ -destination $(MAX_IOS_DESTINATION) \ $(TEST_ACTIONS) \ $(XCPRETTY) diff --git a/Package.swift b/Package.swift index a6f51175e1..36937d039a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import Foundation @@ -10,9 +10,6 @@ var swiftSettings: [SwiftSetting] = [ var cSettings: [CSetting] = [] var dependencies: [PackageDescription.Package.Dependency] = [] -// For Swift 5.8+ -//swiftSettings.append(.enableUpcomingFeature("ExistentialAny")) - // Don't rely on those environment variables. They are ONLY testing conveniences: // $ SQLITE_ENABLE_PREUPDATE_HOOK=1 make test_SPM if ProcessInfo.processInfo.environment["SQLITE_ENABLE_PREUPDATE_HOOK"] == "1" { @@ -34,24 +31,24 @@ let package = Package( name: "GRDB", defaultLocalization: "en", // for tests platforms: [ - .iOS(.v11), - .macOS(.v10_13), - .tvOS(.v11), - .watchOS(.v4), + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v7), ], products: [ - .library(name: "CSQLite", targets: ["CSQLite"]), + .library(name: "GRDBSQLite", targets: ["GRDBSQLite"]), .library(name: "GRDB", targets: ["GRDB"]), .library(name: "GRDB-dynamic", type: .dynamic, targets: ["GRDB"]), ], dependencies: dependencies, targets: [ .systemLibrary( - name: "CSQLite", + name: "GRDBSQLite", providers: [.apt(["libsqlite3-dev"])]), .target( name: "GRDB", - dependencies: ["CSQLite"], + dependencies: ["GRDBSQLite"], path: "GRDB", resources: [.copy("PrivacyInfo.xcprivacy")], cSettings: cSettings, @@ -64,10 +61,12 @@ let package = Package( "CocoaPods", "Crash", "CustomSQLite", + "GRDBManualInstall", "GRDBTests/getThreadsCount.c", "Info.plist", "Performance", "SPM", + "Swift6Migration", "generatePerformanceReport.rb", "parsePerformanceTests.rb", ], @@ -77,7 +76,12 @@ let package = Package( .copy("GRDBTests/Issue1383.sqlite"), ], cSettings: cSettings, - swiftSettings: swiftSettings) + swiftSettings: swiftSettings + [ + // Tests still use the Swift 5 language mode. + .swiftLanguageMode(.v5), + .enableUpcomingFeature("InferSendableFromCaptures"), + .enableUpcomingFeature("GlobalActorIsolatedTypesUsability"), + ]) ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v6] ) diff --git a/README.md b/README.md index d5171c0c0c..9277037842 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ CI Status

-**Latest release**: September 7, 2024 • [version 6.29.3](https://github.com/groue/GRDB.swift/tree/v6.29.3) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md) +**Latest release**: September 7, 2024 • [version 6.29.3](https://github.com/groue/GRDB.swift/tree/v6.29.3) • [CHANGELOG](CHANGELOG.md) • [Migrating From GRDB 6 to GRDB 7](Documentation/GRDB7MigrationGuide.md) -**Requirements**: iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ • SQLite 3.19.3+ • Swift 5.7+ / Xcode 14+ +**Requirements**: iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 7.0+ • SQLite 3.20.0+ • Swift 6+ / Xcode 16+ **Contact**: @@ -276,7 +276,7 @@ Documentation #### Demo Applications & Frequently Asked Questions -- [Demo Applications]: Three flavors: vanilla UIKit, Combine + SwiftUI, and Async/Await + SwiftUI. +- [Demo Applications] - [FAQ] #### Reference @@ -316,7 +316,7 @@ Documentation - [Unicode](#unicode) - [Memory Management](#memory-management) - [Data Protection](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseconnections) -- :bulb: [Migrating From GRDB 5 to GRDB 6](Documentation/GRDB6MigrationGuide.md) +- :bulb: [Migrating From GRDB 6 to GRDB 7](Documentation/Documentation/GRDB7MigrationGuide.md) - :bulb: [Why Adopt GRDB?](Documentation/WhyAdoptGRDB.md) - :bulb: [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices) @@ -347,8 +347,6 @@ The [Swift Package Manager](https://swift.org/package-manager/) automates the di GRDB offers two libraries, `GRDB` and `GRDB-dynamic`. Pick only one. When in doubt, prefer `GRDB`. The `GRDB-dynamic` library can reveal useful if you are going to link it with multiple targets within your app and only wish to link to a shared, dynamic framework once. See [How to link a Swift Package as dynamic](https://forums.swift.org/t/how-to-link-a-swift-package-as-dynamic/32062) for more information. > **Note**: Linux is not currently supported. -> -> **Warning**: Due to an Xcode bug, you will get "No such module 'CSQLite'" errors when you want to embed the GRDB package in other targets than the main application (watch extensions, for example). UI and Unit testing targets are OK, though. See [#642](https://github.com/groue/GRDB.swift/issues/642#issuecomment-575994093) for more information. ## CocoaPods @@ -395,8 +393,6 @@ Due to an [issue](https://github.com/CocoaPods/CocoaPods/issues/11839) in CocoaP 4. Add the `GRDB.framework` to the **Embedded Binaries** section of the **General** tab of your application target (extension target for WatchOS). -> :bulb: **Tip**: see the [Demo Applications] for examples of such integration. - Database Connections ==================== @@ -1632,10 +1628,11 @@ For more information, see [`tableExists(_:)`](https://swiftpackageindex.com/grou **If not all SQLite APIs are exposed in GRDB, you can still use the [SQLite C Interface](https://www.sqlite.org/c3ref/intro.html) and call [SQLite C functions](https://www.sqlite.org/c3ref/funclist.html).** -Those functions are embedded right into the GRDB module, regardless of the underlying SQLite implementation (system SQLite, [SQLCipher](#encryption), or [custom SQLite build]): +To access the C SQLite functions from SQLCipher or the system SQLite, you need to perform an extra import: ```swift -import GRDB +import SQLite3 // System SQLite +import SQLCipher // SQLCipher let sqliteVersion = String(cString: sqlite3_libversion()) ``` @@ -1663,7 +1660,7 @@ try dbQueue.read { db in Records ======= -**On top of the [SQLite API](#sqlite-api), GRDB provides protocols and a class** that help manipulating database rows as regular objects named "records": +**On top of the [SQLite API](#sqlite-api), GRDB provides protocols** that help manipulating database rows as regular objects named "records": ```swift try dbQueue.write { db in @@ -1676,15 +1673,31 @@ try dbQueue.write { db in Of course, you need to open a [database connection], and [create database tables](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/databaseschema) first. -To define your custom records, you subclass the ready-made `Record` class, or you extend your structs and classes with protocols that come with focused sets of features: fetching methods, persistence methods, record comparison... +To define a record type, define a type and extend it with protocols that come with focused sets of features. + +For example: + +``` +struct Player: { + var id: Int64 + var name: String + var score: Int +} -Extending structs with record protocols is more "swifty". Subclassing the Record class is more "classic". You can choose either way. See some [examples of record definitions](#examples-of-record-definitions), and the [list of record methods](#list-of-record-methods) for an overview. +// Players can be fetched from the database. +extension Player: FetchableRecord { ... } -> **Note**: if you are familiar with Core Data's NSManagedObject or Realm's Object, you may experience a cultural shock: GRDB records are not uniqued, do not auto-update, and do not lazy-load. This is both a purpose, and a consequence of protocol-oriented programming. You should read [How to build an iOS application with SQLite and GRDB.swift](https://medium.com/@gwendal.roue/how-to-build-an-ios-application-with-sqlite-and-grdb-swift-d023a06c29b3) for a general introduction. +// Players can be saved into the database. +extension Player: PersistableRecord { ... } +``` + +See some [examples of record definitions](#examples-of-record-definitions). + +> Note: if you are familiar with Core Data's NSManagedObject or Realm's Object, you may experience a cultural shock: GRDB records are not uniqued, do not auto-update, and do not lazy-load. This is both a purpose, and a consequence of protocol-oriented programming. > -> :bulb: **Tip**: after you have read this chapter, check the [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices) Guide. +> Tip: The [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices) guide provides general guidance.. > -> :bulb: **Tip**: see the [Demo Applications] for sample apps that uses records. +> Tip: See the [Demo Applications] for sample apps that uses records. **Overview** @@ -1705,16 +1718,10 @@ Extending structs with record protocols is more "swifty". Subclassing the Record - [Persistence Callbacks] - [Identifiable Records] - [Codable Records] -- [Record Class](#record-class) - [Record Comparison] - [Record Customization Options] - [Record Timestamps and Transaction Date] -**Records in a Glance** - -- [Examples of Record Definitions](#examples-of-record-definitions) -- [List of Record Methods](#list-of-record-methods) - ### Inserting Records @@ -1725,7 +1732,7 @@ let player = Player(name: "Arthur", email: "arthur@example.com") try player.insert(db) ``` -:point_right: `insert` is available for subclasses of the [Record](#record-class) class, and types that adopt the [PersistableRecord] protocol. +:point_right: `insert` is available for types that adopt the [PersistableRecord] protocol. ### Fetching Records @@ -1746,9 +1753,9 @@ let spain = try Country.fetchOne(db, id: "ES") // Country? let italy = try Country.find(db, id: "IT") // Country ``` -:point_right: Fetching from raw SQL is available for subclasses of the [Record](#record-class) class, and types that adopt the [FetchableRecord] protocol. +:point_right: Fetching from raw SQL is available for types that adopt the [FetchableRecord] protocol. -:point_right: Fetching without SQL, using the [query interface](#the-query-interface), is available for subclasses of the [Record](#record-class) class, and types that adopt both [FetchableRecord] and [TableRecord] protocol. +:point_right: Fetching without SQL, using the [query interface](#the-query-interface), is available for types that adopt both [FetchableRecord] and [TableRecord] protocol. ### Updating Records @@ -1778,7 +1785,7 @@ try Player .updateAll(db, Column("score") += 1) ``` -:point_right: update methods are available for subclasses of the [Record](#record-class) class, and types that adopt the [PersistableRecord] protocol. Batch updates are available on the [TableRecord] protocol. +:point_right: update methods are available for types that adopt the [PersistableRecord] protocol. Batch updates are available on the [TableRecord] protocol. ### Deleting Records @@ -1801,7 +1808,7 @@ try Player .deleteAll(db) ``` -:point_right: delete methods are available for subclasses of the [Record](#record-class) class, and types that adopt the [PersistableRecord] protocol. Batch deletes are available on the [TableRecord] protocol. +:point_right: delete methods are available for types that adopt the [PersistableRecord] protocol. Batch deletes are available on the [TableRecord] protocol. ### Counting Records @@ -1816,7 +1823,7 @@ let playerWithEmailCount: Int = try Player .fetchCount(db) ``` -:point_right: `fetchCount` is available for subclasses of the [Record](#record-class) class, and types that adopt the [TableRecord] protocol. +:point_right: `fetchCount` is available for types that adopt the [TableRecord] protocol. Details follow: @@ -1827,11 +1834,9 @@ Details follow: - [PersistableRecord Protocol](#persistablerecord-protocol) - [Identifiable Records] - [Codable Records] -- [Record Class](#record-class) - [Record Comparison] - [Record Customization Options] - [Examples of Record Definitions](#examples-of-record-definitions) -- [List of Record Methods](#list-of-record-methods) ## Record Protocols Overview @@ -1842,6 +1847,7 @@ Details follow: ```swift struct Place: FetchableRecord { ... } + let places = try dbQueue.read { db in try Place.fetchAll(db, sql: "SELECT * FROM place") } @@ -1855,6 +1861,7 @@ Details follow: ```swift struct Place: TableRecord { ... } + let placeCount = try dbQueue.read { db in // Generates and runs `SELECT COUNT(*) FROM place` try Place.fetchCount(db) @@ -1865,6 +1872,7 @@ Details follow: ```swift struct Place: TableRecord, FetchableRecord { ... } + try dbQueue.read { db in let places = try Place.order(Column("title")).fetchAll(db) let paris = try Place.fetchOne(id: 1) @@ -1875,6 +1883,7 @@ Details follow: ```swift struct Place : PersistableRecord { ... } + try dbQueue.write { db in try Place.delete(db, id: 1) try Place(...).insert(db) @@ -1899,7 +1908,7 @@ protocol FetchableRecord { } ``` -**To use FetchableRecord**, subclass the [Record](#record-class) class, or adopt it explicitly. For example: +For example: ```swift struct Place { @@ -1970,7 +1979,7 @@ See [fetching methods](#fetching-methods) for information about the `fetchCursor 📖 [`TableRecord`](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/tablerecord) -**The TableRecord protocol** generates SQL for you. To use TableRecord, subclass the [Record](#record-class) class, or adopt it explicitly: +**The TableRecord protocol** generates SQL for you: ```swift protocol TableRecord { @@ -1985,6 +1994,7 @@ The `databaseTableName` type property is the name of a database table. By defaul ```swift struct Place: TableRecord { } + print(Place.databaseTableName) // prints "place" ``` @@ -2002,16 +2012,8 @@ You can still provide a custom table name: struct Place: TableRecord { static let databaseTableName = "location" } -print(Place.databaseTableName) // prints "location" -``` -Subclasses of the [Record](#record-class) class must always override their superclass's `databaseTableName` property: - -```swift -class Place: Record { - override class var databaseTableName: String { "place" } -} -print(Place.databaseTableName) // prints "place" +print(Place.databaseTableName) // prints "location" ``` When a type adopts both TableRecord and [FetchableRecord](#fetchablerecord-protocol), it can be fetched using the [query interface](#the-query-interface): @@ -2065,7 +2067,7 @@ The `encode(to:)` method defines which [values](#values) (Bool, Int, String, Dat The optional `didInsert` method lets the adopting type store its rowID after successful insertion, and is only useful for tables that have an auto-incremented primary key. It is called from a protected dispatch queue, and serialized with all database updates. -**To use the persistable protocols**, subclass the [Record](#record-class) class, or adopt one of them explicitly. For example: +For example: ```swift extension Place : MutablePersistableRecord { @@ -2128,7 +2130,7 @@ struct Player: Encodable, MutablePersistableRecord { ### Persistence Methods -[Record](#record-class) subclasses and types that adopt [PersistableRecord] are given methods that insert, update, and delete: +Types that adopt the [PersistableRecord] protocol are given methods that insert, update, and delete: ```swift // INSERT @@ -2142,7 +2144,6 @@ try place.update(db, columns: ["title"]) // Maybe UPDATE try place.updateChanges(db, from: otherPlace) try place.updateChanges(db) { $0.isFavorite = true } -try place.updateChanges(db) // Record class only // INSERT or UPDATE try place.save(db) @@ -2330,11 +2331,10 @@ try dbQueue.write { db in let partialPlayer = PartialPlayer(name: "Alice") // INSERT INTO player (name) VALUES ('Alice') RETURNING * - if let player = try partialPlayer.insertAndFetch(db, as: Player.self) { - print(player.id) // The inserted id - print(player.name) // The inserted name - print(player.score) // The default score - } + let player = try partialPlayer.insertAndFetch(db, as: Player.self) + print(player.id) // The inserted id + print(player.name) // The inserted name + print(player.score) // The default score } ``` @@ -2415,20 +2415,6 @@ try dbQueue.write { db in } ``` -When you subclass the [Record](#record-class) class, override the callback, and make sure you call `super` at some point of your implementation: - -```swift -class Player: Record { - var id: Int64? - - // Update auto-incremented id upon successful insertion - func didInsert(_ inserted: InsertionSuccess) { - super.didInsert(inserted) - id = inserted.rowID - } -} -``` - Callbacks can also help implementing record validation: ```swift @@ -2523,7 +2509,7 @@ try Player.deleteOne(db, id: 1) try Player.deleteAll(db, ids: [1, 2, 3]) ``` -> **Note**: `Identifiable` is not available on all application targets, and not all tables have a single-column primary key. GRDB provides other methods that deal with primary and unique keys, but they won't check the type of their arguments: +> **Note**: Not all record types can be made `Identifiable`, and not all tables have a single-column primary key. GRDB provides other methods that deal with primary and unique keys, but they won't check the type of their arguments: > > ```swift > // Available on non-Identifiable types @@ -2700,14 +2686,14 @@ Those behaviors can be overridden: ```swift protocol FetchableRecord { - static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { get } - static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { get } + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy } protocol EncodableRecord { - static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { get } - static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get } - static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get } + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy + static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy } ``` @@ -2727,7 +2713,10 @@ So make sure that those are properly encoded in your requests. For example: ```swift struct Player: Codable, FetchableRecord, PersistableRecord, Identifiable { // UUIDs are stored as strings - static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { + .uppercaseString + } + var id: UUID ... } @@ -2796,7 +2785,9 @@ let player = try decoder.decode(Player.self, from: jsonData) ```swift extension Player: FetchableRecord { - static let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [decoderName: "database row"] + static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { + [decoderName: "database row"] + } } // prints "Decoded from database row" @@ -2829,64 +2820,6 @@ extension Player: FetchableRecord, PersistableRecord { See the [query interface](#the-query-interface) and [Recommended Practices for Designing Record Types](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordrecommendedpractices) for further information. -## Record Class - -**Record** is a class that is designed to be subclassed. It inherits its features from the [FetchableRecord, TableRecord, and PersistableRecord](#record-protocols-overview) protocols. On top of that, Record instances can compare against previous versions of themselves in order to [avoid useless updates](#record-comparison). - -Record subclasses define their custom database relationship by overriding database methods. For example: - -```swift -class Place: Record { - var id: Int64? - var title: String - var isFavorite: Bool - var coordinate: CLLocationCoordinate2D - - init(id: Int64?, title: String, isFavorite: Bool, coordinate: CLLocationCoordinate2D) { - self.id = id - self.title = title - self.isFavorite = isFavorite - self.coordinate = coordinate - super.init() - } - - /// The table name - override class var databaseTableName: String { "place" } - - /// The table columns - enum Columns: String, ColumnExpression { - case id, title, favorite, latitude, longitude - } - - /// Creates a record from a database row - required init(row: Row) throws { - id = row[Columns.id] - title = row[Columns.title] - isFavorite = row[Columns.favorite] - coordinate = CLLocationCoordinate2D( - latitude: row[Columns.latitude], - longitude: row[Columns.longitude]) - try super.init(row: row) - } - - /// The values persisted in the database - override func encode(to container: inout PersistenceContainer) throws { - container[Columns.id] = id - container[Columns.title] = title - container[Columns.favorite] = isFavorite - container[Columns.latitude] = coordinate.latitude - container[Columns.longitude] = coordinate.longitude - } - - /// Update record ID after a successful insertion - override func didInsert(_ inserted: InsertionSuccess) { - super.didInsert(inserted) - id = inserted.rowID - } -} -``` - - ## Record Comparison **Records that adopt the [EncodableRecord] protocol can compare against other records, or against previous versions of themselves.** @@ -2935,23 +2868,6 @@ The `updateChanges` methods perform a database update of the changed columns onl } ``` -- `updateChanges(_:)` (Record class only) - - Instances of the [Record](#record-class) class are able to compare against themselves, and know if they have changes that have not been saved since the last fetch or saving: - - ```swift - // Record class only - if let player = try Player.fetchOne(db, id: 42) { - player.score = 100 - if try player.updateChanges(db) { - print("player was modified, and updated in the database") - } else { - print("player was not modified, and database was not hit") - } - } - ``` - - ### The `databaseEquals` Method This method returns whether two records have the same database representation: @@ -2980,46 +2896,6 @@ for (column, oldValue) in try newPlayer.databaseChanges(from: oldPlayer) { // prints "score was 100" ``` -The [Record](#record-class) class is able to compare against itself: - -```swift -// Record class only -let player = Player(id: 1, name: "Arthur", score: 100) -try player.insert(db) -player.score = 1000 -for (column, oldValue) in try player.databaseChanges { - print("\(column) was \(oldValue)") -} -// prints "score was 100" -``` - -[Record](#record-class) instances also have a `hasDatabaseChanges` property: - -```swift -// Record class only -player.score = 1000 -if player.hasDatabaseChanges { - try player.save(db) -} -``` - -`Record.hasDatabaseChanges` is false after a Record instance has been fetched or saved into the database. Subsequent modifications may set it, or not: `hasDatabaseChanges` is based on value comparison. **Setting a property to the same value does not set the changed flag**: - -```swift -let player = Player(name: "Barbara", score: 750) -player.hasDatabaseChanges // true - -try player.insert(db) -player.hasDatabaseChanges // false - -player.name = "Barbara" -player.hasDatabaseChanges // false - -player.score = 1000 -player.hasDatabaseChanges // true -try player.databaseChanges // ["score": 750] -``` - For an efficient algorithm which synchronizes the content of a database table with a JSON payload, check [groue/SortedDifference](https://github.com/groue/SortedDifference). @@ -3108,15 +2984,16 @@ struct Player : MutablePersistableRecord { try player.insert(db) ``` -> **Note**: If you specify the `ignore` policy for inserts, the [`didInsert` callback](#persistence-callbacks) will be called with some random id in case of failed insert. You can detect failed insertions with `insertAndFetch`: +> **Note**: If you specify the `ignore` policy for inserts, the [`didInsert` callback](#persistence-callbacks) will be called with some random id in case of failed insert. You can detect failed insertions with `insertAndFetch`: > > ```swift > // How to detect failed `INSERT OR IGNORE`: > // INSERT OR IGNORE INTO player ... RETURNING * -> if let insertedPlayer = try player.insertAndFetch(db) { +> do { +> let insertedPlayer = try player.insertAndFetch(db) { > // Succesful insertion -> } else { -> // Ignored failure +> catch RecordError.recordNotFound { +> // Failed insertion due to IGNORE policy > } > ``` > @@ -3299,12 +3176,15 @@ extension Place: TableRecord { } /// Arrange the selected columns and lock their order - static let databaseSelection: [any SQLSelectable] = [ - Columns.id, - Columns.title, - Columns.favorite, - Columns.latitude, - Columns.longitude] + static var databaseSelection: [any SQLSelectable] { + [ + Columns.id, + Columns.title, + Columns.favorite, + Columns.latitude, + Columns.longitude, + ] + } } // Fetching methods @@ -3333,241 +3213,6 @@ extension Place: MutablePersistableRecord { -
- Subclass the Record class - -See the [Record class](#record-class) for more information. - -```swift -class Place: Record { - var id: Int64? - var title: String - var isFavorite: Bool - var coordinate: CLLocationCoordinate2D - - init(id: Int64?, title: String, isFavorite: Bool, coordinate: CLLocationCoordinate2D) { - self.id = id - self.title = title - self.isFavorite = isFavorite - self.coordinate = coordinate - super.init() - } - - /// The table name - override class var databaseTableName: String { "place" } - - /// The table columns - enum Columns: String, ColumnExpression { - case id, title, isFavorite, latitude, longitude - } - - /// Creates a record from a database row - required init(row: Row) throws { - id = row[Columns.id] - title = row[Columns.title] - isFavorite = row[Columns.isFavorite] - coordinate = CLLocationCoordinate2D( - latitude: row[Columns.latitude], - longitude: row[Columns.longitude]) - try super.init(row: row) - } - - /// The values persisted in the database - override func encode(to container: inout PersistenceContainer) throws { - container[Columns.id] = id - container[Columns.title] = title - container[Columns.isFavorite] = isFavorite - container[Columns.latitude] = coordinate.latitude - container[Columns.longitude] = coordinate.longitude - } - - // Update auto-incremented id upon successful insertion - override func didInsert(_ inserted: InsertionSuccess) { - super.didInsert(inserted) - id = inserted.rowID - } -} -``` - -
- - -## List of Record Methods - -This is the list of record methods, along with their required protocols. The [Record](#record-class) class adopts all these protocols, and adds a few extra methods. - -| Method | Protocols | Notes | -| ------ | --------- | :---: | -| **Core Methods** | | | -| `init(row:)` | [FetchableRecord] | | -| `Type.databaseTableName` | [TableRecord] | | -| `Type.databaseSelection` | [TableRecord] | [*](#columns-selected-by-a-request) | -| `Type.persistenceConflictPolicy` | [PersistableRecord] | [*](#conflict-resolution) | -| `record.encode(to:)` | [EncodableRecord] | | -| **Insert and Update Records** | | | -| `record.insert(db)` | [PersistableRecord] | | -| `record.insertAndFetch(db)` | [PersistableRecord] & [FetchableRecord] | | -| `record.insertAndFetch(_:as:)` | [PersistableRecord] | | -| `record.insertAndFetch(_:selection:fetch:)` | [PersistableRecord] | | -| `record.inserted(db)` | [PersistableRecord] | | -| `record.save(db)` | [PersistableRecord] | | -| `record.saveAndFetch(db)` | [PersistableRecord] & [FetchableRecord] | | -| `record.saveAndFetch(_:as:)` | [PersistableRecord] | | -| `record.saveAndFetch(_:selection:fetch:)` | [PersistableRecord] | | -| `record.saved(db)` | [PersistableRecord] | | -| `record.update(db)` | [PersistableRecord] | | -| `record.updateAndFetch(db)` | [PersistableRecord] & [FetchableRecord] | | -| `record.updateAndFetch(_:as:)` | [PersistableRecord] | | -| `record.updateAndFetch(_:selection:fetch:)` | [PersistableRecord] | | -| `record.update(db, columns:...)` | [PersistableRecord] | | -| `record.updateAndFetch(_:columns:selection:fetch:)` | [PersistableRecord] | | -| `record.updateChanges(db, from:...)` | [PersistableRecord] | [*](#record-comparison) | -| `record.updateChanges(db) { ... }` | [PersistableRecord] | [*](#record-comparison) | -| `record.updateChangesAndFetch(_:columns:as:modify:)` | [PersistableRecord] | | -| `record.updateChangesAndFetch(_:columns:selection:fetch:modify:)` | [PersistableRecord] | | -| `record.updateChanges(db)` | [Record](#record-class) | [*](#record-comparison) | -| `record.upsert(db)` | [PersistableRecord] | | -| `record.upsertAndFetch(db)` | [PersistableRecord] & [FetchableRecord] | | -| `record.upsertAndFetch(_:as:)` | [PersistableRecord] | | -| `Type.updateAll(db, ...)` | [TableRecord] | | -| `Type.filter(...).updateAll(db, ...)` | [TableRecord] | ² | -| **Delete Records** | | | -| `record.delete(db)` | [PersistableRecord] | | -| `Type.deleteOne(db, key:...)` | [TableRecord] | ¹ | -| `Type.deleteOne(db, id:...)` | [TableRecord] & [Identifiable] | ¹ | -| `Type.deleteAll(db)` | [TableRecord] | | -| `Type.deleteAll(db, keys:...)` | [TableRecord] | ¹ | -| `Type.deleteAll(db, ids:...)` | [TableRecord] & [Identifiable] | ¹ | -| `Type.filter(...).deleteAll(db)` | [TableRecord] | ² | -| **Persistence Callbacks** | | | -| `record.willInsert(_:)` | [PersistableRecord] | | -| `record.aroundInsert(_:insert:)` | [PersistableRecord] | | -| `record.didInsert(_:)` | [PersistableRecord] | | -| `record.willUpdate(_:columns:)` | [PersistableRecord] | | -| `record.aroundUpdate(_:columns:update:)` | [PersistableRecord] | | -| `record.didUpdate(_:)` | [PersistableRecord] | | -| `record.willSave(_:)` | [PersistableRecord] | | -| `record.aroundSave(_:save:)` | [PersistableRecord] | | -| `record.didSave(_:)` | [PersistableRecord] | | -| `record.willDelete(_:)` | [PersistableRecord] | | -| `record.aroundDelete(_:delete:)` | [PersistableRecord] | | -| `record.didDelete(deleted:)` | [PersistableRecord] | | -| **Check Record Existence** | | | -| `record.exists(db)` | [PersistableRecord] | | -| `Type.exists(db, key: ...)` | [TableRecord] | ¹ | -| `Type.exists(db, id: ...)` | [TableRecord] & [Identifiable] | ¹ | -| `Type.filter(...).isEmpty(db)` | [TableRecord] | ² | -| **Convert Record to Dictionary** | | | -| `record.databaseDictionary` | [EncodableRecord] | | -| **Count Records** | | | -| `Type.fetchCount(db)` | [TableRecord] | | -| `Type.filter(...).fetchCount(db)` | [TableRecord] | ² | -| **Fetch Record [Cursors](#cursors)** | | | -| `Type.fetchCursor(db)` | [FetchableRecord] & [TableRecord] | | -| `Type.fetchCursor(db, keys:...)` | [FetchableRecord] & [TableRecord] | ¹ | -| `Type.fetchCursor(db, ids:...)` | [FetchableRecord] & [TableRecord] & [Identifiable] | ¹ | -| `Type.fetchCursor(db, sql: sql)` | [FetchableRecord] | ³ | -| `Type.fetchCursor(statement)` | [FetchableRecord] | | -| `Type.filter(...).fetchCursor(db)` | [FetchableRecord] & [TableRecord] | ² | -| **Fetch Record Arrays** | | | -| `Type.fetchAll(db)` | [FetchableRecord] & [TableRecord] | | -| `Type.fetchAll(db, keys:...)` | [FetchableRecord] & [TableRecord] | ¹ | -| `Type.fetchAll(db, ids:...)` | [FetchableRecord] & [TableRecord] & [Identifiable] | ¹ | -| `Type.fetchAll(db, sql: sql)` | [FetchableRecord] | ³ | -| `Type.fetchAll(statement)` | [FetchableRecord] | | -| `Type.filter(...).fetchAll(db)` | [FetchableRecord] & [TableRecord] | ² | -| **Fetch Record Sets** | | | -| `Type.fetchSet(db)` | [FetchableRecord] & [TableRecord] | | -| `Type.fetchSet(db, keys:...)` | [FetchableRecord] & [TableRecord] | ¹ | -| `Type.fetchSet(db, ids:...)` | [FetchableRecord] & [TableRecord] & [Identifiable] | ¹ | -| `Type.fetchSet(db, sql: sql)` | [FetchableRecord] | ³ | -| `Type.fetchSet(statement)` | [FetchableRecord] | | -| `Type.filter(...).fetchSet(db)` | [FetchableRecord] & [TableRecord] | ² | -| **Fetch Individual Records** | | | -| `Type.fetchOne(db)` | [FetchableRecord] & [TableRecord] | | -| `Type.fetchOne(db, key:...)` | [FetchableRecord] & [TableRecord] | ¹ | -| `Type.fetchOne(db, id:...)` | [FetchableRecord] & [TableRecord] & [Identifiable] | ¹ | -| `Type.fetchOne(db, sql: sql)` | [FetchableRecord] | ³ | -| `Type.fetchOne(statement)` | [FetchableRecord] | | -| `Type.filter(...).fetchOne(db)` | [FetchableRecord] & [TableRecord] | ² | -| `Type.find(db, key:...)` | [FetchableRecord] & [TableRecord] | ¹ | -| `Type.find(db, id:...)` | [FetchableRecord] & [TableRecord] & [Identifiable] | ¹ | -| **[Codable Records]** | | | -| `Type.databaseDecodingUserInfo` | [FetchableRecord] | [*](#the-userinfo-dictionary) | -| `Type.databaseJSONDecoder(for:)` | [FetchableRecord] | [*](#json-columns) | -| `Type.databaseDateDecodingStrategy` | [FetchableRecord] | [*](#data-date-and-uuid-coding-strategies) | -| `Type.databaseEncodingUserInfo` | [EncodableRecord] | [*](#the-userinfo-dictionary) | -| `Type.databaseJSONEncoder(for:)` | [EncodableRecord] | [*](#json-columns) | -| `Type.databaseDateEncodingStrategy` | [EncodableRecord] | [*](#data-date-and-uuid-coding-strategies) | -| `Type.databaseUUIDEncodingStrategy` | [EncodableRecord] | [*](#data-date-and-uuid-coding-strategies) | -| **Define [Associations]** | | | -| `Type.belongsTo(...)` | [TableRecord] | [*](Documentation/AssociationsBasics.md) | -| `Type.hasMany(...)` | [TableRecord] | [*](Documentation/AssociationsBasics.md) | -| `Type.hasOne(...)` | [TableRecord] | [*](Documentation/AssociationsBasics.md) | -| `Type.hasManyThrough(...)` | [TableRecord] | [*](Documentation/AssociationsBasics.md) | -| `Type.hasOneThrough(...)` | [TableRecord] | [*](Documentation/AssociationsBasics.md) | -| **Building Query Interface [Requests](#requests)** | | | -| `record.request(for:...)` | [TableRecord] & [EncodableRecord] | [*](Documentation/AssociationsBasics.md) | -| `Type.all()` | [TableRecord] | ² | -| `Type.none()` | [TableRecord] | ² | -| `Type.select(...)` | [TableRecord] | ² | -| `Type.select(..., as:...)` | [TableRecord] | ² | -| `Type.selectPrimaryKey(as:...)` | [TableRecord] | ² | -| `Type.annotated(with:...)` | [TableRecord] | ² | -| `Type.filter(...)` | [TableRecord] | ² | -| `Type.filter(id:)` | [TableRecord] & Identifiable | [*](#identifiable-records) | -| `Type.filter(ids:)` | [TableRecord] & Identifiable | [*](#identifiable-records) | -| `Type.matching(...)` | [TableRecord] | ² | -| `Type.including(all:)` | [TableRecord] | ² | -| `Type.including(optional:)` | [TableRecord] | ² | -| `Type.including(required:)` | [TableRecord] | ² | -| `Type.joining(optional:)` | [TableRecord] | ² | -| `Type.joining(required:)` | [TableRecord] | ² | -| `Type.group(...)` | [TableRecord] | ² | -| `Type.groupByPrimaryKey()` | [TableRecord] | ² | -| `Type.having(...)` | [TableRecord] | ² | -| `Type.order(...)` | [TableRecord] | ² | -| `Type.orderByPrimaryKey()` | [TableRecord] | ² | -| `Type.limit(...)` | [TableRecord] | ² | -| `Type.with(...)` | [TableRecord] | ² | -| **[Record Comparison]** | | | -| `record.databaseEquals(...)` | [EncodableRecord] | | -| `record.databaseChanges(from:...)` | [EncodableRecord] | | -| `record.updateChanges(db, from:...)` | [PersistableRecord] | | -| `record.updateChanges(db) { ... }` | [PersistableRecord] | | -| `record.hasDatabaseChanges` | [Record](#record-class) | | -| `record.databaseChanges` | [Record](#record-class) | | -| `record.updateChanges(db)` | [Record](#record-class) | | - -¹ All unique keys are supported: primary keys (single-column, composite, [`rowid`](https://www.sqlite.org/rowidtable.html)) and unique indexes: - -```swift -try Player.fetchOne(db, id: 1) // Player? -try Player.fetchOne(db, key: ["email": "arthur@example.com"]) // Player? -try Country.fetchAll(db, keys: ["FR", "US"]) // [Country] -``` - -² See [Fetch Requests](#requests): - -```swift -let request = Player.filter(emailColumn != nil).order(nameColumn) -let players = try request.fetchAll(db) // [Player] -let count = try request.fetchCount(db) // Int -``` - -³ See [SQL queries](#fetch-queries): - -```swift -let player = try Player.fetchOne(db, sql: "SELECT * FROM player WHERE id = ?", arguments: [1]) // Player? -``` - - See [`Statement`]: - -```swift -let statement = try db.makeStatement(sql: "SELECT * FROM player WHERE id = ?") -let player = try Player.fetchOne(statement, arguments: [1]) // Player? -``` - The Query Interface =================== @@ -3662,10 +3307,10 @@ let players = try request.fetchAll(db) // [Player] let count = try request.fetchCount(db) // Int ``` -Query interface requests usually start from **a type** that adopts the `TableRecord` protocol, such as a `Record` subclass (see [Records](#records)): +Query interface requests usually start from **a type** that adopts the `TableRecord` protocol: ```swift -class Player: Record { ... } +struct Player: TableRecord { ... } // The request for all players: let request = Player.all() @@ -3992,12 +3637,12 @@ The default selection for a record type is controlled by the `databaseSelection` ```swift struct RestrictedPlayer : TableRecord { static let databaseTableName = "player" - static let databaseSelection: [any SQLSelectable] = [Column("id"), Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("id"), Column("name")] } } struct ExtendedPlayer : TableRecord { static let databaseTableName = "player" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } } // SELECT id, name FROM player @@ -6302,6 +5947,10 @@ This chapter has [moved](https://swiftpackageindex.com/groue/grdb.swift/document This chapter was replaced with the documentation of [splittingRowAdapters(columnCounts:)](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/splittingrowadapters(columncounts:)). +#### List of Record Methods + +See [Records and the Query Interface](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/queryinterface). + #### Migrations This chapter has [moved](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/migrations). @@ -6322,6 +5971,10 @@ This error was renamed to [RecordError]. This chapter has [moved](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/statement). +#### Record Class + +The [`Record`](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/record) class is a legacy GRDB type. Since GRDB 7, it is not recommended to define record types by subclassing the `Record` class. + #### Row Adapters This chapter has [moved](https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/rowadapter). diff --git a/SQLiteCustom/GRDB.xcconfig b/SQLiteCustom/GRDB.xcconfig index bb0c769992..85a9b53165 100755 --- a/SQLiteCustom/GRDB.xcconfig +++ b/SQLiteCustom/GRDB.xcconfig @@ -13,3 +13,4 @@ OTHER_CFLAGS = -DUSING_BUILTIN_SQLITE -DGRDBCUSTOMSQLITE $(CUSTOM_SQLLIBRARY_CFL GCC_PREPROCESSOR_DEFINITIONS = "GRDBCUSTOMSQLITE=1" OTHER_SWIFT_FLAGS = -D USING_BUILTIN_SQLITE -D GRDBCUSTOMSQLITE $(CUSTOM_OTHER_SWIFT_FLAGS) HEADER_SEARCH_PATHS = $(SRCROOT)/SQLiteCustom/src +SWIFT_VERSION = 6.0 diff --git a/SQLiteCustom/GRDBCustomSQLite-Testing.xcconfig b/SQLiteCustom/GRDBCustomSQLite-Testing.xcconfig index 62178e5d3c..41762aa8c8 100755 --- a/SQLiteCustom/GRDBCustomSQLite-Testing.xcconfig +++ b/SQLiteCustom/GRDBCustomSQLite-Testing.xcconfig @@ -10,3 +10,7 @@ CUSTOM_OTHER_SWIFT_FLAGS= // do not modify OTHER_CFLAGS = -DUSING_BUILTIN_SQLITE -DGRDBCUSTOMSQLITE $(CUSTOM_SQLLIBRARY_CFLAGS) // Do not modify. OTHER_SWIFT_FLAGS = -D USING_BUILTIN_SQLITE -D GRDBCUSTOMSQLITE $(CUSTOM_OTHER_SWIFT_FLAGS) // Do not modify. + +// Tests still use the Swift 5 language mode. +SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES +OTHER_SWIFT_FLAGS = $(inherited) -enable-upcoming-feature GlobalActorIsolatedTypesUsability diff --git a/SQLiteCustom/GRDBDeploymentTarget.xcconfig b/SQLiteCustom/GRDBDeploymentTarget.xcconfig index 9042041251..045b596962 100644 --- a/SQLiteCustom/GRDBDeploymentTarget.xcconfig +++ b/SQLiteCustom/GRDBDeploymentTarget.xcconfig @@ -1,4 +1,4 @@ -IPHONEOS_DEPLOYMENT_TARGET = 11.0 -MACOSX_DEPLOYMENT_TARGET = 10.13 -TVOS_DEPLOYMENT_TARGET = 11.0 -WATCHOS_DEPLOYMENT_TARGET = 4.0 +IPHONEOS_DEPLOYMENT_TARGET = 13.0 +MACOSX_DEPLOYMENT_TARGET = 10.15 +TVOS_DEPLOYMENT_TARGET = 13.0 +WATCHOS_DEPLOYMENT_TARGET = 7.0 diff --git a/Scripts/swiftlint.yml b/Scripts/swiftlint.yml index 4c2448eb54..9245d10d5d 100644 --- a/Scripts/swiftlint.yml +++ b/Scripts/swiftlint.yml @@ -17,6 +17,7 @@ disabled_rules: - nesting - non_optional_string_data_conversion - opening_brace + - optional_data_string_conversion - redundant_optional_initialization - syntactic_sugar - todo diff --git a/Sources/CSQLite/module.modulemap b/Sources/GRDBSQLite/module.modulemap similarity index 65% rename from Sources/CSQLite/module.modulemap rename to Sources/GRDBSQLite/module.modulemap index 0a291b5e20..95e4e886f9 100644 --- a/Sources/CSQLite/module.modulemap +++ b/Sources/GRDBSQLite/module.modulemap @@ -1,4 +1,4 @@ -module CSQLite [system] { +module GRDBSQLite [system] { header "shim.h" link "sqlite3" export * diff --git a/Sources/CSQLite/shim.h b/Sources/GRDBSQLite/shim.h similarity index 100% rename from Sources/CSQLite/shim.h rename to Sources/GRDBSQLite/shim.h diff --git a/Support/GRDB.xcconfig b/Support/GRDB.xcconfig index 5303f7a071..a3fabb0887 100644 --- a/Support/GRDB.xcconfig +++ b/Support/GRDB.xcconfig @@ -3,6 +3,7 @@ INFOPLIST_FILE = Support/Info.plist PRODUCT_NAME = GRDB PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.$(PRODUCT_NAME:rfc1034identifier) MODULEMAP_FILE = $(SRCROOT)/Support/module.modulemap +SWIFT_VERSION = 6.0 // Slow compilation hunt: // OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -warn-long-expression-type-checking=100 -Xfrontend -warn-long-function-bodies=100 diff --git a/Support/GRDBDeploymentTarget.xcconfig b/Support/GRDBDeploymentTarget.xcconfig index 753467b40d..eb0e48eac5 100644 --- a/Support/GRDBDeploymentTarget.xcconfig +++ b/Support/GRDBDeploymentTarget.xcconfig @@ -1,7 +1,7 @@ -IPHONEOS_DEPLOYMENT_TARGET = 11.0 -MACOSX_DEPLOYMENT_TARGET = 10.13 -TVOS_DEPLOYMENT_TARGET = 11.0 -WATCHOS_DEPLOYMENT_TARGET = 4.0 +IPHONEOS_DEPLOYMENT_TARGET = 13.0 +MACOSX_DEPLOYMENT_TARGET = 10.15 +TVOS_DEPLOYMENT_TARGET = 13.0 +WATCHOS_DEPLOYMENT_TARGET = 7.0 OTHER_SWIFT_FLAGS = $(inherited) -D SQLITE_ENABLE_FTS5 //// Compile with all opt-in APIs diff --git a/Support/GRDBTests.xcconfig b/Support/GRDBTests.xcconfig new file mode 100644 index 0000000000..94ad777847 --- /dev/null +++ b/Support/GRDBTests.xcconfig @@ -0,0 +1,5 @@ +#include "GRDBDeploymentTarget.xcconfig" + +// Tests still use the Swift 5 language mode. +SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES +OTHER_SWIFT_FLAGS = $(inherited) -enable-upcoming-feature GlobalActorIsolatedTypesUsability diff --git a/TODO.md b/TODO.md index 17266e7548..94fd5c4326 100644 --- a/TODO.md +++ b/TODO.md @@ -87,6 +87,117 @@ - [ ] Database.clearSchemaCache() is fine, but what about dbPool readers? Can we invalidate the cache for a whole pool? - [ ] What can we do with `cross-module-optimization`? See https://github.com/apple/swift-homomorphic-encryption +- [X] GRDB7/BREAKING: insertAndFetch, saveAndFetch, and updateAndFetch no longer return optionals (32f41472) +- [X] GRDB7/BREAKING: AsyncValueObservation does not need any scheduler (83c0e643) +- [X] GRDB7/BREAKING: Stop exporting SQLite (679d6463) +- [X] GRDB7/BREAKING: Remove Configuration.defaultTransactionKind (2661ff46) +- [X] GRDB7: Replace LockedBox with Mutex (00ccab06) +- [X] GRDB7: Sendable: BusyCallback (e0d8e20b) +- [X] GRDB7: Sendable: BusyMode (e0d8e20b) +- [X] GRDB7: Sendable: TransactionClock (f7dc72a5) +- [X] GRDB7: Sendable: Configuration (54ffb21f) +- [X] GRDB7: Sendable: DatabaseDataEncodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseDateEncodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseColumnEncodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseDataDecodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseDateDecodingStrategy (264d7fb5) +- [X] GRDB7: Sendable: DatabaseColumnDecodingStrategy (264d7fb5) +- [X] GRDB7/BREAKING: Remove DatabaseFuture and concurrentRead (05f7d3c8) +- [X] GRDB7: Sendable: DatabaseFunction (6e691fe7) +- [X] GRDB7: Sendable: DatabaseMigrator (22114ad4) +- [X] GRDB7: Not Sendable: FilterCursor (b26e9709) +- [X] GRDB7: Sendable: RowAdapter (d138af26) +- [X] GRDB7: Sendable: ValueObservationScheduler (8429eb68) +- [X] GRDB7: Sendable: DatabaseCollation (4d9d67dd) +- [X] GRDB7: Sendable: LogErrorFunction (f362518d) +- [X] GRDB7: Sendable: ReadWriteBox (57a86a0e) +- [X] GRDB7: Sendable: Pool (f13b2d2e) +- [X] GRDB7: Sendable: OnDemandFuture fulfill (2aabc4c1) +- [X] GRDB7: Sendable: WALSnapshotTransaction (7fd34012) +- [-] GRDB7: sending closures for SerializedDatabase +- [-] GRDB7: sending closures for ValueObservationScheduler +- [X] GRDB7: Sendable closures for ValueObservation.handleEvents +- [X] GRDB7: Not Sendable: Record (make it explicit if subclasses can be made sendable) +- [ ] GRDB7: Not Sendable: databasepublishers/databaseregion, migrate, read, value, write +- [X] GRDB7: Sendable closures for writePublisher +- [X] GRDB7: Sendable closures for readPublisher +- [-] GRDB7: Not Sendable: fts5customtokenizer, fts5tokenizer, fts5wrappertokenizer +- [X] GRDB7: Sendable: DatabasePromise (05899228, 5a2c15b8) +- [X] GRDB7: Sendable: TableAlias (f2b0b186) +- [X] GRDB7: Sendable: SQLRelation (9545bf70) +- [X] GRDB7: Sendable: SQL (ac33856f) +- [ ] GRDB7: Split Row.swift (2ce8a619) +- [X] GRDB7: Cleanup ValueReducer (6c73b1c5) +- [X] GRDB7: DatabaseCursor has a primary associated type (b11c5dd2) +- [ ] GRDB7: Enable Strict Concurrency Checks (6aa43ded) +- [X] GRDB7: Sendable: OrderedDictionary (e022c35b) +- [X] GRDB7: Rename ReadWriteBox to ReadWriteLock (7f5205ef) +- [X] GRDB7: Sendable: DatabaseRegionConvertible (b4677ded) +- [X] GRDB7: Sendable: ValueConcurrentObserver (87b9db65, 5465d056) +- [X] GRDB7: Sendable: ValueWriteOnlyObserver (ff2a7548) +- [X] GRDB7: Sendable: DatabaseCancellable (2f93f00b, 8f486a5e) +- [X] GRDB7: ValueObservation closures +- [?] GRDB7: DatabasePublishers.ValueSubscription +- [X] GRDB7: Sendable: ValueObservation (93f6f982) +- [?] GRDB7: Not Sendable: SharedValueObservation +- [X] GRDB7: doc (c0838cf9) +- [X] GRDB7/BREAKING: PersistenceContainer is Sendable (50eefa8c) +- [X] GRDB7: TableRecord.databaseSelection must be declared as a computed property (24d232aa) + - [X] Doc + - [X] Migration Guide +- [X] GRDB7: Sendable: Association (b06aaee4) +- [ ] GRDB7/Tests: Sendable: ValueObservationRecorder (2947b3d7) +- [X] GRDB7: ValueObservation.print cautiously uses its stream argument (5f8b39b7) +- [ ] GRDB7/Tests: use a single and Sendable test TextOutputStream (bbb1a736) +- [X] GRDB7: ValueObservation needs a ValueReducer, not a `_ValueReducer` (08733108) +- [X] GRDB7: Database support for cancellation (4ddf4bca) +- [X] GRDB7: SerializedDatabase support for async db access with support for Task cancellation (737cb149) +- [X] GRDB7: DatabaseWriter async methods support Task cancellation (a5226501) +- [X] GRDB7: DatabaseReader async methods support Task cancellation (10c9d311) +- [X] GRDB7: Document that async methods can throw CancellationError (8df18fb8) +- [-] GRDB7: Sendable: AssociationAggregate (48ad10ae) +- [X] GRDB7: Sendable: AsyncValueObservation (necessary for async algorithm) (ce63cdfa) +- [X] GRDB7: Sendable: DatabaseRegionObservation (b4ff52fb) +- [-] GRDB7: DispatchQueue.asyncSending (7b075e6b) +- [X] GRDB7: Replace sequences with collection (e.g. https://github.com/tidal-music/tidal-sdk-ios/pull/39) +- [X] GRDB7: Replace `some` DatabaseReader/Writer with `any` where possible, in order to avoid issues with accessing DatabaseContext from GRDBQuery (if the problem exists in Xcode 16) +- [X] GRDB7: bump to iOS 13, macOS 10.15, tvOS 13 (for ValueObservation support for MainActor) + +- [ ] GRDB7: DatabasePublishers.Value should carry the type of the Scheduler, so that we can rely on main-actor-isolated callbacks. +- [X] GRDB7: Remove warning about "products" in Package.swift +- [X] GRDB7: Fixits + - [X] defaultTransactionKind + - [X] concurrentRead +- [X] GRDB7: Swift Concurrency recommendations + - [X] Record classe(s) + - [X] InferSendableFromCaptures +- [X] GRDB7: stop fostering the Record class + - Remove all mentions from the README + - Warn about it in the documentation of the class. +- [X] GRDB7: Breaking changes documentation + - [X] [BREAKING] Xcode 16+, Swift 6+ + - [X] [BREAKING] iOS 13+ + - [X] [BREAKING] macOS 10.15+ + - [X] [BREAKING] tvOS 13+ + - [X] [BREAKING] watchOS 7+ + - [-] insertAndFetch, updateAndFetch, saveAndFetch + - [X] CSQLite renamed to GRDBCSQLite + - [X] CSQLite is not exported + - [X] defaultTransactionKind + - [X] concurrentRead + - [X] record column strategies: + - databaseDataEncodingStrategy + - databaseDateEncodingStrategy + - databaseUUIDEncodingStrategy + - databaseDataDecodingStrategy + - databaseDateDecodingStrategy + - [X] PersistenceContainer subscript no longer returns its input value + - [X] cancellation of async database access + - [X] Async sequences built from ValueObservation schedule values and errors on the cooperative thread pool by default. + - [X] `TableRecord.databaseSelection` should be declared as a computed static property + - [-] databaseDecodingUserInfo and databaseEncodingUserInfo must be declared as a computed property +- [ ] GRDB7: Review experimental apis +- [?] GRDB7: Change ValueObservation callback argument so that it could expose snapshots? https://github.com/groue/GRDB.swift/discussions/1523#discussioncomment-9092500 ## Unsure if necessary diff --git a/Tests/CocoaPods/GRDBiOS-framework/AppDelegate.swift b/Tests/CocoaPods/GRDBiOS-framework/AppDelegate.swift index 38d6b51bd8..b010f4c990 100644 --- a/Tests/CocoaPods/GRDBiOS-framework/AppDelegate.swift +++ b/Tests/CocoaPods/GRDBiOS-framework/AppDelegate.swift @@ -13,6 +13,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // test_SQLITE_ENABLE_PREUPDATE_HOOK _ = DatabasePreUpdateEvent.self + // test C functions + _ = sqlite3_libversion_number() + return true } } diff --git a/Tests/CocoaPods/GRDBiOS-framework/Podfile b/Tests/CocoaPods/GRDBiOS-framework/Podfile index 8a4c9e93a9..82bf451d20 100644 --- a/Tests/CocoaPods/GRDBiOS-framework/Podfile +++ b/Tests/CocoaPods/GRDBiOS-framework/Podfile @@ -1,6 +1,6 @@ use_frameworks! target 'iOS' -platform :ios, '11.0' +platform :ios, '13.0' pod 'GRDB.swift', :path => '../../..' post_install do |installer| diff --git a/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj b/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj index 4de5afc205..b04521c007 100644 --- a/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/GRDBiOS-framework/iOS.xcodeproj/project.pbxproj @@ -251,7 +251,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -301,7 +301,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; @@ -318,7 +318,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; INFOPLIST_FILE = Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBTest; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -335,7 +335,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; INFOPLIST_FILE = Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBTest; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Tests/CocoaPods/GRDBiOS-static/AppDelegate.swift b/Tests/CocoaPods/GRDBiOS-static/AppDelegate.swift index 38d6b51bd8..b010f4c990 100644 --- a/Tests/CocoaPods/GRDBiOS-static/AppDelegate.swift +++ b/Tests/CocoaPods/GRDBiOS-static/AppDelegate.swift @@ -13,6 +13,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // test_SQLITE_ENABLE_PREUPDATE_HOOK _ = DatabasePreUpdateEvent.self + // test C functions + _ = sqlite3_libversion_number() + return true } } diff --git a/Tests/CocoaPods/GRDBiOS-static/Podfile b/Tests/CocoaPods/GRDBiOS-static/Podfile index 94614c25b1..f4137605f3 100644 --- a/Tests/CocoaPods/GRDBiOS-static/Podfile +++ b/Tests/CocoaPods/GRDBiOS-static/Podfile @@ -1,5 +1,5 @@ target 'iOS' -platform :ios, '11.0' +platform :ios, '13.0' pod 'GRDB.swift', :path => '../../..' post_install do |installer| diff --git a/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj b/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj index 6255545123..acfaddb1ce 100644 --- a/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/GRDBiOS-static/iOS.xcodeproj/project.pbxproj @@ -232,7 +232,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -282,7 +282,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj index 2869b204eb..a872849dd6 100644 --- a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ 5623B61E2AED39F700436239 /* DatabaseQueueTemporaryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B61B2AED39F700436239 /* DatabaseQueueTemporaryCopyTests.swift */; }; 5623B61F2AED39F700436239 /* DatabaseQueueInMemoryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B61C2AED39F700436239 /* DatabaseQueueInMemoryCopyTests.swift */; }; 5623B6202AED39F700436239 /* DatabaseQueueInMemoryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B61C2AED39F700436239 /* DatabaseQueueInMemoryCopyTests.swift */; }; + 563EA3E52C7B3A4F001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E42C7B3A4F001BE0D4 /* Mutex.swift */; }; + 563EA3E62C7B3A4F001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E42C7B3A4F001BE0D4 /* Mutex.swift */; }; 56419D6724A54062004967E1 /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419C9C24A54053004967E1 /* DatabasePoolTests.swift */; }; 56419D6824A54062004967E1 /* DatabasePoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419C9C24A54053004967E1 /* DatabasePoolTests.swift */; }; 56419D6924A54062004967E1 /* ResultCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419C9D24A54053004967E1 /* ResultCodeTests.swift */; }; @@ -493,6 +495,8 @@ 568C3F802A5AB36900A2309D /* ForeignKeyDefinitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568C3F7E2A5AB36900A2309D /* ForeignKeyDefinitionTests.swift */; }; 5691D97527257CE40021D540 /* AvailableElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5691D97427257CE40021D540 /* AvailableElements.swift */; }; 5691D97627257CE40021D540 /* AvailableElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5691D97427257CE40021D540 /* AvailableElements.swift */; }; + 56DFC3B32C84798300DFE5DC /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DFC3B22C84798300DFE5DC /* AsyncSemaphore.swift */; }; + 56DFC3B42C84798300DFE5DC /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DFC3B22C84798300DFE5DC /* AsyncSemaphore.swift */; }; 56F61DF0283D484700AF9884 /* getThreadsCount.c in Sources */ = {isa = PBXBuildFile; fileRef = 56F61DEE283D484700AF9884 /* getThreadsCount.c */; }; 56F61DF1283D484700AF9884 /* getThreadsCount.c in Sources */ = {isa = PBXBuildFile; fileRef = 56F61DEE283D484700AF9884 /* getThreadsCount.c */; }; 5B33E6E34F941B4C839A714F /* (null) in Frameworks */ = {isa = PBXBuildFile; }; @@ -510,6 +514,7 @@ 561F38F82AC9CE6D0051EEE9 /* DatabaseDataEncodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataEncodingStrategyTests.swift; sourceTree = ""; }; 5623B61B2AED39F700436239 /* DatabaseQueueTemporaryCopyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTemporaryCopyTests.swift; sourceTree = ""; }; 5623B61C2AED39F700436239 /* DatabaseQueueInMemoryCopyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueInMemoryCopyTests.swift; sourceTree = ""; }; + 563EA3E42C7B3A4F001BE0D4 /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 56419C9C24A54053004967E1 /* DatabasePoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolTests.swift; sourceTree = ""; }; 56419C9D24A54053004967E1 /* ResultCodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultCodeTests.swift; sourceTree = ""; }; 56419C9E24A54053004967E1 /* DatabaseQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTests.swift; sourceTree = ""; }; @@ -749,6 +754,7 @@ 567B5C4A2AD32F7000629622 /* DatabaseReaderDumpTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseReaderDumpTests.swift; sourceTree = ""; }; 568C3F7E2A5AB36900A2309D /* ForeignKeyDefinitionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForeignKeyDefinitionTests.swift; sourceTree = ""; }; 5691D97427257CE40021D540 /* AvailableElements.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvailableElements.swift; sourceTree = ""; }; + 56DFC3B22C84798300DFE5DC /* AsyncSemaphore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSemaphore.swift; sourceTree = ""; }; 56F61DEC283D484700AF9884 /* GRDBTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GRDBTests-Bridging-Header.h"; sourceTree = ""; }; 56F61DEE283D484700AF9884 /* getThreadsCount.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = getThreadsCount.c; sourceTree = ""; }; 56F61DEF283D484700AF9884 /* getThreadsCount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = getThreadsCount.h; sourceTree = ""; }; @@ -881,6 +887,7 @@ 56419CAD24A54054004967E1 /* AssociationPrefetchingSQLTests.swift */, 56419D5824A54061004967E1 /* AssociationRowScopeSearchTests.swift */, 56419D6324A54062004967E1 /* AssociationTableAliasTestsSQLTests.swift */, + 56DFC3B22C84798300DFE5DC /* AsyncSemaphore.swift */, 565A27CB27871FFF00659A62 /* BackupTestCase.swift */, 567B5C272AD32A2D00629622 /* CaseInsensitiveIdentifierTests.swift */, 56419CD424A54057004967E1 /* CGFloatTests.swift */, @@ -996,6 +1003,7 @@ 56419D2F24A5405E004967E1 /* MutablePersistableRecordEncodableTests.swift */, 56419D4424A5405F004967E1 /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */, 56419D3B24A5405F004967E1 /* MutablePersistableRecordTests.swift */, + 563EA3E42C7B3A4F001BE0D4 /* Mutex.swift */, 56419CFD24A5405A004967E1 /* NumericOverflowTests.swift */, 56419D4F24A54060004967E1 /* OrderedDictionaryTests.swift */, 56419D4524A5405F004967E1 /* PersistableRecordTests.swift */, @@ -1435,6 +1443,7 @@ 56419E0924A54062004967E1 /* DatabaseValueConversionTests.swift in Sources */, 56419DB124A54062004967E1 /* AssociationBelongsToDecodableRecordTests.swift in Sources */, 56419D9324A54062004967E1 /* PrimaryKeyInfoTests.swift in Sources */, + 563EA3E62C7B3A4F001BE0D4 /* Mutex.swift in Sources */, 567B5C392AD32A2D00629622 /* FoundationDecimalTests.swift in Sources */, 5641A1BA24A540D6004967E1 /* Inverted.swift in Sources */, 5641A1B624A540D6004967E1 /* Prefix.swift in Sources */, @@ -1488,6 +1497,7 @@ 56419ED724A54063004967E1 /* ColumnInfoTests.swift in Sources */, 56419D9F24A54062004967E1 /* CompilationSubClassTests.swift in Sources */, 567B5C3F2AD32A2D00629622 /* CaseInsensitiveIdentifierTests.swift in Sources */, + 56DFC3B32C84798300DFE5DC /* AsyncSemaphore.swift in Sources */, 56419DBB24A54062004967E1 /* ValueObservationRegionRecordingTests.swift in Sources */, 56419EA924A54063004967E1 /* DatabasePoolReleaseMemoryTests.swift in Sources */, 56419E7F24A54063004967E1 /* DatabaseQueueConcurrencyTests.swift in Sources */, @@ -1683,6 +1693,7 @@ 56419E0A24A54062004967E1 /* DatabaseValueConversionTests.swift in Sources */, 56419DB224A54062004967E1 /* AssociationBelongsToDecodableRecordTests.swift in Sources */, 56419D9424A54062004967E1 /* PrimaryKeyInfoTests.swift in Sources */, + 563EA3E52C7B3A4F001BE0D4 /* Mutex.swift in Sources */, 567B5C3A2AD32A2D00629622 /* FoundationDecimalTests.swift in Sources */, 5641A1BB24A540D6004967E1 /* Inverted.swift in Sources */, 5641A1B724A540D6004967E1 /* Prefix.swift in Sources */, @@ -1736,6 +1747,7 @@ 56419ED824A54063004967E1 /* ColumnInfoTests.swift in Sources */, 56419DA024A54062004967E1 /* CompilationSubClassTests.swift in Sources */, 567B5C402AD32A2D00629622 /* CaseInsensitiveIdentifierTests.swift in Sources */, + 56DFC3B42C84798300DFE5DC /* AsyncSemaphore.swift in Sources */, 56419DBC24A54062004967E1 /* ValueObservationRegionRecordingTests.swift in Sources */, 56419EAA24A54063004967E1 /* DatabasePoolReleaseMemoryTests.swift in Sources */, 56419E8024A54063004967E1 /* DatabaseQueueConcurrencyTests.swift in Sources */, @@ -1810,7 +1822,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; ONLY_ACTIVE_ARCH = YES; SWIFT_VERSION = 5.0; }; @@ -1844,7 +1856,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/Tests/CocoaPods/SQLCipher3/Podfile b/Tests/CocoaPods/SQLCipher3/Podfile index fbe38136b0..66346dd900 100644 --- a/Tests/CocoaPods/SQLCipher3/Podfile +++ b/Tests/CocoaPods/SQLCipher3/Podfile @@ -1,4 +1,4 @@ -platform :macos, '10.13' +platform :macos, '10.15' use_frameworks! def common @@ -19,7 +19,7 @@ post_install do |installer| target.build_configurations.each do |config| # Workaround for Xcode 14.3+ # https://github.com/CocoaPods/CocoaPods/issues/11839 - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.13' + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.15' config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '3' end end diff --git a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj index f8c3e463fe..64c03419a8 100644 --- a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ 5623B6242AED3A2200436239 /* DatabaseQueueInMemoryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B6212AED3A2200436239 /* DatabaseQueueInMemoryCopyTests.swift */; }; 5623B6252AED3A2200436239 /* DatabaseQueueTemporaryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B6222AED3A2200436239 /* DatabaseQueueTemporaryCopyTests.swift */; }; 5623B6262AED3A2200436239 /* DatabaseQueueTemporaryCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623B6222AED3A2200436239 /* DatabaseQueueTemporaryCopyTests.swift */; }; + 563EA3E82C7B3A78001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E72C7B3A78001BE0D4 /* Mutex.swift */; }; + 563EA3E92C7B3A78001BE0D4 /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563EA3E72C7B3A78001BE0D4 /* Mutex.swift */; }; 56419FC824A540A1004967E1 /* FetchRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419EFD24A54093004967E1 /* FetchRequestTests.swift */; }; 56419FC924A540A1004967E1 /* FetchRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419EFD24A54093004967E1 /* FetchRequestTests.swift */; }; 56419FCA24A540A1004967E1 /* DatabasePoolBackupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56419EFE24A54093004967E1 /* DatabasePoolBackupTests.swift */; }; @@ -495,6 +497,8 @@ 568C3F982A5AB3A800A2309D /* RecordMinimalNonOptionalPrimaryKeySingleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568C3F8A2A5AB3A800A2309D /* RecordMinimalNonOptionalPrimaryKeySingleTests.swift */; }; 5691D97227257C930021D540 /* AvailableElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5691D97127257C930021D540 /* AvailableElements.swift */; }; 5691D97327257C930021D540 /* AvailableElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5691D97127257C930021D540 /* AvailableElements.swift */; }; + 56DFC3B02C84797400DFE5DC /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DFC3AF2C84797400DFE5DC /* AsyncSemaphore.swift */; }; + 56DFC3B12C84797400DFE5DC /* AsyncSemaphore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DFC3AF2C84797400DFE5DC /* AsyncSemaphore.swift */; }; 56F61DF6283D4AB100AF9884 /* getThreadsCount.c in Sources */ = {isa = PBXBuildFile; fileRef = 56F61DF4283D4AB100AF9884 /* getThreadsCount.c */; }; 56F61DF7283D4AB100AF9884 /* getThreadsCount.c in Sources */ = {isa = PBXBuildFile; fileRef = 56F61DF4283D4AB100AF9884 /* getThreadsCount.c */; }; 5B33E6E34F941B4C839A714F /* (null) in Frameworks */ = {isa = PBXBuildFile; }; @@ -512,6 +516,7 @@ 561F38FE2AC9CE870051EEE9 /* DatabaseDataDecodingStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseDataDecodingStrategyTests.swift; sourceTree = ""; }; 5623B6212AED3A2200436239 /* DatabaseQueueInMemoryCopyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueInMemoryCopyTests.swift; sourceTree = ""; }; 5623B6222AED3A2200436239 /* DatabaseQueueTemporaryCopyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTemporaryCopyTests.swift; sourceTree = ""; }; + 563EA3E72C7B3A78001BE0D4 /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 56419EFD24A54093004967E1 /* FetchRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequestTests.swift; sourceTree = ""; }; 56419EFE24A54093004967E1 /* DatabasePoolBackupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolBackupTests.swift; sourceTree = ""; }; 56419EFF24A54093004967E1 /* TableRecordDeleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRecordDeleteTests.swift; sourceTree = ""; }; @@ -752,6 +757,7 @@ 568C3F892A5AB3A800A2309D /* DatabaseSnapshotPoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSnapshotPoolTests.swift; sourceTree = ""; }; 568C3F8A2A5AB3A800A2309D /* RecordMinimalNonOptionalPrimaryKeySingleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecordMinimalNonOptionalPrimaryKeySingleTests.swift; sourceTree = ""; }; 5691D97127257C930021D540 /* AvailableElements.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvailableElements.swift; sourceTree = ""; }; + 56DFC3AF2C84797400DFE5DC /* AsyncSemaphore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSemaphore.swift; sourceTree = ""; }; 56F61DF2283D4AB100AF9884 /* GRDBTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GRDBTests-Bridging-Header.h"; sourceTree = ""; }; 56F61DF4283D4AB100AF9884 /* getThreadsCount.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = getThreadsCount.c; sourceTree = ""; }; 56F61DF5283D4AB100AF9884 /* getThreadsCount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = getThreadsCount.h; sourceTree = ""; }; @@ -885,6 +891,7 @@ 56419F4424A54097004967E1 /* AssociationPrefetchingSQLTests.swift */, 56419F5024A54098004967E1 /* AssociationRowScopeSearchTests.swift */, 56419F9E24A5409D004967E1 /* AssociationTableAliasTestsSQLTests.swift */, + 56DFC3AF2C84797400DFE5DC /* AsyncSemaphore.swift */, 565A27C827871FE500659A62 /* BackupTestCase.swift */, 568C3F882A5AB3A800A2309D /* CaseInsensitiveIdentifierTests.swift */, 56419F6B24A5409A004967E1 /* CGFloatTests.swift */, @@ -1000,6 +1007,7 @@ 56419F0224A54093004967E1 /* MutablePersistableRecordEncodableTests.swift */, 56419F3124A54096004967E1 /* MutablePersistableRecordPersistenceConflictPolicyTests.swift */, 56419F0124A54093004967E1 /* MutablePersistableRecordTests.swift */, + 563EA3E72C7B3A78001BE0D4 /* Mutex.swift */, 56419F0824A54093004967E1 /* NumericOverflowTests.swift */, 56419F0324A54093004967E1 /* OrderedDictionaryTests.swift */, 56419F1A24A54094004967E1 /* PersistableRecordTests.swift */, @@ -1441,6 +1449,7 @@ 5641A08E24A540A1004967E1 /* DatabaseLogErrorTests.swift in Sources */, 5641A01624A540A1004967E1 /* UtilsTests.swift in Sources */, 5641A12A24A540A1004967E1 /* FlattenCursorTests.swift in Sources */, + 563EA3E82C7B3A78001BE0D4 /* Mutex.swift in Sources */, 5641A10424A540A1004967E1 /* DatabaseValueTests.swift in Sources */, 5641A06824A540A1004967E1 /* RowFetchTests.swift in Sources */, 5641A0E024A540A1004967E1 /* QueryInterfaceExtensibilityTests.swift in Sources */, @@ -1494,6 +1503,7 @@ 5641A02424A540A1004967E1 /* ValueObservationPrintTests.swift in Sources */, 5641A12024A540A1004967E1 /* AssociationHasManyThroughOrderingTests.swift in Sources */, 5641A04224A540A1004967E1 /* FTS5TableBuilderTests.swift in Sources */, + 56DFC3B02C84797400DFE5DC /* AsyncSemaphore.swift in Sources */, 5641A10624A540A1004967E1 /* TableRecord+QueryInterfaceRequestTests.swift in Sources */, 5641A11A24A540A1004967E1 /* StatementColumnConvertibleFetchTests.swift in Sources */, 5641A05424A540A1004967E1 /* DatabaseWriterTests.swift in Sources */, @@ -1689,6 +1699,7 @@ 5641A08F24A540A1004967E1 /* DatabaseLogErrorTests.swift in Sources */, 5641A01724A540A1004967E1 /* UtilsTests.swift in Sources */, 5641A12B24A540A1004967E1 /* FlattenCursorTests.swift in Sources */, + 563EA3E92C7B3A78001BE0D4 /* Mutex.swift in Sources */, 5641A10524A540A1004967E1 /* DatabaseValueTests.swift in Sources */, 5641A06924A540A1004967E1 /* RowFetchTests.swift in Sources */, 5641A0E124A540A1004967E1 /* QueryInterfaceExtensibilityTests.swift in Sources */, @@ -1742,6 +1753,7 @@ 5641A02524A540A1004967E1 /* ValueObservationPrintTests.swift in Sources */, 5641A12124A540A1004967E1 /* AssociationHasManyThroughOrderingTests.swift in Sources */, 5641A04324A540A1004967E1 /* FTS5TableBuilderTests.swift in Sources */, + 56DFC3B12C84797400DFE5DC /* AsyncSemaphore.swift in Sources */, 5641A10724A540A1004967E1 /* TableRecord+QueryInterfaceRequestTests.swift in Sources */, 5641A11B24A540A1004967E1 /* StatementColumnConvertibleFetchTests.swift in Sources */, 5641A05524A540A1004967E1 /* DatabaseWriterTests.swift in Sources */, @@ -1816,7 +1828,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; ONLY_ACTIVE_ARCH = YES; SWIFT_VERSION = 5.0; }; @@ -1850,7 +1862,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/Tests/CocoaPods/SQLCipher4/Podfile b/Tests/CocoaPods/SQLCipher4/Podfile index 031d49b72e..d6e3762437 100644 --- a/Tests/CocoaPods/SQLCipher4/Podfile +++ b/Tests/CocoaPods/SQLCipher4/Podfile @@ -1,4 +1,4 @@ -platform :macos, '10.13' +platform :macos, '10.15' use_frameworks! def common @@ -19,7 +19,7 @@ post_install do |installer| target.build_configurations.each do |config| # Workaround for Xcode 14.3+ # https://github.com/CocoaPods/CocoaPods/issues/11839 - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.13' + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.15' config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '3' end end diff --git a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift index 402b3f5535..f37f2eb1ec 100644 --- a/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift +++ b/Tests/CombineExpectations/PublisherExpectations/AvailableElements.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension PublisherExpectations { /// A publisher expectation which waits for the timeout to expire, or /// the recorded publisher to complete. @@ -46,7 +45,7 @@ extension PublisherExpectations { } /// A waiter that waits but never fails - private class Waiter: XCTWaiter, XCTWaiterDelegate { + private class Waiter: XCTWaiter, XCTWaiterDelegate, @unchecked Sendable { init() { super.init(delegate: nil) delegate = self diff --git a/Tests/CombineExpectations/PublisherExpectations/Finished.swift b/Tests/CombineExpectations/PublisherExpectations/Finished.swift index 0a47d09f62..c7b7ecf3dc 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Finished.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Finished.swift @@ -17,7 +17,6 @@ import XCTest // try wait(for: recorder.finished.inverted, timeout: 1) // } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Inverted.swift b/Tests/CombineExpectations/PublisherExpectations/Inverted.swift index c7f8d72cc3..755897823d 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Inverted.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Inverted.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension PublisherExpectations { /// A publisher expectation that fails if the base expectation is fulfilled. /// diff --git a/Tests/CombineExpectations/PublisherExpectations/Map.swift b/Tests/CombineExpectations/PublisherExpectations/Map.swift index ab4f95c733..87276c91de 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Map.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Map.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension PublisherExpectations { /// A publisher expectation that transforms the value of a base expectation. /// @@ -20,7 +19,6 @@ extension PublisherExpectations { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension PublisherExpectation { /// Returns a publisher expectation that transforms the value of the /// base expectation. diff --git a/Tests/CombineExpectations/PublisherExpectations/Next.swift b/Tests/CombineExpectations/PublisherExpectations/Next.swift index 76ad0c1055..6a27fa7df7 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Next.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Next.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `count` elements, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/NextOne.swift b/Tests/CombineExpectations/PublisherExpectations/NextOne.swift index 84ee6233e5..65ebe04b12 100644 --- a/Tests/CombineExpectations/PublisherExpectations/NextOne.swift +++ b/Tests/CombineExpectations/PublisherExpectations/NextOne.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// one element, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Prefix.swift b/Tests/CombineExpectations/PublisherExpectations/Prefix.swift index 11aced69da..c9bae6cf56 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Prefix.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Prefix.swift @@ -1,7 +1,6 @@ #if canImport(Combine) import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `maxLength` elements, or to complete. diff --git a/Tests/CombineExpectations/PublisherExpectations/Recording.swift b/Tests/CombineExpectations/PublisherExpectations/Recording.swift index 0b95292dc0..1368f42252 100644 --- a/Tests/CombineExpectations/PublisherExpectations/Recording.swift +++ b/Tests/CombineExpectations/PublisherExpectations/Recording.swift @@ -2,7 +2,6 @@ import Combine import XCTest -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. diff --git a/Tests/CombineExpectations/Recorder.swift b/Tests/CombineExpectations/Recorder.swift index 0d7031aaf1..bc61f110dd 100644 --- a/Tests/CombineExpectations/Recorder.swift +++ b/Tests/CombineExpectations/Recorder.swift @@ -13,7 +13,6 @@ import XCTest /// /// let elements = try wait(for: recorder.elements, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public class Recorder: Subscriber { public typealias Input = Input public typealias Failure = Failure @@ -287,7 +286,6 @@ public class Recorder: Subscriber { // MARK: - Publisher Expectations -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension PublisherExpectations { /// The type of the publisher expectation returned by `Recorder.completion`. public typealias Completion = Map, Subscribers.Completion> @@ -302,7 +300,6 @@ extension PublisherExpectations { public typealias Single = Map, Input> } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Recorder { /// Returns a publisher expectation which waits for the timeout to expire, /// or the recorded publisher to complete. @@ -584,7 +581,6 @@ extension Recorder { // MARK: - Publisher + Recorder -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Publisher { /// Returns a subscribed Recorder. /// diff --git a/Tests/CustomSQLite/CustomSQLite.xcodeproj/project.pbxproj b/Tests/CustomSQLite/CustomSQLite.xcodeproj/project.pbxproj index 4d0fd10cde..315344000f 100644 --- a/Tests/CustomSQLite/CustomSQLite.xcodeproj/project.pbxproj +++ b/Tests/CustomSQLite/CustomSQLite.xcodeproj/project.pbxproj @@ -342,7 +342,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -397,7 +397,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/Tests/CustomSQLite/CustomSQLite/AppDelegate.swift b/Tests/CustomSQLite/CustomSQLite/AppDelegate.swift index 12798376f0..e8f1bf3367 100644 --- a/Tests/CustomSQLite/CustomSQLite/AppDelegate.swift +++ b/Tests/CustomSQLite/CustomSQLite/AppDelegate.swift @@ -7,5 +7,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { _ = try! DatabaseQueue() _ = FTS5() _ = sqlite3_preupdate_new(nil, 0, nil) + let sqliteVersion = String(cString: sqlite3_libversion()) + print(sqliteVersion) } } diff --git a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift index 2b1cf9394f..9d7f4a813f 100644 --- a/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseReaderReadPublisherTests.swift @@ -22,10 +22,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -44,7 +40,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() } #endif } @@ -128,10 +124,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // frame #71: 0x00007fff72311cc9 libdyld.dylib`start + 1 // frame #72: 0x00007fff72311cc9 libdyld.dylib`start + 1 func testReadPublisherError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(reader: some DatabaseReader) throws { let publisher = reader.readPublisher(value: { db in try Row.fetchAll(db, sql: "THIS IS NOT SQL") @@ -149,7 +141,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshotPool() } #endif } @@ -157,10 +149,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -189,7 +177,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() } #endif } @@ -197,10 +185,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherDefaultScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -229,7 +213,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() } #endif } @@ -237,10 +221,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherCustomScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -270,7 +250,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) } try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() } #endif } @@ -278,10 +258,6 @@ class DatabaseReaderReadPublisherTests : XCTestCase { // MARK: - func testReadPublisherIsReadonly() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(reader: some DatabaseReader) throws { let publisher = reader.readPublisher(value: { db in try Player.createTable(db) @@ -298,7 +274,7 @@ class DatabaseReaderReadPublisherTests : XCTestCase { try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshot() } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshotPool() } #endif } diff --git a/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift b/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift index e126c54b39..a83adfa02a 100644 --- a/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift @@ -20,10 +20,6 @@ private struct Player: Codable, FetchableRecord, PersistableRecord { class DatabaseRegionObservationPublisherTests : XCTestCase { func testChangesNotifications() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -61,10 +57,6 @@ class DatabaseRegionObservationPublisherTests : XCTestCase { // TODO: do the same, but asynchronously. If this is too hard, update the // public API so that users can easily do it. func testPrependInitialDatabaseSync() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer diff --git a/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift b/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift index 1ab1420818..7d1b2d0a57 100644 --- a/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift +++ b/Tests/GRDBCombineTests/DatabaseWriterWritePublisherTests.swift @@ -22,10 +22,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -49,10 +45,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherValue() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -76,10 +68,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = writer.writePublisher(updates: { db in try db.execute(sql: "THIS IS NOT SQL") @@ -99,10 +87,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { } func testWritePublisherErrorRollbacksTransaction() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -132,10 +116,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -168,10 +148,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherDefaultScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -206,10 +182,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWritePublisherCustomScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -247,10 +219,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // TODO: Fix flaky test with both pool and on-disk queue: // - Expectation timeout func testWriteThenReadPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -274,10 +242,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWriteThenReadPublisherIsReadonly() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = writer .writePublisher( @@ -299,10 +263,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // MARK: - func testWriteThenReadPublisherWriteError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = writer.writePublisher( updates: { db in try db.execute(sql: "THIS IS NOT SQL") }, @@ -322,10 +282,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { } func testWriteThenReadPublisherWriteErrorRollbacksTransaction() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -359,10 +315,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // TODO: Fix flaky test with both pool and on-disk queue: // - Expectation timeout func testWriteThenReadPublisherReadError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = writer.writePublisher( updates: { _ in }, @@ -386,10 +338,6 @@ class DatabaseWriterWritePublisherTests : XCTestCase { // Regression test against deadlocks created by concurrent completion // and cancellations triggered by .switchToLatest().prefix(1) func testDeadlockPrevention() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer diff --git a/Tests/GRDBCombineTests/Support.swift b/Tests/GRDBCombineTests/Support.swift index d8f2ec8381..6d35dd7718 100644 --- a/Tests/GRDBCombineTests/Support.swift +++ b/Tests/GRDBCombineTests/Support.swift @@ -51,7 +51,6 @@ final class Test { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) final class AsyncTest { // Raise the repeatCount in order to help spotting flaky tests. private let repeatCount: Int @@ -100,7 +99,6 @@ final class AsyncTest { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func assertNoFailure( _ completion: Subscribers.Completion, file: StaticString = #file, @@ -111,7 +109,6 @@ public func assertNoFailure( } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func assertFailure( _ completion: Subscribers.Completion, file: StaticString = #file, diff --git a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift index b55fd98c6d..0849a0085e 100644 --- a/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift +++ b/Tests/GRDBCombineTests/ValueObservationPublisherTests.swift @@ -22,10 +22,6 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Default Scheduler func testDefaultSchedulerChangesNotifications() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -64,10 +60,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDefaultSchedulerFirstValueIsEmittedAsynchronously() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -97,10 +89,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDefaultSchedulerError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = ValueObservation .trackingConstantRegion { try $0.execute(sql: "THIS IS NOT SQL") } @@ -123,10 +111,6 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Immediate Scheduler func testImmediateSchedulerChangesNotifications() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -165,10 +149,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testImmediateSchedulerEmitsFirstValueSynchronously() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -201,10 +181,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testImmediateSchedulerError() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let publisher = ValueObservation .trackingConstantRegion { try $0.execute(sql: "THIS IS NOT SQL") } @@ -226,7 +202,6 @@ class ValueObservationPublisherTests : XCTestCase { // MARK: - Demand - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) private class DemandSubscriber: Subscriber { private var subscription: Subscription? let subject = PassthroughSubject() @@ -257,10 +232,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandNoneReceivesNoElement() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -292,10 +263,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandOneReceivesOneElement() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -330,10 +297,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandOneDoesNotReceiveTwoElements() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -372,10 +335,6 @@ class ValueObservationPublisherTests : XCTestCase { } func testDemandTwoReceivesTwoElements() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func setUp(_ writer: Writer) throws -> Writer { try writer.write(Player.createTable) return writer @@ -418,10 +377,6 @@ class ValueObservationPublisherTests : XCTestCase { /// Regression test for https://github.com/groue/GRDB.swift/issues/1194 func testIssue1194() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - struct Record: Codable, FetchableRecord, PersistableRecord { var id: Int64 } diff --git a/Tests/GRDBManualInstall/GRDBManualInstall.xcodeproj/project.pbxproj b/Tests/GRDBManualInstall/GRDBManualInstall.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..bd3054f188 --- /dev/null +++ b/Tests/GRDBManualInstall/GRDBManualInstall.xcodeproj/project.pbxproj @@ -0,0 +1,401 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 56CFC70E2C9F5BDB000B5023 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56CFC7082C9F5BB4000B5023 /* GRDB.framework */; }; + 56CFC70F2C9F5BDB000B5023 /* GRDB.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 56CFC7082C9F5BB4000B5023 /* GRDB.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 56CFC7072C9F5BB4000B5023 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 56CFC7002C9F5BB4000B5023 /* GRDB.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DC3773F319C8CBB3004FCF85; + remoteInfo = GRDB; + }; + 56CFC70B2C9F5BC7000B5023 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 56CFC7002C9F5BB4000B5023 /* GRDB.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = DC3773F219C8CBB3004FCF85; + remoteInfo = GRDB; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 56CFC7102C9F5BDB000B5023 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 56CFC70F2C9F5BDB000B5023 /* GRDB.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 56CFC6EE2C9F5B8F000B5023 /* GRDBManualInstall.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GRDBManualInstall.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 56CFC7002C9F5BB4000B5023 /* GRDB.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GRDB.xcodeproj; path = /Users/groue/Documents/git/groue/GRDB.swift/GRDB.xcodeproj; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 56CFC6F02C9F5B8F000B5023 /* GRDBManualInstall */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = GRDBManualInstall; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 56CFC6EB2C9F5B8F000B5023 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 56CFC70E2C9F5BDB000B5023 /* GRDB.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 56CFC6E52C9F5B8F000B5023 = { + isa = PBXGroup; + children = ( + 56CFC7002C9F5BB4000B5023 /* GRDB.xcodeproj */, + 56CFC6F02C9F5B8F000B5023 /* GRDBManualInstall */, + 56CFC70D2C9F5BDB000B5023 /* Frameworks */, + 56CFC6EF2C9F5B8F000B5023 /* Products */, + ); + sourceTree = ""; + }; + 56CFC6EF2C9F5B8F000B5023 /* Products */ = { + isa = PBXGroup; + children = ( + 56CFC6EE2C9F5B8F000B5023 /* GRDBManualInstall.app */, + ); + name = Products; + sourceTree = ""; + }; + 56CFC7032C9F5BB4000B5023 /* Products */ = { + isa = PBXGroup; + children = ( + 56CFC7082C9F5BB4000B5023 /* GRDB.framework */, + ); + name = Products; + sourceTree = ""; + }; + 56CFC70D2C9F5BDB000B5023 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 56CFC6ED2C9F5B8F000B5023 /* GRDBManualInstall */ = { + isa = PBXNativeTarget; + buildConfigurationList = 56CFC6FD2C9F5B91000B5023 /* Build configuration list for PBXNativeTarget "GRDBManualInstall" */; + buildPhases = ( + 56CFC6EA2C9F5B8F000B5023 /* Sources */, + 56CFC6EB2C9F5B8F000B5023 /* Frameworks */, + 56CFC6EC2C9F5B8F000B5023 /* Resources */, + 56CFC7102C9F5BDB000B5023 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 56CFC70C2C9F5BC7000B5023 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 56CFC6F02C9F5B8F000B5023 /* GRDBManualInstall */, + ); + name = GRDBManualInstall; + packageProductDependencies = ( + ); + productName = GRDBManualInstall; + productReference = 56CFC6EE2C9F5B8F000B5023 /* GRDBManualInstall.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 56CFC6E62C9F5B8F000B5023 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + 56CFC6ED2C9F5B8F000B5023 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 56CFC6E92C9F5B8F000B5023 /* Build configuration list for PBXProject "GRDBManualInstall" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 56CFC6E52C9F5B8F000B5023; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 56CFC6EF2C9F5B8F000B5023 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 56CFC7032C9F5BB4000B5023 /* Products */; + ProjectRef = 56CFC7002C9F5BB4000B5023 /* GRDB.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 56CFC6ED2C9F5B8F000B5023 /* GRDBManualInstall */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 56CFC7082C9F5BB4000B5023 /* GRDB.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = GRDB.framework; + remoteRef = 56CFC7072C9F5BB4000B5023 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 56CFC6EC2C9F5B8F000B5023 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 56CFC6EA2C9F5B8F000B5023 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 56CFC70C2C9F5BC7000B5023 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = GRDB; + targetProxy = 56CFC70B2C9F5BC7000B5023 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 56CFC6FB2C9F5B91000B5023 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.6; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 56CFC6FC2C9F5B91000B5023 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.6; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 56CFC6FE2C9F5B91000B5023 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = GRDBManualInstall/GRDBManualInstall.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"GRDBManualInstall/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBManualInstall; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 56CFC6FF2C9F5B91000B5023 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = GRDBManualInstall/GRDBManualInstall.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"GRDBManualInstall/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBManualInstall; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 56CFC6E92C9F5B8F000B5023 /* Build configuration list for PBXProject "GRDBManualInstall" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 56CFC6FB2C9F5B91000B5023 /* Debug */, + 56CFC6FC2C9F5B91000B5023 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 56CFC6FD2C9F5B91000B5023 /* Build configuration list for PBXNativeTarget "GRDBManualInstall" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 56CFC6FE2C9F5B91000B5023 /* Debug */, + 56CFC6FF2C9F5B91000B5023 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 56CFC6E62C9F5B8F000B5023 /* Project object */; +} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Tests/GRDBManualInstall/GRDBManualInstall.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Tests/GRDBManualInstall/GRDBManualInstall.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Tests/GRDBManualInstall/GRDBManualInstall/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/AccentColor.colorset/Contents.json rename to Tests/GRDBManualInstall/GRDBManualInstall/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Tests/GRDBManualInstall/GRDBManualInstall/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tests/GRDBManualInstall/GRDBManualInstall/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..3f00db43ec --- /dev/null +++ b/Tests/GRDBManualInstall/GRDBManualInstall/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Preview Content/Preview Assets.xcassets/Contents.json b/Tests/GRDBManualInstall/GRDBManualInstall/Assets.xcassets/Contents.json similarity index 100% rename from Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Preview Content/Preview Assets.xcassets/Contents.json rename to Tests/GRDBManualInstall/GRDBManualInstall/Assets.xcassets/Contents.json diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Tests/GRDBManualInstall/GRDBManualInstall/GRDBManualInstall.entitlements similarity index 63% rename from Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Tests/GRDBManualInstall/GRDBManualInstall/GRDBManualInstall.entitlements index 18d981003d..18aff0ce43 100644 --- a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/Tests/GRDBManualInstall/GRDBManualInstall/GRDBManualInstall.entitlements @@ -2,7 +2,9 @@ - IDEDidComputeMac32BitWarning + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only diff --git a/Tests/GRDBManualInstall/GRDBManualInstall/GRDBManualInstallApp.swift b/Tests/GRDBManualInstall/GRDBManualInstall/GRDBManualInstallApp.swift new file mode 100644 index 0000000000..56fb2f55d0 --- /dev/null +++ b/Tests/GRDBManualInstall/GRDBManualInstall/GRDBManualInstallApp.swift @@ -0,0 +1,27 @@ +import GRDB +import SwiftUI + +@main +struct GRDBManualInstallApp: App { + var body: some Scene { + WindowGroup { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("SQLite version: \(sqliteVersion)") + } + .padding() + } + } + + var sqliteVersion: String { + do { + return try DatabaseQueue().read { db in + try String.fetchOne(db, sql: "SELECT sqlite_version()")! + } + } catch { + return "error" + } + } +} diff --git a/Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/Contents.json b/Tests/GRDBManualInstall/GRDBManualInstall/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from Documentation/DemoApps/GRDBCombineDemo/GRDBCombineDemo/Resources/Assets.xcassets/Contents.json rename to Tests/GRDBManualInstall/GRDBManualInstall/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/Tests/GRDBTests/AssociationBelongsToSQLDerivationTests.swift b/Tests/GRDBTests/AssociationBelongsToSQLDerivationTests.swift index b51ec961ec..829ff6c025 100644 --- a/Tests/GRDBTests/AssociationBelongsToSQLDerivationTests.swift +++ b/Tests/GRDBTests/AssociationBelongsToSQLDerivationTests.swift @@ -16,12 +16,12 @@ private struct B : TableRecord { private struct RestrictedB : TableRecord { static let databaseTableName = "b" - static let databaseSelection: [any SQLSelectable] = [Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("name")] } } private struct ExtendedB : TableRecord { static let databaseTableName = "b" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } } /// Test SQL generation diff --git a/Tests/GRDBTests/AssociationHasOneSQLDerivationTests.swift b/Tests/GRDBTests/AssociationHasOneSQLDerivationTests.swift index 932167d5eb..32bd7f4658 100644 --- a/Tests/GRDBTests/AssociationHasOneSQLDerivationTests.swift +++ b/Tests/GRDBTests/AssociationHasOneSQLDerivationTests.swift @@ -16,12 +16,12 @@ private struct B : TableRecord { private struct RestrictedB : TableRecord { static let databaseTableName = "b" - static let databaseSelection: [any SQLSelectable] = [Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("name")] } } private struct ExtendedB : TableRecord { static let databaseTableName = "b" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } } /// Test SQL generation diff --git a/Tests/GRDBTests/AssociationHasOneThroughSQLDerivationTests.swift b/Tests/GRDBTests/AssociationHasOneThroughSQLDerivationTests.swift index ce9a335e02..47dff47104 100644 --- a/Tests/GRDBTests/AssociationHasOneThroughSQLDerivationTests.swift +++ b/Tests/GRDBTests/AssociationHasOneThroughSQLDerivationTests.swift @@ -19,12 +19,12 @@ private struct C: TableRecord { private struct RestrictedC : TableRecord { static let databaseTableName = "c" - static let databaseSelection: [any SQLSelectable] = [Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("name")] } } private struct ExtendedC : TableRecord { static let databaseTableName = "c" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } } /// Test SQL generation diff --git a/Tests/GRDBTests/AssociationPrefetchingRowTests.swift b/Tests/GRDBTests/AssociationPrefetchingRowTests.swift index 5962b2e7b7..ead7d89b92 100644 --- a/Tests/GRDBTests/AssociationPrefetchingRowTests.swift +++ b/Tests/GRDBTests/AssociationPrefetchingRowTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/AssociationPrefetchingSQLTests.swift b/Tests/GRDBTests/AssociationPrefetchingSQLTests.swift index 5a96828c6f..3813344ffe 100644 --- a/Tests/GRDBTests/AssociationPrefetchingSQLTests.swift +++ b/Tests/GRDBTests/AssociationPrefetchingSQLTests.swift @@ -80,7 +80,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -105,7 +105,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -131,7 +131,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("bs2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -170,7 +170,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .order(Column("colb2"))) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -221,7 +221,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .hasMany(Child.self)) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -244,7 +244,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .hasMany(Child.self)) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -263,7 +263,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .filter(Column("parentA") == "foo") .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -286,7 +286,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey() .limit(1) - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -318,7 +318,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -352,7 +352,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -404,7 +404,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("cs2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -502,7 +502,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: Child.hasMany(GrandChild.self))) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -534,7 +534,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: Child.hasMany(GrandChild.self))) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -550,7 +550,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: Child.hasMany(GrandChild.self))) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -576,7 +576,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .filter(Column("name") == "foo") .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -617,7 +617,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -646,7 +646,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -699,7 +699,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("cs2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -741,7 +741,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -777,7 +777,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("ds3")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -821,7 +821,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -856,7 +856,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -894,7 +894,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -936,7 +936,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -970,7 +970,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey()) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1010,7 +1010,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: A.hasMany(C.self)) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1052,7 +1052,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: A.hasMany(C.self)) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1098,7 +1098,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .including(all: A.hasMany(C.self)) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1139,7 +1139,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { ) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1191,7 +1191,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("a2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1249,7 +1249,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { ) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1301,7 +1301,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("c2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1361,7 +1361,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey() .filter(Column("cold2") != 8) - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1399,7 +1399,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey() .filter(Column("cold2") != 8) - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1437,7 +1437,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { ) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1492,7 +1492,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("a2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1553,7 +1553,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { ) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1608,7 +1608,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .forKey("c2")) .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1671,7 +1671,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey() .filter(Column("cold2") != 8) - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1712,7 +1712,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .orderByPrimaryKey() .filter(Column("cold2") != 8) - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1755,7 +1755,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { .filter(sql: "1 + 1") .orderByPrimaryKey() - sqlQueries.removeAll() + clearSQLQueries() _ = try Row.fetchAll(db, request) let selectQueries = sqlQueries.filter(isSelectQuery) @@ -1802,7 +1802,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { do { // Group an association - sqlQueries.removeAll() + clearSQLQueries() let association = Team.players.select(max(Column("score"))).group(Column("category")) let request = Team.including(all: association) _ = try Row.fetchAll(db, request) @@ -1821,7 +1821,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { do { // Filter an association with an association aggregate - sqlQueries.removeAll() + clearSQLQueries() let association = Team.players.having(Player.awards.isEmpty) let request = Team.including(all: association) _ = try Row.fetchAll(db, request) @@ -1842,7 +1842,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { do { // Annotate an association with an association aggregate - sqlQueries.removeAll() + clearSQLQueries() let association = Team.players.annotated(with: Player.awards.count) let request = Team.including(all: association) _ = try Row.fetchAll(db, request) @@ -1880,7 +1880,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { try db.execute(sql: "INSERT INTO team DEFAULT VALUES") do { - sqlQueries.removeAll() + clearSQLQueries() let association = Team.players.distinct() let request = Team.including(all: association) _ = try Row.fetchAll(db, request) @@ -1916,7 +1916,7 @@ class AssociationPrefetchingSQLTests: GRDBTestCase { try db.execute(sql: "INSERT INTO team DEFAULT VALUES") do { - sqlQueries.removeAll() + clearSQLQueries() let cte = CommonTableExpression(named: "cte", sql: "SELECT 42") let association = Team.players.with(cte).filter(Column("playerId") == cte.all()) let request = Team.including(all: association) diff --git a/Tests/GRDBTests/AsyncSemaphore.swift b/Tests/GRDBTests/AsyncSemaphore.swift new file mode 100644 index 0000000000..97691ab67c --- /dev/null +++ b/Tests/GRDBTests/AsyncSemaphore.swift @@ -0,0 +1,255 @@ +// Vendored from + +// Copyright (C) 2022 Gwendal Roué +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import Foundation + +/// An object that controls access to a resource across multiple execution +/// contexts through use of a traditional counting semaphore. +/// +/// You increment a semaphore count by calling the ``signal()`` method, and +/// decrement a semaphore count by calling ``wait()`` or one of its variants. +/// +/// ## Topics +/// +/// ### Creating a Semaphore +/// +/// - ``init(value:)`` +/// +/// ### Signaling the Semaphore +/// +/// - ``signal()`` +/// +/// ### Waiting for the Semaphore +/// +/// - ``wait()`` +/// - ``waitUnlessCancelled()`` +public final class AsyncSemaphore: @unchecked Sendable { + /// `Suspension` is the state of a task waiting for a signal. + /// + /// It is a class because instance identity helps `waitUnlessCancelled()` + /// deal with both early and late cancellation. + /// + /// We make it @unchecked Sendable in order to prevent compiler warnings: + /// instances are always protected by the semaphore's lock. + private class Suspension: @unchecked Sendable { + enum State { + /// Initial state. Next is suspendedUnlessCancelled, or cancelled. + case pending + + /// Waiting for a signal, with support for cancellation. + case suspendedUnlessCancelled(UnsafeContinuation) + + /// Waiting for a signal, with no support for cancellation. + case suspended(UnsafeContinuation) + + /// Cancelled before we have started waiting. + case cancelled + } + + var state: State + + init(state: State) { + self.state = state + } + } + + // MARK: - Internal State + + /// The semaphore value. + private var value: Int + + /// As many elements as there are suspended tasks waiting for a signal. + private var suspensions: [Suspension] = [] + + /// The lock that protects `value` and `suspensions`. + /// + /// It is recursive in order to handle cancellation (see the implementation + /// of ``waitUnlessCancelled()``). + private let _lock = NSRecursiveLock() + + // MARK: - Creating a Semaphore + + /// Creates a semaphore. + /// + /// - parameter value: The starting value for the semaphore. Do not pass a + /// value less than zero. + public init(value: Int) { + precondition(value >= 0, "AsyncSemaphore requires a value equal or greater than zero") + self.value = value + } + + deinit { + precondition(suspensions.isEmpty, "AsyncSemaphore is deallocated while some task(s) are suspended waiting for a signal.") + } + + // MARK: - Locking + + // Let's hide the locking primitive in order to avoid a compiler warning: + // + // > Instance method 'lock' is unavailable from asynchronous contexts; + // > Use async-safe scoped locking instead; this is an error in Swift 6. + // + // We're not sweeping bad stuff under the rug. We really need to protect + // our inner state (`value` and `suspension`) across the calls to + // `withUnsafeContinuation`. Unfortunately, this method introduces a + // suspension point. So we need a lock. + private func lock() { _lock.lock() } + private func unlock() { _lock.unlock() } + + // MARK: - Waiting for the Semaphore + + /// Waits for, or decrements, a semaphore. + /// + /// Decrement the counting semaphore. If the resulting value is less than + /// zero, this function suspends the current task until a signal occurs, + /// without blocking the underlying thread. Otherwise, no suspension happens. + public func wait() async { + lock() + + value -= 1 + if value >= 0 { + unlock() + return + } + + await withUnsafeContinuation { continuation in + // Register the continuation that `signal` will resume. + let suspension = Suspension(state: .suspended(continuation)) + suspensions.insert(suspension, at: 0) // FIFO + unlock() + } + } + + /// Waits for, or decrements, a semaphore, with support for cancellation. + /// + /// Decrement the counting semaphore. If the resulting value is less than + /// zero, this function suspends the current task until a signal occurs, + /// without blocking the underlying thread. Otherwise, no suspension happens. + /// + /// If the task is canceled before a signal occurs, this function + /// throws `CancellationError`. + public func waitUnlessCancelled() async throws { + lock() + + value -= 1 + if value >= 0 { + defer { unlock() } + + do { + // All code paths check for cancellation + try Task.checkCancellation() + } catch { + // Cancellation is like a signal: we don't really "consume" + // the semaphore, and restore the value. + value += 1 + throw error + } + + return + } + + // Get ready for being suspended waiting for a continuation, or for + // early cancellation. + let suspension = Suspension(state: .pending) + + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in + if case .cancelled = suspension.state { + // Early cancellation: waitUnlessCancelled() is called from + // a cancelled task, and the `onCancel` closure below + // has marked the suspension as cancelled. + // Resume with a CancellationError. + unlock() + continuation.resume(throwing: CancellationError()) + } else { + // Current task is not cancelled: register the continuation + // that `signal` will resume. + suspension.state = .suspendedUnlessCancelled(continuation) + suspensions.insert(suspension, at: 0) // FIFO + unlock() + } + } + } onCancel: { + // withTaskCancellationHandler may immediately call this block (if + // the current task is cancelled), or call it later (if the task is + // cancelled later). In the first case, we're still holding the lock, + // waiting for the continuation. In the second case, we do not hold + // the lock. Being able to handle both situations is the reason why + // we use a recursive lock. + lock() + + // We're no longer waiting for a signal + value += 1 + if let index = suspensions.firstIndex(where: { $0 === suspension }) { + suspensions.remove(at: index) + } + + if case let .suspendedUnlessCancelled(continuation) = suspension.state { + // Late cancellation: the task is cancelled while waiting + // from the semaphore. Resume with a CancellationError. + unlock() + continuation.resume(throwing: CancellationError()) + } else { + // Early cancellation: waitUnlessCancelled() is called from + // a cancelled task. + // + // The next step is the `withTaskCancellationHandler` + // operation closure right above. + suspension.state = .cancelled + unlock() + } + } + } + + // MARK: - Signaling the Semaphore + + /// Signals (increments) a semaphore. + /// + /// Increment the counting semaphore. If the previous value was less than + /// zero, this function resumes a task currently suspended in ``wait()`` + /// or ``waitUnlessCancelled()``. + /// + /// - returns: This function returns true if a suspended task is + /// resumed. Otherwise, the result is false, meaning that no task was + /// waiting for the semaphore. + @discardableResult + public func signal() -> Bool { + lock() + + value += 1 + + switch suspensions.popLast()?.state { // FIFO + case let .suspendedUnlessCancelled(continuation): + unlock() + continuation.resume() + return true + case let .suspended(continuation): + unlock() + continuation.resume() + return true + default: + unlock() + return false + } + } +} diff --git a/Tests/GRDBTests/BackupTestCase.swift b/Tests/GRDBTests/BackupTestCase.swift index 33b2f1ac6a..4ea5d049fd 100644 --- a/Tests/GRDBTests/BackupTestCase.swift +++ b/Tests/GRDBTests/BackupTestCase.swift @@ -84,7 +84,7 @@ class BackupTestCase: GRDBTestCase { let sourceDbPageCount = try setupBackupSource(source) try setupBackupDestination(destination) - try source.write { sourceDb in + try source.read { sourceDb in try destination.barrierWriteWithoutTransaction { destDb in XCTAssertThrowsError( try sourceDb.backup(to: destDb, pagesPerStep: 1) { progress in @@ -102,7 +102,7 @@ class BackupTestCase: GRDBTestCase { XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT id FROM items")!, 1) } - try source.write { dbSource in + try source.read { dbSource in try destination.barrierWriteWithoutTransaction { dbDest in var progressCount: Int = 1 var isCompleted: Bool = false diff --git a/Tests/GRDBTests/ColumnExpressionTests.swift b/Tests/GRDBTests/ColumnExpressionTests.swift index b716a07002..537b43f4fd 100644 --- a/Tests/GRDBTests/ColumnExpressionTests.swift +++ b/Tests/GRDBTests/ColumnExpressionTests.swift @@ -20,7 +20,9 @@ class ColumnExpressionTests: GRDBTestCase { } // Test databaseSelection - static let databaseSelection: [any SQLSelectable] = [Columns.id, Columns.name, Columns.score] + static var databaseSelection: [any SQLSelectable] { + [Columns.id, Columns.name, Columns.score] + } init(row: Row) { // Test row subscript @@ -80,7 +82,9 @@ class ColumnExpressionTests: GRDBTestCase { } // Test databaseSelection - static let databaseSelection: [any SQLSelectable] = [Columns.id, Columns.name, Columns.score] + static var databaseSelection: [any SQLSelectable] { + [Columns.id, Columns.name, Columns.score] + } init(row: Row) { // Test row subscript @@ -148,7 +152,9 @@ class ColumnExpressionTests: GRDBTestCase { } // Test databaseSelection - static let databaseSelection: [any SQLSelectable] = [Columns.id, Columns.name, Columns.score] + static var databaseSelection: [any SQLSelectable] { + [Columns.id, Columns.name, Columns.score] + } static var testRequest: QueryInterfaceRequest { // Test expression derivation @@ -196,7 +202,9 @@ class ColumnExpressionTests: GRDBTestCase { } // Test databaseSelection - static let databaseSelection: [any SQLSelectable] = [CodingKeys.id, CodingKeys.name, CodingKeys.score] + static var databaseSelection: [any SQLSelectable] { + [CodingKeys.id, CodingKeys.name, CodingKeys.score] + } static var testRequest: QueryInterfaceRequest { // Test expression derivation diff --git a/Tests/GRDBTests/DataMemoryTests.swift b/Tests/GRDBTests/DataMemoryTests.swift index a6f3292983..3f572fc304 100644 --- a/Tests/GRDBTests/DataMemoryTests.swift +++ b/Tests/GRDBTests/DataMemoryTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest @testable import GRDB diff --git a/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift b/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift index d6229e88d8..1ac85a08fe 100644 --- a/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift +++ b/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift @@ -40,7 +40,7 @@ class DatabaseAbortedTransactionTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -85,7 +85,7 @@ class DatabaseAbortedTransactionTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } diff --git a/Tests/GRDBTests/DatabaseAfterNextTransactionCommitTests.swift b/Tests/GRDBTests/DatabaseAfterNextTransactionCommitTests.swift index 3daa8f55cf..9e01b79a8b 100644 --- a/Tests/GRDBTests/DatabaseAfterNextTransactionCommitTests.swift +++ b/Tests/GRDBTests/DatabaseAfterNextTransactionCommitTests.swift @@ -42,7 +42,7 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.writeWithoutTransaction { db in - var commitCount = 0 + let commitCountMutex = Mutex(0) weak var deallocationWitness: Witness? = nil do { let witness = Witness() @@ -51,19 +51,19 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { db.afterNextTransaction { _ in // use witness withExtendedLifetime(witness, { }) - commitCount += 1 + commitCountMutex.increment() } } XCTAssertNotNil(deallocationWitness) - XCTAssertEqual(commitCount, 0) + XCTAssertEqual(commitCountMutex.load(), 0) try db.execute(sql: startSQL) try db.execute(sql: endSQL) switch expectedCompletion { case .commit: - XCTAssertEqual(commitCount, 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 1, "\(startSQL); \(endSQL)") case .rollback: - XCTAssertEqual(commitCount, 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 0, "\(startSQL); \(endSQL)") } XCTAssertNil(deallocationWitness) @@ -73,9 +73,9 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { } switch expectedCompletion { case .commit: - XCTAssertEqual(commitCount, 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 1, "\(startSQL); \(endSQL)") case .rollback: - XCTAssertEqual(commitCount, 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 0, "\(startSQL); \(endSQL)") } } } @@ -85,8 +85,8 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { let dbQueue = try makeDatabaseQueue() try dbQueue.writeWithoutTransaction { db in - var commitCount = 0 - var rollbackCount = 0 + let commitCountMutex = Mutex(0) + let rollbackCountMutex = Mutex(0) try db.execute(sql: startSQL) weak var deallocationWitness: Witness? = nil @@ -98,25 +98,25 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { onCommit: { _ in // use witness withExtendedLifetime(witness, { }) - commitCount += 1 + commitCountMutex.increment() }, onRollback: { _ in // use witness withExtendedLifetime(witness, { }) - rollbackCount += 1 + rollbackCountMutex.increment() }) } XCTAssertNotNil(deallocationWitness) - XCTAssertEqual(commitCount, 0) + XCTAssertEqual(commitCountMutex.load(), 0) try db.execute(sql: endSQL) switch expectedCompletion { case .commit: - XCTAssertEqual(commitCount, 1, "\(startSQL); \(endSQL)") - XCTAssertEqual(rollbackCount, 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(rollbackCountMutex.load(), 0, "\(startSQL); \(endSQL)") case .rollback: - XCTAssertEqual(commitCount, 0, "\(startSQL); \(endSQL)") - XCTAssertEqual(rollbackCount, 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(rollbackCountMutex.load(), 1, "\(startSQL); \(endSQL)") } XCTAssertNil(deallocationWitness) @@ -126,11 +126,11 @@ class DatabaseAfterNextTransactionCommitTests: GRDBTestCase { } switch expectedCompletion { case .commit: - XCTAssertEqual(commitCount, 1, "\(startSQL); \(endSQL)") - XCTAssertEqual(rollbackCount, 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(rollbackCountMutex.load(), 0, "\(startSQL); \(endSQL)") case .rollback: - XCTAssertEqual(commitCount, 0, "\(startSQL); \(endSQL)") - XCTAssertEqual(rollbackCount, 1, "\(startSQL); \(endSQL)") + XCTAssertEqual(commitCountMutex.load(), 0, "\(startSQL); \(endSQL)") + XCTAssertEqual(rollbackCountMutex.load(), 1, "\(startSQL); \(endSQL)") } } } diff --git a/Tests/GRDBTests/DatabaseConfigurationTests.swift b/Tests/GRDBTests/DatabaseConfigurationTests.swift index 88fdb622b2..7c64c91e5f 100644 --- a/Tests/GRDBTests/DatabaseConfigurationTests.swift +++ b/Tests/GRDBTests/DatabaseConfigurationTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB @@ -6,40 +15,40 @@ class DatabaseConfigurationTests: GRDBTestCase { func testPrepareDatabase() throws { // prepareDatabase is called when connection opens - var connectionCount = 0 + let connectionCountMutex = Mutex(0) var configuration = Configuration() configuration.prepareDatabase { db in - connectionCount += 1 + connectionCountMutex.increment() } _ = try DatabaseQueue(configuration: configuration) - XCTAssertEqual(connectionCount, 1) + XCTAssertEqual(connectionCountMutex.load(), 1) _ = try makeDatabaseQueue(configuration: configuration) - XCTAssertEqual(connectionCount, 2) + XCTAssertEqual(connectionCountMutex.load(), 2) let pool = try makeDatabasePool(configuration: configuration) - XCTAssertEqual(connectionCount, 3) + XCTAssertEqual(connectionCountMutex.load(), 3) try pool.read { _ in } - XCTAssertEqual(connectionCount, 4) + XCTAssertEqual(connectionCountMutex.load(), 4) try pool.makeSnapshot().read { _ in } - XCTAssertEqual(connectionCount, 5) + XCTAssertEqual(connectionCountMutex.load(), 5) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try pool.makeSnapshotPool().read { _ in } - XCTAssertEqual(connectionCount, 6) + XCTAssertEqual(connectionCountMutex.load(), 6) #endif } func testPrepareDatabaseError() throws { struct TestError: Error { } - var error: TestError? + let errorMutex: Mutex = Mutex(nil) var configuration = Configuration() configuration.prepareDatabase { db in - if let error { + if let error = errorMutex.load() { throw error } } @@ -47,36 +56,36 @@ class DatabaseConfigurationTests: GRDBTestCase { // TODO: what about in-memory DatabaseQueue??? do { - error = TestError() + errorMutex.store(TestError()) _ = try makeDatabaseQueue(configuration: configuration) XCTFail("Expected TestError") } catch is TestError { } do { - error = TestError() + errorMutex.store(TestError()) _ = try makeDatabasePool(configuration: configuration) XCTFail("Expected TestError") } catch is TestError { } do { - error = nil + errorMutex.store(nil) let pool = try makeDatabasePool(configuration: configuration) do { - error = TestError() + errorMutex.store(TestError()) try pool.read { _ in } XCTFail("Expected TestError") } catch is TestError { } do { - error = TestError() + errorMutex.store(TestError()) _ = try pool.makeSnapshot() XCTFail("Expected TestError") } catch is TestError { } -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) do { - error = TestError() + errorMutex.store(TestError()) _ = try pool.makeSnapshotPool() XCTFail("Expected TestError") } catch is TestError { } diff --git a/Tests/GRDBTests/DatabaseCursorTests.swift b/Tests/GRDBTests/DatabaseCursorTests.swift index d18191488f..6b43570b48 100644 --- a/Tests/GRDBTests/DatabaseCursorTests.swift +++ b/Tests/GRDBTests/DatabaseCursorTests.swift @@ -175,42 +175,49 @@ class DatabaseCursorTests: GRDBTestCase { // with raw C SQLite3 apis. The faulty line is the call to // sqlite3_set_authorizer during the statement iteration. -// if #available(OSX 10.14, *) { -// var connection: SQLiteConnection? = nil -// sqlite3_open_v2(":memory:", &connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_NOMUTEX, nil) -// sqlite3_extended_result_codes(connection, 1) +// var connection: SQLiteConnection? = nil +// sqlite3_open_v2(":memory:", &connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_NOMUTEX, nil) +// sqlite3_extended_result_codes(connection, 1) // -// sqlite3_exec(connection, """ -// CREATE TABLE user (username TEXT NOT NULL); -// CREATE TABLE flagUser (username TEXT NOT NULL); -// INSERT INTO flagUser (username) VALUES ('User1'); -// INSERT INTO flagUser (username) VALUES ('User2'); -// """, nil, nil, nil) +// sqlite3_exec(connection, """ +// CREATE TABLE user (username TEXT NOT NULL); +// CREATE TABLE flagUser (username TEXT NOT NULL); +// INSERT INTO flagUser (username) VALUES ('User1'); +// INSERT INTO flagUser (username) VALUES ('User2'); +// """, nil, nil, nil) // -// var statement: SQLiteStatement? = nil -// sqlite3_set_authorizer(connection, { (_, _, _, _, _, _) in SQLITE_OK }, nil) -// sqlite3_prepare_v3(connection, """ -// SELECT * FROM flagUser WHERE (SELECT COUNT(*) FROM user WHERE username = flagUser.username) = 0 -// """, -1, 0, &statement, nil) -// sqlite3_set_authorizer(connection, nil, nil) -// while true { -// let code = sqlite3_step(statement) -// if code == SQLITE_DONE { -// break -// } else if code == SQLITE_ROW { -// // part of the compilation of another statement, here -// // reduced to the strict minimum that reproduces -// // the error. -// sqlite3_set_authorizer(connection, nil, nil) -// } else { -// print(String(cString: sqlite3_errmsg(connection))) -// XCTFail("Error \(code)") -// break -// } +// var statement: SQLiteStatement? = nil +// sqlite3_set_authorizer(connection, { (_, _, _, _, _, _) in SQLITE_OK }, nil) +// sqlite3_prepare_v3(connection, """ +// SELECT * FROM flagUser WHERE (SELECT COUNT(*) FROM user WHERE username = flagUser.username) = 0 +// """, -1, 0, &statement, nil) +// sqlite3_set_authorizer(connection, nil, nil) +// while true { +// let code = sqlite3_step(statement) +// if code == SQLITE_DONE { +// break +// } else if code == SQLITE_ROW { +// // part of the compilation of another statement, here +// // reduced to the strict minimum that reproduces +// // the error. +// sqlite3_set_authorizer(connection, nil, nil) +// } else { +// print(String(cString: sqlite3_errmsg(connection))) +// XCTFail("Error \(code)") +// break // } -// sqlite3_finalize(statement) -// sqlite3_close_v2(connection) // } +// sqlite3_finalize(statement) +// sqlite3_close_v2(connection) + } + + // This test passes if it compiles + func testAssociatedType() throws { + func accept(_ cursor: some DatabaseCursor) { } + func useCursor(_ db: Database) throws { + let cursor = try String.fetchCursor(db, sql: "SELECT 'foo'") + accept(cursor) + } } // For profiling tests diff --git a/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift index 8ff1da13b2..a11493297e 100644 --- a/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDataDecodingStrategyTests.swift @@ -20,12 +20,18 @@ private enum StrategyCustom: StrategyProvider { } private struct RecordWithData: FetchableRecord, Decodable { - static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { Strategy.strategy } + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { + Strategy.strategy + } + var data: Data } private struct RecordWithOptionalData: FetchableRecord, Decodable { - static var databaseDataDecodingStrategy: DatabaseDataDecodingStrategy { Strategy.strategy } + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { + Strategy.strategy + } + var data: Data? } diff --git a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift index 6fdf7d8759..61f42d7515 100644 --- a/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDataEncodingStrategyTests.swift @@ -19,21 +19,23 @@ private enum StrategyCustom: StrategyProvider { } private struct RecordWithData: EncodableRecord, Encodable { - static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { Strategy.strategy } + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { + Strategy.strategy + } var data: Data } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension RecordWithData: Identifiable { var id: Data { data } } private struct RecordWithOptionalData: EncodableRecord, Encodable { - static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { Strategy.strategy } + static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy { + Strategy.strategy + } var data: Data? } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension RecordWithOptionalData: Identifiable { var id: Data? { data } } @@ -150,10 +152,6 @@ extension DatabaseDataEncodingStrategyTests { } func testFilterID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .blob) } @@ -230,10 +228,6 @@ extension DatabaseDataEncodingStrategyTests { } func testDeleteID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .blob) } @@ -266,7 +260,7 @@ extension DatabaseDataEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } @@ -286,7 +280,7 @@ extension DatabaseDataEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } diff --git a/Tests/GRDBTests/DatabaseDateDecodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDateDecodingStrategyTests.swift index 5f920fe736..c813d377fb 100644 --- a/Tests/GRDBTests/DatabaseDateDecodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDateDecodingStrategyTests.swift @@ -47,12 +47,18 @@ private enum StrategyCustom: StrategyProvider { } private struct RecordWithDate: FetchableRecord, Decodable { - static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { Strategy.strategy } + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { + Strategy.strategy + } + var date: Date } private struct RecordWithOptionalDate: FetchableRecord, Decodable { - static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { Strategy.strategy } + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { + Strategy.strategy + } + var date: Date? } diff --git a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift index 98b1ab605b..90ab8c4102 100644 --- a/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseDateEncodingStrategyTests.swift @@ -46,21 +46,23 @@ private enum StrategyCustom: StrategyProvider { } private struct RecordWithDate: EncodableRecord, Encodable { - static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { Strategy.strategy } + static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy { + Strategy.strategy + } var date: Date } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension RecordWithDate: Identifiable { var id: Date { date } } private struct RecordWithOptionalDate: EncodableRecord, Encodable { - static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { Strategy.strategy } + static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy { + Strategy.strategy + } var date: Date? } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension RecordWithOptionalDate: Identifiable { var id: Date? { date } } @@ -260,10 +262,6 @@ extension DatabaseDateEncodingStrategyTests { } func testFilterID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .datetime) } @@ -340,10 +338,6 @@ extension DatabaseDateEncodingStrategyTests { } func testDeleteID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .datetime) } @@ -376,7 +370,7 @@ extension DatabaseDateEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } @@ -396,7 +390,7 @@ extension DatabaseDateEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } diff --git a/Tests/GRDBTests/DatabaseDumpTests.swift b/Tests/GRDBTests/DatabaseDumpTests.swift index 06d86ccb22..29d59caa72 100644 --- a/Tests/GRDBTests/DatabaseDumpTests.swift +++ b/Tests/GRDBTests/DatabaseDumpTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB @@ -229,10 +238,6 @@ final class DatabaseDumpTests: GRDBTestCase { // MARK: - JSON func test_json_value_formatting() throws { - guard #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) else { - throw XCTSkip("Skip because this test relies on JSONEncoder.OutputFormatting.withoutEscapingSlashes") - } - try makeValuesDatabase().read { db in let stream = TestStream() try db.dumpSQL("SELECT * FROM value ORDER BY name", format: .json(), to: stream) diff --git a/Tests/GRDBTests/DatabaseErrorTests.swift b/Tests/GRDBTests/DatabaseErrorTests.swift index 8bba886ff5..e968b44074 100644 --- a/Tests/GRDBTests/DatabaseErrorTests.swift +++ b/Tests/GRDBTests/DatabaseErrorTests.swift @@ -17,7 +17,7 @@ class DatabaseErrorTests: GRDBTestCase { try dbQueue.inTransaction { db in try db.execute(sql: "CREATE TABLE persons (id INTEGER PRIMARY KEY)") try db.execute(sql: "CREATE TABLE pets (masterId INTEGER NOT NULL REFERENCES persons(id), name TEXT)") - sqlQueries.removeAll() + clearSQLQueries() try db.execute(sql: "INSERT INTO pets (masterId, name) VALUES (?, ?)", arguments: [1, "Bobby"]) XCTFail() return .commit @@ -44,7 +44,7 @@ class DatabaseErrorTests: GRDBTestCase { XCTAssertTrue(db.isInsideTransaction) try db.execute(sql: "CREATE TABLE persons (id INTEGER PRIMARY KEY)") try db.execute(sql: "CREATE TABLE pets (masterId INTEGER NOT NULL REFERENCES persons(id), name TEXT)") - sqlQueries.removeAll() + clearSQLQueries() try db.execute(sql: "INSERT INTO pets (masterId, name) VALUES (?, ?)", arguments: [1, "Bobby"]) XCTFail() return .commit diff --git a/Tests/GRDBTests/DatabaseFunctionTests.swift b/Tests/GRDBTests/DatabaseFunctionTests.swift index 8d252c6a83..4c670bbd8a 100644 --- a/Tests/GRDBTests/DatabaseFunctionTests.swift +++ b/Tests/GRDBTests/DatabaseFunctionTests.swift @@ -347,13 +347,13 @@ class DatabaseFunctionTests: GRDBTestCase { func testFunctionsAreClosures() throws { let dbQueue = try makeDatabaseQueue() - var x = 123 + let mutex = Mutex(123) let fn = DatabaseFunction("f", argumentCount: 0) { dbValues in - return x + return mutex.load() } try dbQueue.inDatabase { db in db.add(function: fn) - x = 321 + mutex.store(321) XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT f()")!, 321) } } diff --git a/Tests/GRDBTests/DatabaseMigratorTests.swift b/Tests/GRDBTests/DatabaseMigratorTests.swift index 9f98158249..d6b118923b 100644 --- a/Tests/GRDBTests/DatabaseMigratorTests.swift +++ b/Tests/GRDBTests/DatabaseMigratorTests.swift @@ -41,10 +41,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testEmptyMigratorPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let migrator = DatabaseMigrator() let publisher = migrator.migratePublisher(writer) @@ -128,7 +124,7 @@ class DatabaseMigratorTests : GRDBTestCase { } let expectation = self.expectation(description: "") - migrator.asyncMigrate(writer, completion: { dbResult in + migrator.asyncMigrate(writer, completion: { [migrator2] dbResult in // No migration error let db = try! dbResult.get() @@ -153,10 +149,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testNonEmptyMigratorPublisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { var migrator = DatabaseMigrator() migrator.registerMigration("createPersons") { db in @@ -209,10 +201,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testEmptyMigratorPublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { let migrator = DatabaseMigrator() let expectation = self.expectation(description: "") @@ -235,10 +223,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testNonEmptyMigratorPublisherIsAsynchronous() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: some DatabaseWriter) throws { var migrator = DatabaseMigrator() migrator.registerMigration("first", migrate: { _ in }) @@ -262,10 +246,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testMigratorPublisherDefaultScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: Writer) { var migrator = DatabaseMigrator() migrator.registerMigration("first", migrate: { _ in }) @@ -291,10 +271,6 @@ class DatabaseMigratorTests : GRDBTestCase { } func testMigratorPublisherCustomScheduler() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - func test(writer: Writer) { var migrator = DatabaseMigrator() migrator.registerMigration("first", migrate: { _ in }) @@ -795,13 +771,13 @@ class DatabaseMigratorTests : GRDBTestCase { var migrator = DatabaseMigrator() migrator.eraseDatabaseOnSchemaChange = true - var witness = 1 + let mutex = Mutex(0) migrator.registerMigration("1") { db in + let value = mutex.increment() try db.execute(sql: """ CREATE TABLE t1(id INTEGER PRIMARY KEY); INSERT INTO t1(id) VALUES (?) - """, arguments: [witness]) - witness += 1 + """, arguments: [value]) } let dbQueue = try makeDatabaseQueue() diff --git a/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift b/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift index 7a329c38cf..7d2478545b 100644 --- a/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift +++ b/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift @@ -1074,85 +1074,17 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { try test(qos: .userInitiated) } - // MARK: - ConcurrentRead - - func testConcurrentReadOpensATransaction() throws { - let dbPool = try makeDatabasePool() - let future = dbPool.writeWithoutTransaction { db in - dbPool.concurrentRead { db in - XCTAssertTrue(db.isInsideTransaction) - do { - try db.execute(sql: "BEGIN DEFERRED TRANSACTION") - XCTFail("Expected error") - } catch { - } - } - } - try future.wait() - } - - func testConcurrentReadOutsideOfTransaction() throws { - let dbPool = try makeDatabasePool() - try dbPool.write { db in - try db.create(table: "persons") { t in - t.primaryKey("id", .integer) - } - } - - // Writer Reader - // dbPool.writeWithoutTransaction { - // > - // dbPool.concurrentRead { - // < - // INSERT INTO items (id) VALUES (NULL) - // > - let s1 = DispatchSemaphore(value: 0) - // } SELECT COUNT(*) FROM persons -> 0 - // < - // } - - let future: DatabaseFuture = try dbPool.writeWithoutTransaction { db in - let future: DatabaseFuture = dbPool.concurrentRead { db in - _ = s1.wait(timeout: .distantFuture) - return try! Int.fetchOne(db, sql: "SELECT COUNT(*) FROM persons")! - } - try db.execute(sql: "INSERT INTO persons DEFAULT VALUES") - s1.signal() - return future - } - XCTAssertEqual(try future.wait(), 0) - } - - func testConcurrentReadError() throws { - // Necessary for this test to run as quickly as possible - dbConfiguration.readonlyBusyMode = .immediateError - let dbPool = try makeDatabasePool() - try dbPool.writeWithoutTransaction { db in - try db.execute(sql: "PRAGMA locking_mode=EXCLUSIVE") - try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)") - let future = dbPool.concurrentRead { db in - fatalError("Should not run") - } - do { - try future.wait() - } catch let error as DatabaseError { - XCTAssertEqual(error.resultCode, .SQLITE_BUSY) - XCTAssertEqual(error.message!, "database is locked") - } - } - } - // MARK: - AsyncConcurrentRead func testAsyncConcurrentReadOpensATransaction() throws { let dbPool = try makeDatabasePool() - var isInsideTransaction: Bool? = nil + let isInsideTransactionMutex: Mutex = Mutex(nil) let expectation = self.expectation(description: "read") dbPool.writeWithoutTransaction { db in dbPool.asyncConcurrentRead { dbResult in do { let db = try dbResult.get() - isInsideTransaction = db.isInsideTransaction + isInsideTransactionMutex.store(db.isInsideTransaction) do { try db.execute(sql: "BEGIN DEFERRED TRANSACTION") XCTFail("Expected error") @@ -1165,7 +1097,7 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { } } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(isInsideTransaction, true) + XCTAssertEqual(isInsideTransactionMutex.load(), true) } func testAsyncConcurrentReadOutsideOfTransaction() throws { @@ -1179,7 +1111,7 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { // Writer Reader // dbPool.writeWithoutTransaction { // > - // dbPool.concurrentRead { + // dbPool.asyncConcurrentRead { // < // INSERT INTO items (id) VALUES (NULL) // > @@ -1188,14 +1120,14 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { // < // } - var count: Int? = nil + let countMutex: Mutex = Mutex(nil) let expectation = self.expectation(description: "read") try dbPool.writeWithoutTransaction { db in dbPool.asyncConcurrentRead { dbResult in do { _ = s1.wait(timeout: .distantFuture) let db = try dbResult.get() - count = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM persons")! + try countMutex.store(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM persons")!) } catch { XCTFail("Unexpected error: \(error)") } @@ -1205,14 +1137,14 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { s1.signal() } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 0) + XCTAssertEqual(countMutex.load(), 0) } func testAsyncConcurrentReadError() throws { // Necessary for this test to run as quickly as possible dbConfiguration.readonlyBusyMode = .immediateError let dbPool = try makeDatabasePool() - var readError: DatabaseError? = nil + let readErrorMutex: Mutex = Mutex(nil) let expectation = self.expectation(description: "read") try dbPool.writeWithoutTransaction { db in try db.execute(sql: "PRAGMA locking_mode=EXCLUSIVE") @@ -1224,12 +1156,12 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { XCTFail("Unexpected result: \(dbResult)") return } - readError = dbError + readErrorMutex.store(dbError) expectation.fulfill() } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(readError!.resultCode, .SQLITE_BUSY) - XCTAssertEqual(readError!.message!, "database is locked") + XCTAssertEqual(readErrorMutex.load()!.resultCode, .SQLITE_BUSY) + XCTAssertEqual(readErrorMutex.load()!.message!, "database is locked") } } diff --git a/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift b/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift index 9b6bb306c4..afa8bd32a5 100644 --- a/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift +++ b/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift @@ -1,24 +1,28 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest @testable import GRDB class DatabasePoolReleaseMemoryTests: GRDBTestCase { func testDatabasePoolDeinitClosesAllConnections() throws { - let countQueue = DispatchQueue(label: "GRDB") - var openConnectionCount = 0 - var totalOpenConnectionCount = 0 - - dbConfiguration.SQLiteConnectionDidOpen = { - countQueue.sync { - totalOpenConnectionCount += 1 - openConnectionCount += 1 - } + let openConnectionCountMutex = Mutex(0) + let totalOpenConnectionCountMutex = Mutex(0) + + dbConfiguration.onConnectionDidOpen { + totalOpenConnectionCountMutex.increment() + openConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - countQueue.sync { - openConnectionCount -= 1 - } + dbConfiguration.onConnectionDidClose { + openConnectionCountMutex.decrement() } // write & read @@ -35,19 +39,19 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { } // One reader, one writer - XCTAssertEqual(totalOpenConnectionCount, 2) + XCTAssertEqual(totalOpenConnectionCountMutex.load(), 2) // All connections are closed - XCTAssertEqual(openConnectionCount, 0) + XCTAssertEqual(openConnectionCountMutex.load(), 0) } - + #if os(iOS) func testDatabasePoolReleasesMemoryOnPressureEvent() throws { // Create a database pool, and expect a reader connection to be closed let expectation = self.expectation(description: "Reader connection closed") var configuration = Configuration() - configuration.SQLiteConnectionWillClose = { conn in + configuration.onConnectionWillClose { conn in if sqlite3_db_readonly(conn, nil) != 0 { expectation.fulfill() } @@ -67,7 +71,7 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { waitForExpectations(timeout: 0.5) } } - + func testDatabasePoolDoesNotReleaseMemoryOnPressureEventIfDisabled() throws { // Create a database pool, and do not expect any reader connection to be closed let expectation = self.expectation(description: "Reader connection closed") @@ -75,7 +79,7 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { var configuration = Configuration() configuration.automaticMemoryManagement = false - configuration.SQLiteConnectionWillClose = { conn in + configuration.onConnectionWillClose { conn in if sqlite3_db_readonly(conn, nil) != 0 { expectation.fulfill() } @@ -117,26 +121,21 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { // Cleanup semaphore.signal() } - + #endif - + func test_DatabasePool_releaseMemory_closes_reader_connections() throws { // A complicated test setup that opens multiple reader connections. - let countQueue = DispatchQueue(label: "GRDB") - var openConnectionCount = 0 - var totalOpenConnectionCount = 0 - - dbConfiguration.SQLiteConnectionDidOpen = { - countQueue.sync { - totalOpenConnectionCount += 1 - openConnectionCount += 1 - } + let openConnectionCountMutex = Mutex(0) + let totalOpenConnectionCountMutex = Mutex(0) + + dbConfiguration.onConnectionDidOpen { + totalOpenConnectionCountMutex.increment() + openConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - countQueue.sync { - openConnectionCount -= 1 - } + dbConfiguration.onConnectionDidClose { + openConnectionCountMutex.decrement() } let dbPool = try makeDatabasePool() @@ -192,74 +191,69 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { } // Two readers, one writer - XCTAssertEqual(totalOpenConnectionCount, 3) + XCTAssertEqual(totalOpenConnectionCountMutex.load(), 3) // Writer is still open - XCTAssertEqual(openConnectionCount, 1) + XCTAssertEqual(openConnectionCountMutex.load(), 1) } func test_DatabasePool_releaseMemory_closes_reader_connections_when_persistentReadOnlyConnections_is_false() throws { - var persistentConnectionCount = 0 + let persistentConnectionCountMutex = Mutex(0) - dbConfiguration.SQLiteConnectionDidOpen = { - persistentConnectionCount += 1 + dbConfiguration.onConnectionDidOpen { + persistentConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - persistentConnectionCount -= 1 + dbConfiguration.onConnectionDidClose { + persistentConnectionCountMutex.decrement() } dbConfiguration.persistentReadOnlyConnections = false let dbPool = try makeDatabasePool() - XCTAssertEqual(persistentConnectionCount, 1) // writer + XCTAssertEqual(persistentConnectionCountMutex.load(), 1) // writer try dbPool.read { _ in } - XCTAssertEqual(persistentConnectionCount, 2) // writer + reader + XCTAssertEqual(persistentConnectionCountMutex.load(), 2) // writer + reader dbPool.releaseMemory() - XCTAssertEqual(persistentConnectionCount, 1) // writer + XCTAssertEqual(persistentConnectionCountMutex.load(), 1) // writer } func test_DatabasePool_releaseMemory_does_not_close_reader_connections_when_persistentReadOnlyConnections_is_true() throws { - var persistentConnectionCount = 0 + let persistentConnectionCountMutex = Mutex(0) - dbConfiguration.SQLiteConnectionDidOpen = { - persistentConnectionCount += 1 + dbConfiguration.onConnectionDidOpen { + persistentConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - persistentConnectionCount -= 1 + dbConfiguration.onConnectionDidClose { + persistentConnectionCountMutex.decrement() } dbConfiguration.persistentReadOnlyConnections = true let dbPool = try makeDatabasePool() - XCTAssertEqual(persistentConnectionCount, 1) // writer + XCTAssertEqual(persistentConnectionCountMutex.load(), 1) // writer try dbPool.read { _ in } - XCTAssertEqual(persistentConnectionCount, 2) // writer + reader + XCTAssertEqual(persistentConnectionCountMutex.load(), 2) // writer + reader dbPool.releaseMemory() - XCTAssertEqual(persistentConnectionCount, 2) // writer + reader + XCTAssertEqual(persistentConnectionCountMutex.load(), 2) // writer + reader } - + func testBlocksRetainConnection() throws { - let countQueue = DispatchQueue(label: "GRDB") - var openConnectionCount = 0 - var totalOpenConnectionCount = 0 - - dbConfiguration.SQLiteConnectionDidOpen = { - countQueue.sync { - totalOpenConnectionCount += 1 - openConnectionCount += 1 - } + let openConnectionCountMutex = Mutex(0) + let totalOpenConnectionCountMutex = Mutex(0) + + dbConfiguration.onConnectionDidOpen { + totalOpenConnectionCountMutex.increment() + openConnectionCountMutex.increment() } - dbConfiguration.SQLiteConnectionDidClose = { - countQueue.sync { - openConnectionCount -= 1 - } + dbConfiguration.onConnectionDidClose { + openConnectionCountMutex.decrement() } // Block 1 Block 2 @@ -302,10 +296,10 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { } // one writer, one reader - XCTAssertEqual(totalOpenConnectionCount, 2) + XCTAssertEqual(totalOpenConnectionCountMutex.load(), 2) // All connections are closed - XCTAssertEqual(openConnectionCount, 0) + XCTAssertEqual(openConnectionCountMutex.load(), 0) } func testStatementDoNotRetainDatabaseConnection() throws { diff --git a/Tests/GRDBTests/DatabasePoolTests.swift b/Tests/GRDBTests/DatabasePoolTests.swift index 8b45af6ab7..e1ab57bbd5 100644 --- a/Tests/GRDBTests/DatabasePoolTests.swift +++ b/Tests/GRDBTests/DatabasePoolTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB @@ -41,7 +50,7 @@ class DatabasePoolTests: GRDBTestCase { XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal")) XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm")) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // A non-empty wal file makes sure ValueObservation can use wal snapshots. // See let walURL = URL(fileURLWithPath: dbPool.path + "-wal") @@ -65,7 +74,7 @@ class DatabasePoolTests: GRDBTestCase { XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal")) XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm")) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // A non-empty wal file makes sure ValueObservation can use wal snapshots. // See let walURL = URL(fileURLWithPath: dbPool.path + "-wal") @@ -91,7 +100,7 @@ class DatabasePoolTests: GRDBTestCase { XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal")) XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm")) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // A non-empty wal file makes sure ValueObservation can use wal snapshots. // See let walURL = URL(fileURLWithPath: dbPool.path + "-wal") @@ -114,7 +123,7 @@ class DatabasePoolTests: GRDBTestCase { XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal")) XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm")) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // A non-empty wal file makes sure ValueObservation can use wal snapshots. // See let walURL = URL(fileURLWithPath: dbPool.path + "-wal") @@ -228,9 +237,7 @@ class DatabasePoolTests: GRDBTestCase { let group = DispatchGroup() // The maximum number of threads we could witness - var maxThreadCount: CInt = 0 - let lock = NSLock() - + let maxThreadCountMutex: Mutex = Mutex(0) for _ in (0.. = Mutex(0) for _ in (0..=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(setup(makeDatabasePool()).makeSnapshotPool()) #endif } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncAwait_ReadCanRead() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -67,7 +66,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(setup(makeDatabaseQueue())) try await test(setup(makeDatabasePool())) try await test(setup(makeDatabasePool()).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(setup(makeDatabasePool()).makeSnapshotPool()) #endif } @@ -86,12 +85,11 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncAwait_ReadPreventsDatabaseModification() async throws { func test(_ dbReader: some DatabaseReader) async throws { do { @@ -106,7 +104,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) try await test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -130,12 +128,11 @@ class DatabaseReaderTests : GRDBTestCase { try test(setup(makeDatabaseQueue())) try test(setup(makeDatabasePool())) try test(setup(makeDatabasePool()).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(setup(makeDatabasePool()).makeSnapshotPool()) #endif } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncAwait_UnsafeReadCanRead() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -153,7 +150,7 @@ class DatabaseReaderTests : GRDBTestCase { try await test(setup(makeDatabaseQueue())) try await test(setup(makeDatabasePool())) try await test(setup(makeDatabasePool()).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try await test(setup(makeDatabasePool()).makeSnapshotPool()) #endif } @@ -177,7 +174,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(setup(makeDatabaseQueue())) try test(setup(makeDatabasePool())) try test(setup(makeDatabasePool()).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(setup(makeDatabasePool()).makeSnapshotPool()) #endif } @@ -197,7 +194,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -224,12 +221,12 @@ class DatabaseReaderTests : GRDBTestCase { func test(_ dbReader: some DatabaseReader) throws { let expectation = self.expectation(description: "updates") let semaphore = DispatchSemaphore(value: 0) - var count: Int? + let countMutex: Mutex = Mutex(nil) dbReader.asyncRead { dbResult in // Make sure this block executes asynchronously semaphore.wait() do { - count = try Int.fetchOne(dbResult.get(), sql: "SELECT COUNT(*) FROM sqlite_master") + try countMutex.store(Int.fetchOne(dbResult.get(), sql: "SELECT COUNT(*) FROM sqlite_master")) } catch { XCTFail("Unexpected error: \(error)") } @@ -238,13 +235,13 @@ class DatabaseReaderTests : GRDBTestCase { semaphore.signal() waitForExpectations(timeout: 1, handler: nil) - XCTAssertNotNil(count) + XCTAssertNotNil(countMutex.load()) } try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -273,7 +270,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -293,7 +290,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -313,7 +310,7 @@ class DatabaseReaderTests : GRDBTestCase { try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool().makeSnapshotPool()) #endif } @@ -340,8 +337,312 @@ class DatabaseReaderTests : GRDBTestCase { try test(setup(makeDatabaseQueue(configuration: Configuration()))) try test(setup(makeDatabasePool(configuration: Configuration()))) try test(setup(makeDatabasePool(configuration: Configuration())).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(setup(makeDatabasePool(configuration: Configuration())).makeSnapshotPool()) #endif } + + // MARK: - Task Cancellation + + func test_read_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let task = Task { + await semaphore.wait() + try await dbReader.read { db in + XCTFail("Should not be executed") + } + } + task.cancel() + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.read { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.read { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.read { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_statement_execution_from_read_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.read { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + try db.execute(sql: "SELECT 0") + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.read { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_cursor_iteration_from_read_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.read { db in + let cursor = try Int.fetchCursor(db, sql: """ + SELECT 1 UNION ALL SELECT 2 + """) + _ = try cursor.next() + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + _ = try cursor.next() + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.read { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_unsafeRead_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let task = Task { + await semaphore.wait() + try await dbReader.unsafeRead { db in + XCTFail("Should not be executed") + } + } + task.cancel() + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.unsafeRead { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.unsafeRead { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.unsafeRead { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_statement_execution_from_unsafeRead_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.unsafeRead { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + try db.execute(sql: "SELECT 0") + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.unsafeRead { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_cursor_iteration_from_unsafeRead_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbReader: some DatabaseReader) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbReader.unsafeRead { db in + let cursor = try Int.fetchCursor(db, sql: """ + SELECT 1 UNION ALL SELECT 2 + """) + _ = try cursor.next() + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + _ = try cursor.next() + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbReader.unsafeRead { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(makeDatabasePool().makeSnapshot()) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) + try await test(makeDatabasePool().makeSnapshotPool()) +#endif + try await test(AnyDatabaseReader(makeDatabaseQueue())) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } } diff --git a/Tests/GRDBTests/DatabaseRegionObservationTests.swift b/Tests/GRDBTests/DatabaseRegionObservationTests.swift index 0625d91c6b..3ffdfc39eb 100644 --- a/Tests/GRDBTests/DatabaseRegionObservationTests.swift +++ b/Tests/GRDBTests/DatabaseRegionObservationTests.swift @@ -8,10 +8,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: .fullDatabase) _ = observation.start(in: writer, onError: { _ in }, onChange: { _ in }) - - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - _ = observation.publisher(in: writer) - } + _ = observation.publisher(in: writer) } func testDatabaseRegionObservation_FullDatabase() throws { @@ -27,12 +24,12 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: .fullDatabase) - var count = 0 + let countMutex = Mutex(0) let cancellable = observation.start( in: dbQueue, onError: { XCTFail("Unexpected error: \($0)") }, onChange: { db in - count += 1 + countMutex.increment() notificationExpectation.fulfill() }) @@ -49,7 +46,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 3) + XCTAssertEqual(countMutex.load(), 3) } } @@ -96,12 +93,12 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: request1, request2) - var count = 0 + let countMutex = Mutex(0) let cancellable = observation.start( in: dbQueue, onError: { XCTFail("Unexpected error: \($0)") }, onChange: { db in - count += 1 + countMutex.increment() notificationExpectation.fulfill() }) @@ -118,7 +115,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 3) + XCTAssertEqual(countMutex.load(), 3) } } @@ -138,12 +135,12 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: [request1, request2]) - var count = 0 + let countMutex = Mutex(0) let cancellable = observation.start( in: dbQueue, onError: { XCTFail("Unexpected error: \($0)") }, onChange: { db in - count += 1 + countMutex.increment() notificationExpectation.fulfill() }) @@ -160,7 +157,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 3) + XCTAssertEqual(countMutex.load(), 3) } } @@ -174,13 +171,13 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: SQLRequest(sql: "SELECT * FROM t ORDER BY id")) - var count = 0 + let countMutex = Mutex(0) do { let cancellable = observation.start( in: dbQueue, onError: { XCTFail("Unexpected error: \($0)") }, onChange: { db in - count += 1 + countMutex.increment() notificationExpectation.fulfill() }) @@ -199,7 +196,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 2) + XCTAssertEqual(countMutex.load(), 2) } func testDatabaseRegionExtentNextTransaction() throws { @@ -212,14 +209,14 @@ class DatabaseRegionObservationTests: GRDBTestCase { let observation = DatabaseRegionObservation(tracking: SQLRequest(sql: "SELECT * FROM t ORDER BY id")) - var count = 0 - var cancellable: AnyDatabaseCancellable? + let countMutex = Mutex(0) + nonisolated(unsafe) var cancellable: AnyDatabaseCancellable? cancellable = observation.start( in: dbQueue, onError: { XCTFail("Unexpected error: \($0)") }, onChange: { db in cancellable?.cancel() - count += 1 + countMutex.increment() notificationExpectation.fulfill() }) @@ -233,7 +230,7 @@ class DatabaseRegionObservationTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(count, 1) + XCTAssertEqual(countMutex.load(), 1) } } diff --git a/Tests/GRDBTests/DatabaseRegionTests.swift b/Tests/GRDBTests/DatabaseRegionTests.swift index 7044aa3a88..848187ed0f 100644 --- a/Tests/GRDBTests/DatabaseRegionTests.swift +++ b/Tests/GRDBTests/DatabaseRegionTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest @testable import GRDB diff --git a/Tests/GRDBTests/DatabaseSavepointTests.swift b/Tests/GRDBTests/DatabaseSavepointTests.swift index 1483d1a682..b2fe86d256 100644 --- a/Tests/GRDBTests/DatabaseSavepointTests.swift +++ b/Tests/GRDBTests/DatabaseSavepointTests.swift @@ -96,230 +96,12 @@ class DatabaseSavepointTests: GRDBTestCase { XCTAssertThrowsError(try db.execute(sql: "COMMIT")) } } - - func testReleaseTopLevelSavepointFromDatabaseWithDefaultDeferredTransactions() throws { - dbConfiguration.defaultTransactionKind = .deferred - let dbQueue = try makeDatabaseQueue() - let observer = Observer() - dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - return .commit - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item3") - } - - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "COMMIT TRANSACTION", - "INSERT INTO items (name) VALUES ('item3')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item3"]) - XCTAssertEqual(observer.allRecordedEvents.count, 3) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 3) - #endif - } - - func testRollbackTopLevelSavepointFromDatabaseWithDefaultDeferredTransactions() throws { - dbConfiguration.defaultTransactionKind = .deferred - let dbQueue = try makeDatabaseQueue() - let observer = Observer() - dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - return .rollback - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item3") - } - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "ROLLBACK TRANSACTION", - "INSERT INTO items (name) VALUES ('item3')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item3"]) - XCTAssertEqual(observer.allRecordedEvents.count, 3) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 3) - #endif - } - - func testNestedSavepointFromDatabaseWithDefaultDeferredTransactions() throws { - dbConfiguration.defaultTransactionKind = .deferred - let dbQueue = try makeDatabaseQueue() - let observer = Observer() - dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item3") - return .commit - } - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item4") - return .commit - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item5") - } - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item3')", - "RELEASE SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item4')", - "COMMIT TRANSACTION", - "INSERT INTO items (name) VALUES ('item5')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item3", "item4", "item5"]) - XCTAssertEqual(observer.allRecordedEvents.count, 5) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 5) - #endif - try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } - observer.reset() - - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item3") - return .commit - } - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item4") - return .rollback - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item5") - } - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item3')", - "RELEASE SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item4')", - "ROLLBACK TRANSACTION", - "INSERT INTO items (name) VALUES ('item5')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item5"]) - XCTAssertEqual(observer.allRecordedEvents.count, 5) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 5) - #endif - try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } - observer.reset() - - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item3") - return .rollback - } - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item4") - return .commit - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item5") - } - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item3')", - "ROLLBACK TRANSACTION TO SAVEPOINT grdb", - "RELEASE SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item4')", - "COMMIT TRANSACTION", - "INSERT INTO items (name) VALUES ('item5')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item4", "item5"]) - XCTAssertEqual(observer.allRecordedEvents.count, 4) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 4) - #endif - try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } - observer.reset() - - sqlQueries.removeAll() - try dbQueue.writeWithoutTransaction { db in - try insertItem(db, name: "item1") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item2") - try db.inSavepoint { - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item3") - return .rollback - } - XCTAssertTrue(db.isInsideTransaction) - try insertItem(db, name: "item4") - return .rollback - } - XCTAssertFalse(db.isInsideTransaction) - try insertItem(db, name: "item5") - } - XCTAssertEqual(sqlQueries, [ - "INSERT INTO items (name) VALUES ('item1')", - "BEGIN DEFERRED TRANSACTION", - "INSERT INTO items (name) VALUES ('item2')", - "SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item3')", - "ROLLBACK TRANSACTION TO SAVEPOINT grdb", - "RELEASE SAVEPOINT grdb", - "INSERT INTO items (name) VALUES ('item4')", - "ROLLBACK TRANSACTION", - "INSERT INTO items (name) VALUES ('item5')" - ]) - XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item5"]) - XCTAssertEqual(observer.allRecordedEvents.count, 4) - #if SQLITE_ENABLE_PREUPDATE_HOOK - XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 4) - #endif - try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } - observer.reset() - } - - func testReleaseTopLevelSavepointFromDatabaseWithDefaultImmediateTransactions() throws { - dbConfiguration.defaultTransactionKind = .immediate + + func testReleaseTopLevelSavepoint() throws { let dbQueue = try makeDatabaseQueue() let observer = Observer() dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { @@ -344,12 +126,11 @@ class DatabaseSavepointTests: GRDBTestCase { #endif } - func testRollbackTopLevelSavepointFromDatabaseWithDefaultImmediateTransactions() throws { - dbConfiguration.defaultTransactionKind = .immediate + func testRollbackTopLevelSavepoint() throws { let dbQueue = try makeDatabaseQueue() let observer = Observer() dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { @@ -374,12 +155,11 @@ class DatabaseSavepointTests: GRDBTestCase { #endif } - func testNestedSavepointFromDatabaseWithDefaultImmediateTransactions() throws { - dbConfiguration.defaultTransactionKind = .immediate + func testNestedSavepoint() throws { let dbQueue = try makeDatabaseQueue() let observer = Observer() dbQueue.add(transactionObserver: observer) - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { @@ -416,7 +196,7 @@ class DatabaseSavepointTests: GRDBTestCase { try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } observer.reset() - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { @@ -453,7 +233,7 @@ class DatabaseSavepointTests: GRDBTestCase { try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } observer.reset() - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { @@ -491,7 +271,7 @@ class DatabaseSavepointTests: GRDBTestCase { try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") } observer.reset() - sqlQueries.removeAll() + clearSQLQueries() try dbQueue.writeWithoutTransaction { db in try insertItem(db, name: "item1") try db.inSavepoint { diff --git a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift index 6176865e8e..3235f5db9e 100644 --- a/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift +++ b/Tests/GRDBTests/DatabaseSnapshotPoolTests.swift @@ -1,4 +1,4 @@ -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) import XCTest import GRDB @@ -42,7 +42,14 @@ final class DatabaseSnapshotPoolTests: GRDBTestCase { let dbPool = try makeDatabasePool() let counter = try Counter(dbPool: dbPool) // 0 try dbPool.write(counter.increment) // 1 - let snapshot = try dbPool.write { db in try DatabaseSnapshotPool(db) } // locked at 1 + // We can't open a DatabaseSnapshotPool from an IMMEDIATE + // transaction (as documented by sqlite3_snapshot_get). So we + // force a DEFERRED transaction: + var snapshot: DatabaseSnapshotPool! + try dbPool.writeInTransaction(.deferred) { db in + snapshot = try DatabaseSnapshotPool(db) // locked at 1 + return .commit + } try dbPool.write(counter.increment) // 2 try XCTAssertEqual(dbPool.read(counter.value), 2) @@ -55,7 +62,9 @@ final class DatabaseSnapshotPoolTests: GRDBTestCase { let dbPool = try makeDatabasePool() let counter = try Counter(dbPool: dbPool) // 0 try dbPool.write(counter.increment) // 1 - let snapshot = try dbPool.writeWithoutTransaction { db in try DatabaseSnapshotPool(db) } // locked at 1 + let snapshot = try dbPool.writeWithoutTransaction { db in + try DatabaseSnapshotPool(db) // locked at 1 + } try dbPool.write(counter.increment) // 2 try XCTAssertEqual(dbPool.read(counter.value), 2) @@ -220,7 +229,6 @@ final class DatabaseSnapshotPoolTests: GRDBTestCase { try XCTAssertEqual(dbPool.read(counter.value), 2) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func test_read_async() async throws { let dbPool = try makeDatabasePool() let counter = try Counter(dbPool: dbPool) // 0 diff --git a/Tests/GRDBTests/DatabaseSnapshotTests.swift b/Tests/GRDBTests/DatabaseSnapshotTests.swift index 6730a8984f..1cb422ecdd 100644 --- a/Tests/GRDBTests/DatabaseSnapshotTests.swift +++ b/Tests/GRDBTests/DatabaseSnapshotTests.swift @@ -424,7 +424,7 @@ class DatabaseSnapshotTests: GRDBTestCase { XCTFail("Expected Error") } catch DatabaseError.SQLITE_BUSY { } } - XCTAssert(lastMessage!.contains("unfinalized statement: SELECT * FROM sqlite_master")) + XCTAssert(lastSQLiteDiagnostic!.message.contains("unfinalized statement: SELECT * FROM sqlite_master")) // Database is not closed: no error try snapshot.read { db in diff --git a/Tests/GRDBTests/DatabaseSuspensionTests.swift b/Tests/GRDBTests/DatabaseSuspensionTests.swift index eea8a31c7e..ce205c6cf5 100644 --- a/Tests/GRDBTests/DatabaseSuspensionTests.swift +++ b/Tests/GRDBTests/DatabaseSuspensionTests.swift @@ -507,7 +507,7 @@ class DatabaseSuspensionTests : GRDBTestCase { try db.execute(sql: "SELECT * FROM sqlite_master") XCTAssertEqual(db.journalModeCache, "wal") } - try dbPool.write { db in + dbPool.writeWithoutTransaction { db in XCTAssertEqual(db.journalModeCache, "wal") } try dbPool.read { db in diff --git a/Tests/GRDBTests/DatabaseTests.swift b/Tests/GRDBTests/DatabaseTests.swift index 145340ab3c..6b37ab2ad1 100644 --- a/Tests/GRDBTests/DatabaseTests.swift +++ b/Tests/GRDBTests/DatabaseTests.swift @@ -505,16 +505,54 @@ class DatabaseTests : GRDBTestCase { try dbQueue.inTransaction { db in .commit } } - func testExplicitTransactionManagement() throws { + func testImplicitTransactionManagement() throws { let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + XCTAssertEqual(lastSQLQuery, "BEGIN DEFERRED TRANSACTION") + } + + try dbQueue.write { db in + XCTAssertEqual(lastSQLQuery, "BEGIN IMMEDIATE TRANSACTION") + } + try dbQueue.writeWithoutTransaction { db in try db.beginTransaction() - XCTAssertEqual(lastSQLQuery, "BEGIN DEFERRED TRANSACTION") + XCTAssertEqual(lastSQLQuery, "BEGIN IMMEDIATE TRANSACTION") try db.rollback() XCTAssertEqual(lastSQLQuery, "ROLLBACK TRANSACTION") - try db.beginTransaction(.immediate) - XCTAssertEqual(lastSQLQuery, "BEGIN IMMEDIATE TRANSACTION") + + try db.inSavepoint { + XCTAssertEqual(lastSQLQuery, "BEGIN IMMEDIATE TRANSACTION") + return .commit + } + XCTAssertEqual(lastSQLQuery, "COMMIT TRANSACTION") + + try db.readOnly { + try db.beginTransaction() + XCTAssertEqual(lastSQLQuery, "BEGIN DEFERRED TRANSACTION") + try db.rollback() + XCTAssertEqual(lastSQLQuery, "ROLLBACK TRANSACTION") + + try db.inSavepoint { + XCTAssertEqual(lastSQLQuery, "BEGIN DEFERRED TRANSACTION") + return .rollback + } + XCTAssertEqual(lastSQLQuery, "ROLLBACK TRANSACTION") + } + } + } + + func testExplicitTransactionManagement() throws { + let dbQueue = try makeDatabaseQueue() + + try dbQueue.writeWithoutTransaction { db in + try db.beginTransaction(.deferred) + XCTAssertEqual(lastSQLQuery, "BEGIN DEFERRED TRANSACTION") + try db.commit() + XCTAssertEqual(lastSQLQuery, "COMMIT TRANSACTION") + try db.beginTransaction(.exclusive) + XCTAssertEqual(lastSQLQuery, "BEGIN EXCLUSIVE TRANSACTION") try db.commit() XCTAssertEqual(lastSQLQuery, "COMMIT TRANSACTION") } @@ -561,11 +599,10 @@ class DatabaseTests : GRDBTestCase { } func testReadOnlyTransaction() throws { - dbConfiguration.defaultTransactionKind = .immediate let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() try db.inSavepoint { .commit } try db.inTransaction { .commit } try db.inTransaction(.immediate) { .commit } @@ -573,7 +610,7 @@ class DatabaseTests : GRDBTestCase { } try db.readOnly { - sqlQueries.removeAll() + clearSQLQueries() try db.inSavepoint { .commit } try db.inTransaction { .commit } XCTAssertEqual(Set(sqlQueries), ["BEGIN DEFERRED TRANSACTION", "COMMIT TRANSACTION"]) diff --git a/Tests/GRDBTests/DatabaseTraceTests.swift b/Tests/GRDBTests/DatabaseTraceTests.swift index df9d73119e..8ad6eedc76 100644 --- a/Tests/GRDBTests/DatabaseTraceTests.swift +++ b/Tests/GRDBTests/DatabaseTraceTests.swift @@ -114,11 +114,11 @@ class DatabaseTraceTests : GRDBTestCase { } func testTraceFromConfigurationWithDefaultOptions() throws { - var events: [String] = [] + let eventsMutex: Mutex<[String]> = Mutex([]) var configuration = Configuration() configuration.prepareDatabase { db in db.trace { event in - events.append("SQL: \(event)") + eventsMutex.withLock { $0.append("SQL: \(event)") } } } let dbQueue = try makeDatabaseQueue(configuration: configuration) @@ -127,19 +127,19 @@ class DatabaseTraceTests : GRDBTestCase { CREATE table t(a); INSERT INTO t (a) VALUES (?) """, arguments: [1]) - XCTAssertEqual(events.suffix(2), [ + XCTAssertEqual(eventsMutex.load().suffix(2), [ "SQL: CREATE table t(a)", "SQL: INSERT INTO t (a) VALUES (?)"]) } } func testTraceFromConfigurationWithPublicStatementArguments() throws { - var events: [String] = [] + let eventsMutex: Mutex<[String]> = Mutex([]) var configuration = Configuration() configuration.publicStatementArguments = true configuration.prepareDatabase { db in db.trace { event in - events.append("SQL: \(event)") + eventsMutex.withLock { $0.append("SQL: \(event)") } } } let dbQueue = try makeDatabaseQueue(configuration: configuration) @@ -148,7 +148,7 @@ class DatabaseTraceTests : GRDBTestCase { CREATE table t(a); INSERT INTO t (a) VALUES (?) """, arguments: [1]) - XCTAssertEqual(events.suffix(2), [ + XCTAssertEqual(eventsMutex.load().suffix(2), [ "SQL: CREATE table t(a)", "SQL: INSERT INTO t (a) VALUES (1)"]) } diff --git a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift index 2245a8be6a..14dce9d843 100644 --- a/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift +++ b/Tests/GRDBTests/DatabaseUUIDEncodingStrategyTests.swift @@ -19,21 +19,25 @@ private enum StrategyLowercaseString: StrategyProvider { } private struct RecordWithUUID: EncodableRecord, Encodable { - static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { Strategy.strategy } + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { + Strategy.strategy + } + var uuid: UUID } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension RecordWithUUID: Identifiable { var id: UUID { uuid } } private struct RecordWithOptionalUUID: EncodableRecord, Encodable { - static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { Strategy.strategy } + static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy { + Strategy.strategy + } + var uuid: UUID? } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension RecordWithOptionalUUID: Identifiable { var id: UUID? { uuid } } @@ -184,10 +188,6 @@ extension DatabaseUUIDEncodingStrategyTests { } func testFilterID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .blob) } let uuids = [ @@ -303,10 +303,6 @@ extension DatabaseUUIDEncodingStrategyTests { } func testDeleteID() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Identifiable not available") - } - try makeDatabaseQueue().write { db in try db.create(table: "t") { $0.primaryKey("id", .blob) } let uuids = [ @@ -357,7 +353,7 @@ extension DatabaseUUIDEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } @@ -377,7 +373,7 @@ extension DatabaseUUIDEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } @@ -397,7 +393,7 @@ extension DatabaseUUIDEncodingStrategyTests { } do { - sqlQueries.removeAll() + clearSQLQueries() try Table>("t").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit } diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 9139b1f9eb..221f01063e 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB @@ -195,7 +204,7 @@ class DatabaseWriterTests : GRDBTestCase { } func testVacuumInto() throws { - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("VACUUM INTO is not available") } // Prevent SQLCipher failures @@ -266,7 +275,6 @@ class DatabaseWriterTests : GRDBTestCase { try DatabaseQueue().backup(to: dbQueue) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncAwait_write() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -286,7 +294,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncAwait_writeWithoutTransaction() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -309,7 +316,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncAwait_barrierWriteWithoutTransaction() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -332,7 +338,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncAwait_erase() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -350,7 +355,6 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncAwait_vacuum() async throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in @@ -366,7 +370,7 @@ class DatabaseWriterTests : GRDBTestCase { try await test(setup(makeDatabasePool())) } - @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) // async + vacuum into + @available(iOS 14, macOS 10.16, tvOS 14, *) // async + vacuum into func testAsyncAwait_vacuumInto() async throws { // Prevent SQLCipher failures guard sqlite3_libversion_number() >= 3027000 else { @@ -397,7 +401,6 @@ class DatabaseWriterTests : GRDBTestCase { } /// A test related to - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncWriteThenRead() async throws { /// An async read performed after an async write should see the write. func test(_ dbWriter: some DatabaseWriter) async throws { @@ -418,4 +421,399 @@ class DatabaseWriterTests : GRDBTestCase { try await test(makeDatabaseQueue()) try await test(makeDatabasePool()) } + + // MARK: - Task Cancellation + + func test_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let task = Task { + await semaphore.wait() + try await dbWriter.writeWithoutTransaction { db in + XCTFail("Should not be executed") + } + } + task.cancel() + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.writeWithoutTransaction { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_statement_execution_from_writeWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.writeWithoutTransaction { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + try db.execute(sql: "SELECT 0") + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_cursor_iteration_from_writeWithoutTransaction_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.writeWithoutTransaction { db in + let cursor = try Int.fetchCursor(db, sql: """ + SELECT 1 UNION ALL SELECT 2 + """) + _ = try cursor.next() + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + _ = try cursor.next() + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.writeWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_write_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let task = Task { + await semaphore.wait() + try await dbWriter.write { db in + XCTFail("Should not be executed") + } + } + task.cancel() + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.write { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_write_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.write { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.write { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_statement_execution_from_write_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.write { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + try db.execute(sql: "SELECT 0") + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.write { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_cursor_iteration_from_write_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.write { db in + let cursor = try Int.fetchCursor(db, sql: """ + SELECT 1 UNION ALL SELECT 2 + """) + _ = try cursor.next() + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + _ = try cursor.next() + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.write { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_before_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let task = Task { + await semaphore.wait() + try await dbWriter.barrierWriteWithoutTransaction { db in + XCTFail("Should not be executed") + } + } + task.cancel() + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.barrierWriteWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.barrierWriteWithoutTransaction { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.barrierWriteWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_statement_execution_from_barrierWriteWithoutTransaction_is_cancelled_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.barrierWriteWithoutTransaction { db in + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + try db.execute(sql: "SELECT 0") + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.barrierWriteWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } + + func test_cursor_iteration_from_barrierWriteWithoutTransaction_is_interrupted_by_Task_cancellation_performed_after_database_access() async throws { + func test(_ dbWriter: some DatabaseWriter) async throws { + let semaphore = AsyncSemaphore(value: 0) + let cancelledTaskMutex = Mutex?>(nil) + let task = Task { + await semaphore.wait() + try await dbWriter.barrierWriteWithoutTransaction { db in + let cursor = try Int.fetchCursor(db, sql: """ + SELECT 1 UNION ALL SELECT 2 + """) + _ = try cursor.next() + try XCTUnwrap(cancelledTaskMutex.load()).cancel() + _ = try cursor.next() + XCTFail("Expected error") + } + } + cancelledTaskMutex.store(task) + semaphore.signal() + + do { + try await task.value + XCTFail("Expected error") + } catch { + XCTAssert(error is CancellationError) + } + + // Database access is restored after cancellation (no error is thrown) + try await dbWriter.barrierWriteWithoutTransaction { db in + try db.execute(sql: "SELECT 0") + } + } + + try await test(makeDatabaseQueue()) + try await test(makeDatabasePool()) + try await test(AnyDatabaseWriter(makeDatabaseQueue())) + } } diff --git a/Tests/GRDBTests/DerivableRequestTests.swift b/Tests/GRDBTests/DerivableRequestTests.swift index 1cb5f91b25..98b0c87fb2 100644 --- a/Tests/GRDBTests/DerivableRequestTests.swift +++ b/Tests/GRDBTests/DerivableRequestTests.swift @@ -180,7 +180,7 @@ class DerivableRequestTests: GRDBTestCase { .forKey("fullName"))) // ... for one table - sqlQueries.removeAll() + clearSQLQueries() let authorNames = try Author.all() .orderByFullName() .fetchAll(db) @@ -192,7 +192,7 @@ class DerivableRequestTests: GRDBTestCase { "firstName" COLLATE swiftLocalizedCaseInsensitiveCompare """) - sqlQueries.removeAll() + clearSQLQueries() let reversedAuthorNames = try Author.all() .orderByFullName() .reversed() @@ -205,7 +205,7 @@ class DerivableRequestTests: GRDBTestCase { "firstName" COLLATE swiftLocalizedCaseInsensitiveCompare DESC """) - sqlQueries.removeAll() + clearSQLQueries() _ /* unorderedAuthors */ = try Author.all() .orderByFullName() .unordered() @@ -214,7 +214,7 @@ class DerivableRequestTests: GRDBTestCase { SELECT * FROM "author" """) - sqlQueries.removeAll() + clearSQLQueries() _ /* stableOrderAuthors */ = try Author.all() .withStableOrder() .fetchAll(db) @@ -222,7 +222,7 @@ class DerivableRequestTests: GRDBTestCase { SELECT * FROM "author" ORDER BY "id" """) - sqlQueries.removeAll() + clearSQLQueries() _ /* stableOrderAuthors */ = try Author.all() .orderByFullName() .withStableOrder() @@ -232,7 +232,7 @@ class DerivableRequestTests: GRDBTestCase { """) // ... for one view - sqlQueries.removeAll() + clearSQLQueries() _ /* authorViewNames */ = try Table("authorView").all() .order(Column("fullName")) .fetchAll(db) @@ -241,7 +241,7 @@ class DerivableRequestTests: GRDBTestCase { ORDER BY "fullName" """) - sqlQueries.removeAll() + clearSQLQueries() _ /* reversedAuthorViewNames */ = try Table("authorView").all() .order(Column("fullName")) .reversed() @@ -251,7 +251,7 @@ class DerivableRequestTests: GRDBTestCase { ORDER BY "fullName" DESC """) - sqlQueries.removeAll() + clearSQLQueries() _ /* unorderedAuthorViews */ = try Table("authorView").all() .order(Column("fullName")) .unordered() @@ -260,7 +260,7 @@ class DerivableRequestTests: GRDBTestCase { SELECT * FROM "authorView" """) - sqlQueries.removeAll() + clearSQLQueries() _ /* stableOrderAuthorViews */ = try Table("authorView").all() .withStableOrder() .fetchAll(db) @@ -268,7 +268,7 @@ class DerivableRequestTests: GRDBTestCase { SELECT * FROM "authorView" ORDER BY 1, 2, 3, 4, 5 """) - sqlQueries.removeAll() + clearSQLQueries() _ /* stableOrderAuthorViews */ = try Table("authorView").all() .order(Column("fullName")) .withStableOrder() @@ -278,7 +278,7 @@ class DerivableRequestTests: GRDBTestCase { """) // ... for two tables (2) - sqlQueries.removeAll() + clearSQLQueries() let bookTitles = try Book .joining(required: Book.author.orderByFullName()) .orderByTitle() @@ -294,7 +294,7 @@ class DerivableRequestTests: GRDBTestCase { "author"."firstName" COLLATE swiftLocalizedCaseInsensitiveCompare """) - sqlQueries.removeAll() + clearSQLQueries() let reversedBookTitles = try Book .joining(required: Book.author.orderByFullName()) .orderByTitle() @@ -311,7 +311,7 @@ class DerivableRequestTests: GRDBTestCase { "author"."firstName" COLLATE swiftLocalizedCaseInsensitiveCompare DESC """) - sqlQueries.removeAll() + clearSQLQueries() _ /* unorderedBooks */ = try Book .joining(required: Book.author.orderByFullName()) .orderByTitle() @@ -322,7 +322,7 @@ class DerivableRequestTests: GRDBTestCase { JOIN "author" ON "author"."id" = "book"."authorId" """) - sqlQueries.removeAll() + clearSQLQueries() _ /* stableOrderBooks */ = try Book .joining(required: Book.author.orderByFullName()) .orderByTitle() @@ -346,7 +346,7 @@ class DerivableRequestTests: GRDBTestCase { try libraryMigrator.migrate(dbQueue) try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let request = Author.all().selectCountry() let authorCountries = try Set(String.fetchAll(db, request)) XCTAssertEqual(authorCountries, ["FR", "US"]) @@ -356,7 +356,7 @@ class DerivableRequestTests: GRDBTestCase { } do { - sqlQueries.removeAll() + clearSQLQueries() let request = Book.including(required: Book.author.selectCountry()) _ = try Row.fetchAll(db, request) XCTAssertEqual(lastSQLQuery, """ @@ -373,7 +373,7 @@ class DerivableRequestTests: GRDBTestCase { try libraryMigrator.migrate(dbQueue) try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let frenchBookTitles = try Book.all() .filter(authorCountry: "FR") .order(Column("title")) @@ -389,7 +389,7 @@ class DerivableRequestTests: GRDBTestCase { } do { - sqlQueries.removeAll() + clearSQLQueries() let frenchAuthorFullNames = try Author .joining(required: Author.books.filter(authorCountry: "FR")) .order(Column("firstName")) @@ -436,7 +436,7 @@ class DerivableRequestTests: GRDBTestCase { // matchingFts4 do { - sqlQueries.removeAll() + clearSQLQueries() let title = try Book.all() .matchingFts4(FTS3Pattern(rawPattern: "moby dick")) .fetchOne(db) @@ -448,7 +448,7 @@ class DerivableRequestTests: GRDBTestCase { LIMIT 1 """)) - sqlQueries.removeAll() + clearSQLQueries() let fullName = try Author .joining(required: Author.books.matchingFts4(FTS3Pattern(rawPattern: "moby dick"))) .fetchOne(db) @@ -465,7 +465,7 @@ class DerivableRequestTests: GRDBTestCase { #if SQLITE_ENABLE_FTS5 // matchingFts5 do { - sqlQueries.removeAll() + clearSQLQueries() let title = try Book.all() .matchingFts5(FTS3Pattern(rawPattern: "cote swann")) .fetchOne(db) @@ -477,7 +477,7 @@ class DerivableRequestTests: GRDBTestCase { LIMIT 1 """)) - sqlQueries.removeAll() + clearSQLQueries() let fullName = try Author .joining(required: Author.books.matchingFts5(FTS3Pattern(rawPattern: "cote swann"))) .fetchOne(db) diff --git a/Tests/GRDBTests/FTS3RecordTests.swift b/Tests/GRDBTests/FTS3RecordTests.swift index b37c08bb54..0369fc959c 100644 --- a/Tests/GRDBTests/FTS3RecordTests.swift +++ b/Tests/GRDBTests/FTS3RecordTests.swift @@ -19,7 +19,7 @@ extension Book : FetchableRecord { extension Book : MutablePersistableRecord { static let databaseTableName = "books" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } func encode(to container: inout PersistenceContainer) { container[.rowID] = id @@ -102,14 +102,14 @@ class FTS3RecordTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let pattern = try FTS3Pattern(rawPattern: "Herman Melville") XCTAssertEqual(try Book.matching(pattern).fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\" WHERE \"books\" MATCH 'Herman Melville'")) } do { - sqlQueries = [] + clearSQLQueries() XCTAssertEqual(try Book.fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\"")) } diff --git a/Tests/GRDBTests/FTS3TableBuilderTests.swift b/Tests/GRDBTests/FTS3TableBuilderTests.swift index b2434da37a..d0f1bbaa61 100644 --- a/Tests/GRDBTests/FTS3TableBuilderTests.swift +++ b/Tests/GRDBTests/FTS3TableBuilderTests.swift @@ -76,7 +76,7 @@ class FTS3TableBuilderTests: GRDBTestCase { } #elseif !GRDBCIPHER func testUnicode61TokenizerDiacriticsRemove() throws { - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip() } let dbQueue = try makeDatabaseQueue() diff --git a/Tests/GRDBTests/FTS4RecordTests.swift b/Tests/GRDBTests/FTS4RecordTests.swift index 646c3852ef..c197dfad93 100644 --- a/Tests/GRDBTests/FTS4RecordTests.swift +++ b/Tests/GRDBTests/FTS4RecordTests.swift @@ -19,7 +19,7 @@ extension Book : FetchableRecord { extension Book : MutablePersistableRecord { static let databaseTableName = "books" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } func encode(to container: inout PersistenceContainer) { container[.rowID] = id @@ -102,14 +102,14 @@ class FTS4RecordTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let pattern = try FTS3Pattern(rawPattern: "Herman Melville") XCTAssertEqual(try Book.matching(pattern).fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\" WHERE \"books\" MATCH 'Herman Melville'")) } do { - sqlQueries = [] + clearSQLQueries() XCTAssertEqual(try Book.fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\"")) } diff --git a/Tests/GRDBTests/FTS4TableBuilderTests.swift b/Tests/GRDBTests/FTS4TableBuilderTests.swift index ad7287b4d2..20b19384c1 100644 --- a/Tests/GRDBTests/FTS4TableBuilderTests.swift +++ b/Tests/GRDBTests/FTS4TableBuilderTests.swift @@ -272,16 +272,16 @@ class FTS4TableBuilderTests: GRDBTestCase { func testFTS4Compression() throws { // Based on https://github.com/groue/GRDB.swift/issues/369 - var compressCalled = false - var uncompressCalled = false + let compressCalledMutex = Mutex(false) + let uncompressCalledMutex = Mutex(false) dbConfiguration.prepareDatabase { db in db.add(function: DatabaseFunction("zipit", argumentCount: 1, pure: true, function: { dbValues in - compressCalled = true + compressCalledMutex.store(true) return dbValues[0] })) db.add(function: DatabaseFunction("unzipit", argumentCount: 1, pure: true, function: { dbValues in - uncompressCalled = true + uncompressCalledMutex.store(true) return dbValues[0] })) } @@ -296,12 +296,12 @@ class FTS4TableBuilderTests: GRDBTestCase { assertDidExecute(sql: "CREATE VIRTUAL TABLE \"documents\" USING fts4(content, compress=\"zipit\", uncompress=\"unzipit\")") try db.execute(sql: "INSERT INTO documents (content) VALUES (?)", arguments: ["abc"]) - XCTAssertTrue(compressCalled) + XCTAssertTrue(compressCalledMutex.load()) } try dbPool.read { db in _ = try Row.fetchOne(db, sql: "SELECT * FROM documents") - XCTAssertTrue(uncompressCalled) + XCTAssertTrue(uncompressCalledMutex.load()) } } } diff --git a/Tests/GRDBTests/FTS5RecordTests.swift b/Tests/GRDBTests/FTS5RecordTests.swift index b25b0a0135..0a47ce2ce8 100644 --- a/Tests/GRDBTests/FTS5RecordTests.swift +++ b/Tests/GRDBTests/FTS5RecordTests.swift @@ -20,7 +20,7 @@ extension Book : FetchableRecord { extension Book : MutablePersistableRecord { static let databaseTableName = "books" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } func encode(to container: inout PersistenceContainer) { container[.rowID] = id @@ -101,14 +101,14 @@ class FTS5RecordTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let pattern = FTS5Pattern(matchingAllTokensIn: "Herman Melville")! XCTAssertEqual(try Book.matching(pattern).fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\" WHERE \"books\" MATCH 'herman melville'")) } do { - sqlQueries = [] + clearSQLQueries() XCTAssertEqual(try Book.fetchCount(db), 1) XCTAssertTrue(sqlQueries.contains("SELECT COUNT(*) FROM \"books\"")) } diff --git a/Tests/GRDBTests/FTS5TableBuilderTests.swift b/Tests/GRDBTests/FTS5TableBuilderTests.swift index b44dc6589f..fe63cfc427 100644 --- a/Tests/GRDBTests/FTS5TableBuilderTests.swift +++ b/Tests/GRDBTests/FTS5TableBuilderTests.swift @@ -130,7 +130,7 @@ class FTS5TableBuilderTests: GRDBTestCase { } #elseif !GRDBCIPHER func testUnicode61TokenizerDiacriticsRemove() throws { - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip() } diff --git a/Tests/GRDBTests/FTS5TokenizerTests.swift b/Tests/GRDBTests/FTS5TokenizerTests.swift index af88e73515..d7efff5c07 100644 --- a/Tests/GRDBTests/FTS5TokenizerTests.swift +++ b/Tests/GRDBTests/FTS5TokenizerTests.swift @@ -1,4 +1,13 @@ #if SQLITE_ENABLE_FTS5 +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index 8c7d0c195c..1cbba07d8b 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -121,6 +121,92 @@ extension FetchableRecordDecodableTests { } } + func testSingleValueDataProperty() throws { + struct Value : Decodable { + let data: Data + + init(from decoder: Decoder) throws { + data = try decoder.singleValueContainer().decode(Data.self) + } + } + + struct Struct : FetchableRecord, Decodable { + static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy { + if column == "value" { + return .custom { _ in Data([1, 2, 3]) } + } else { + return .deferredToData + } + } + let value: Value + let optionalValue: Value? + } + + do { + // No null values + let s = try Struct(row: ["value": "foo", "optionalValue": "bar"]) + XCTAssertEqual(s.value.data, Data([1, 2, 3])) + XCTAssertEqual(s.optionalValue?.data, Data([98, 97, 114])) + } + + do { + // Null values + let s = try Struct(row: ["value": "foo", "optionalValue": nil]) + XCTAssertEqual(s.value.data, Data([1, 2, 3])) + XCTAssertNil(s.optionalValue) + } + + do { + // Missing and extra values + let s = try Struct(row: ["value": "foo", "ignored": "?"]) + XCTAssertEqual(s.value.data, Data([1, 2, 3])) + XCTAssertNil(s.optionalValue) + } + } + + func testSingleValueDateProperty() throws { + struct Value : Decodable { + let date: Date + + init(from decoder: Decoder) throws { + date = try decoder.singleValueContainer().decode(Date.self) + } + } + + struct Struct : FetchableRecord, Decodable { + static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy { + if column == "value" { + return .custom { _ in Date(timeIntervalSince1970: 0) } + } else { + return .deferredToDate + } + } + let value: Value + let optionalValue: Value? + } + + do { + // No null values + let s = try Struct(row: ["value": "foo", "optionalValue": "2001-01-01 00:00:00"]) + XCTAssertEqual(s.value.date, Date(timeIntervalSince1970: 0)) + XCTAssertEqual(s.optionalValue?.date, Date(timeIntervalSinceReferenceDate: 0)) + } + + do { + // Null values + let s = try Struct(row: ["value": "foo", "optionalValue": nil]) + XCTAssertEqual(s.value.date, Date(timeIntervalSince1970: 0)) + XCTAssertNil(s.optionalValue) + } + + do { + // Missing and extra values + let s = try Struct(row: ["value": "foo", "ignored": "?"]) + XCTAssertEqual(s.value.date, Date(timeIntervalSince1970: 0)) + XCTAssertNil(s.optionalValue) + } + } + func testNonTrivialSingleValueDecodableProperty() throws { struct NestedValue : Decodable { let string: String @@ -1275,9 +1361,12 @@ extension FetchableRecordDecodableTests { context = decoder.userInfo[testKeyRoot] as? String } - static let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [ - testKeyRoot: "GRDB root", - testKeyNested: "GRDB column or scope"] + static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { + [ + testKeyRoot: "GRDB root", + testKeyNested: "GRDB column or scope", + ] + } static func databaseJSONDecoder(for column: String) -> JSONDecoder { let decoder = JSONDecoder() @@ -1580,7 +1669,9 @@ extension FetchableRecordDecodableTests { struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" - static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [CodingUserInfoKey.testKey: "correct"] + static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { + [CodingUserInfoKey.testKey: "correct"] + } let nested: NestedStruct? } diff --git a/Tests/GRDBTests/ForeignKeyDefinitionTests.swift b/Tests/GRDBTests/ForeignKeyDefinitionTests.swift index d6bacfc5a6..8a4940476c 100644 --- a/Tests/GRDBTests/ForeignKeyDefinitionTests.swift +++ b/Tests/GRDBTests/ForeignKeyDefinitionTests.swift @@ -20,7 +20,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent") @@ -99,7 +99,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child", options: .ifNotExists) { t in t.column("a") t.belongsTo("parent") @@ -155,7 +155,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").unique() @@ -197,7 +197,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", indexed: false) @@ -239,7 +239,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").notNull() @@ -295,7 +295,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.column("name", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", onDelete: .cascade, onUpdate: .setNull, deferred: true) @@ -411,7 +411,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent") @@ -494,7 +494,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child", options: .ifNotExists) { t in t.column("a") t.belongsTo("parent") @@ -556,7 +556,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").unique() @@ -604,7 +604,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", indexed: false) @@ -652,7 +652,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").notNull() @@ -714,7 +714,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { t.primaryKey("id", .integer) } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", onDelete: .cascade, onUpdate: .setNull, deferred: true) @@ -757,7 +757,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { func testTable_belongsTo_singleColumnPrimaryKey_autoreference_singular() throws { try makeDatabaseQueue().inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "employee") { t in t.autoIncrementedPrimaryKey("id") t.column("a") @@ -786,7 +786,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } do { - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "node") { t in t.primaryKey { t.column("code") } t.column("a") @@ -820,7 +820,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { func testTable_belongsTo_singleColumnPrimaryKey_autoreference_plural() throws { try makeDatabaseQueue().inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "employees") { t in t.autoIncrementedPrimaryKey("id") t.column("a") @@ -849,7 +849,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } do { - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "nodes") { t in t.primaryKey { t.column("code") } t.column("a") @@ -913,7 +913,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent") @@ -1025,7 +1025,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child", options: .ifNotExists) { t in t.column("a") t.belongsTo("parent") @@ -1107,7 +1107,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").unique() @@ -1189,7 +1189,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", indexed: false) @@ -1257,7 +1257,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent").notNull() @@ -1339,7 +1339,7 @@ class ForeignKeyDefinitionTests: GRDBTestCase { } } - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "child") { t in t.column("a") t.belongsTo("parent", onDelete: .cascade, onUpdate: .setNull, deferred: true) diff --git a/Tests/GRDBTests/FoundationDateComponentsTests.swift b/Tests/GRDBTests/FoundationDateComponentsTests.swift index b336a75252..bf3b7b0820 100644 --- a/Tests/GRDBTests/FoundationDateComponentsTests.swift +++ b/Tests/GRDBTests/FoundationDateComponentsTests.swift @@ -708,7 +708,7 @@ class FoundationDateComponentsTests : GRDBTestCase { } let record = Record(date: DatabaseDateComponents(DateComponents(year: 2018, month: 12, day: 31), format: .YMD)) let jsonData = try JSONEncoder().encode(record) - let json = String(data: jsonData, encoding: .utf8)! + let json = String(decoding: jsonData, as: UTF8.self) XCTAssertEqual(json, """ {"date":"2018-12-31"} """) diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index 9028d491f4..ffb1f1a0b8 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -1,17 +1,26 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import Foundation import XCTest @testable import GRDB // Support for Database.logError -var lastResultCode: ResultCode? = nil -var lastMessage: String? = nil +struct SQLiteDiagnostic { + var resultCode: ResultCode + var message: String +} +private let lastSQLiteDiagnosticMutex = Mutex(nil) +var lastSQLiteDiagnostic: SQLiteDiagnostic? { lastSQLiteDiagnosticMutex.load() } let logErrorSetup: Void = { - let lock = NSLock() Database.logError = { (resultCode, message) in - lock.lock() - defer { lock.unlock() } - lastResultCode = resultCode - lastMessage = message + lastSQLiteDiagnosticMutex.store(SQLiteDiagnostic(resultCode: resultCode, message: message)) } }() @@ -55,10 +64,12 @@ class GRDBTestCase: XCTestCase { // The default path for database pool directory private var dbDirectoryPath: String! - // Populated by default configuration - @LockedBox var sqlQueries: [String] = [] + let _sqlQueriesMutex: Mutex<[String]> = Mutex([]) - // Populated by default configuration + // Automatically updated by default dbConfiguration + var sqlQueries: [String] { _sqlQueriesMutex.load() } + + // Automatically updated by default dbConfiguration var lastSQLQuery: String? { sqlQueries.last } override func setUp() { @@ -73,7 +84,7 @@ class GRDBTestCase: XCTestCase { dbConfiguration = Configuration() // Test that database are deallocated in a clean state - dbConfiguration.SQLiteConnectionWillClose = { sqliteConnection in + dbConfiguration.onConnectionWillClose { sqliteConnection in // https://www.sqlite.org/capi3ref.html#sqlite3_close: // > If sqlite3_close_v2() is called on a database connection that still // > has outstanding prepared statements, BLOB handles, and/or @@ -103,9 +114,11 @@ class GRDBTestCase: XCTestCase { } } - dbConfiguration.prepareDatabase { db in + dbConfiguration.prepareDatabase { [_sqlQueriesMutex] db in db.trace { event in - self.sqlQueries.append(event.expandedDescription) + _sqlQueriesMutex.withLock { + $0.append(event.expandedDescription) + } } #if GRDBCIPHER_USE_ENCRYPTION @@ -113,7 +126,7 @@ class GRDBTestCase: XCTestCase { #endif } - sqlQueries = [] + clearSQLQueries() } override func tearDown() { @@ -121,6 +134,10 @@ class GRDBTestCase: XCTestCase { do { try FileManager.default.removeItem(atPath: dbDirectoryPath) } catch { } } + func clearSQLQueries() { + _sqlQueriesMutex.store([]) + } + func assertNoError(file: StaticString = #file, line: UInt = #line, _ test: () throws -> Void) { do { try test() @@ -218,26 +235,39 @@ extension FetchRequest { } /// A type-erased ValueReducer. -public struct AnyValueReducer: ValueReducer { - private var __fetch: (Database) throws -> Fetched +struct AnyValueReducer: ValueReducer { + private var __fetch: @Sendable (Database) throws -> Fetched private var __value: (Fetched) -> Value? - public init( - fetch: @escaping (Database) throws -> Fetched, + init( + fetch: @escaping @Sendable (Database) throws -> Fetched, value: @escaping (Fetched) -> Value?) { self.__fetch = fetch self.__value = value } - public func _fetch(_ db: Database) throws -> Fetched { - try __fetch(db) + func _makeFetcher() -> AnyValueReducerFetcher { + AnyValueReducerFetcher(fetch: __fetch) } - public func _value(_ fetched: Fetched) -> Value? { + func _value(_ fetched: Fetched) -> Value? { __value(fetched) } } +/// A type-erased _ValueReducerFetcher. +struct AnyValueReducerFetcher: _ValueReducerFetcher { + private var _fetch: @Sendable (Database) throws -> Fetched + + init(fetch: @escaping @Sendable (Database) throws -> Fetched) { + self._fetch = fetch + } + + func fetch(_ db: Database) throws -> Fetched { + try _fetch(db) + } +} + // Assume this is correct :-/ extension XCTestExpectation: @unchecked Sendable { } diff --git a/Tests/GRDBTests/JSONColumnTests.swift b/Tests/GRDBTests/JSONColumnTests.swift index b73c21128d..e544fd148f 100644 --- a/Tests/GRDBTests/JSONColumnTests.swift +++ b/Tests/GRDBTests/JSONColumnTests.swift @@ -9,7 +9,7 @@ final class JSONColumnTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -28,7 +28,7 @@ final class JSONColumnTests: GRDBTestCase { static let info = JSONColumn(CodingKeys.info) } - static let databaseSelection: [any SQLSelectable] = [Columns.id, Columns.info] + static var databaseSelection: [any SQLSelectable] { [Columns.id, Columns.info] } } let dbQueue = try makeDatabaseQueue() @@ -51,7 +51,7 @@ final class JSONColumnTests: GRDBTestCase { throw XCTSkip("JSON_EXTRACT is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON_EXTRACT is not available") } #endif diff --git a/Tests/GRDBTests/JSONExpressionsTests.swift b/Tests/GRDBTests/JSONExpressionsTests.swift index f0b4390927..465fc06923 100644 --- a/Tests/GRDBTests/JSONExpressionsTests.swift +++ b/Tests/GRDBTests/JSONExpressionsTests.swift @@ -9,7 +9,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -44,7 +44,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -105,7 +105,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -281,7 +281,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -316,7 +316,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -392,7 +392,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -435,7 +435,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -470,7 +470,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -525,7 +525,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -580,7 +580,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -635,7 +635,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -738,7 +738,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -821,7 +821,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -904,7 +904,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -947,7 +947,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -990,7 +990,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1025,7 +1025,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1060,7 +1060,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1103,7 +1103,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1138,7 +1138,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1185,7 +1185,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1216,7 +1216,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1282,7 +1282,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1309,7 +1309,7 @@ final class JSONExpressionsTests: GRDBTestCase { throw XCTSkip("JSON support is not available") } #else - guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { + guard #available(iOS 16, tvOS 17, watchOS 9, *) else { throw XCTSkip("JSON support is not available") } #endif @@ -1397,7 +1397,7 @@ final class JSONExpressionsTests: GRDBTestCase { // throw XCTSkip("JSON support is not available") // } // #else -// guard #available(iOS 16, macOS 10.15, tvOS 17, watchOS 9, *) else { +// guard #available(iOS 16, tvOS 17, watchOS 9, *) else { // throw XCTSkip("JSON support is not available") // } // #endif diff --git a/Tests/GRDBTests/JoinSupportTests.swift b/Tests/GRDBTests/JoinSupportTests.swift index b2ae7eeeac..81ec2c3ab7 100644 --- a/Tests/GRDBTests/JoinSupportTests.swift +++ b/Tests/GRDBTests/JoinSupportTests.swift @@ -50,7 +50,7 @@ private struct T2: Codable, FetchableRecord, TableRecord { private struct T3: Codable, FetchableRecord, TableRecord { static let databaseTableName = "t3" - static let databaseSelection: [any SQLSelectable] = [Column("t1id"), Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("t1id"), Column("name")] } var t1id: Int64 var name: String } @@ -92,7 +92,6 @@ private struct FlatModel: FetchableRecord { self.t5count = row.scopes[Scopes.suffix]!["t5count"] } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) static func modernAll() -> some FetchRequest { all() } @@ -138,7 +137,6 @@ private struct CodableFlatModel: FetchableRecord, Codable { var t3: T3? var t5count: Int - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) static func modernAll() -> some FetchRequest { all() } @@ -186,7 +184,6 @@ private struct CodableNestedModel: FetchableRecord, Codable { var t3: T3? var t5count: Int - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) static func modernAll() -> some FetchRequest { all() } diff --git a/Tests/GRDBTests/MutablePersistableRecordChangesTests.swift b/Tests/GRDBTests/MutablePersistableRecordChangesTests.swift index f530149f8a..e3a3485037 100644 --- a/Tests/GRDBTests/MutablePersistableRecordChangesTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordChangesTests.swift @@ -319,14 +319,14 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { try record.insert(db) do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } @@ -335,7 +335,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = nil } @@ -346,7 +346,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" @@ -391,14 +391,14 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { try record.insert(db) do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } @@ -407,7 +407,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = nil } @@ -418,7 +418,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" @@ -457,14 +457,14 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { try record.insert(db) do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } @@ -473,7 +473,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = nil } @@ -484,7 +484,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" @@ -519,14 +519,14 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { try record.insert(db) do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { _ in } XCTAssertFalse(modified) XCTAssert(sqlQueries.isEmpty) } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Arthur" } @@ -535,7 +535,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = nil } @@ -546,7 +546,7 @@ class MutablePersistableRecordChangesTests: GRDBTestCase { } do { - sqlQueries = [] + clearSQLQueries() let modified = try record.updateChanges(db) { $0.firstName = "Bob" $0.lastName = "Johnson" diff --git a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift index 339a4a4973..93552bcde2 100644 --- a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift @@ -746,9 +746,12 @@ extension MutablePersistableRecordEncodableTests { case nestedKeyed, nestedSingle, nestedUnkeyed, key, context } - static let databaseEncodingUserInfo: [CodingUserInfoKey: Any] = [ - testKeyRoot: "GRDB root", - testKeyNested: "GRDB nested"] + static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { + [ + testKeyRoot: "GRDB root", + testKeyNested: "GRDB nested", + ] + } static func databaseJSONEncoder(for column: String) -> JSONEncoder { let encoder = JSONEncoder() @@ -779,7 +782,7 @@ extension MutablePersistableRecordEncodableTests { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys, .prettyPrinted] - let json = try String(data: encoder.encode(record), encoding: .utf8)! + let json = try String(decoding: encoder.encode(record), as: UTF8.self) XCTAssertEqual(json, """ { "nestedKeyed" : { @@ -805,7 +808,7 @@ extension MutablePersistableRecordEncodableTests { let encoder = JSONEncoder() encoder.userInfo = [testKeyRoot: "root", testKeyNested: "nested"] encoder.outputFormatting = [.sortedKeys, .prettyPrinted] - let json = try String(data: encoder.encode(record), encoding: .utf8) + let json = try String(decoding: encoder.encode(record), as: UTF8.self) XCTAssertEqual(json, """ { "context" : "root", @@ -905,7 +908,9 @@ extension MutablePersistableRecordEncodableTests { struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { static let databaseTableName = "t1" - static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] = [CodingUserInfoKey.testKey: "correct"] + static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { + [CodingUserInfoKey.testKey: "correct"] + } let nested: NestedStruct? } diff --git a/Tests/GRDBTests/MutablePersistableRecordTests.swift b/Tests/GRDBTests/MutablePersistableRecordTests.swift index 18a15c1eeb..a5da3c08d4 100644 --- a/Tests/GRDBTests/MutablePersistableRecordTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordTests.swift @@ -1261,7 +1261,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let player = FullPlayer(id: nil, name: "Arthur", score: 1000) - let insertedPlayer = try XCTUnwrap(player.insertAndFetch(db)) + let insertedPlayer = try player.insertAndFetch(db) XCTAssertEqual(insertedPlayer.id, 1) XCTAssertEqual(insertedPlayer.name, "Arthur") XCTAssertEqual(insertedPlayer.score, 1000) @@ -1282,9 +1282,9 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.insertAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (NULL,'Arthur') RETURNING * @@ -1332,7 +1332,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(name: "Arthur") let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement)! @@ -1368,7 +1368,7 @@ extension MutablePersistableRecordTests { do { // Test onConflict: .ignore - sqlQueries.removeAll() + clearSQLQueries() var player = FullPlayer(id: 1, name: "Barbara", score: 100) try XCTAssertTrue(player.exists(db)) let score = try player.insertAndFetch(db, onConflict: .ignore, selection: [Column("score")]) { (statement: Statement) in @@ -1423,7 +1423,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in let player = FullPlayer(id: nil, name: "Arthur", score: 1000) - let savedPlayer = try XCTUnwrap(player.saveAndFetch(db)) + let savedPlayer = try player.saveAndFetch(db) XCTAssertEqual(savedPlayer.id, 1) XCTAssertEqual(savedPlayer.name, "Arthur") XCTAssertEqual(savedPlayer.score, 1000) @@ -1444,9 +1444,9 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("UPDATE") }) XCTAssert(sqlQueries.contains(""" @@ -1482,8 +1482,8 @@ extension MutablePersistableRecordTests { do { var partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) - sqlQueries.removeAll() - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + clearSQLQueries() + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" UPDATE "player" SET "name"='Arthur' WHERE "id"=1 RETURNING * @@ -1519,9 +1519,9 @@ extension MutablePersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(id: 1, name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("INSERT") }) XCTAssert(sqlQueries.contains(""" @@ -1570,7 +1570,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(name: "Arthur") let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) @@ -1608,7 +1608,7 @@ extension MutablePersistableRecordTests { do { var partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) - sqlQueries.removeAll() + clearSQLQueries() let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) } @@ -1645,7 +1645,7 @@ extension MutablePersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(id: 1, name: "Arthur") let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) @@ -1709,7 +1709,7 @@ extension MutablePersistableRecordTests { player.name = "Barbara" do { - let updatedPlayer = try XCTUnwrap(player.updateAndFetch(db)) + let updatedPlayer = try player.updateAndFetch(db) XCTAssertEqual(updatedPlayer.id, 1) XCTAssertEqual(updatedPlayer.name, "Barbara") XCTAssertEqual(updatedPlayer.score, 1000) @@ -1760,7 +1760,7 @@ extension MutablePersistableRecordTests { player.name = "Barbara" do { - let updatedPlayer = try XCTUnwrap(player.updateAndFetch(db, as: PartialPlayer.self)) + let updatedPlayer = try player.updateAndFetch(db, as: PartialPlayer.self) XCTAssertEqual(updatedPlayer.id, 1) XCTAssertEqual(updatedPlayer.name, "Barbara") } @@ -2041,11 +2041,15 @@ extension MutablePersistableRecordTests { try player.insert(db) do { - let updatedRow = try player.updateChangesAndFetch( + // Update with no change + let update = try player.updateChangesAndFetch( db, selection: [AllColumns()], - fetch: { statement in try Row.fetchOne(statement) }, + fetch: { statement in + XCTFail("Should not be called") + return "ignored" + }, modify: { $0.name = "Barbara" }) - XCTAssertNil(updatedRow) + XCTAssertNil(update) } do { @@ -2401,7 +2405,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var player = FullPlayer(id: 1, name: "Arthur", score: 1000) let upsertedPlayer = try player.upsertAndFetch(db) @@ -2445,7 +2449,7 @@ extension MutablePersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() var player = FullPlayer(id: 1, name: "Barbara", score: 100) let upsertedPlayer = try player.upsertAndFetch(db) @@ -2504,7 +2508,7 @@ extension MutablePersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() var partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try partialPlayer.upsertAndFetch(db, as: FullPlayer.self) diff --git a/Tests/GRDBTests/Mutex.swift b/Tests/GRDBTests/Mutex.swift new file mode 100644 index 0000000000..1fb69c2e22 --- /dev/null +++ b/Tests/GRDBTests/Mutex.swift @@ -0,0 +1,54 @@ +import Foundation + +/// A Mutex protects a value with an NSLock. +/// +/// We'll replace it with the SE-0433 Mutex when it is available. +/// +final class Mutex { + private var _value: T + private var lock = NSLock() + + init(_ value: T) { + _value = value + } + + /// Runs the provided closure while holding a lock on the value. + /// + /// - parameter body: A closure that can modify the value. + func withLock(_ body: (inout T) throws -> U) rethrows -> U { + lock.lock() + defer { lock.unlock() } + return try body(&_value) + } +} + +// Inspired by +extension Mutex { + func load() -> T { + withLock { $0 } + } + + func store(_ value: T) { + withLock { $0 = value } + } +} + +extension Mutex where T: Numeric { + @discardableResult + func increment() -> T { + withLock { n in + n += 1 + return n + } + } + + @discardableResult + func decrement() -> T { + withLock { n in + n -= 1 + return n + } + } +} + +extension Mutex: @unchecked Sendable where T: Sendable { } diff --git a/Tests/GRDBTests/PersistableRecordTests.swift b/Tests/GRDBTests/PersistableRecordTests.swift index d919eeb989..c391e27da0 100644 --- a/Tests/GRDBTests/PersistableRecordTests.swift +++ b/Tests/GRDBTests/PersistableRecordTests.swift @@ -1340,9 +1340,9 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.insertAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.insertAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" INSERT INTO "player" ("id", "name") VALUES (NULL,'Arthur') RETURNING * @@ -1389,7 +1389,7 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(name: "Arthur") let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement)! @@ -1442,9 +1442,9 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("UPDATE") }) XCTAssert(sqlQueries.contains(""" @@ -1479,8 +1479,8 @@ extension PersistableRecordTests { do { let partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) - sqlQueries.removeAll() - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + clearSQLQueries() + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.contains(""" UPDATE "player" SET "name"='Arthur' WHERE "id"=1 RETURNING * @@ -1515,9 +1515,9 @@ extension PersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(id: 1, name: "Arthur") - let fullPlayer = try XCTUnwrap(partialPlayer.saveAndFetch(db, as: FullPlayer.self)) + let fullPlayer = try partialPlayer.saveAndFetch(db, as: FullPlayer.self) XCTAssert(sqlQueries.allSatisfy { !$0.contains("INSERT") }) XCTAssert(sqlQueries.contains(""" @@ -1565,7 +1565,7 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(name: "Arthur") let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) @@ -1602,7 +1602,7 @@ extension PersistableRecordTests { do { let partialPlayer = PartialPlayer(id: 1, name: "Arthur") try partialPlayer.delete(db) - sqlQueries.removeAll() + clearSQLQueries() let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) } @@ -1638,7 +1638,7 @@ extension PersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(id: 1, name: "Arthur") let score = try partialPlayer.saveAndFetch(db, selection: [Column("score")]) { (statement: Statement) in try Int.fetchOne(statement) @@ -1974,7 +1974,7 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let player = FullPlayer(id: 1, name: "Arthur", score: 1000) let upsertedPlayer = try player.upsertAndFetch(db) @@ -2018,7 +2018,7 @@ extension PersistableRecordTests { } do { - sqlQueries.removeAll() + clearSQLQueries() let player = FullPlayer(id: 1, name: "Barbara", score: 100) let upsertedPlayer = try player.upsertAndFetch(db) @@ -2077,7 +2077,7 @@ extension PersistableRecordTests { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in do { - sqlQueries.removeAll() + clearSQLQueries() let partialPlayer = PartialPlayer(name: "Arthur") let fullPlayer = try partialPlayer.upsertAndFetch(db, as: FullPlayer.self) diff --git a/Tests/GRDBTests/PoolTests.swift b/Tests/GRDBTests/PoolTests.swift index 29c8fcaf73..8729ae8171 100644 --- a/Tests/GRDBTests/PoolTests.swift +++ b/Tests/GRDBTests/PoolTests.swift @@ -4,8 +4,10 @@ import XCTest class PoolTests: XCTestCase { /// Returns a Pool whose elements are incremented integers: 1, 2, 3... private func makeCounterPool(maximumCount: Int) -> Pool { - let count = ReadWriteBox(wrappedValue: 0) - return Pool(maximumCount: maximumCount, makeElement: count.increment) + let countMutex = Mutex(0) + return Pool(maximumCount: maximumCount, makeElement: { _ in + countMutex.increment() + }) } func testElementsAreReused() throws { diff --git a/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift b/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift index e2e50cb240..a4248160fc 100644 --- a/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift +++ b/Tests/GRDBTests/QueryInterfaceExpressionsTests.swift @@ -1512,7 +1512,7 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { throw XCTSkip("FILTER clause on aggregate functions is not available") } #else - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("FILTER clause on aggregate functions is not available") } #endif @@ -1561,7 +1561,7 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { throw XCTSkip("FILTER clause on aggregate functions is not available") } #else - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("FILTER clause on aggregate functions is not available") } #endif @@ -1594,7 +1594,7 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { throw XCTSkip("FILTER clause on aggregate functions is not available") } #else - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("FILTER clause on aggregate functions is not available") } #endif @@ -1627,7 +1627,7 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { throw XCTSkip("FILTER clause on aggregate functions is not available") } #else - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("FILTER clause on aggregate functions is not available") } #endif @@ -1684,7 +1684,7 @@ class QueryInterfaceExpressionsTests: GRDBTestCase { throw XCTSkip("FILTER clause on aggregate functions is not available") } #else - guard #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) else { + guard #available(iOS 14, macOS 10.16, tvOS 14, *) else { throw XCTSkip("FILTER clause on aggregate functions is not available") } #endif diff --git a/Tests/GRDBTests/QueryInterfaceRequestTests.swift b/Tests/GRDBTests/QueryInterfaceRequestTests.swift index 91f0323549..0e075fb28b 100644 --- a/Tests/GRDBTests/QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/QueryInterfaceRequestTests.swift @@ -779,7 +779,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { sql(dbQueue, tableRequest.order(Col.age.descNullsFirst)), "SELECT * FROM \"readers\" ORDER BY \"age\" DESC NULLS FIRST") #elseif !GRDBCIPHER - if #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) { + if #available(iOS 14, macOS 10.16, tvOS 14, *) { XCTAssertEqual( sql(dbQueue, tableRequest.order(Col.age.ascNullsLast)), "SELECT * FROM \"readers\" ORDER BY \"age\" ASC NULLS LAST") @@ -809,7 +809,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { sql(dbQueue, tableRequest.order(Col.name.collating(.nocase).descNullsFirst)), "SELECT * FROM \"readers\" ORDER BY \"name\" COLLATE NOCASE DESC NULLS FIRST") #elseif !GRDBCIPHER - if #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) { + if #available(iOS 14, macOS 10.16, tvOS 14, *) { XCTAssertEqual( sql(dbQueue, tableRequest.order(Col.name.collating(.nocase).ascNullsLast)), "SELECT * FROM \"readers\" ORDER BY \"name\" COLLATE NOCASE ASC NULLS LAST") @@ -858,7 +858,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { sql(dbQueue, tableRequest.order(Col.age.ascNullsLast).reversed()), "SELECT * FROM \"readers\" ORDER BY \"age\" DESC NULLS FIRST") #elseif !GRDBCIPHER - if #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) { + if #available(iOS 14, macOS 10.16, tvOS 14, *) { XCTAssertEqual( sql(dbQueue, tableRequest.order(Col.age.descNullsFirst).reversed()), "SELECT * FROM \"readers\" ORDER BY \"age\" ASC NULLS LAST") @@ -888,7 +888,7 @@ class QueryInterfaceRequestTests: GRDBTestCase { sql(dbQueue, tableRequest.order(Col.name.collating(.nocase).descNullsFirst).reversed()), "SELECT * FROM \"readers\" ORDER BY \"name\" COLLATE NOCASE ASC NULLS LAST") #elseif !GRDBCIPHER - if #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) { + if #available(iOS 14, macOS 10.16, tvOS 14, *) { XCTAssertEqual( sql(dbQueue, tableRequest.order(Col.name.collating(.nocase).ascNullsLast).reversed()), "SELECT * FROM \"readers\" ORDER BY \"name\" COLLATE NOCASE DESC NULLS FIRST") diff --git a/Tests/GRDBTests/RecordEditedTests.swift b/Tests/GRDBTests/RecordEditedTests.swift index 31316f5c0f..64573f4c4d 100644 --- a/Tests/GRDBTests/RecordEditedTests.swift +++ b/Tests/GRDBTests/RecordEditedTests.swift @@ -15,7 +15,7 @@ private class Person : Record { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE persons ( id INTEGER PRIMARY KEY, @@ -131,7 +131,9 @@ class RecordEditedTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createPerson", migrate: Person.setup) + migrator.registerMigration("createPerson") { + try Person.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift index 1ce98c1829..11bd3e4288 100644 --- a/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalNonOptionalPrimaryKeySingleTests.swift @@ -13,7 +13,7 @@ private class MinimalNonOptionalPrimaryKeySingle: Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: "CREATE TABLE minimalSingles (id TEXT NOT NULL PRIMARY KEY)") } @@ -41,14 +41,15 @@ private class MinimalNonOptionalPrimaryKeySingle: Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension MinimalNonOptionalPrimaryKeySingle: Identifiable { } class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createMinimalNonOptionalPrimaryKeySingle", migrate: MinimalNonOptionalPrimaryKeySingle.setup) + migrator.registerMigration("createMinimalNonOptionalPrimaryKeySingle") { + try MinimalNonOptionalPrimaryKeySingle.setup($0) + } try migrator.migrate(dbWriter) } @@ -471,20 +472,18 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [String] = [] - let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id, record2.id] - let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [String] = [] + let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id, record2.id] + let cursor = try MinimalNonOptionalPrimaryKeySingle.fetchCursor(db, ids: ids) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -510,19 +509,17 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [String] = [] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id, record2.id] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - } + do { + let ids: [String] = [] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id, record2.id] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } } } @@ -548,19 +545,17 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [String] = [] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id, record2.id] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - } + do { + let ids: [String] = [] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id, record2.id] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } } } @@ -583,12 +578,10 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.fetchOne(db, id: record.id)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") - } + do { + let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.fetchOne(db, id: record.id)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } } } @@ -611,17 +604,15 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - _ = try MinimalNonOptionalPrimaryKeySingle.find(db, key: "missing") - XCTFail("Expected RecordError") - } catch RecordError.recordNotFound(databaseTableName: "minimalSingles", key: ["id": "missing".databaseValue]) { } - - do { - let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.find(db, id: record.id) - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") - } + do { + _ = try MinimalNonOptionalPrimaryKeySingle.find(db, key: "missing") + XCTFail("Expected RecordError") + } catch RecordError.recordNotFound(databaseTableName: "minimalSingles", key: ["id": "missing".databaseValue]) { } + + do { + let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.find(db, id: record.id) + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } } } @@ -651,20 +642,18 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [String] = [] - let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id, record2.id] - let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [String] = [] + let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id, record2.id] + let cursor = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchCursor(db) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -690,19 +679,17 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [String] = [] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id, record2.id] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - } + do { + let ids: [String] = [] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id, record2.id] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } } } @@ -728,19 +715,17 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [String] = [] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id, record2.id] - let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) - } + do { + let ids: [String] = [] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id, record2.id] + let fetchedRecords = try MinimalNonOptionalPrimaryKeySingle.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.id }), Set(ids)) } } } @@ -763,12 +748,10 @@ class RecordMinimalNonOptionalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.filter(id: record.id).fetchOne(db)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") - } + do { + let fetchedRecord = try MinimalNonOptionalPrimaryKeySingle.filter(id: record.id).fetchOne(db)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"id\" = '\(record.id)'") } } } diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift index 43e649f467..91f38ad018 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeyRowIDTests.swift @@ -12,7 +12,7 @@ class MinimalRowID : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: "CREATE TABLE minimalRowIDs (id INTEGER PRIMARY KEY)") } @@ -45,14 +45,15 @@ class MinimalRowID : Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension MinimalRowID: Identifiable { } class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createMinimalRowID", migrate: MinimalRowID.setup) + migrator.registerMigration("createMinimalRowID") { + try MinimalRowID.setup($0) + } try migrator.migrate(dbWriter) } @@ -505,20 +506,18 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let cursor = try MinimalRowID.fetchCursor(db, ids: ids) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id!, record2.id!] - let cursor = try MinimalRowID.fetchCursor(db, ids: ids) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [Int64] = [] + let cursor = try MinimalRowID.fetchCursor(db, ids: ids) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id!, record2.id!] + let cursor = try MinimalRowID.fetchCursor(db, ids: ids) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -544,19 +543,17 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try MinimalRowID.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -582,19 +579,17 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try MinimalRowID.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -617,15 +612,13 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let fetchedRecord = try MinimalRowID.fetchOne(db, id: record.id!)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") - } - do { - try XCTAssertNil(MinimalRowID.fetchOne(db, id: nil)) - } + do { + let fetchedRecord = try MinimalRowID.fetchOne(db, id: record.id!)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") + } + do { + try XCTAssertNil(MinimalRowID.fetchOne(db, id: nil)) } } } @@ -648,20 +641,18 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - _ = try MinimalRowID.find(db, id: -1) - XCTFail("Expected RecordError") - } catch RecordError.recordNotFound(databaseTableName: "minimalRowIDs", key: ["id": (-1).databaseValue]) { } - - do { - let fetchedRecord = try MinimalRowID.find(db, id: record.id!) - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") - } - do { - try XCTAssertNil(MinimalRowID.fetchOne(db, id: nil)) - } + do { + _ = try MinimalRowID.find(db, id: -1) + XCTFail("Expected RecordError") + } catch RecordError.recordNotFound(databaseTableName: "minimalRowIDs", key: ["id": (-1).databaseValue]) { } + + do { + let fetchedRecord = try MinimalRowID.find(db, id: record.id!) + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") + } + do { + try XCTAssertNil(MinimalRowID.fetchOne(db, id: nil)) } } } @@ -691,20 +682,18 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id!, record2.id!] - let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [Int64] = [] + let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id!, record2.id!] + let cursor = try MinimalRowID.filter(ids: ids).fetchCursor(db) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -730,19 +719,17 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -768,19 +755,17 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try MinimalRowID.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -803,15 +788,13 @@ class RecordMinimalPrimaryKeyRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let fetchedRecord = try MinimalRowID.filter(id: record.id!).fetchOne(db)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") - } - do { - try XCTAssertNil(MinimalRowID.filter(id: nil).fetchOne(db)) - } + do { + let fetchedRecord = try MinimalRowID.filter(id: record.id!).fetchOne(db)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalRowIDs\" WHERE \"id\" = \(record.id!)") + } + do { + try XCTAssertNil(MinimalRowID.filter(id: nil).fetchOne(db)) } } } diff --git a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift index 8c5c1f0369..cd2b838296 100644 --- a/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordMinimalPrimaryKeySingleTests.swift @@ -11,7 +11,7 @@ class MinimalSingle: Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: "CREATE TABLE minimalSingles (UUID TEXT NOT NULL PRIMARY KEY)") } @@ -39,7 +39,6 @@ class MinimalSingle: Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension MinimalSingle: Identifiable { /// Test non-optional ID type var id: String { UUID! } @@ -49,7 +48,9 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createMinimalSingle", migrate: MinimalSingle.setup) + migrator.registerMigration("createMinimalSingle") { + try MinimalSingle.setup($0) + } try migrator.migrate(dbWriter) } @@ -529,20 +530,18 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let UUIDs: [String] = [] - let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) - try XCTAssertNil(cursor.next()) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let UUIDs: [String] = [] + let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) + try XCTAssertNil(cursor.next()) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let cursor = try MinimalSingle.fetchCursor(db, ids: UUIDs) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -570,19 +569,17 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let UUIDs: [String] = [] - let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - } + do { + let UUIDs: [String] = [] + let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let fetchedRecords = try MinimalSingle.fetchAll(db, ids: UUIDs) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } } } @@ -610,19 +607,17 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let UUIDs: [String] = [] - let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - } + do { + let UUIDs: [String] = [] + let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let fetchedRecords = try MinimalSingle.fetchSet(db, ids: UUIDs) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } } } @@ -646,12 +641,10 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let fetchedRecord = try MinimalSingle.fetchOne(db, id: record.UUID!)! - XCTAssertTrue(fetchedRecord.UUID == record.UUID) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") - } + do { + let fetchedRecord = try MinimalSingle.fetchOne(db, id: record.UUID!)! + XCTAssertTrue(fetchedRecord.UUID == record.UUID) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } } } @@ -675,17 +668,15 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - _ = try MinimalSingle.find(db, id: "missing") - XCTFail("Expected RecordError") - } catch RecordError.recordNotFound(databaseTableName: "minimalSingles", key: ["UUID": "missing".databaseValue]) { } - - do { - let fetchedRecord = try MinimalSingle.find(db, id: record.UUID!) - XCTAssertTrue(fetchedRecord.UUID == record.UUID) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") - } + do { + _ = try MinimalSingle.find(db, id: "missing") + XCTFail("Expected RecordError") + } catch RecordError.recordNotFound(databaseTableName: "minimalSingles", key: ["UUID": "missing".databaseValue]) { } + + do { + let fetchedRecord = try MinimalSingle.find(db, id: record.UUID!) + XCTAssertTrue(fetchedRecord.UUID == record.UUID) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } } } @@ -717,20 +708,18 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let UUIDs: [String] = [] - let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) - try XCTAssertNil(cursor.next()) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let UUIDs: [String] = [] + let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) + try XCTAssertNil(cursor.next()) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let cursor = try MinimalSingle.filter(ids: UUIDs).fetchCursor(db) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -758,19 +747,17 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let UUIDs: [String] = [] - let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - } + do { + let UUIDs: [String] = [] + let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } } } @@ -798,19 +785,17 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let UUIDs: [String] = [] - let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let UUIDs = [record1.UUID!, record2.UUID!] - let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) - } + do { + let UUIDs: [String] = [] + let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let UUIDs = [record1.UUID!, record2.UUID!] + let fetchedRecords = try MinimalSingle.filter(ids: UUIDs).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map { $0.UUID! }), Set(UUIDs)) } } } @@ -834,12 +819,10 @@ class RecordMinimalPrimaryKeySingleTests: GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let fetchedRecord = try MinimalSingle.filter(id: record.UUID!).fetchOne(db)! - XCTAssertTrue(fetchedRecord.UUID == record.UUID) - XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") - } + do { + let fetchedRecord = try MinimalSingle.filter(id: record.UUID!).fetchOne(db)! + XCTAssertTrue(fetchedRecord.UUID == record.UUID) + XCTAssertEqual(lastSQLQuery, "SELECT * FROM \"minimalSingles\" WHERE \"UUID\" = '\(record.UUID!)'") } } } diff --git a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift index 6777722816..ca99bceb34 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyHiddenRowIDTests.swift @@ -16,7 +16,7 @@ private class Person : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE persons ( creationDate TEXT NOT NULL, @@ -77,14 +77,15 @@ private class Person : Record, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Person: Identifiable { } class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createPerson", migrate: Person.setup) + migrator.registerMigration("createPerson") { + try Person.setup($0) + } try migrator.migrate(dbWriter) } @@ -597,20 +598,18 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let cursor = try Person.fetchCursor(db, ids: ids) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id!, record2.id!] - let cursor = try Person.fetchCursor(db, ids: ids) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [Int64] = [] + let cursor = try Person.fetchCursor(db, ids: ids) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id!, record2.id!] + let cursor = try Person.fetchCursor(db, ids: ids) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -636,19 +635,17 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try Person.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try Person.fetchAll(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try Person.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try Person.fetchAll(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -674,19 +671,17 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try Person.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try Person.fetchSet(db, ids: ids) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try Person.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try Person.fetchSet(db, ids: ids) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -712,18 +707,16 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let fetchedRecord = try Person.fetchOne(db, id: record.id!)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertTrue(fetchedRecord.name == record.name) - XCTAssertTrue(fetchedRecord.age == record.age) - XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. - XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") - } - do { - try XCTAssertNil(Person.fetchOne(db, id: nil)) - } + do { + let fetchedRecord = try Person.fetchOne(db, id: record.id!)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertTrue(fetchedRecord.age == record.age) + XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") + } + do { + try XCTAssertNil(Person.fetchOne(db, id: nil)) } } } @@ -749,23 +742,21 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - _ = try Person.find(db, id: -1) - XCTFail("Expected RecordError") - } catch RecordError.recordNotFound(databaseTableName: "persons", key: ["rowid": (-1).databaseValue]) { } - - do { - let fetchedRecord = try Person.find(db, id: record.id!) - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertTrue(fetchedRecord.name == record.name) - XCTAssertTrue(fetchedRecord.age == record.age) - XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. - XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") - } - do { - try XCTAssertNil(Person.fetchOne(db, id: nil)) - } + do { + _ = try Person.find(db, id: -1) + XCTFail("Expected RecordError") + } catch RecordError.recordNotFound(databaseTableName: "persons", key: ["rowid": (-1).databaseValue]) { } + + do { + let fetchedRecord = try Person.find(db, id: record.id!) + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertTrue(fetchedRecord.age == record.age) + XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") + } + do { + try XCTAssertNil(Person.fetchOne(db, id: nil)) } } } @@ -795,20 +786,18 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertTrue(try cursor.next() == nil) // end } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let cursor = try Person.filter(ids: ids).fetchCursor(db) - try XCTAssertNil(cursor.next()) - } - - do { - let ids = [record1.id!, record2.id!] - let cursor = try Person.filter(ids: ids).fetchCursor(db) - let fetchedRecords = try [cursor.next()!, cursor.next()!] - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - XCTAssertTrue(try cursor.next() == nil) // end - } + do { + let ids: [Int64] = [] + let cursor = try Person.filter(ids: ids).fetchCursor(db) + try XCTAssertNil(cursor.next()) + } + + do { + let ids = [record1.id!, record2.id!] + let cursor = try Person.filter(ids: ids).fetchCursor(db) + let fetchedRecords = try [cursor.next()!, cursor.next()!] + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) + XCTAssertTrue(try cursor.next() == nil) // end } } } @@ -834,19 +823,17 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try Person.filter(ids: ids).fetchAll(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -872,19 +859,17 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let ids: [Int64] = [] - let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 0) - } - - do { - let ids = [record1.id!, record2.id!] - let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) - XCTAssertEqual(fetchedRecords.count, 2) - XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) - } + do { + let ids: [Int64] = [] + let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 0) + } + + do { + let ids = [record1.id!, record2.id!] + let fetchedRecords = try Person.filter(ids: ids).fetchSet(db) + XCTAssertEqual(fetchedRecords.count, 2) + XCTAssertEqual(Set(fetchedRecords.map(\.id)), Set(ids)) } } } @@ -910,18 +895,16 @@ class RecordPrimaryKeyHiddenRowIDTests : GRDBTestCase { XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - do { - let fetchedRecord = try Person.filter(id: record.id!).fetchOne(db)! - XCTAssertTrue(fetchedRecord.id == record.id) - XCTAssertTrue(fetchedRecord.name == record.name) - XCTAssertTrue(fetchedRecord.age == record.age) - XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. - XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") - } - do { - try XCTAssertNil(Person.filter(id: nil).fetchOne(db)) - } + do { + let fetchedRecord = try Person.filter(id: record.id!).fetchOne(db)! + XCTAssertTrue(fetchedRecord.id == record.id) + XCTAssertTrue(fetchedRecord.name == record.name) + XCTAssertTrue(fetchedRecord.age == record.age) + XCTAssertTrue(abs(fetchedRecord.creationDate.timeIntervalSince(record.creationDate)) < 1e-3) // ISO-8601 is precise to the millisecond. + XCTAssertEqual(lastSQLQuery, "SELECT *, \"rowid\" FROM \"persons\" WHERE \"rowid\" = \(record.id!)") + } + do { + try XCTAssertNil(Person.filter(id: nil).fetchOne(db)) } } } diff --git a/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift b/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift index f8b8c8e109..cf792c6865 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyMultipleTests.swift @@ -14,7 +14,7 @@ private class Citizenship : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE citizenships ( personName TEXT NOT NULL, @@ -61,7 +61,9 @@ class RecordPrimaryKeyMultipleTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createCitizenship", migrate: Citizenship.setup) + migrator.registerMigration("createCitizenship") { + try Citizenship.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift b/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift index 8582a5e488..a96bd496e1 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyNoneTests.swift @@ -14,7 +14,7 @@ private class Item : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE items ( name TEXT, @@ -58,7 +58,9 @@ class RecordPrimaryKeyNoneTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createItem", migrate: Item.setup) + migrator.registerMigration("createItem") { + try Item.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift b/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift index eb9023cf24..dabb5c9084 100644 --- a/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeyRowIDTests.swift @@ -16,7 +16,7 @@ private class Person : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE persons ( id INTEGER PRIMARY KEY, @@ -78,7 +78,9 @@ class RecordPrimaryKeyRowIDTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createPerson", migrate: Person.setup) + migrator.registerMigration("createPerson") { + try Person.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift b/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift index 69664f7295..ca70ff5b44 100644 --- a/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeySingleTests.swift @@ -12,7 +12,7 @@ class Pet : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE pets ( UUID TEXT NOT NULL PRIMARY KEY, @@ -51,7 +51,9 @@ class RecordPrimaryKeySingleTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createPet", migrate: Pet.setup) + migrator.registerMigration("createPet") { + try Pet.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift b/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift index e1444cdf31..8451feeab9 100644 --- a/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift +++ b/Tests/GRDBTests/RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift @@ -11,7 +11,7 @@ class Email : Record, Hashable { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE emails ( email TEXT NOT NULL PRIMARY KEY ON CONFLICT REPLACE, @@ -50,7 +50,9 @@ class RecordPrimaryKeySingleWithReplaceConflictResolutionTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createEmail", migrate: Email.setup) + migrator.registerMigration("createEmail") { + try Email.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordSubClassTests.swift b/Tests/GRDBTests/RecordSubClassTests.swift index fc3d8dc3fc..ce82183f51 100644 --- a/Tests/GRDBTests/RecordSubClassTests.swift +++ b/Tests/GRDBTests/RecordSubClassTests.swift @@ -15,7 +15,7 @@ private class Person : Record { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: """ CREATE TABLE persons ( id INTEGER PRIMARY KEY, @@ -105,7 +105,9 @@ class RecordSubClassTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createPerson", migrate: Person.setup) + migrator.registerMigration("createPerson") { + try Person.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/RecordWithColumnNameManglingTests.swift b/Tests/GRDBTests/RecordWithColumnNameManglingTests.swift index 0ce7021e02..e9dba83836 100644 --- a/Tests/GRDBTests/RecordWithColumnNameManglingTests.swift +++ b/Tests/GRDBTests/RecordWithColumnNameManglingTests.swift @@ -13,7 +13,7 @@ class BadlyMangledStuff : Record { super.init() } - static func setup(inDatabase db: Database) throws { + static func setup(_ db: Database) throws { try db.execute(sql: "CREATE TABLE stuffs (id INTEGER PRIMARY KEY, name TEXT)") } @@ -48,7 +48,9 @@ class RecordWithColumnNameManglingTests: GRDBTestCase { override func setup(_ dbWriter: some DatabaseWriter) throws { var migrator = DatabaseMigrator() - migrator.registerMigration("createBadlyMangledStuff", migrate: BadlyMangledStuff.setup) + migrator.registerMigration("createBadlyMangledStuff") { + try BadlyMangledStuff.setup($0) + } try migrator.migrate(dbWriter) } diff --git a/Tests/GRDBTests/SQLLiteralTests.swift b/Tests/GRDBTests/SQLLiteralTests.swift index e5ef6272b1..d49de427aa 100644 --- a/Tests/GRDBTests/SQLLiteralTests.swift +++ b/Tests/GRDBTests/SQLLiteralTests.swift @@ -309,7 +309,9 @@ extension SQLLiteralTests { try makeDatabaseQueue().inDatabase { db in struct Player: TableRecord { } struct AltPlayer: TableRecord { - static let databaseSelection: [any SQLSelectable] = [Column("id"), Column("name")] + static var databaseSelection: [any SQLSelectable] { + [Column("id"), Column("name")] + } } do { let query: SQL = """ diff --git a/Tests/GRDBTests/SelectStatementTests.swift b/Tests/GRDBTests/SelectStatementTests.swift index b3263d651f..95d8d39c49 100644 --- a/Tests/GRDBTests/SelectStatementTests.swift +++ b/Tests/GRDBTests/SelectStatementTests.swift @@ -139,20 +139,20 @@ class SelectStatementTests : GRDBTestCase { func testCachedSelectStatementStepFailure() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - var needsThrow = false + let needsThrowMutex = Mutex(false) db.add(function: DatabaseFunction("bomb", argumentCount: 0, pure: false) { _ in - if needsThrow { + if needsThrowMutex.load() { throw DatabaseError(message: "boom") } return "success" }) let sql = "SELECT bomb()" - needsThrow = false + needsThrowMutex.store(false) XCTAssertEqual(try String.fetchAll(db.cachedStatement(sql: sql)), ["success"]) do { - needsThrow = true + needsThrowMutex.store(true) _ = try String.fetchAll(db.cachedStatement(sql: sql)) XCTFail() } catch let error as DatabaseError { @@ -162,7 +162,7 @@ class SelectStatementTests : GRDBTestCase { XCTAssertEqual(error.description, "SQLite error 1: boom - while executing `\(sql)`") } - needsThrow = false + needsThrowMutex.store(false) XCTAssertEqual(try String.fetchAll(db.cachedStatement(sql: sql)), ["success"]) } } diff --git a/Tests/GRDBTests/SharedValueObservationTests.swift b/Tests/GRDBTests/SharedValueObservationTests.swift index 156960805c..4a8d629a56 100644 --- a/Tests/GRDBTests/SharedValueObservationTests.swift +++ b/Tests/GRDBTests/SharedValueObservationTests.swift @@ -39,12 +39,12 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated withExtendedLifetime(sharedObservation) { sharedObservation in do { - var value: Int? + let valueMutex: Mutex = Mutex(nil) let cancellable = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) + onChange: { value in valueMutex.store(value) }) - XCTAssertEqual(value, 0) + XCTAssertEqual(valueMutex.load(), 0) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) cancellable.cancel() @@ -52,12 +52,12 @@ class SharedValueObservationTests: GRDBTestCase { } do { - var value: Int? + let valueMutex: Mutex = Mutex(nil) let cancellable = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) + onChange: { value in valueMutex.store(value) }) - XCTAssertEqual(value, 0) + XCTAssertEqual(valueMutex.load(), 0) XCTAssertEqual(log.flush(), []) cancellable.cancel() @@ -91,21 +91,21 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated withExtendedLifetime(sharedObservation) { sharedObservation in do { - var value1: Int? - var value2: Int? + let value1Mutex: Mutex = Mutex(nil) + let value2Mutex: Mutex = Mutex(nil) let cancellable1 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, onChange: { value in - value1 = value + value1Mutex.store(value) _ = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, onChange: { value in - value2 = value + value2Mutex.store(value) }) }) - XCTAssertEqual(value1, 0) - XCTAssertEqual(value2, 0) + XCTAssertEqual(value1Mutex.load(), 0) + XCTAssertEqual(value2Mutex.load(), 0) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) cancellable1.cancel() @@ -120,10 +120,6 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_immediate_publisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -175,12 +171,12 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated withExtendedLifetime(sharedObservation) { sharedObservation in do { - var value: Int? + let valueMutex: Mutex = Mutex(nil) let cancellable = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) + onChange: { value in valueMutex.store(value) }) - XCTAssertEqual(value, 0) + XCTAssertEqual(valueMutex.load(), 0) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) cancellable.cancel() @@ -188,12 +184,12 @@ class SharedValueObservationTests: GRDBTestCase { } do { - var value: Int? + let valueMutex: Mutex = Mutex(nil) let cancellable = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { value = $0 }) + onChange: { value in valueMutex.store(value) }) - XCTAssertEqual(value, 0) + XCTAssertEqual(valueMutex.load(), 0) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) cancellable.cancel() @@ -226,21 +222,21 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated withExtendedLifetime(sharedObservation) { sharedObservation in - var value1: Int? - var value2: Int? + let value1Mutex: Mutex = Mutex(nil) + let value2Mutex: Mutex = Mutex(nil) let cancellable1 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, onChange: { value in - value1 = value + value1Mutex.store(value) _ = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, onChange: { value in - value2 = value + value2Mutex.store(value) }) }) - XCTAssertEqual(value1, 0) - XCTAssertEqual(value2, 0) + XCTAssertEqual(value1Mutex.load(), 0) + XCTAssertEqual(value2Mutex.load(), 0) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 0"]) cancellable1.cancel() @@ -273,40 +269,40 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated try withExtendedLifetime(sharedObservation) { sharedObservation in // --- Start observation 1 - var values1: [Int] = [] + let values1Mutex: Mutex<[Int]> = Mutex([]) let exp1 = expectation(description: "") exp1.expectedFulfillmentCount = 2 exp1.assertForOverFulfill = false let cancellable1 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values1.append($0) + onChange: { value in + values1Mutex.withLock { $0.append(value) } exp1.fulfill() }) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} wait(for: [exp1], timeout: 1) - XCTAssertEqual(values1, [0, 1]) + XCTAssertEqual(values1Mutex.load(), [0, 1]) XCTAssertEqual(log.flush(), [ "start", "fetch", "tracked region: player(*)", "value: 0", "database did change", "fetch", "value: 1"]) // --- Start observation 2 - var values2: [Int] = [] + let values2Mutex: Mutex<[Int]> = Mutex([]) let exp2 = expectation(description: "") exp2.expectedFulfillmentCount = 2 exp2.assertForOverFulfill = false let cancellable2 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values2.append($0) + onChange: { value in + values2Mutex.withLock { $0.append(value) } exp2.fulfill() }) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} wait(for: [exp2], timeout: 1) - XCTAssertEqual(values1, [0, 1, 2]) - XCTAssertEqual(values2, [1, 2]) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2]) XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) // --- Stop observation 1 @@ -314,22 +310,22 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), []) // --- Start observation 3 - var values3: [Int] = [] + let values3Mutex: Mutex<[Int]> = Mutex([]) let exp3 = expectation(description: "") exp3.expectedFulfillmentCount = 2 exp3.assertForOverFulfill = false let cancellable3 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values3.append($0) + onChange: { value in + values3Mutex.withLock { $0.append(value) } exp3.fulfill() }) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} wait(for: [exp3], timeout: 1) - XCTAssertEqual(values1, [0, 1, 2]) - XCTAssertEqual(values2, [1, 2, 3]) - XCTAssertEqual(values3, [2, 3]) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2, 3]) + XCTAssertEqual(values3Mutex.load(), [2, 3]) XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 3"]) // --- Stop observation 2 @@ -355,7 +351,7 @@ class SharedValueObservationTests: GRDBTestCase { } let log = Log() - var sharedObservation: SharedValueObservation? = ValueObservation + nonisolated(unsafe) var sharedObservation: SharedValueObservation? = ValueObservation .tracking(Table("player").fetchCount) .print(to: log) .shared( @@ -397,10 +393,6 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_async_publisher() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -452,40 +444,40 @@ class SharedValueObservationTests: GRDBTestCase { // We want to control when the shared observation is deallocated try withExtendedLifetime(sharedObservation) { sharedObservation in // --- Start observation 1 - var values1: [Int] = [] + let values1Mutex: Mutex<[Int]> = Mutex([]) let exp1 = expectation(description: "") exp1.expectedFulfillmentCount = 2 exp1.assertForOverFulfill = false let cancellable1 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values1.append($0) + onChange: { value in + values1Mutex.withLock { $0.append(value) } exp1.fulfill() }) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} wait(for: [exp1], timeout: 1) - XCTAssertEqual(values1, [0, 1]) + XCTAssertEqual(values1Mutex.load(), [0, 1]) XCTAssertEqual(log.flush(), [ "start", "fetch", "tracked region: player(*)", "value: 0", "database did change", "fetch", "value: 1"]) // --- Start observation 2 - var values2: [Int] = [] + let values2Mutex: Mutex<[Int]> = Mutex([]) let exp2 = expectation(description: "") exp2.expectedFulfillmentCount = 2 exp2.assertForOverFulfill = false let cancellable2 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values2.append($0) + onChange: { value in + values2Mutex.withLock { $0.append(value) } exp2.fulfill() }) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} wait(for: [exp2], timeout: 1) - XCTAssertEqual(values1, [0, 1, 2]) - XCTAssertEqual(values2, [1, 2]) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2]) XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) // --- Stop observation 1 @@ -493,22 +485,22 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), []) // --- Start observation 3 - var values3: [Int] = [] + let values3Mutex: Mutex<[Int]> = Mutex([]) let exp3 = expectation(description: "") exp3.expectedFulfillmentCount = 2 exp3.assertForOverFulfill = false let cancellable3 = sharedObservation!.start( onError: { XCTFail("Unexpected error \($0)") }, - onChange: { - values3.append($0) + onChange: { value in + values3Mutex.withLock { $0.append(value) } exp3.fulfill() }) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} wait(for: [exp3], timeout: 1) - XCTAssertEqual(values1, [0, 1, 2]) - XCTAssertEqual(values2, [1, 2, 3]) - XCTAssertEqual(values3, [2, 3]) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2, 3]) + XCTAssertEqual(values3Mutex.load(), [2, 3]) XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 3"]) // --- Stop observation 2 @@ -525,12 +517,228 @@ class SharedValueObservationTests: GRDBTestCase { XCTAssertEqual(log.flush(), []) } + func test_task_observationLifetime() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + } + } + + let log = Log() + var sharedObservation: SharedValueObservation? = ValueObservation + .tracking(Table("player").fetchCount) + .print(to: log) + .shared( + in: dbQueue, + scheduling: .task, + extent: .observationLifetime) + XCTAssertEqual(log.flush(), []) + + // We want to control when the shared observation is deallocated + try withExtendedLifetime(sharedObservation) { sharedObservation in + // --- Start observation 1 + let values1Mutex: Mutex<[Int]> = Mutex([]) + let exp1 = expectation(description: "") + exp1.expectedFulfillmentCount = 2 + exp1.assertForOverFulfill = false + let cancellable1 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values1Mutex.withLock { $0.append(value) } + exp1.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp1], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1]) + XCTAssertEqual(log.flush(), [ + "start", "fetch", "tracked region: player(*)", "value: 0", + "database did change", "fetch", "value: 1"]) + + // --- Start observation 2 + let values2Mutex: Mutex<[Int]> = Mutex([]) + let exp2 = expectation(description: "") + exp2.expectedFulfillmentCount = 2 + exp2.assertForOverFulfill = false + let cancellable2 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values2Mutex.withLock { $0.append(value) } + exp2.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp2], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2]) + XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) + + // --- Stop observation 1 + cancellable1.cancel() + XCTAssertEqual(log.flush(), []) + + // --- Start observation 3 + let values3Mutex: Mutex<[Int]> = Mutex([]) + let exp3 = expectation(description: "") + exp3.expectedFulfillmentCount = 2 + exp3.assertForOverFulfill = false + let cancellable3 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values3Mutex.withLock { $0.append(value) } + exp3.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp3], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2, 3]) + XCTAssertEqual(values3Mutex.load(), [2, 3]) + XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 3"]) + + // --- Stop observation 2 + cancellable2.cancel() + XCTAssertEqual(log.flush(), []) + + // --- Stop observation 3 + cancellable3.cancel() + XCTAssertEqual(log.flush(), []) + } + + // --- Release shared observation + sharedObservation = nil + XCTAssertEqual(log.flush(), ["cancel"]) + } + #if canImport(Combine) - func test_error_recovery_observationLifetime() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") + func test_task_publisher() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + } + } + + let publisher = ValueObservation + .tracking(Table("player").fetchCount) + .shared(in: dbQueue, scheduling: .task) + .publisher() + + do { + let recorder = publisher.record() + try XCTAssert(recorder.availableElements.get().isEmpty) + try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 0) + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) + } + + do { + let recorder = publisher.record() + try XCTAssert(recorder.availableElements.get().isEmpty) + try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 2) + } + } +#endif + + func test_task_whileObserved() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + } } + let log = Log() + var sharedObservation: SharedValueObservation? = ValueObservation + .tracking(Table("player").fetchCount) + .print(to: log) + .shared( + in: dbQueue, + scheduling: .task, + extent: .whileObserved) + XCTAssertEqual(log.flush(), []) + + // We want to control when the shared observation is deallocated + try withExtendedLifetime(sharedObservation) { sharedObservation in + // --- Start observation 1 + let values1Mutex: Mutex<[Int]> = Mutex([]) + let exp1 = expectation(description: "") + exp1.expectedFulfillmentCount = 2 + exp1.assertForOverFulfill = false + let cancellable1 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values1Mutex.withLock { $0.append(value) } + exp1.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp1], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1]) + XCTAssertEqual(log.flush(), [ + "start", "fetch", "tracked region: player(*)", "value: 0", + "database did change", "fetch", "value: 1"]) + + // --- Start observation 2 + let values2Mutex: Mutex<[Int]> = Mutex([]) + let exp2 = expectation(description: "") + exp2.expectedFulfillmentCount = 2 + exp2.assertForOverFulfill = false + let cancellable2 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values2Mutex.withLock { $0.append(value) } + exp2.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp2], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2]) + XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 2"]) + + // --- Stop observation 1 + cancellable1.cancel() + XCTAssertEqual(log.flush(), []) + + // --- Start observation 3 + let values3Mutex: Mutex<[Int]> = Mutex([]) + let exp3 = expectation(description: "") + exp3.expectedFulfillmentCount = 2 + exp3.assertForOverFulfill = false + let cancellable3 = sharedObservation!.start( + onError: { XCTFail("Unexpected error \($0)") }, + onChange: { value in + values3Mutex.withLock { $0.append(value) } + exp3.fulfill() + }) + + try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} + wait(for: [exp3], timeout: 1) + XCTAssertEqual(values1Mutex.load(), [0, 1, 2]) + XCTAssertEqual(values2Mutex.load(), [1, 2, 3]) + XCTAssertEqual(values3Mutex.load(), [2, 3]) + XCTAssertEqual(log.flush(), ["database did change", "fetch", "value: 3"]) + + // --- Stop observation 2 + cancellable2.cancel() + XCTAssertEqual(log.flush(), []) + + // --- Stop observation 3 + cancellable3.cancel() + XCTAssertEqual(log.flush(), ["cancel"]) + } + + // --- Release shared observation + sharedObservation = nil + XCTAssertEqual(log.flush(), []) + } + +#if canImport(Combine) + func test_error_recovery_observationLifetime() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -539,10 +747,12 @@ class SharedValueObservationTests: GRDBTestCase { } let log = Log() - var fetchError: Error? = nil + let fetchErrorMutex: Mutex = Mutex(nil) let publisher = ValueObservation .tracking { db -> Int in - if let error = fetchError { throw error } + try fetchErrorMutex.withLock { error in + if let error { throw error } + } return try Table("player").fetchCount(db) } .print(to: log) @@ -556,7 +766,7 @@ class SharedValueObservationTests: GRDBTestCase { try XCTAssertEqual(wait(for: recorder1.next(), timeout: 1), 0) try XCTAssertEqual(wait(for: recorder2.next(), timeout: 1), 0) - fetchError = TestError() + fetchErrorMutex.store(TestError()) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} if case .finished = try wait(for: recorder1.completion, timeout: 1) { XCTFail("Expected error") } @@ -573,7 +783,7 @@ class SharedValueObservationTests: GRDBTestCase { } do { - fetchError = nil + fetchErrorMutex.store(nil) let recorder = publisher.record() if case .finished = try wait(for: recorder.completion, timeout: 1) { XCTFail("Expected error") } XCTAssertEqual(log.flush(), []) @@ -583,10 +793,6 @@ class SharedValueObservationTests: GRDBTestCase { #if canImport(Combine) func test_error_recovery_whileObserved() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Combine is not available") - } - let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -595,10 +801,12 @@ class SharedValueObservationTests: GRDBTestCase { } let log = Log() - var fetchError: Error? = nil + let fetchErrorMutex: Mutex = Mutex(nil) let publisher = ValueObservation .tracking { db -> Int in - if let error = fetchError { throw error } + try fetchErrorMutex.withLock { error in + if let error { throw error } + } return try Table("player").fetchCount(db) } .print(to: log) @@ -612,7 +820,7 @@ class SharedValueObservationTests: GRDBTestCase { try XCTAssertEqual(wait(for: recorder1.next(), timeout: 1), 0) try XCTAssertEqual(wait(for: recorder2.next(), timeout: 1), 0) - fetchError = TestError() + fetchErrorMutex.store(TestError()) try dbQueue.write { try $0.execute(sql: "INSERT INTO player DEFAULT VALUES")} if case .finished = try wait(for: recorder1.completion, timeout: 1) { XCTFail("Expected error") } @@ -629,7 +837,7 @@ class SharedValueObservationTests: GRDBTestCase { } do { - fetchError = nil + fetchErrorMutex.store(nil) let recorder = publisher.record() try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) XCTAssertEqual(log.flush(), ["start", "fetch", "tracked region: player(*)", "value: 1"]) @@ -637,8 +845,7 @@ class SharedValueObservationTests: GRDBTestCase { } #endif - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - func testAsyncAwait() async throws { + func testAsyncAwait_mainQueue() async throws { let dbQueue = try makeDatabaseQueue() try await dbQueue.write { db in try db.create(table: "player") { t in @@ -656,6 +863,25 @@ class SharedValueObservationTests: GRDBTestCase { break } } + + func testAsyncAwait_task() async throws { + let dbQueue = try makeDatabaseQueue() + try await dbQueue.write { db in + try db.create(table: "player") { t in + t.autoIncrementedPrimaryKey("id") + } + } + + let values = ValueObservation + .tracking(Table("player").fetchCount) + .shared(in: dbQueue, scheduling: .task) + .values() + + for try await value in values { + XCTAssertEqual(value, 0) + break + } + } } private class Log: TextOutputStream { diff --git a/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift b/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift index ed94e9e699..cee0f6e954 100644 --- a/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift +++ b/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB diff --git a/Tests/GRDBTests/TableDefinitionTests.swift b/Tests/GRDBTests/TableDefinitionTests.swift index 0092b26d79..12b666ca1a 100644 --- a/Tests/GRDBTests/TableDefinitionTests.swift +++ b/Tests/GRDBTests/TableDefinitionTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB @@ -195,7 +204,7 @@ class TableDefinitionTests: GRDBTestCase { func testColumnIndexed() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "test") { t in t.column("a", .integer).indexed() t.column("b", .integer).indexed() @@ -210,7 +219,7 @@ class TableDefinitionTests: GRDBTestCase { func testColumnIndexedInheritsIfNotExistsFlag() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - sqlQueries.removeAll() + clearSQLQueries() try db.create(table: "test", options: [.ifNotExists]) { t in t.column("a", .integer).indexed() t.column("b", .integer).indexed() @@ -724,7 +733,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("a", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "test") { t in t.add(column: "b", .text) t.add(column: "c", .integer).notNull().defaults(to: 1) @@ -751,7 +760,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("a", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "hiddenRowIdTable") { t in t.add(column: "ref").references("hiddenRowIdTable") } @@ -766,7 +775,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("a", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "explicitPrimaryKey") { t in t.add(column: "ref").references("explicitPrimaryKey") } @@ -796,18 +805,13 @@ class TableDefinitionTests: GRDBTestCase { guard sqlite3_libversion_number() >= 3025000 else { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } - #if !GRDBCUSTOMSQLITE && !GRDBCIPHER - guard #available(iOS 13, tvOS 13, watchOS 6, *) else { - throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") - } - #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "test") { t in t.column("a", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "test") { t in t.rename(column: "a", to: "b") t.add(column: "c") @@ -824,11 +828,6 @@ class TableDefinitionTests: GRDBTestCase { guard sqlite3_libversion_number() >= 3025000 else { throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") } - #if !GRDBCUSTOMSQLITE && !GRDBCIPHER - guard #available(iOS 13, tvOS 13, watchOS 6, *) else { - throw XCTSkip("ALTER TABLE RENAME COLUMN is not available") - } - #endif let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in try db.create(table: "test") { t in @@ -861,7 +860,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("c", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "test") { t in t.add(column: "d", .integer).generatedAs(sql: "a*abs(b)", .virtual) t.add(column: "e", .text).generatedAs(sql: "substr(c,b,b+1)", .virtual) @@ -895,7 +894,7 @@ class TableDefinitionTests: GRDBTestCase { t.column("b", .text) } - sqlQueries.removeAll() + clearSQLQueries() try db.alter(table: "test") { t in t.drop(column: "b") } diff --git a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift index a141e18e91..e803c37335 100644 --- a/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift +++ b/Tests/GRDBTests/TableRecord+QueryInterfaceRequestTests.swift @@ -259,7 +259,7 @@ class TableRecordQueryInterfaceRequestTests: GRDBTestCase { sql(dbQueue, Reader.order(Col.age.descNullsFirst)), "SELECT * FROM \"readers\" ORDER BY \"age\" DESC NULLS FIRST") #elseif !GRDBCIPHER - if #available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) { + if #available(iOS 14, macOS 10.16, tvOS 14, *) { XCTAssertEqual( sql(dbQueue, Reader.order(Col.age.ascNullsLast)), "SELECT * FROM \"readers\" ORDER BY \"age\" ASC NULLS LAST") @@ -357,10 +357,6 @@ class TableRecordQueryInterfaceRequestTests: GRDBTestCase { } func testExistsIdentifiable() throws { - guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else { - throw XCTSkip("Identifiable is not available") - } - let dbQueue = try makeDatabaseQueue() try dbQueue.inTransaction { db in struct Player: TableRecord, Identifiable { @@ -386,7 +382,7 @@ class TableRecordQueryInterfaceRequestTests: GRDBTestCase { try XCTAssertFalse(Player.exists(db, id: 1)) XCTAssertEqual(lastSQLQuery, "SELECT EXISTS (SELECT * FROM \"player\" WHERE \"id\" = 1)") - sqlQueries.removeAll() + clearSQLQueries() try XCTAssertFalse(Player.exists(db, id: nil)) XCTAssertNil(lastSQLQuery) // Database not hit diff --git a/Tests/GRDBTests/TableRecordDeleteTests.swift b/Tests/GRDBTests/TableRecordDeleteTests.swift index e45252a555..758d314ed8 100644 --- a/Tests/GRDBTests/TableRecordDeleteTests.swift +++ b/Tests/GRDBTests/TableRecordDeleteTests.swift @@ -6,7 +6,6 @@ private struct Hacker : TableRecord { var id: Int64? // Optional } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Hacker: Identifiable { } private struct Person : Codable, PersistableRecord, FetchableRecord, Hashable { @@ -16,7 +15,6 @@ private struct Person : Codable, PersistableRecord, FetchableRecord, Hashable { var email: String } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Person: Identifiable { } private struct Citizenship : TableRecord { @@ -46,23 +44,23 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertTrue(deleted) XCTAssertEqual(try Hacker.fetchCount(db), 0) - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [1, "Arthur"]) + try XCTAssertFalse(Hacker.deleteOne(db, id: nil)) + deleted = try Hacker.deleteOne(db, id: 1) + XCTAssertTrue(deleted) + XCTAssertEqual(try Hacker.fetchCount(db), 0) + + do { try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [1, "Arthur"]) - try XCTAssertFalse(Hacker.deleteOne(db, id: nil)) - deleted = try Hacker.deleteOne(db, id: 1) - XCTAssertTrue(deleted) - XCTAssertEqual(try Hacker.fetchCount(db), 0) + try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [2, "Barbara"]) + try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [3, "Craig"]) + let deletedCount = try Hacker.deleteAll(db, keys: [2, 3, 4]) + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"hackers\" WHERE \"rowid\" IN (2, 3, 4)") + XCTAssertEqual(deletedCount, 2) + XCTAssertEqual(try Hacker.fetchCount(db), 1) } - try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [1, "Arthur"]) - try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [2, "Barbara"]) - try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [3, "Craig"]) - let deletedCount = try Hacker.deleteAll(db, keys: [2, 3, 4]) - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"hackers\" WHERE \"rowid\" IN (2, 3, 4)") - XCTAssertEqual(deletedCount, 2) - XCTAssertEqual(try Hacker.fetchCount(db), 1) - - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + do { try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [2, "Barbara"]) try db.execute(sql: "INSERT INTO hackers (rowid, name) VALUES (?, ?)", arguments: [3, "Craig"]) let deletedCount = try Hacker.deleteAll(db, ids: [2, 3, 4]) @@ -85,22 +83,22 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertTrue(deleted) XCTAssertEqual(try Person.fetchCount(db), 0) - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [1, "Arthur", "arthur@example.com"]) + deleted = try Person.deleteOne(db, id: 1) + XCTAssertTrue(deleted) + XCTAssertEqual(try Person.fetchCount(db), 0) + + do { try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [1, "Arthur", "arthur@example.com"]) - deleted = try Person.deleteOne(db, id: 1) - XCTAssertTrue(deleted) - XCTAssertEqual(try Person.fetchCount(db), 0) + try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [2, "Barbara", "barbara@example.com"]) + try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [3, "Craig", "craig@example.com"]) + let deletedCount = try Person.deleteAll(db, keys: [2, 3, 4]) + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (2, 3, 4)") + XCTAssertEqual(deletedCount, 2) + XCTAssertEqual(try Person.fetchCount(db), 1) } - try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [1, "Arthur", "arthur@example.com"]) - try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [2, "Barbara", "barbara@example.com"]) - try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [3, "Craig", "craig@example.com"]) - let deletedCount = try Person.deleteAll(db, keys: [2, 3, 4]) - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (2, 3, 4)") - XCTAssertEqual(deletedCount, 2) - XCTAssertEqual(try Person.fetchCount(db), 1) - - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + do { try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [2, "Barbara", "barbara@example.com"]) try db.execute(sql: "INSERT INTO persons (id, name, email) VALUES (?, ?, ?)", arguments: [3, "Craig", "craig@example.com"]) let deletedCount = try Person.deleteAll(db, ids: [2, 3, 4]) @@ -189,15 +187,13 @@ class TableRecordDeleteTests: GRDBTestCase { try Person.filter(keys: [1, 2]).deleteAll(db) XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2)") - - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - try Person.filter(id: 1).deleteAll(db) - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1") - - try Person.filter(ids: [1, 2]).deleteAll(db) - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2)") - } - + + try Person.filter(id: 1).deleteAll(db) + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1") + + try Person.filter(ids: [1, 2]).deleteAll(db) + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2)") + try Person.filter(sql: "id = 1").deleteAll(db) XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE id = 1") @@ -279,13 +275,11 @@ class TableRecordDeleteTests: GRDBTestCase { XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2) RETURNING *") #if GRDBCUSTOMSQLITE || GRDBCIPHER - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - _ = try Person.filter(id: 1).deleteAndFetchCursor(db).next() - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1 RETURNING *") - - _ = try Person.filter(ids: [1, 2]).deleteAndFetchCursor(db).next() - XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2) RETURNING *") - } + _ = try Person.filter(id: 1).deleteAndFetchCursor(db).next() + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1 RETURNING *") + + _ = try Person.filter(ids: [1, 2]).deleteAndFetchCursor(db).next() + XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" IN (1, 2) RETURNING *") #else _ = try Person.filter(id: 1).deleteAndFetchCursor(db).next() XCTAssertEqual(self.lastSQLQuery, "DELETE FROM \"persons\" WHERE \"id\" = 1 RETURNING *") @@ -364,7 +358,6 @@ class TableRecordDeleteTests: GRDBTestCase { } } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) // Identifiable func testRequestDeleteAndFetchIds() throws { #if GRDBCUSTOMSQLITE || GRDBCIPHER guard sqlite3_libversion_number() >= 3035000 else { @@ -468,7 +461,7 @@ class TableRecordDeleteTests: GRDBTestCase { struct Team: MutablePersistableRecord, FetchableRecord { // Test RETURNING - static let databaseSelection: [any SQLSelectable] = [Column("id"), Column("name")] + static var databaseSelection: [any SQLSelectable] { [Column("id"), Column("name")] } static let players = hasMany(Player.self) func encode(to container: inout PersistenceContainer) { preconditionFailure("should not be called") } init(row: Row) { preconditionFailure("should not be called") } diff --git a/Tests/GRDBTests/TableRecordTests.swift b/Tests/GRDBTests/TableRecordTests.swift index f59fcd1c5e..e1c6536352 100644 --- a/Tests/GRDBTests/TableRecordTests.swift +++ b/Tests/GRDBTests/TableRecordTests.swift @@ -103,7 +103,7 @@ class TableRecordTests: GRDBTestCase { func testExtendedDatabaseSelection() throws { struct Record: TableRecord { static let databaseTableName = "t1" - static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] + static var databaseSelection: [any SQLSelectable] { [AllColumns(), Column.rowID] } } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in @@ -116,7 +116,9 @@ class TableRecordTests: GRDBTestCase { func testRestrictedDatabaseSelection() throws { struct Record: TableRecord { static let databaseTableName = "t1" - static let databaseSelection: [any SQLSelectable] = [Column("a"), Column("b")] + static var databaseSelection: [any SQLSelectable] { + [Column("a"), Column("b")] + } } let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in diff --git a/Tests/GRDBTests/TableRecordUpdateTests.swift b/Tests/GRDBTests/TableRecordUpdateTests.swift index b5de3fab7a..44982695a0 100644 --- a/Tests/GRDBTests/TableRecordUpdateTests.swift +++ b/Tests/GRDBTests/TableRecordUpdateTests.swift @@ -17,7 +17,6 @@ private struct Player: Codable, PersistableRecord, FetchableRecord, Hashable { } } -@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Player: Identifiable { } private enum Columns: String, ColumnExpression { @@ -56,18 +55,16 @@ class TableRecordUpdateTests: GRDBTestCase { UPDATE "player" SET "score" = 0 WHERE "id" IN (1, 2) """) - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { - try Player.filter(id: 1).updateAll(db, assignment) - XCTAssertEqual(self.lastSQLQuery, """ + try Player.filter(id: 1).updateAll(db, assignment) + XCTAssertEqual(self.lastSQLQuery, """ UPDATE "player" SET "score" = 0 WHERE "id" = 1 """) - - try Player.filter(ids: [1, 2]).updateAll(db, assignment) - XCTAssertEqual(self.lastSQLQuery, """ + + try Player.filter(ids: [1, 2]).updateAll(db, assignment) + XCTAssertEqual(self.lastSQLQuery, """ UPDATE "player" SET "score" = 0 WHERE "id" IN (1, 2) """) - } - + try Player.filter(sql: "id = 1").updateAll(db, assignment) XCTAssertEqual(self.lastSQLQuery, """ UPDATE "player" SET "score" = 0 WHERE id = 1 @@ -480,7 +477,7 @@ class TableRecordUpdateTests: GRDBTestCase { func testUpdateAllWithoutAssignmentDoesNotAccessTheDatabase() throws { try makeDatabaseQueue().write { db in try Player.createTable(db) - sqlQueries.removeAll() + clearSQLQueries() try XCTAssertEqual(Player.updateAll(db, []), 0) try XCTAssertEqual(Player.all().updateAll(db, []), 0) XCTAssert(sqlQueries.isEmpty) diff --git a/Tests/GRDBTests/TableTests.swift b/Tests/GRDBTests/TableTests.swift index 46c8f5a192..780fab6eed 100644 --- a/Tests/GRDBTests/TableTests.swift +++ b/Tests/GRDBTests/TableTests.swift @@ -117,7 +117,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + do { struct Player: Identifiable { var id: Int64 } let t = Table("player") @@ -129,7 +129,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + do { struct Player: Identifiable { var id: Int64? } let t = Table("player") @@ -806,7 +806,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + do { // Non-optional ID struct Country: Identifiable { var id: String } @@ -821,7 +821,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + do { // Optional ID struct Country: Identifiable { var id: String? } @@ -830,7 +830,7 @@ class TableTests: GRDBTestCase { DELETE FROM "country" WHERE "code" = 'FR' """) - sqlQueries.removeAll() + clearSQLQueries() try Table("country").deleteOne(db, id: nil) XCTAssertNil(lastSQLQuery) // Database not hit @@ -920,7 +920,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + do { // Non-optional ID struct Country: Identifiable { var id: String } @@ -930,7 +930,7 @@ class TableTests: GRDBTestCase { """) } - if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { + do { // Optional ID struct Country: Identifiable { var id: String? } @@ -939,7 +939,7 @@ class TableTests: GRDBTestCase { SELECT EXISTS (SELECT * FROM "country" WHERE "code" = 'FR') """) - sqlQueries.removeAll() + clearSQLQueries() try XCTAssertFalse(Table("country").exists(db, id: nil)) XCTAssertNil(lastSQLQuery) // Database not hit } diff --git a/Tests/GRDBTests/TransactionDateTests.swift b/Tests/GRDBTests/TransactionDateTests.swift index 5f2707223e..59bec6641f 100644 --- a/Tests/GRDBTests/TransactionDateTests.swift +++ b/Tests/GRDBTests/TransactionDateTests.swift @@ -8,9 +8,9 @@ class TransactionDateTests: GRDBTestCase { Date(), Date.distantFuture, ] - var dateIterator = dates.makeIterator() + let dateIteratorMutex = Mutex(dates.makeIterator()) dbConfiguration.transactionClock = .custom { _ in - dateIterator.next()! + dateIteratorMutex.withLock { $0.next()! } } var collectedDates: [Date] = [] @@ -28,9 +28,9 @@ class TransactionDateTests: GRDBTestCase { Date(), Date.distantFuture, ] - var dateIterator = dates.makeIterator() + let dateIteratorMutex = Mutex(dates.makeIterator()) dbConfiguration.transactionClock = .custom { _ in - dateIterator.next()! + dateIteratorMutex.withLock { $0.next()! } } var collectedDates: [Date] = [] @@ -51,9 +51,9 @@ class TransactionDateTests: GRDBTestCase { Date(), Date.distantFuture, ] - var dateIterator = dates.makeIterator() + let dateIteratorMutex = Mutex(dates.makeIterator()) dbConfiguration.transactionClock = .custom { _ in - dateIterator.next()! + dateIteratorMutex.withLock { $0.next()! } } var collectedDates: [Date] = [] @@ -74,9 +74,9 @@ class TransactionDateTests: GRDBTestCase { Date(), Date.distantFuture, ] - var dateIterator = dates.makeIterator() + let dateIteratorMutex = Mutex(dates.makeIterator()) dbConfiguration.transactionClock = .custom { _ in - dateIterator.next()! + dateIteratorMutex.withLock { $0.next()! } } var collectedDates: [Date] = [] @@ -107,8 +107,7 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + dbConfiguration.transactionClock = .custom { _ in .distantPast } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -119,7 +118,6 @@ class TransactionDateTests: GRDBTestCase { } } - currentDate = Date.distantPast try dbQueue.write { db in do { var player = Player(name: "Arthur") @@ -152,8 +150,8 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + let currentDate = Mutex(Date.distantPast) + dbConfiguration.transactionClock = .custom { _ in currentDate.load() } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -164,14 +162,13 @@ class TransactionDateTests: GRDBTestCase { } } - currentDate = Date.distantPast try dbQueue.write { db in var player = Player(name: "Arthur") try player.insert(db) } let newTransactionDate = Date() - currentDate = newTransactionDate + currentDate.store(newTransactionDate) try dbQueue.write { db in var player = try Player.find(db, key: 1) @@ -198,8 +195,8 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + let currentDate = Mutex(Date.distantPast) + dbConfiguration.transactionClock = .custom { _ in currentDate.load() } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -210,14 +207,13 @@ class TransactionDateTests: GRDBTestCase { } } - currentDate = Date.distantPast try dbQueue.write { db in var player = Player(name: "Arthur") try player.insert(db) } let newTransactionDate = Date() - currentDate = newTransactionDate + currentDate.store(newTransactionDate) try dbQueue.write { db in var player = try Player.find(db, key: 1) @@ -251,8 +247,8 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + let currentDate = Mutex(Date.distantPast) + dbConfiguration.transactionClock = .custom { _ in currentDate.load() } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -267,7 +263,7 @@ class TransactionDateTests: GRDBTestCase { } let newTransactionDate = Date() - currentDate = newTransactionDate + currentDate.store(newTransactionDate) try dbQueue.write { db in var player = try Player.find(db, key: 1) try player.touch(db) @@ -303,8 +299,7 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + dbConfiguration.transactionClock = .custom { _ in .distantPast } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -315,7 +310,6 @@ class TransactionDateTests: GRDBTestCase { } } - currentDate = Date.distantPast try dbQueue.write { db in var player = Player(name: "Arthur", isInserted: false) try player.insert(db) @@ -354,8 +348,7 @@ class TransactionDateTests: GRDBTestCase { } } - var currentDate = Date.distantPast - dbConfiguration.transactionClock = .custom { _ in currentDate } + dbConfiguration.transactionClock = .custom { _ in .distantPast } let dbQueue = try makeDatabaseQueue() try dbQueue.write { db in try db.create(table: "player") { t in @@ -366,7 +359,6 @@ class TransactionDateTests: GRDBTestCase { } } - currentDate = Date.distantPast try dbQueue.write { db in let player = Player(name: "Arthur") try player.insert(db) diff --git a/Tests/GRDBTests/TransactionObserverTests.swift b/Tests/GRDBTests/TransactionObserverTests.swift index 0730f42f29..cedbb23020 100644 --- a/Tests/GRDBTests/TransactionObserverTests.swift +++ b/Tests/GRDBTests/TransactionObserverTests.swift @@ -1384,7 +1384,7 @@ class TransactionObserverTests: GRDBTestCase { dbQueue.add(transactionObserver: observer) try dbQueue.writeWithoutTransaction { db in - try MinimalRowID.setup(inDatabase: db) + try MinimalRowID.setup(db) let record = MinimalRowID() try record.save(db) diff --git a/Tests/GRDBTests/UpdateStatementTests.swift b/Tests/GRDBTests/UpdateStatementTests.swift index faf20f6763..6a3233fdc8 100644 --- a/Tests/GRDBTests/UpdateStatementTests.swift +++ b/Tests/GRDBTests/UpdateStatementTests.swift @@ -1,3 +1,12 @@ +// Import C SQLite functions +#if SWIFT_PACKAGE +import GRDBSQLite +#elseif GRDBCIPHER +import SQLCipher +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER +import SQLite3 +#endif + import XCTest import GRDB @@ -169,17 +178,16 @@ class UpdateStatementTests : GRDBTestCase { func testUpdateStatementAcceptsSelectQueriesAndConsumeAllRows() throws { let dbQueue = try makeDatabaseQueue() - var index = 0 + let indexMutex = Mutex(0) try dbQueue.inDatabase { db in db.add(function: DatabaseFunction("seq", argumentCount: 0, pure: false) { _ in - defer { index += 1 } - return index + indexMutex.increment() }) try db.execute(sql: "SELECT seq() UNION ALL SELECT seq() UNION ALL SELECT seq()") let statement = try db.makeStatement(sql: "SELECT seq() UNION ALL SELECT seq() UNION ALL SELECT seq()") try statement.execute() } - XCTAssertEqual(index, 3 + 3) + XCTAssertEqual(indexMutex.load(), 3 + 3) } func testExecuteNothing() throws { diff --git a/Tests/GRDBTests/ValueObservationPrintTests.swift b/Tests/GRDBTests/ValueObservationPrintTests.swift index 51f57f5b6f..9ed1a44333 100644 --- a/Tests/GRDBTests/ValueObservationPrintTests.swift +++ b/Tests/GRDBTests/ValueObservationPrintTests.swift @@ -4,9 +4,10 @@ import Dispatch class ValueObservationPrintTests: GRDBTestCase { class TestStream: TextOutputStream { - @LockedBox var strings: [String] = [] + private var stringsMutex: Mutex<[String]> = Mutex([]) + var strings: [String] { stringsMutex.load() } func write(_ string: String) { - strings.append(string) + stringsMutex.withLock { $0.append(string) } } } @@ -54,7 +55,7 @@ class ValueObservationPrintTests: GRDBTestCase { try test(makeDatabaseQueue(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshotPool()) #endif } @@ -91,7 +92,7 @@ class ValueObservationPrintTests: GRDBTestCase { try test(makeDatabaseQueue(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshotPool()) #endif } @@ -129,7 +130,7 @@ class ValueObservationPrintTests: GRDBTestCase { try test(makeDatabaseQueue(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshotPool()) #endif } @@ -167,7 +168,7 @@ class ValueObservationPrintTests: GRDBTestCase { try test(makeDatabaseQueue(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config)) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshot()) -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) try test(makeDatabasePool(filename: "test", configuration: config).makeSnapshotPool()) #endif } @@ -410,11 +411,15 @@ class ValueObservationPrintTests: GRDBTestCase { // Force DatabasePool to perform two initial fetches, because between // its first read access, and its write access that installs the // transaction observer, some write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation .trackingConstantRegion { db -> Int? in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false try dbPool.write { db in try db.execute(sql: """ INSERT INTO player DEFAULT VALUES; @@ -461,11 +466,15 @@ class ValueObservationPrintTests: GRDBTestCase { // Force DatabasePool to perform two initial fetches, because between // its first read access, and its write access that installs the // transaction observer, some write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation .trackingConstantRegion { db -> Int? in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false try dbPool.write { db in try db.execute(sql: """ INSERT INTO player DEFAULT VALUES; diff --git a/Tests/GRDBTests/ValueObservationRecorder.swift b/Tests/GRDBTests/ValueObservationRecorder.swift index fdc799f5a6..8a895c5645 100644 --- a/Tests/GRDBTests/ValueObservationRecorder.swift +++ b/Tests/GRDBTests/ValueObservationRecorder.swift @@ -576,9 +576,10 @@ extension GRDBTestCase { func test( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, + description: String, testErrorDispatching: @escaping () -> Void) throws { - func test(writer: some DatabaseWriter) throws { + func test(writer: some DatabaseWriter, description: String) throws { try writer.write(setup) let recorder = observation.record( @@ -586,7 +587,7 @@ extension GRDBTestCase { scheduling: scheduler, onError: { _ in testErrorDispatching() }) - let (_, error) = try wait(for: recorder.failure(), timeout: 5) + let (_, error) = try wait(for: recorder.failure(), timeout: 5, description: description) if let error = error as? Failure { try testFailure(error, writer) } else { @@ -594,9 +595,9 @@ extension GRDBTestCase { } } - try test(writer: DatabaseQueue()) - try test(writer: makeDatabaseQueue()) - try test(writer: makeDatabasePool()) + try test(writer: DatabaseQueue(), description: description + " (in-memory DatabaseQueue)") + try test(writer: makeDatabaseQueue(), description: description + " (on-disk DatabaseQueue)") + try test(writer: makeDatabasePool(), description: description + " (DatabasePool)") } do { @@ -606,6 +607,7 @@ extension GRDBTestCase { try test( observation: observation, scheduling: .immediate, + description: "Immediate scheduling", testErrorDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) }) } @@ -616,6 +618,7 @@ extension GRDBTestCase { try test( observation: observation, scheduling: .async(onQueue: .main), + description: "Async on main queue scheduling", testErrorDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) }) } @@ -627,6 +630,7 @@ extension GRDBTestCase { try test( observation: observation, scheduling: .async(onQueue: queue), + description: "Async on custom queue scheduling", testErrorDispatching: { XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) }) } } diff --git a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift index 90bd8beb8b..390df4af0a 100644 --- a/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift +++ b/Tests/GRDBTests/ValueObservationRegionRecordingTests.swift @@ -130,24 +130,26 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { """) } - var results: [Int] = [] + let resultsMutex: Mutex<[Int]> = Mutex([]) let notificationExpectation = expectation(description: "notification") notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var regions: [DatabaseRegion] = [] + let regionsMutex: Mutex<[DatabaseRegion]> = Mutex([]) let observation = ValueObservation .tracking { db -> Int in let table = try String.fetchOne(db, sql: "SELECT name FROM source")! return try Int.fetchOne(db, sql: "SELECT IFNULL(SUM(value), 0) FROM \(table)")! } - .handleEvents(willTrackRegion: { regions.append($0) }) + .handleEvents(willTrackRegion: { region in + regionsMutex.withLock { $0.append(region) } + }) let observer = observation.start( in: dbQueue, onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - results.append(count) + resultsMutex.withLock { $0.append(count) } notificationExpectation.fulfill() }) @@ -162,9 +164,9 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results, [0, 1, 2, 3]) + XCTAssertEqual(resultsMutex.load(), [0, 1, 2, 3]) - XCTAssertEqual(regions.map(\.description), [ + XCTAssertEqual(regionsMutex.load().map(\.description), [ "a(value),source(name)", "b(value),source(name)"]) } @@ -181,25 +183,27 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { """) } - var results: [Int] = [] + let resultsMutex: Mutex<[Int]> = Mutex([]) let notificationExpectation = expectation(description: "notification") notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 4 - var regions: [DatabaseRegion] = [] + let regionsMutex: Mutex<[DatabaseRegion]> = Mutex([]) let observation = ValueObservation .tracking { db -> Int in let table = try String.fetchOne(db, sql: "SELECT name FROM source")! return try Int.fetchOne(db, sql: "SELECT IFNULL(SUM(value), 0) FROM \(table)")! } - .handleEvents(willTrackRegion: { regions.append($0) }) + .handleEvents(willTrackRegion: { region in + regionsMutex.withLock { $0.append(region) } + }) let observer = observation.start( in: dbQueue, scheduling: .async(onQueue: .main), onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - results.append(count) + resultsMutex.withLock { $0.append(count) } notificationExpectation.fulfill() }) @@ -214,9 +218,9 @@ class ValueObservationRegionRecordingTests: GRDBTestCase { } waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(results, [0, 1, 2, 3]) + XCTAssertEqual(resultsMutex.load(), [0, 1, 2, 3]) - XCTAssertEqual(regions.map(\.description), [ + XCTAssertEqual(regionsMutex.load().map(\.description), [ "a(value),source(name)", "b(value),source(name)"]) } diff --git a/Tests/GRDBTests/ValueObservationTests.swift b/Tests/GRDBTests/ValueObservationTests.swift index 7beeb4d6f5..ba59108d1e 100644 --- a/Tests/GRDBTests/ValueObservationTests.swift +++ b/Tests/GRDBTests/ValueObservationTests.swift @@ -21,10 +21,9 @@ class ValueObservationTests: GRDBTestCase { // Test passes if it compiles. // See - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testValuesFromAnyDatabaseWriter(writer: any DatabaseWriter) { func observe( - fetch: @Sendable @escaping (Database) throws -> T + fetch: @escaping @Sendable (Database) throws -> T ) throws -> AsyncValueObservation { ValueObservation.tracking(fetch).values(in: writer) } @@ -38,13 +37,15 @@ class ValueObservationTests: GRDBTestCase { let observation = ValueObservation.trackingConstantRegion { _ in throw TestError() } // Start observation - var error: TestError? + let errorMutex: Mutex = Mutex(nil) _ = observation.start( in: dbWriter, scheduling: .immediate, - onError: { error = $0 as? TestError }, + onError: { error in + errorMutex.store(error as? TestError) + }, onChange: { _ in }) - XCTAssertNotNil(error) + XCTAssertNotNil(errorMutex.load()) } try test(makeDatabaseQueue()) @@ -64,25 +65,25 @@ class ValueObservationTests: GRDBTestCase { notificationExpectation.expectedFulfillmentCount = 4 notificationExpectation.isInverted = true - var nextError: Error? = nil // If not null, observation throws an error + let nextErrorMutex: Mutex = Mutex(nil) // If not null, observation throws an error let observation = ValueObservation.trackingConstantRegion { _ = try Int.fetchOne($0, sql: "SELECT COUNT(*) FROM t") - if let error = nextError { - throw error + try nextErrorMutex.withLock { error in + if let error { throw error } } } // Start observation - var errorCaught = false + let errorCaughtMutex = Mutex(false) let cancellable = observation.start( in: dbWriter, onError: { _ in - errorCaught = true + errorCaughtMutex.store(true) notificationExpectation.fulfill() }, onChange: { - XCTAssertFalse(errorCaught) - nextError = TestError() + XCTAssertFalse(errorCaughtMutex.load()) + nextErrorMutex.store(TestError()) notificationExpectation.fulfill() // Trigger another change try! dbWriter.writeWithoutTransaction { db in @@ -92,7 +93,7 @@ class ValueObservationTests: GRDBTestCase { withExtendedLifetime(cancellable) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertTrue(errorCaught) + XCTAssertTrue(errorCaughtMutex.load()) } } @@ -119,12 +120,12 @@ class ValueObservationTests: GRDBTestCase { // Test that view v is not included in the observed region. // This optimization helps observation of views that feed from a // single table. - var region: DatabaseRegion? + let regionMutex: Mutex = Mutex(nil) let expectation = self.expectation(description: "") let observation = ValueObservation - .trackingConstantRegion(request.fetchAll) - .handleEvents(willTrackRegion: { - region = $0 + .trackingConstantRegion { _ = try request.fetchAll($0) } + .handleEvents(willTrackRegion: { region in + regionMutex.store(region) expectation.fulfill() }) let observer = observation.start( @@ -133,7 +134,7 @@ class ValueObservationTests: GRDBTestCase { onChange: { _ in }) withExtendedLifetime(observer) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(region!.description, "t(id,name)") // view is NOT tracked + XCTAssertEqual(regionMutex.load()!.description, "t(id,name)") // view is NOT tracked } } @@ -152,12 +153,12 @@ class ValueObservationTests: GRDBTestCase { // Test that no pragma table is included in the observed region. // This optimization helps observation that feed from a single table. - var region: DatabaseRegion? + let regionMutex: Mutex = Mutex(nil) let expectation = self.expectation(description: "") let observation = ValueObservation - .trackingConstantRegion(request.fetchAll) - .handleEvents(willTrackRegion: { - region = $0 + .trackingConstantRegion{ _ = try request.fetchAll($0) } + .handleEvents(willTrackRegion: { region in + regionMutex.store(region) expectation.fulfill() }) let observer = observation.start( @@ -166,7 +167,7 @@ class ValueObservationTests: GRDBTestCase { onChange: { _ in }) withExtendedLifetime(observer) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(region!.description, "t(id,name)[1]") // pragma_table_xinfo is NOT tracked + XCTAssertEqual(regionMutex.load()?.description, "t(id,name)[1]") // pragma_table_xinfo is NOT tracked } } @@ -174,9 +175,10 @@ class ValueObservationTests: GRDBTestCase { func testTrackingExplicitRegion() throws { class TestStream: TextOutputStream { - @LockedBox var strings: [String] = [] + private var stringsMutex: Mutex<[String]> = Mutex([]) + var strings: [String] { stringsMutex.load() } func write(_ string: String) { - strings.append(string) + stringsMutex.withLock { $0.append(string) } } } @@ -352,10 +354,14 @@ class ValueObservationTests: GRDBTestCase { // Force DatabasePool to perform two initial fetches, because between // its first read access, and its write access that installs the // transaction observer, some write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation.trackingConstantRegion { db -> Int in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false try dbPool.write { db in try db.execute(sql: """ INSERT INTO t DEFAULT VALUES; @@ -368,18 +374,18 @@ class ValueObservationTests: GRDBTestCase { let expectation = self.expectation(description: "") expectation.expectedFulfillmentCount = 2 - var observedCounts: [Int] = [] + let observedCountsMutex: Mutex<[Int]> = Mutex([]) let cancellable = observation.start( in: dbPool, scheduling: .async(onQueue: .main), onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - observedCounts.append(count) + observedCountsMutex.withLock { $0.append(count) } expectation.fulfill() }) withExtendedLifetime(cancellable) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(observedCounts, [0, 0]) + XCTAssertEqual(observedCountsMutex.load(), [0, 0]) } } @@ -392,10 +398,14 @@ class ValueObservationTests: GRDBTestCase { // Force DatabasePool to perform two initial fetches, because between // its first read access, and its write access that installs the // transaction observer, some write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation.trackingConstantRegion { db -> Int in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false try dbPool.write { db in try db.execute(sql: """ INSERT INTO t DEFAULT VALUES; @@ -408,18 +418,18 @@ class ValueObservationTests: GRDBTestCase { let expectation = self.expectation(description: "") expectation.expectedFulfillmentCount = 2 - var observedCounts: [Int] = [] + let observedCountsMutex: Mutex<[Int]> = Mutex([]) let cancellable = observation.start( in: dbPool, scheduling: .immediate, onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - observedCounts.append(count) + observedCountsMutex.withLock { $0.append(count) } expectation.fulfill() }) withExtendedLifetime(cancellable) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(observedCounts, [0, 0]) + XCTAssertEqual(observedCountsMutex.load(), [0, 0]) } } @@ -432,10 +442,14 @@ class ValueObservationTests: GRDBTestCase { // Allow pool to perform a single initial fetch, because between // its first read access, and its write access that installs the // transaction observer, no write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation.trackingConstantRegion { db -> Int in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false DispatchQueue.main.asyncAfter(deadline: .now() + 1) { try! dbPool.write { db in try db.execute(sql: """ @@ -448,7 +462,7 @@ class ValueObservationTests: GRDBTestCase { } let expectedCounts: [Int] -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // Optimization available expectedCounts = [0, 1] #else @@ -458,18 +472,18 @@ class ValueObservationTests: GRDBTestCase { let expectation = self.expectation(description: "") expectation.expectedFulfillmentCount = expectedCounts.count - var observedCounts: [Int] = [] + let observedCountsMutex: Mutex<[Int]> = Mutex([]) let cancellable = observation.start( in: dbPool, scheduling: .async(onQueue: .main), onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - observedCounts.append(count) + observedCountsMutex.withLock { $0.append(count) } expectation.fulfill() }) withExtendedLifetime(cancellable) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(observedCounts, expectedCounts) + XCTAssertEqual(observedCountsMutex.load(), expectedCounts) } } @@ -482,10 +496,14 @@ class ValueObservationTests: GRDBTestCase { // Allow pool to perform a single initial fetch, because between // its first read access, and its write access that installs the // transaction observer, no write did happen. - var needsChange = true + let needsChangeMutex = Mutex(true) let observation = ValueObservation.trackingConstantRegion { db -> Int in + let needsChange = needsChangeMutex.withLock { needed in + let wasNeeded = needed + needed = false + return wasNeeded + } if needsChange { - needsChange = false DispatchQueue.main.asyncAfter(deadline: .now() + 1) { try! dbPool.write { db in try db.execute(sql: """ @@ -498,7 +516,7 @@ class ValueObservationTests: GRDBTestCase { } let expectedCounts: [Int] -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) // Optimization available expectedCounts = [0, 1] #else @@ -508,24 +526,24 @@ class ValueObservationTests: GRDBTestCase { let expectation = self.expectation(description: "") expectation.expectedFulfillmentCount = expectedCounts.count - var observedCounts: [Int] = [] + let observedCountsMutex: Mutex<[Int]> = Mutex([]) let cancellable = observation.start( in: dbPool, scheduling: .immediate, onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { count in - observedCounts.append(count) + observedCountsMutex.withLock { $0.append(count) } expectation.fulfill() }) withExtendedLifetime(cancellable) { waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(observedCounts, expectedCounts) + XCTAssertEqual(observedCountsMutex.load(), expectedCounts) } } // MARK: - Snapshot Observation -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) func testDatabaseSnapshotPoolObservation() throws { let dbPool = try makeDatabasePool() try dbPool.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } @@ -601,7 +619,7 @@ class ValueObservationTests: GRDBTestCase { try dbQueue.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } // Track reducer process - var changesCount = 0 + let changesCountMutex = Mutex(0) let notificationExpectation = expectation(description: "notification") notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 2 @@ -612,13 +630,12 @@ class ValueObservationTests: GRDBTestCase { } // Start observation and deallocate cancellable after second change - var cancellable: (any DatabaseCancellable)? + nonisolated(unsafe) var cancellable: (any DatabaseCancellable)? cancellable = observation.start( in: dbQueue, onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { _ in - changesCount += 1 - if changesCount == 2 { + if changesCountMutex.increment() == 2 { cancellable = nil } notificationExpectation.fulfill() @@ -638,7 +655,7 @@ class ValueObservationTests: GRDBTestCase { _ = cancellable waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(changesCount, 2) + XCTAssertEqual(changesCountMutex.load(), 2) } func testCancellableExplicitCancellation() throws { @@ -647,7 +664,7 @@ class ValueObservationTests: GRDBTestCase { try dbQueue.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } // Track reducer process - var changesCount = 0 + let changesCountMutex = Mutex(0) let notificationExpectation = expectation(description: "notification") notificationExpectation.assertForOverFulfill = true notificationExpectation.expectedFulfillmentCount = 2 @@ -658,13 +675,12 @@ class ValueObservationTests: GRDBTestCase { } // Start observation and cancel cancellable after second change - var cancellable: (any DatabaseCancellable)! + nonisolated(unsafe) var cancellable: (any DatabaseCancellable)! cancellable = observation.start( in: dbQueue, onError: { error in XCTFail("Unexpected error: \(error)") }, onChange: { _ in - changesCount += 1 - if changesCount == 2 { + if changesCountMutex.increment() == 2 { cancellable.cancel() } notificationExpectation.fulfill() @@ -682,7 +698,7 @@ class ValueObservationTests: GRDBTestCase { } waitForExpectations(timeout: 2, handler: nil) - XCTAssertEqual(changesCount, 2) + XCTAssertEqual(changesCountMutex.load(), 2) } } @@ -696,18 +712,20 @@ class ValueObservationTests: GRDBTestCase { notificationExpectation.expectedFulfillmentCount = 2 do { - var cancellable: (any DatabaseCancellable)? = nil + nonisolated(unsafe) var cancellable: (any DatabaseCancellable)? = nil _ = cancellable // Avoid "Variable 'cancellable' was written to, but never read" warning - var shouldStopObservation = false + let shouldStopObservationMutex = Mutex(false) let observation = ValueObservation( trackingMode: .nonConstantRegionRecordedFromSelection, makeReducer: { AnyValueReducer( fetch: { _ in - if shouldStopObservation { - cancellable = nil /* deallocation */ + shouldStopObservationMutex.withLock { shouldStopObservation in + if shouldStopObservation { + cancellable = nil /* deallocation */ + } + shouldStopObservation = true } - shouldStopObservation = true }, value: { _ in () }) }) @@ -740,19 +758,21 @@ class ValueObservationTests: GRDBTestCase { notificationExpectation.expectedFulfillmentCount = 2 do { - var cancellable: (any DatabaseCancellable)? = nil + nonisolated(unsafe) var cancellable: (any DatabaseCancellable)? = nil _ = cancellable // Avoid "Variable 'cancellable' was written to, but never read" warning - var shouldStopObservation = false + let shouldStopObservationMutex = Mutex(false) let observation = ValueObservation( trackingMode: .nonConstantRegionRecordedFromSelection, makeReducer: { AnyValueReducer( fetch: { _ in }, value: { _ in - if shouldStopObservation { - cancellable = nil /* deallocation right before notification */ + shouldStopObservationMutex.withLock { shouldStopObservation in + if shouldStopObservation { + cancellable = nil /* deallocation right before notification */ + } + shouldStopObservation = true } - shouldStopObservation = true return () }) }) @@ -780,13 +800,13 @@ class ValueObservationTests: GRDBTestCase { try writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } // Start observing - var counts: [Int] = [] + let countsMutex: Mutex<[Int]> = Mutex([]) let cancellable = ValueObservation .trackingConstantRegion { try Table("t").fetchCount($0) } .start(in: writer) { error in XCTFail("Unexpected error: \(error)") } onChange: { count in - counts.append(count) + countsMutex.withLock { $0.append(count) } } // Perform a write after cancellation, but before the @@ -815,7 +835,7 @@ class ValueObservationTests: GRDBTestCase { // We should not have been notified of the first write, because // it was performed after cancellation. - XCTAssertFalse(counts.contains(1)) + XCTAssertFalse(countsMutex.load().contains(1)) } try test(makeDatabaseQueue()) @@ -871,60 +891,55 @@ class ValueObservationTests: GRDBTestCase { try test(makeDatabasePool()) } - // MARK: - Async Await - - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - func testAsyncAwait_values_prefix() async throws { - func test(_ writer: some DatabaseWriter) async throws { - // We need something to change - try await writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } - - let cancellationExpectation = expectation(description: "cancelled") - let task = Task { () -> [Int] in - var counts: [Int] = [] - let observation = ValueObservation - .trackingConstantRegion(Table("t").fetchCount) - .handleEvents(didCancel: { cancellationExpectation.fulfill() }) - - for try await count in try observation.values(in: writer).prefix(while: { $0 <= 3 }) { - counts.append(count) - try await writer.write { try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") } - } - return counts + // MARK: - Main Actor + @MainActor func test_mainActor_observation() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.write { db in + try db.create(table: "test") { t in + t.autoIncrementedPrimaryKey("id") } - - let counts = try await task.value - XCTAssertTrue(counts.contains(0)) - XCTAssertTrue(counts.contains(where: { $0 >= 2 })) - XCTAssertEqual(counts.sorted(), counts) - - // Observation was ended -#if compiler(>=5.8) - await fulfillment(of: [cancellationExpectation], timeout: 2) -#else - wait(for: [cancellationExpectation], timeout: 2) -#endif } - try await AsyncTest(test).run { try DatabaseQueue() } - try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } - try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } + let observation = ValueObservation.tracking { + try Table("test").fetchCount($0) + } + + var value = 0 // No mutex necessary! + let expectation = self.expectation(description: "completion") + let cancellable = observation.start( + in: dbQueue, + onError: { error in XCTFail("Unexpected error: \(error)") }, + onChange: { + value = $0 + if value == 2 { + expectation.fulfill() + } + }) + + try dbQueue.write { db in + try db.execute(sql: "INSERT INTO test DEFAULT VALUES") + try db.execute(sql: "INSERT INTO test DEFAULT VALUES") + } + withExtendedLifetime(cancellable) { _ in + wait(for: [expectation], timeout: 2) + } } + + // MARK: - Async Await - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - func testAsyncAwait_values_prefix_immediate_scheduling() async throws { + func testAsyncAwait_values_prefix() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change try await writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } let cancellationExpectation = expectation(description: "cancelled") - let task = Task { @MainActor () -> [Int] in + let task = Task { () -> [Int] in var counts: [Int] = [] let observation = ValueObservation .trackingConstantRegion(Table("t").fetchCount) .handleEvents(didCancel: { cancellationExpectation.fulfill() }) - for try await count in try observation.values(in: writer, scheduling: .immediate).prefix(while: { $0 <= 3 }) { + for try await count in try observation.values(in: writer).prefix(while: { $0 <= 3 }) { counts.append(count) try await writer.write { try $0.execute(sql: "INSERT INTO t DEFAULT VALUES") } } @@ -949,7 +964,6 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncAwait_values_break() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -991,46 +1005,6 @@ class ValueObservationTests: GRDBTestCase { try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } } - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) - func testAsyncAwait_values_immediate_break() async throws { - func test(_ writer: some DatabaseWriter) async throws { - // We need something to change - try await writer.write { try $0.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT)") } - - let cancellationExpectation = expectation(description: "cancelled") - - let task = Task { @MainActor () -> [Int] in - var counts: [Int] = [] - let observation = ValueObservation - .trackingConstantRegion(Table("t").fetchCount) - .handleEvents(didCancel: { cancellationExpectation.fulfill() }) - - for try await count in observation.values(in: writer, scheduling: .immediate) { - counts.append(count) - break - } - return counts - } - - let counts = try await task.value - - // A single value was published - assertValueObservationRecordingMatch(recorded: counts, expected: [0]) - - // Observation was ended -#if compiler(>=5.8) - await fulfillment(of: [cancellationExpectation], timeout: 2) -#else - wait(for: [cancellationExpectation], timeout: 2) -#endif - } - - try await AsyncTest(test).run { try DatabaseQueue() } - try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) } - try await AsyncTest(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) } - } - - @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) func testAsyncAwait_values_cancelled() async throws { func test(_ writer: some DatabaseWriter) async throws { // We need something to change @@ -1083,6 +1057,13 @@ class ValueObservationTests: GRDBTestCase { // An attempt at finding a regression test for func testManyObservations() throws { + // TODO: Fix flaky test with SQLCipher 3 + #if GRDBCIPHER + if sqlite3_libversion_number() <= 3020001 { + throw XCTSkip("Skip flaky test with SQLCipher 3") + } + #endif + // We'll start many observations let observationCount = 100 dbConfiguration.maximumReaderCount = 5 @@ -1095,8 +1076,8 @@ class ValueObservationTests: GRDBTestCase { try Table("t").fetchCount($0) } - let initialValueExpectation = self.expectation(description: "") -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) + let initialValueExpectation = self.expectation(description: "initialValue") +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) initialValueExpectation.assertForOverFulfill = true #else // ValueObservation on DatabasePool will notify the first value twice @@ -1104,7 +1085,7 @@ class ValueObservationTests: GRDBTestCase { #endif initialValueExpectation.expectedFulfillmentCount = observationCount - let secondValueExpectation = self.expectation(description: "") + let secondValueExpectation = self.expectation(description: "secondValue") secondValueExpectation.expectedFulfillmentCount = observationCount var cancellables: [AnyDatabaseCancellable] = [] @@ -1154,7 +1135,7 @@ class ValueObservationTests: GRDBTestCase { } let initialValueExpectation = self.expectation(description: "") -#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) +#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER) initialValueExpectation.assertForOverFulfill = true #else // ValueObservation on DatabasePool will notify the first value twice diff --git a/Tests/Performance/GRDBPerformance/FetchPositionalValuesTests.swift b/Tests/Performance/GRDBPerformance/FetchPositionalValuesTests.swift index 90f39ded92..72b41d0e04 100644 --- a/Tests/Performance/GRDBPerformance/FetchPositionalValuesTests.swift +++ b/Tests/Performance/GRDBPerformance/FetchPositionalValuesTests.swift @@ -1,5 +1,6 @@ import XCTest import GRDB +import SQLite3 #if GRDB_COMPARE import SQLite #endif diff --git a/Tests/Performance/GRDBPerformance/FetchRecordOptimizedTests.swift b/Tests/Performance/GRDBPerformance/FetchRecordOptimizedTests.swift index 3195e3003b..8cdfd53948 100644 --- a/Tests/Performance/GRDBPerformance/FetchRecordOptimizedTests.swift +++ b/Tests/Performance/GRDBPerformance/FetchRecordOptimizedTests.swift @@ -29,18 +29,20 @@ private struct Item: Codable, FetchableRecord, PersistableRecord { i9 = row[9] } - static let databaseSelection: [any SQLSelectable] = [ - Column("i0"), - Column("i1"), - Column("i2"), - Column("i3"), - Column("i4"), - Column("i5"), - Column("i6"), - Column("i7"), - Column("i8"), - Column("i9"), - ] + static var databaseSelection: [any SQLSelectable] { + [ + Column("i0"), + Column("i1"), + Column("i2"), + Column("i3"), + Column("i4"), + Column("i5"), + Column("i6"), + Column("i7"), + Column("i8"), + Column("i9"), + ] + } } /// Here we test the extraction of a plain Swift struct diff --git a/Tests/Performance/GRDBPerformance/FetchRecordStructTests.swift b/Tests/Performance/GRDBPerformance/FetchRecordStructTests.swift index a8b734954f..13387d5e1d 100644 --- a/Tests/Performance/GRDBPerformance/FetchRecordStructTests.swift +++ b/Tests/Performance/GRDBPerformance/FetchRecordStructTests.swift @@ -1,5 +1,6 @@ import XCTest import GRDB +import SQLite3 #if GRDB_COMPARE import SQLite #endif diff --git a/Tests/Performance/GRDBPerformance/InsertPositionalValuesTests.swift b/Tests/Performance/GRDBPerformance/InsertPositionalValuesTests.swift index 382c6b516d..ec6edb99b1 100644 --- a/Tests/Performance/GRDBPerformance/InsertPositionalValuesTests.swift +++ b/Tests/Performance/GRDBPerformance/InsertPositionalValuesTests.swift @@ -1,5 +1,6 @@ import XCTest import GRDB +import SQLite3 #if GRDB_COMPARE import SQLite #endif diff --git a/Tests/Performance/GRDBProfiling/GRDBProfiling.xcodeproj/project.pbxproj b/Tests/Performance/GRDBProfiling/GRDBProfiling.xcodeproj/project.pbxproj index 2851c7774d..73c21f9549 100644 --- a/Tests/Performance/GRDBProfiling/GRDBProfiling.xcodeproj/project.pbxproj +++ b/Tests/Performance/GRDBProfiling/GRDBProfiling.xcodeproj/project.pbxproj @@ -364,7 +364,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -416,7 +416,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_VERSION = 5.0; diff --git a/Tests/SPM/PlainPackage/Package.swift b/Tests/SPM/PlainPackage/Package.swift index 3673c7afcb..6b69969994 100644 --- a/Tests/SPM/PlainPackage/Package.swift +++ b/Tests/SPM/PlainPackage/Package.swift @@ -1,14 +1,20 @@ -// swift-tools-version:5.2 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "SPM", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v7), + ], dependencies: [ .package(name: "GRDB", path: "../../.."), ], targets: [ - .target(name: "SPM", dependencies: ["GRDB"]), + .executableTarget(name: "SPM", dependencies: ["GRDB"]), ] ) diff --git a/Tests/SPM/PlainPackage/Sources/SPM/main.swift b/Tests/SPM/PlainPackage/Sources/SPM/main.swift index 1e00a3a2b6..01ba311513 100644 --- a/Tests/SPM/PlainPackage/Sources/SPM/main.swift +++ b/Tests/SPM/PlainPackage/Sources/SPM/main.swift @@ -1,4 +1,5 @@ import GRDB +import SQLite3 let cVersion = String(cString: sqlite3_libversion()) print("SQLite version from C API: \(cVersion)") diff --git a/Tests/SPM/PlainProject/Plain.xcodeproj/project.pbxproj b/Tests/SPM/PlainProject/Plain.xcodeproj/project.pbxproj index 00bcf692ef..46a8734ad3 100644 --- a/Tests/SPM/PlainProject/Plain.xcodeproj/project.pbxproj +++ b/Tests/SPM/PlainProject/Plain.xcodeproj/project.pbxproj @@ -187,7 +187,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -241,7 +241,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/Tests/SPM/ios-dynamic/ios-dynamic/ViewController.swift b/Tests/SPM/ios-dynamic/ios-dynamic/ViewController.swift index 110634c81f..9092d3992e 100644 --- a/Tests/SPM/ios-dynamic/ios-dynamic/ViewController.swift +++ b/Tests/SPM/ios-dynamic/ios-dynamic/ViewController.swift @@ -7,13 +7,15 @@ import UIKit import GRDB +import SQLite3 class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - try! print(DatabaseQueue().read { try String.fetchOne($0, sql: "SELECT 'Hello world!'")! }) - } + override func viewDidLoad() { + super.viewDidLoad() + + try! print(DatabaseQueue().read { try String.fetchOne($0, sql: "SELECT 'Hello world!'")! }) + _ = sqlite3_libversion_number() + } } diff --git a/Tests/SPM/ios/ios/AppDelegate.swift b/Tests/SPM/ios/ios/AppDelegate.swift index b257fc310b..973983272c 100644 --- a/Tests/SPM/ios/ios/AppDelegate.swift +++ b/Tests/SPM/ios/ios/AppDelegate.swift @@ -1,5 +1,6 @@ import UIKit import GRDB +import SQLite3 @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -7,6 +8,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ = try! DatabaseQueue() .readPublisher(value: { _ in }) .assertNoFailure() + _ = sqlite3_libversion_number() return true } diff --git a/Tests/SPM/macos/macos/AppDelegate.swift b/Tests/SPM/macos/macos/AppDelegate.swift index e056c1961a..b788baabba 100644 --- a/Tests/SPM/macos/macos/AppDelegate.swift +++ b/Tests/SPM/macos/macos/AppDelegate.swift @@ -1,10 +1,12 @@ import Cocoa import GRDB +import SQLite3 @main class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { _ = try! DatabaseQueue() + _ = sqlite3_libversion_number() } func applicationWillTerminate(_ aNotification: Notification) { diff --git a/Tests/Swift6Migration/LanguageMode5.xcconfig b/Tests/Swift6Migration/LanguageMode5.xcconfig new file mode 100644 index 0000000000..4556c2d2af --- /dev/null +++ b/Tests/Swift6Migration/LanguageMode5.xcconfig @@ -0,0 +1,5 @@ +// Swift 5 mode +SWIFT_VERSION = 5.0 + +// Recommended settings +SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES diff --git a/Tests/Swift6Migration/LanguageMode5StrictConcurrency.xcconfig b/Tests/Swift6Migration/LanguageMode5StrictConcurrency.xcconfig new file mode 100644 index 0000000000..d74feda27a --- /dev/null +++ b/Tests/Swift6Migration/LanguageMode5StrictConcurrency.xcconfig @@ -0,0 +1,6 @@ +// Swift 5 mode with strict concurrency checkings +SWIFT_VERSION = 5.0 +SWIFT_STRICT_CONCURRENCY = complete + +// Recommended settings +SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES diff --git a/Tests/Swift6Migration/LanguageMode6.xcconfig b/Tests/Swift6Migration/LanguageMode6.xcconfig new file mode 100644 index 0000000000..3ce1f8ea29 --- /dev/null +++ b/Tests/Swift6Migration/LanguageMode6.xcconfig @@ -0,0 +1,5 @@ +// Swift 6 mode +SWIFT_VERSION = 6.0 + +// Recommended settings +// SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES diff --git a/Tests/Swift6Migration/Swift6Migration.xcodeproj/project.pbxproj b/Tests/Swift6Migration/Swift6Migration.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..31c7cd6bc4 --- /dev/null +++ b/Tests/Swift6Migration/Swift6Migration.xcodeproj/project.pbxproj @@ -0,0 +1,361 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 56CFC7D22CA03647000B5023 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 56CFC7D12CA03647000B5023 /* GRDB */; }; + 56CFC7ED2CA06ADD000B5023 /* LanguageMode5.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 56CFC7EC2CA06ADD000B5023 /* LanguageMode5.xcconfig */; }; + 56CFC7F02CA06B7E000B5023 /* LanguageMode6.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 56CFC7EF2CA06B7E000B5023 /* LanguageMode6.xcconfig */; }; + 56CFC7F12CA06B7E000B5023 /* LanguageMode5StrictConcurrency.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 56CFC7EE2CA06B7E000B5023 /* LanguageMode5StrictConcurrency.xcconfig */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 56CFC7BF2CA035E7000B5023 /* Swift6Migration.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Swift6Migration.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 56CFC7EC2CA06ADD000B5023 /* LanguageMode5.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = LanguageMode5.xcconfig; sourceTree = ""; }; + 56CFC7EE2CA06B7E000B5023 /* LanguageMode5StrictConcurrency.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = LanguageMode5StrictConcurrency.xcconfig; sourceTree = ""; }; + 56CFC7EF2CA06B7E000B5023 /* LanguageMode6.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = LanguageMode6.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 56CFC7C12CA035E7000B5023 /* Swift6Migration */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Swift6Migration; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 56CFC7BC2CA035E7000B5023 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 56CFC7D22CA03647000B5023 /* GRDB in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 56CFC7B62CA035E7000B5023 = { + isa = PBXGroup; + children = ( + 56CFC7EC2CA06ADD000B5023 /* LanguageMode5.xcconfig */, + 56CFC7EE2CA06B7E000B5023 /* LanguageMode5StrictConcurrency.xcconfig */, + 56CFC7EF2CA06B7E000B5023 /* LanguageMode6.xcconfig */, + 56CFC7C12CA035E7000B5023 /* Swift6Migration */, + 56CFC7C02CA035E7000B5023 /* Products */, + ); + sourceTree = ""; + }; + 56CFC7C02CA035E7000B5023 /* Products */ = { + isa = PBXGroup; + children = ( + 56CFC7BF2CA035E7000B5023 /* Swift6Migration.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 56CFC7BE2CA035E7000B5023 /* Swift6Migration */ = { + isa = PBXNativeTarget; + buildConfigurationList = 56CFC7CD2CA035E8000B5023 /* Build configuration list for PBXNativeTarget "Swift6Migration" */; + buildPhases = ( + 56CFC7BB2CA035E7000B5023 /* Sources */, + 56CFC7BC2CA035E7000B5023 /* Frameworks */, + 56CFC7BD2CA035E7000B5023 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 56CFC7C12CA035E7000B5023 /* Swift6Migration */, + ); + name = Swift6Migration; + packageProductDependencies = ( + 56CFC7D12CA03647000B5023 /* GRDB */, + ); + productName = Swift6Migration; + productReference = 56CFC7BF2CA035E7000B5023 /* Swift6Migration.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 56CFC7B72CA035E7000B5023 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + 56CFC7BE2CA035E7000B5023 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 56CFC7BA2CA035E7000B5023 /* Build configuration list for PBXProject "Swift6Migration" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 56CFC7B62CA035E7000B5023; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 56CFC7D02CA03647000B5023 /* XCLocalSwiftPackageReference "../../../GRDB.swift" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 56CFC7C02CA035E7000B5023 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 56CFC7BE2CA035E7000B5023 /* Swift6Migration */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 56CFC7BD2CA035E7000B5023 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 56CFC7ED2CA06ADD000B5023 /* LanguageMode5.xcconfig in Resources */, + 56CFC7F02CA06B7E000B5023 /* LanguageMode6.xcconfig in Resources */, + 56CFC7F12CA06B7E000B5023 /* LanguageMode5StrictConcurrency.xcconfig in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 56CFC7BB2CA035E7000B5023 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 56CFC7CB2CA035E8000B5023 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 56CFC7EF2CA06B7E000B5023 /* LanguageMode6.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 56CFC7CC2CA035E8000B5023 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 56CFC7CE2CA035E8000B5023 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Swift6Migration/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.Swift6Migration; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 56CFC7CF2CA035E8000B5023 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Swift6Migration/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.Swift6Migration; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 56CFC7BA2CA035E7000B5023 /* Build configuration list for PBXProject "Swift6Migration" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 56CFC7CB2CA035E8000B5023 /* Debug */, + 56CFC7CC2CA035E8000B5023 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 56CFC7CD2CA035E8000B5023 /* Build configuration list for PBXNativeTarget "Swift6Migration" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 56CFC7CE2CA035E8000B5023 /* Debug */, + 56CFC7CF2CA035E8000B5023 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 56CFC7D02CA03647000B5023 /* XCLocalSwiftPackageReference "../../../GRDB.swift" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../GRDB.swift; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 56CFC7D12CA03647000B5023 /* GRDB */ = { + isa = XCSwiftPackageProductDependency; + productName = GRDB; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 56CFC7B72CA035E7000B5023 /* Project object */; +} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Tests/Swift6Migration/Swift6Migration.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Tests/Swift6Migration/Swift6Migration.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json b/Tests/Swift6Migration/Swift6Migration/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 67% rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json rename to Tests/Swift6Migration/Swift6Migration/Assets.xcassets/AccentColor.colorset/Contents.json index 2cbe59d5ec..eb87897008 100644 --- a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/LaunchIcon.imageset/Contents.json +++ b/Tests/Swift6Migration/Swift6Migration/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,7 +1,6 @@ { - "images" : [ + "colors" : [ { - "filename" : "LaunchIcon.pdf", "idiom" : "universal" } ], diff --git a/Tests/Swift6Migration/Swift6Migration/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tests/Swift6Migration/Swift6Migration/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..2305880107 --- /dev/null +++ b/Tests/Swift6Migration/Swift6Migration/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/Contents.json b/Tests/Swift6Migration/Swift6Migration/Assets.xcassets/Contents.json similarity index 100% rename from Documentation/DemoApps/GRDBDemoiOS/GRDBDemoiOS/Resources/Assets.xcassets/Contents.json rename to Tests/Swift6Migration/Swift6Migration/Assets.xcassets/Contents.json diff --git a/Tests/Swift6Migration/Swift6Migration/AsyncOverload.swift b/Tests/Swift6Migration/Swift6Migration/AsyncOverload.swift new file mode 100644 index 0000000000..5c366d2791 --- /dev/null +++ b/Tests/Swift6Migration/Swift6Migration/AsyncOverload.swift @@ -0,0 +1,30 @@ +import GRDB + +private struct Player: Codable, FetchableRecord, PersistableRecord { } +let writer: any DatabaseWriter = { fatalError() }() + +private func fetchPlayers() async throws -> [Player] { + try await writer.read(Player.fetchAll) +} + +private func foo() { + Task { + let players = try writer.read(Player.fetchAll) + } +} + +private struct PlayerRepository { + var writer: any DatabaseWriter + + func fetchPlayers() throws -> [Player] { + try writer.read(Player.fetchAll) + } +} + + +private func bar() { + let repository = try! PlayerRepository(writer: DatabaseQueue()) + Task { + let players = try repository.fetchPlayers() + } +} diff --git a/Tests/Swift6Migration/Swift6Migration/ContentView.swift b/Tests/Swift6Migration/Swift6Migration/ContentView.swift new file mode 100644 index 0000000000..a7b0b63685 --- /dev/null +++ b/Tests/Swift6Migration/Swift6Migration/ContentView.swift @@ -0,0 +1,24 @@ +// +// ContentView.swift +// Swift6Migration +// +// Created by Gwendal Roué on 22/09/2024. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/Tests/Swift6Migration/Swift6Migration/InferSendableFromCaptures.swift b/Tests/Swift6Migration/Swift6Migration/InferSendableFromCaptures.swift new file mode 100644 index 0000000000..0e510ba662 --- /dev/null +++ b/Tests/Swift6Migration/Swift6Migration/InferSendableFromCaptures.swift @@ -0,0 +1,9 @@ +import GRDB + +private struct Player: TableRecord { } + +private func fetchCount(_ writer: any DatabaseWriter) async throws -> Int { + // Converting non-sendable function value to + // '@Sendable (Database) throws -> Int' may introduce data races. + try await writer.read(Player.fetchCount) +} diff --git a/Tests/Swift6Migration/Swift6Migration/NonSendableConfiguration.swift b/Tests/Swift6Migration/Swift6Migration/NonSendableConfiguration.swift new file mode 100644 index 0000000000..668682b93e --- /dev/null +++ b/Tests/Swift6Migration/Swift6Migration/NonSendableConfiguration.swift @@ -0,0 +1,21 @@ +import GRDB + +private struct Player1: Codable { } +private struct Player2: Codable { } + +#if swift(<6) +extension Player1: FetchableRecord, MutablePersistableRecord { + // Static property 'databaseSelection' is not concurrency-safe + // because non-'Sendable' type '[any SQLSelectable]' + // may have shared mutable state + static let databaseSelection: [any SQLSelectable] = [ + Column("id"), Column("name"), Column("score") + ] +} +#endif + +extension Player2: FetchableRecord, MutablePersistableRecord { + static var databaseSelection: [any SQLSelectable] { + [Column("id"), Column("name"), Column("score")] + } +} diff --git a/Tests/Swift6Migration/Swift6Migration/NonSendableRecord.swift b/Tests/Swift6Migration/Swift6Migration/NonSendableRecord.swift new file mode 100644 index 0000000000..42ab943378 --- /dev/null +++ b/Tests/Swift6Migration/Swift6Migration/NonSendableRecord.swift @@ -0,0 +1,43 @@ +import GRDB + +private final class Player: Codable, Identifiable { + var id: Int64 + var name: String + var score: Int + + init(id: Int64, name: String, score: Int) { + self.id = id + self.name = name + self.score = score + } +} + +extension Player: FetchableRecord, PersistableRecord { } + +#if swift(<6) +private struct PlayerRepository { + var writer: any DatabaseWriter + + func fetch() async throws -> Player? { + // Type 'Player' does not conform to the 'Sendable' protocol + try await writer.read { db in + try Player.fetchOne(db, id: 42) + } + } + + func insert(_ player: Player) async throws { + // Capture of 'player' with non-sendable type 'Player' in a `@Sendable` closure + try await writer.read { db in + try player.insert(db) + } + } + + func observe() { + // Type 'Player' does not conform to the 'Sendable' protocol + let observation = ValueObservation.tracking { db in + try Player.fetchAll(db) + } + _ = observation + } +} +#endif diff --git a/Tests/Swift6Migration/Swift6Migration/Preview Content/Preview Assets.xcassets/Contents.json b/Tests/Swift6Migration/Swift6Migration/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Tests/Swift6Migration/Swift6Migration/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/Swift6Migration/Swift6Migration/Swift6MigrationApp.swift b/Tests/Swift6Migration/Swift6Migration/Swift6MigrationApp.swift new file mode 100644 index 0000000000..9ef8b0b0c9 --- /dev/null +++ b/Tests/Swift6Migration/Swift6Migration/Swift6MigrationApp.swift @@ -0,0 +1,17 @@ +// +// Swift6MigrationApp.swift +// Swift6Migration +// +// Created by Gwendal Roué on 22/09/2024. +// + +import SwiftUI + +@main +struct Swift6MigrationApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +}