diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..04f178c7 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(swift --version:*)", + "Bash(~/.swiftly/bin/swiftly list:*)", + "Bash(~/.swiftly/bin/swiftly use:*)" + ] + } +} diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 00000000..8452b0f2 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,57 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + + Please review this pull request and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 00000000..d300267f --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + diff --git a/Package.resolved b/Package.resolved index 419fc30f..ba38b909 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,22 +1,22 @@ { - "originHash" : "41f93013537f670f4fe1a235c318bee33b54528c76e4b1dd2a7675bea6c0bcde", + "originHash" : "c53c34c164015d035216379ca730f2630209ddabb19d29107d35f34491225ea9", "pins" : [ { "identity" : "combine-schedulers", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "5928286acce13def418ec36d05a001a9641086f2", - "version" : "1.0.3" + "revision" : "fd16d76fd8b9a976d88bfb6cacc05ca8d19c91b6", + "version" : "1.1.0" } }, { "identity" : "grdb.swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/GRDB.swift", + "location" : "https://github.com/doozMen/GRDB.swift", "state" : { - "revision" : "18497b68fdbb3a09528d260a0a0e1e7e61c8c53d", - "version" : "7.8.0" + "branch" : "master", + "revision" : "aa0079aeb82a4bf00324561a40bffe68c6fe1c26" } }, { @@ -94,19 +94,19 @@ { "identity" : "swift-perception", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-perception", + "location" : "https://github.com/doozMen/swift-perception", "state" : { - "revision" : "4f47ebafed5f0b0172cf5c661454fa8e28fb2ac4", - "version" : "2.0.9" + "branch" : "main", + "revision" : "9237a92716ccbcd4be8c5330ed372f243c9d23d9" } }, { "identity" : "swift-sharing", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-sharing", + "location" : "https://github.com/doozMen/swift-sharing", "state" : { - "revision" : "3bfc408cc2d0bee2287c174da6b1c76768377818", - "version" : "2.7.4" + "branch" : "main", + "revision" : "a367f93207aa67a7a9953946ae3b8d47d223f6af" } }, { @@ -121,10 +121,10 @@ { "identity" : "swift-structured-queries", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-structured-queries", + "location" : "https://github.com/doozMen/swift-structured-queries", "state" : { - "revision" : "3a95b70a81b7027b8a5117e7dd08188837e5f54e", - "version" : "0.24.0" + "branch" : "main", + "revision" : "85f39901d6c6046fa2f5119717741d9cc8a50b7f" } }, { @@ -141,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "4c27acf5394b645b70d8ba19dc249c0472d5f618", - "version" : "1.7.0" + "revision" : "31073495cae9caf243c440eac94b3ab067e3d7bc", + "version" : "1.8.0" } } ], diff --git a/Package.swift b/Package.swift index 0d607e9d..7cc044a1 100644 --- a/Package.swift +++ b/Package.swift @@ -1,110 +1,112 @@ -// swift-tools-version: 6.1 +// swift-tools-version: 6.2.1 import PackageDescription let package = Package( - name: "sqlite-data", - platforms: [ - .iOS(.v13), - .macOS(.v10_15), - .tvOS(.v13), - .watchOS(.v7), - ], - products: [ - .library( - name: "SQLiteData", - targets: ["SQLiteData"] - ), - .library( - name: "SQLiteDataTestSupport", - targets: ["SQLiteDataTestSupport"] - ), - ], - traits: [ - .trait( - name: "SQLiteDataTagged", - description: "Introduce SQLiteData conformances to the swift-tagged package." - ) - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-collections", from: "1.0.0"), - .package(url: "https://github.com/groue/GRDB.swift", from: "7.6.0"), - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"), - .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"), - .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"), - .package( - url: "https://github.com/pointfreeco/swift-structured-queries", - from: "0.24.0", - traits: [ - .trait(name: "StructuredQueriesTagged", condition: .when(traits: ["SQLiteDataTagged"])) - ] - ), - .package(url: "https://github.com/pointfreeco/swift-tagged", from: "0.10.0"), - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), - ], - targets: [ - .target( - name: "SQLiteData", - dependencies: [ - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), - .product(name: "Dependencies", package: "swift-dependencies"), - .product(name: "GRDB", package: "GRDB.swift"), - .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), - .product(name: "OrderedCollections", package: "swift-collections"), - .product(name: "Sharing", package: "swift-sharing"), - .product(name: "StructuredQueriesSQLite", package: "swift-structured-queries"), - .product( - name: "Tagged", - package: "swift-tagged", - condition: .when(traits: ["SQLiteDataTagged"]) + name: "sqlite-data", + platforms: [ + .macOS(.v15) + ], + products: [ + .library( + name: "SQLiteData", + targets: ["SQLiteData"] + ), + .library( + name: "SQLiteDataTestSupport", + targets: ["SQLiteDataTestSupport"] + ), + ], + traits: [ + .trait( + name: "SQLiteDataTagged", + description: "Introduce SQLiteData conformances to the swift-tagged package." + ) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-collections", from: "1.0.0"), + // NB: Fork synced with upstream v7.9.0 + .package(url: "https://github.com/doozMen/GRDB.swift", revision: "aa0079aeb82a4bf00324561a40bffe68c6fe1c26"), + .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"), + // NB: Fork with Swift 6.3 fixes (uses doozMen/swift-perception) + .package(url: "https://github.com/doozMen/swift-sharing", revision: "f1170dc9b28faea3edec20705839eb2bc349bcdb"), + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"), + // NB: Fork with Swift 6.3 fixes + .package( + url: "https://github.com/doozMen/swift-structured-queries", + revision: "85f39901d6c6046fa2f5119717741d9cc8a50b7f", + traits: [ + .trait( + name: "StructuredQueriesTagged", + condition: .when(traits: ["SQLiteDataTagged"])) + ] + ), + .package(url: "https://github.com/pointfreeco/swift-tagged", from: "0.10.0"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), + ], + targets: [ + .target( + name: "SQLiteData", + dependencies: [ + .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "GRDB", package: "GRDB.swift"), + .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), + .product(name: "OrderedCollections", package: "swift-collections"), + .product(name: "Sharing", package: "swift-sharing"), + .product(name: "StructuredQueriesSQLite", package: "swift-structured-queries"), + .product( + name: "Tagged", + package: "swift-tagged", + condition: .when(traits: ["SQLiteDataTagged"]) + ), + ] ), - ] - ), - .target( - name: "SQLiteDataTestSupport", - dependencies: [ - "SQLiteData", - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), - .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "Dependencies", package: "swift-dependencies"), - .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), - .product(name: "StructuredQueriesTestSupport", package: "swift-structured-queries"), - ] - ), - .testTarget( - name: "SQLiteDataTests", - dependencies: [ - "SQLiteData", - "SQLiteDataTestSupport", - .product(name: "DependenciesTestSupport", package: "swift-dependencies"), - .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), - .product(name: "SnapshotTestingCustomDump", package: "swift-snapshot-testing"), - .product(name: "StructuredQueries", package: "swift-structured-queries"), - ] - ), - ], - swiftLanguageModes: [.v6] + .target( + name: "SQLiteDataTestSupport", + dependencies: [ + "SQLiteData", + .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), + .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), + .product(name: "StructuredQueriesTestSupport", package: "swift-structured-queries"), + ] + ), + .testTarget( + name: "SQLiteDataTests", + dependencies: [ + "SQLiteData", + "SQLiteDataTestSupport", + .product(name: "DependenciesTestSupport", package: "swift-dependencies"), + .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), + .product(name: "SnapshotTestingCustomDump", package: "swift-snapshot-testing"), + .product(name: "StructuredQueries", package: "swift-structured-queries"), + ] + ), + ], + swiftLanguageModes: [.v6] ) let swiftSettings: [SwiftSetting] = [ - .enableUpcomingFeature("MemberImportVisibility") - // .unsafeFlags([ - // "-Xfrontend", - // "-warn-long-function-bodies=50", - // "-Xfrontend", - // "-warn-long-expression-type-checking=50", - // ]) + .enableUpcomingFeature("MemberImportVisibility") + // .unsafeFlags([ + // "-Xfrontend", + // "-warn-long-function-bodies=50", + // "-Xfrontend", + // "-warn-long-expression-type-checking=50", + // ]) ] for index in package.targets.indices { - package.targets[index].swiftSettings = swiftSettings + package.targets[index].swiftSettings = swiftSettings } #if !os(Windows) - // Add the documentation compiler plugin if possible - package.dependencies.append( - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") - ) + // Add the documentation compiler plugin if possible + package.dependencies.append( + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") + ) #endif diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift deleted file mode 100644 index fc886e60..00000000 --- a/Package@swift-6.0.swift +++ /dev/null @@ -1,92 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "sqlite-data", - platforms: [ - .iOS(.v13), - .macOS(.v10_15), - .tvOS(.v13), - .watchOS(.v7), - ], - products: [ - .library( - name: "SQLiteData", - targets: ["SQLiteData"] - ), - .library( - name: "SQLiteDataTestSupport", - targets: ["SQLiteDataTestSupport"] - ), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-collections", from: "1.0.0"), - .package(url: "https://github.com/groue/GRDB.swift", from: "7.6.0"), - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"), - .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.9.0"), - .package(url: "https://github.com/pointfreeco/swift-sharing", from: "2.3.0"), - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.4"), - .package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.24.0"), - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), - ], - targets: [ - .target( - name: "SQLiteData", - dependencies: [ - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), - .product(name: "Dependencies", package: "swift-dependencies"), - .product(name: "GRDB", package: "GRDB.swift"), - .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), - .product(name: "OrderedCollections", package: "swift-collections"), - .product(name: "Sharing", package: "swift-sharing"), - .product(name: "StructuredQueriesSQLite", package: "swift-structured-queries"), - ] - ), - .target( - name: "SQLiteDataTestSupport", - dependencies: [ - "SQLiteData", - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), - .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "Dependencies", package: "swift-dependencies"), - .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), - .product(name: "StructuredQueriesTestSupport", package: "swift-structured-queries"), - ] - ), - .testTarget( - name: "SQLiteDataTests", - dependencies: [ - "SQLiteData", - "SQLiteDataTestSupport", - .product(name: "DependenciesTestSupport", package: "swift-dependencies"), - .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), - .product(name: "SnapshotTestingCustomDump", package: "swift-snapshot-testing"), - .product(name: "StructuredQueries", package: "swift-structured-queries"), - ] - ), - ], - swiftLanguageModes: [.v6] -) - -let swiftSettings: [SwiftSetting] = [ - .enableUpcomingFeature("MemberImportVisibility") - // .unsafeFlags([ - // "-Xfrontend", - // "-warn-long-function-bodies=50", - // "-Xfrontend", - // "-warn-long-expression-type-checking=50", - // ]) -] - -for index in package.targets.indices { - package.targets[index].swiftSettings = swiftSettings -} - -#if !os(Windows) - // Add the documentation compiler plugin if possible - package.dependencies.append( - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") - ) -#endif diff --git a/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift b/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift index 6e9eacd9..7387b693 100644 --- a/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift +++ b/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift @@ -1,4 +1,6 @@ -#if canImport(CloudKit) +// NB: Swift 6.3-dev compiler crashes on the generic `open` function in _update. +// Tracking: https://github.com/doozMen/sqlite-data/issues/2 +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import CryptoKit import StructuredQueriesCore @@ -283,10 +285,29 @@ typealias EquatableCKRecordValueProtocol = CKRecordValueProtocol & Equatable self.userModificationTime = other.userModificationTime - for column in T.TableColumns.writableColumns { + _update( + with: other, + row: row, + columnNames: &columnNames, + parentForeignKey: parentForeignKey, + columns: T.TableColumns.writableColumns + ) + } + + private func _update( + with other: CKRecord, + row: T, + columnNames: inout [String], + parentForeignKey: ForeignKey?, + columns: [any WritableTableColumnExpression] + ) { + typealias EquatableCKRecordValueProtocol = CKRecordValueProtocol & Equatable + + for column in columns { func open(_ column: some WritableTableColumnExpression) { let key = column.name - let keyPath = column.keyPath as! KeyPath + let column = column as! any WritableTableColumnExpression + let keyPath = column.keyPath let didSet: Bool if let value = other[key] as? CKAsset { didSet = setAsset(value, forKey: key, at: other.encryptedValues[at: key]) diff --git a/Sources/SQLiteData/CloudKit/CloudKitSharing.swift b/Sources/SQLiteData/CloudKit/CloudKitSharing.swift index 09f4181b..dda920be 100644 --- a/Sources/SQLiteData/CloudKit/CloudKitSharing.swift +++ b/Sources/SQLiteData/CloudKit/CloudKitSharing.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import Dependencies import SwiftUI diff --git a/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift b/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift index 9a2c27be..2eaf080a 100644 --- a/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/DefaultSyncEngine.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import Dependencies diff --git a/Sources/SQLiteData/CloudKit/Internal/CloudContainer.swift b/Sources/SQLiteData/CloudKit/Internal/CloudContainer.swift index 4829102f..b0cc5191 100644 --- a/Sources/SQLiteData/CloudKit/Internal/CloudContainer.swift +++ b/Sources/SQLiteData/CloudKit/Internal/CloudContainer.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) diff --git a/Sources/SQLiteData/CloudKit/Internal/CloudDatabase.swift b/Sources/SQLiteData/CloudKit/Internal/CloudDatabase.swift index 94a0dc61..76da5142 100644 --- a/Sources/SQLiteData/CloudKit/Internal/CloudDatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/CloudDatabase.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit package protocol CloudDatabase: AnyObject, Hashable, Sendable { diff --git a/Sources/SQLiteData/CloudKit/Internal/CloudKitFunctions.swift b/Sources/SQLiteData/CloudKit/Internal/CloudKitFunctions.swift index 5d166e16..64545208 100644 --- a/Sources/SQLiteData/CloudKit/Internal/CloudKitFunctions.swift +++ b/Sources/SQLiteData/CloudKit/Internal/CloudKitFunctions.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import Foundation diff --git a/Sources/SQLiteData/CloudKit/Internal/DataManager.swift b/Sources/SQLiteData/CloudKit/Internal/DataManager.swift index 0bf280c4..27f848c8 100644 --- a/Sources/SQLiteData/CloudKit/Internal/DataManager.swift +++ b/Sources/SQLiteData/CloudKit/Internal/DataManager.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) && canImport(CryptoKit) +#if canImport(CloudKit) && canImport(CryptoKit) && !compiler(>=6.3) import CryptoKit import Dependencies import Foundation diff --git a/Sources/SQLiteData/CloudKit/Internal/DatetimeGenerator.swift b/Sources/SQLiteData/CloudKit/Internal/DatetimeGenerator.swift index dd161e50..675babfb 100644 --- a/Sources/SQLiteData/CloudKit/Internal/DatetimeGenerator.swift +++ b/Sources/SQLiteData/CloudKit/Internal/DatetimeGenerator.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import Dependencies import Foundation diff --git a/Sources/SQLiteData/CloudKit/Internal/DefaultNotificationCenter.swift b/Sources/SQLiteData/CloudKit/Internal/DefaultNotificationCenter.swift index d1423e4e..3fca7742 100644 --- a/Sources/SQLiteData/CloudKit/Internal/DefaultNotificationCenter.swift +++ b/Sources/SQLiteData/CloudKit/Internal/DefaultNotificationCenter.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) && canImport(UIKit) +#if canImport(CloudKit) && canImport(UIKit) && !compiler(>=6.3) import UIKit private enum DefaultNotificationCenterKey: DependencyKey { diff --git a/Sources/SQLiteData/CloudKit/Internal/ForeignKey.swift b/Sources/SQLiteData/CloudKit/Internal/ForeignKey.swift index ae329bb6..d8bec02e 100644 --- a/Sources/SQLiteData/CloudKit/Internal/ForeignKey.swift +++ b/Sources/SQLiteData/CloudKit/Internal/ForeignKey.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import Foundation import StructuredQueriesCore diff --git a/Sources/SQLiteData/CloudKit/Internal/IsolatedWeakVar.swift b/Sources/SQLiteData/CloudKit/Internal/IsolatedWeakVar.swift index 4a17ef04..28674b35 100644 --- a/Sources/SQLiteData/CloudKit/Internal/IsolatedWeakVar.swift +++ b/Sources/SQLiteData/CloudKit/Internal/IsolatedWeakVar.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import Foundation final class IsolatedWeakVar: @unchecked Sendable { diff --git a/Sources/SQLiteData/CloudKit/Internal/Logging.swift b/Sources/SQLiteData/CloudKit/Internal/Logging.swift index b7d4b214..d0f7f059 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Logging.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Logging.swift @@ -1,4 +1,4 @@ -#if DEBUG && canImport(CloudKit) +#if DEBUG && canImport(CloudKit) && !compiler(>=6.3) import CloudKit import TabularData import os diff --git a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift index 8ea33c42..9dc19936 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import Foundation import os diff --git a/Sources/SQLiteData/CloudKit/Internal/MockCloudContainer.swift b/Sources/SQLiteData/CloudKit/Internal/MockCloudContainer.swift index f0421c82..65cb276a 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockCloudContainer.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockCloudContainer.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) diff --git a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift index 7edfeccb..17000d63 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import IssueReporting diff --git a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift index 31f1c02c..608a223f 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import OrderedCollections diff --git a/Sources/SQLiteData/CloudKit/Internal/PendingRecordZoneChange.swift b/Sources/SQLiteData/CloudKit/Internal/PendingRecordZoneChange.swift index fad64e0d..4772ad69 100644 --- a/Sources/SQLiteData/CloudKit/Internal/PendingRecordZoneChange.swift +++ b/Sources/SQLiteData/CloudKit/Internal/PendingRecordZoneChange.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit @Table("sqlitedata_icloud_pendingRecordZoneChanges") diff --git a/Sources/SQLiteData/CloudKit/Internal/Pragmas.swift b/Sources/SQLiteData/CloudKit/Internal/Pragmas.swift index e30a254f..798f58c2 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Pragmas.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Pragmas.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) @Table struct PragmaDatabaseList { static var tableAlias: String? { "databases" } diff --git a/Sources/SQLiteData/CloudKit/Internal/RecordType.swift b/Sources/SQLiteData/CloudKit/Internal/RecordType.swift index 4c610b80..ea440f28 100644 --- a/Sources/SQLiteData/CloudKit/Internal/RecordType.swift +++ b/Sources/SQLiteData/CloudKit/Internal/RecordType.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) @Table("sqlitedata_icloud_recordTypes") package struct RecordType: Hashable { @Column(primaryKey: true) diff --git a/Sources/SQLiteData/CloudKit/Internal/SQLiteSchema.swift b/Sources/SQLiteData/CloudKit/Internal/SQLiteSchema.swift index 40fa7d87..2d9b629d 100644 --- a/Sources/SQLiteData/CloudKit/Internal/SQLiteSchema.swift +++ b/Sources/SQLiteData/CloudKit/Internal/SQLiteSchema.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) @Table("sqlite_schema") package struct SQLiteSchema { package let type: ObjectType diff --git a/Sources/SQLiteData/CloudKit/Internal/StateSerialization.swift b/Sources/SQLiteData/CloudKit/Internal/StateSerialization.swift index 2b76b33b..5febee44 100644 --- a/Sources/SQLiteData/CloudKit/Internal/StateSerialization.swift +++ b/Sources/SQLiteData/CloudKit/Internal/StateSerialization.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import StructuredQueriesCore diff --git a/Sources/SQLiteData/CloudKit/Internal/SyncEngine.Event.swift b/Sources/SQLiteData/CloudKit/Internal/SyncEngine.Event.swift index 230aac02..12f42947 100644 --- a/Sources/SQLiteData/CloudKit/Internal/SyncEngine.Event.swift +++ b/Sources/SQLiteData/CloudKit/Internal/SyncEngine.Event.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) diff --git a/Sources/SQLiteData/CloudKit/Internal/SyncEngineProtocol+Live.swift b/Sources/SQLiteData/CloudKit/Internal/SyncEngineProtocol+Live.swift index d462e198..4055aaf6 100644 --- a/Sources/SQLiteData/CloudKit/Internal/SyncEngineProtocol+Live.swift +++ b/Sources/SQLiteData/CloudKit/Internal/SyncEngineProtocol+Live.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) diff --git a/Sources/SQLiteData/CloudKit/Internal/SyncEngineProtocol.swift b/Sources/SQLiteData/CloudKit/Internal/SyncEngineProtocol.swift index 58cff003..8857f998 100644 --- a/Sources/SQLiteData/CloudKit/Internal/SyncEngineProtocol.swift +++ b/Sources/SQLiteData/CloudKit/Internal/SyncEngineProtocol.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) diff --git a/Sources/SQLiteData/CloudKit/Internal/TableInfo.swift b/Sources/SQLiteData/CloudKit/Internal/TableInfo.swift index 2207ce94..415c4036 100644 --- a/Sources/SQLiteData/CloudKit/Internal/TableInfo.swift +++ b/Sources/SQLiteData/CloudKit/Internal/TableInfo.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import StructuredQueriesCore @Table diff --git a/Sources/SQLiteData/CloudKit/Internal/Triggers.swift b/Sources/SQLiteData/CloudKit/Internal/Triggers.swift index c0aa01b4..b0908649 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Triggers.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Triggers.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import Foundation diff --git a/Sources/SQLiteData/CloudKit/Internal/UnsyncedRecordID.swift b/Sources/SQLiteData/CloudKit/Internal/UnsyncedRecordID.swift index f24a3e04..d138c951 100644 --- a/Sources/SQLiteData/CloudKit/Internal/UnsyncedRecordID.swift +++ b/Sources/SQLiteData/CloudKit/Internal/UnsyncedRecordID.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import StructuredQueriesCore diff --git a/Sources/SQLiteData/CloudKit/Internal/UserDatabase.swift b/Sources/SQLiteData/CloudKit/Internal/UserDatabase.swift index b0fb5edc..1c651213 100644 --- a/Sources/SQLiteData/CloudKit/Internal/UserDatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/UserDatabase.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import Dependencies package struct UserDatabase { diff --git a/Sources/SQLiteData/CloudKit/Internal/_SendableMetatype.swift b/Sources/SQLiteData/CloudKit/Internal/_SendableMetatype.swift index 55ccae7f..28ea67c0 100644 --- a/Sources/SQLiteData/CloudKit/Internal/_SendableMetatype.swift +++ b/Sources/SQLiteData/CloudKit/Internal/_SendableMetatype.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) #if swift(>=6.2) public typealias _SendableMetatype = SendableMetatype #else diff --git a/Sources/SQLiteData/CloudKit/PrimaryKeyMigration.swift b/Sources/SQLiteData/CloudKit/PrimaryKeyMigration.swift index 9a97fb0b..9473c61c 100644 --- a/Sources/SQLiteData/CloudKit/PrimaryKeyMigration.swift +++ b/Sources/SQLiteData/CloudKit/PrimaryKeyMigration.swift @@ -1,104 +1,118 @@ -#if canImport(CloudKit) && canImport(CryptoKit) +#if canImport(CloudKit) && canImport(CryptoKit) && !compiler(>=6.3) import CryptoKit import Foundation - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - extension SyncEngine { - /// Migrates integer primary-keyed tables and tables without primary keys to - /// CloudKit-compatible, UUID primary keys. - /// - /// To synchronize a table to CloudKit it must have a primary key, and that primary key must - /// be a globally unique identifier, such as a UUID. However, changing the type of a column - /// in SQLite is a [multi-step process] that must be followed very carefully, otherwise you run - /// the risk of corrupting your users' data. - /// - /// [multi-step process]: https://sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes - /// - /// This method is a general purpose tool that analyzes a set of tables to try to automatically - /// perform that migration for you. It performs the following steps: - /// - /// * Computes a random salt to use for backfilling existing integer primary keys with UUIDs. - /// * For each table passed to this method: - /// * Creates a new table with essentially the same schema, but the following changes: - /// * A new temporary name is given to the table. - /// * If an integer primary key exists, it is changed to a "TEXT" column with a - /// "NOT NULL PRIMARY KEY ON CONFLICT REPLACE DEFAULT" constraint, and a default of - /// "uuid()" if no `uuid` argument is given, otherwise the argument is used. - /// * If no primary key exists, one is added with the same constraints as above. - /// * All integer foreign keys are changed to "TEXT" columns with no other changes. - /// * All data from the existing table is copied over into the new table, but all integer - /// IDs (both primary and foreign keys) are transformed into UUIDs by MD5 hashing the - /// integer, the table name, and the salt mentioned above, and turning that hash into a - /// UUID. - /// * The existing table is dropped. - /// * Thew new table is renamed to have the same name as the table just dropped. - /// * Any indexes and stored triggers that were removed from dropping tables in the steps - /// above are recreated. - /// * Executes a "PRAGMA foreign_key_check;" query to make sure that the integrity of the data - /// is preserved. - /// - /// If all of those steps are performed without throwing an error, then your schema and data - /// should have been successfully migrated to UUIDs. If an error is thrown for any reason, - /// then it means the tool was not able to safely migrate your data and so you will need to - /// perform the migration [manually](). - /// - /// - Parameters: - /// - db: A database connection. - /// - tables: Tables to migrate. - /// - uuidFunction: A UUID function to use for the default value of primary keys in your - /// tables' schemas. If `nil`, SQLite's `uuid` function will be used. - public static func migratePrimaryKeys( - _ db: Database, - tables: repeat (each T).Type, - dropUniqueConstraints: Bool = false, - uuid uuidFunction: (any ScalarDatabaseFunction<(), UUID>)? = nil - ) throws - where - repeat (each T).PrimaryKey.QueryOutput: IdentifierStringConvertible, - repeat (each T).TableColumns.PrimaryColumn: TableColumnExpression - { - let salt = - (try uuidFunction.flatMap { uuid -> UUID? in - try #sql("SELECT \(quote: uuid.name)()", as: UUID.self).fetchOne(db) - } - ?? UUID()).uuidString + // NB: Swift 6.2.3 and 6.3-dev crash when compiling the primary key migration feature + // due to a compiler bug with #sql macro interpolation combined with $backfillUUID + // (macro-generated database function). + // + // This entire feature is disabled on Swift 6.2.3+ until the compiler bug is fixed. + // See detailed comments below in the PrimaryKeyedTable extension. + // + // Tracking: https://github.com/doozMen/sqlite-data/issues/2 + // TODO: Re-enable when Swift 6.3 stabilizes or compiler bug is fixed. + #if !compiler(>=6.2.3) + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + extension SyncEngine { + /// Migrates integer primary-keyed tables and tables without primary keys to + /// CloudKit-compatible, UUID primary keys. + /// + /// To synchronize a table to CloudKit it must have a primary key, and that primary key must + /// be a globally unique identifier, such as a UUID. However, changing the type of a column + /// in SQLite is a [multi-step process] that must be followed very carefully, otherwise you run + /// the risk of corrupting your users' data. + /// + /// [multi-step process]: https://sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes + /// + /// This method is a general purpose tool that analyzes a set of tables to try to automatically + /// perform that migration for you. It performs the following steps: + /// + /// * Computes a random salt to use for backfilling existing integer primary keys with UUIDs. + /// * For each table passed to this method: + /// * Creates a new table with essentially the same schema, but the following changes: + /// * A new temporary name is given to the table. + /// * If an integer primary key exists, it is changed to a "TEXT" column with a + /// "NOT NULL PRIMARY KEY ON CONFLICT REPLACE DEFAULT" constraint, and a default of + /// "uuid()" if no `uuid` argument is given, otherwise the argument is used. + /// * If no primary key exists, one is added with the same constraints as above. + /// * All integer foreign keys are changed to "TEXT" columns with no other changes. + /// * All data from the existing table is copied over into the new table, but all integer + /// IDs (both primary and foreign keys) are transformed into UUIDs by MD5 hashing the + /// integer, the table name, and the salt mentioned above, and turning that hash into a + /// UUID. + /// * The existing table is dropped. + /// * Thew new table is renamed to have the same name as the table just dropped. + /// * Any indexes and stored triggers that were removed from dropping tables in the steps + /// above are recreated. + /// * Executes a "PRAGMA foreign_key_check;" query to make sure that the integrity of the data + /// is preserved. + /// + /// If all of those steps are performed without throwing an error, then your schema and data + /// should have been successfully migrated to UUIDs. If an error is thrown for any reason, + /// then it means the tool was not able to safely migrate your data and so you will need to + /// perform the migration [manually](). + /// + /// - Note: This method is unavailable on Swift 6.2.3+ due to a compiler bug. + /// See https://github.com/doozMen/sqlite-data/issues/2 + /// + /// - Parameters: + /// - db: A database connection. + /// - tables: Tables to migrate. + /// - uuidFunction: A UUID function to use for the default value of primary keys in your + /// tables' schemas. If `nil`, SQLite's `uuid` function will be used. + public static func migratePrimaryKeys( + _ db: Database, + tables: repeat (each T).Type, + dropUniqueConstraints: Bool = false, + uuid uuidFunction: (any ScalarDatabaseFunction<(), UUID>)? = nil + ) throws + where + repeat (each T).PrimaryKey.QueryOutput: IdentifierStringConvertible, + repeat (each T).TableColumns.PrimaryColumn: TableColumnExpression + { + let salt = + (try uuidFunction.flatMap { uuid -> UUID? in + try #sql("SELECT \(quote: uuid.name)()", as: UUID.self).fetchOne(db) + } + ?? UUID()).uuidString - db.add(function: $backfillUUID) - defer { db.remove(function: $backfillUUID) } + db.add(function: $backfillUUID) + defer { db.remove(function: $backfillUUID) } - var migratedTableNames: [String] = [] - for table in repeat each tables { - migratedTableNames.append(table.tableName) - } - let indicesAndTriggersSQL = - try SQLiteSchema - .select(\.sql) - .where { - $0.tableName.in(migratedTableNames) - && $0.type.in([#bind(.index), #bind(.trigger)]) - && $0.sql.isNot(nil) + var migratedTableNames: [String] = [] + for table in repeat each tables { + migratedTableNames.append(table.tableName) + } + let indicesAndTriggersSQL = + try SQLiteSchema + .select(\.sql) + .where { + $0.tableName.in(migratedTableNames) + && $0.type.in([#bind(.index), #bind(.trigger)]) + && $0.sql.isNot(nil) + } + .fetchAll(db) + .compactMap(\.self) + for table in repeat each tables { + try table.migratePrimaryKeyToUUID( + db: db, + dropUniqueConstraints: dropUniqueConstraints, + uuidFunction: uuidFunction, + migratedTableNames: migratedTableNames, + salt: salt + ) + } + for sql in indicesAndTriggersSQL { + try #sql(QueryFragment(stringLiteral: sql)).execute(db) } - .fetchAll(db) - .compactMap(\.self) - for table in repeat each tables { - try table.migratePrimaryKeyToUUID( - db: db, - dropUniqueConstraints: dropUniqueConstraints, - uuidFunction: uuidFunction, - migratedTableNames: migratedTableNames, - salt: salt - ) - } - for sql in indicesAndTriggersSQL { - try #sql(QueryFragment(stringLiteral: sql)).execute(db) - } - let foreignKeyChecks = try PragmaForeignKeyCheck.all.fetchAll(db) - if !foreignKeyChecks.isEmpty { - throw ForeignKeyCheckError(checks: foreignKeyChecks) + let foreignKeyChecks = try PragmaForeignKeyCheck.all.fetchAll(db) + if !foreignKeyChecks.isEmpty { + throw ForeignKeyCheckError(checks: foreignKeyChecks) + } } } - } + #endif private struct MigrationError: LocalizedError { let reason: Reason @@ -124,108 +138,127 @@ } } - @available(iOS 16, macOS 13, tvOS 13, watchOS 9, *) - extension PrimaryKeyedTable where TableColumns.PrimaryColumn: TableColumnExpression { - fileprivate static func migratePrimaryKeyToUUID( - db: Database, - dropUniqueConstraints: Bool, - uuidFunction: (any ScalarDatabaseFunction<(), UUID>)? = nil, - migratedTableNames: [String], - salt: String - ) throws { - let schema = - try SQLiteSchema - .select(\.sql) - .where { $0.type.eq(#bind(.table)) && $0.tableName.eq(tableName) } - .fetchOne(db) - ?? nil - - guard let schema - else { - throw MigrationError(reason: .tableNotFound(tableName)) - } + // NB: Swift 6.2.3 and 6.3-dev crash when compiling this extension due to a compiler bug + // with #sql macro interpolation combined with $backfillUUID (macro-generated database + // function) in complex control flow. + // + // Error: "Assertion failed: (Start.isValid() == End.isValid() && 'Start and end should + // either both be valid or both be invalid!'), function SourceRange, file SourceLoc.h" + // + // This is a CloudKit-specific migration feature that: + // 1. Is only relevant on Apple platforms (CloudKit doesn't exist on Linux/Android) + // 2. Is used for migrating existing integer primary keys to UUID primary keys + // 3. Is not needed for new databases or cross-platform builds + // + // Workaround: Disable on Swift 6.2.3+ until the compiler bug is fixed. + // Tracking: https://github.com/doozMen/sqlite-data/issues/2 + // TODO: Re-enable when Swift 6.3 stabilizes or compiler bug is fixed. + #if !compiler(>=6.2.3) + @available(iOS 16, macOS 13, tvOS 13, watchOS 9, *) + extension PrimaryKeyedTable where TableColumns.PrimaryColumn: TableColumnExpression { + fileprivate static func migratePrimaryKeyToUUID( + db: Database, + dropUniqueConstraints: Bool, + uuidFunction: (any ScalarDatabaseFunction<(), UUID>)? = nil, + migratedTableNames: [String], + salt: String + ) throws { + let schema = + try SQLiteSchema + .select(\.sql) + .where { $0.type.eq(#bind(.table)) && $0.tableName.eq(tableName) } + .fetchOne(db) + ?? nil + + guard let schema + else { + throw MigrationError(reason: .tableNotFound(tableName)) + } - let tableInfo = try PragmaTableInfo.all.fetchAll(db) - let primaryKeys = tableInfo.filter(\.isPrimaryKey) - guard - (primaryKeys.count == 1 && primaryKeys[0].isInt) - || primaryKeys.isEmpty - else { - throw MigrationError(reason: .invalidPrimaryKey) - } - guard primaryKeys.count <= 1 - else { - throw MigrationError(reason: .invalidPrimaryKey) - } + let tableInfo = try PragmaTableInfo.all.fetchAll(db) + let primaryKeys = tableInfo.filter(\.isPrimaryKey) + guard + (primaryKeys.count == 1 && primaryKeys[0].isInt) + || primaryKeys.isEmpty + else { + throw MigrationError(reason: .invalidPrimaryKey) + } + guard primaryKeys.count <= 1 + else { + throw MigrationError(reason: .invalidPrimaryKey) + } - let foreignKeys = try PragmaForeignKeyList.all.fetchAll(db) - guard foreignKeys.allSatisfy({ migratedTableNames.contains($0.table) }) - else { - throw MigrationError(reason: .invalidForeignKey) - } + let foreignKeys = try PragmaForeignKeyList.all.fetchAll(db) + guard foreignKeys.allSatisfy({ migratedTableNames.contains($0.table) }) + else { + throw MigrationError(reason: .invalidForeignKey) + } - let newTableName = "new_\(tableName)" - let uuidFunction = uuidFunction?.name ?? "uuid" - let newSchema = try schema.rewriteSchema( - dropUniqueConstraints: dropUniqueConstraints, - oldPrimaryKey: primaryKeys.first?.name, - newPrimaryKey: columns.primaryKey.name, - foreignKeys: foreignKeys.map(\.from), - uuidFunction: uuidFunction - ) + let newTableName = "new_\(tableName)" + let uuidFunction = uuidFunction?.name ?? "uuid" + let newSchema = try schema.rewriteSchema( + dropUniqueConstraints: dropUniqueConstraints, + oldPrimaryKey: primaryKeys.first?.name, + newPrimaryKey: columns.primaryKey.name, + foreignKeys: foreignKeys.map(\.from), + uuidFunction: uuidFunction + ) - var newColumns: [String] = [] - var convertedColumns: [QueryFragment] = [] - if primaryKeys.first == nil { - convertedColumns.append("NULL") - newColumns.append(columns.primaryKey.name) - } - newColumns.append(contentsOf: tableInfo.map(\.name)) - convertedColumns.append( - contentsOf: tableInfo.map { tableInfo -> QueryFragment in - if tableInfo.name == primaryKey.name, tableInfo.isInt { - return $backfillUUID(id: #sql("\(quote: tableInfo.name)"), table: tableName, salt: salt) + var newColumns: [String] = [] + var convertedColumns: [QueryFragment] = [] + if primaryKeys.first == nil { + convertedColumns.append("NULL") + newColumns.append(columns.primaryKey.name) + } + newColumns.append(contentsOf: tableInfo.map(\.name)) + convertedColumns.append( + contentsOf: tableInfo.map { tableInfo -> QueryFragment in + if tableInfo.name == primaryKey.name, tableInfo.isInt { + return $backfillUUID( + id: #sql("\(quote: tableInfo.name)"), table: tableName, salt: salt + ) .queryFragment - } else if tableInfo.isInt, - let foreignKey = foreignKeys.first(where: { $0.from == tableInfo.name }) - { - return $backfillUUID( - id: #sql("\(quote: foreignKey.from)"), - table: foreignKey.table, - salt: salt - ) - .queryFragment - } else { - return QueryFragment(quote: tableInfo.name) + } else if tableInfo.isInt, + let foreignKey = foreignKeys.first(where: { $0.from == tableInfo.name }) + { + return $backfillUUID( + id: #sql("\(quote: foreignKey.from)"), + table: foreignKey.table, + salt: salt + ) + .queryFragment + } else { + return QueryFragment(quote: tableInfo.name) + } } - } - ) + ) - try #sql(QueryFragment(stringLiteral: newSchema)).execute(db) - try #sql( - """ - INSERT INTO \(quote: newTableName) \ - ("rowid", \(newColumns.map { "\(quote: $0)" }.joined(separator: ", "))) - SELECT "rowid", \(convertedColumns.joined(separator: ", ")) \ - FROM \(Self.self) - ORDER BY "rowid" - """ - ) - .execute(db) - try #sql( - """ - DROP TABLE \(Self.self) - """ - ) - .execute(db) - try #sql( - """ - ALTER TABLE \(quote: newTableName) RENAME TO \(Self.self) - """ - ) - .execute(db) + try #sql(QueryFragment(stringLiteral: newSchema)).execute(db) + try #sql( + """ + INSERT INTO \(quote: newTableName) \ + ("rowid", \(newColumns.map { "\(quote: $0)" }.joined(separator: ", "))) + SELECT "rowid", \(convertedColumns.joined(separator: ", ")) \ + FROM \(Self.self) + ORDER BY "rowid" + """ + ) + .execute(db) + try #sql( + """ + DROP TABLE \(Self.self) + """ + ) + .execute(db) + try #sql( + """ + ALTER TABLE \(quote: newTableName) RENAME TO \(Self.self) + """ + ) + .execute(db) + } } - } + #endif extension StringProtocol { fileprivate func quoted() -> String { diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 76bbbb71..0dcd48ab 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import ConcurrencyExtras import Dependencies @@ -166,7 +166,7 @@ database: container.privateCloudDatabase, stateSerialization: try? metadatabase.read { db in try StateSerialization - .find(#bind(.private)) + .where { $0.scope == #bind(.private) } .select(\.data) .fetchOne(db) }, @@ -178,7 +178,7 @@ database: container.sharedCloudDatabase, stateSerialization: try? metadatabase.read { db in try StateSerialization - .find(#bind(.shared)) + .where { $0.scope == #bind(.shared) } .select(\.data) .fetchOne(db) }, diff --git a/Sources/SQLiteData/CloudKit/SyncEngineDelegate.swift b/Sources/SQLiteData/CloudKit/SyncEngineDelegate.swift index b373dfc4..60c16adb 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngineDelegate.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngineDelegate.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit import CustomDump diff --git a/Sources/SQLiteData/CloudKit/SyncMetadata.swift b/Sources/SQLiteData/CloudKit/SyncMetadata.swift index 2d64e7a4..683f544a 100644 --- a/Sources/SQLiteData/CloudKit/SyncMetadata.swift +++ b/Sources/SQLiteData/CloudKit/SyncMetadata.swift @@ -1,4 +1,4 @@ -#if canImport(CloudKit) +#if canImport(CloudKit) && !compiler(>=6.3) import CloudKit /// A table that tracks metadata related to synchronized data. diff --git a/Sources/SQLiteData/FetchSubscription.swift b/Sources/SQLiteData/FetchSubscription.swift new file mode 100644 index 00000000..9ce8f893 --- /dev/null +++ b/Sources/SQLiteData/FetchSubscription.swift @@ -0,0 +1,46 @@ +import ConcurrencyExtras +import Sharing + +/// A subscription associated with `@FetchAll`, `@FetchOne`, and `@Fetch` observation. +/// +/// This value can be useful in associating the lifetime of observing a query to the lifetime of a +/// SwiftUI view _via_ the `task` view modifier. For example, loading a query in a view's `task` +/// will automatically cancel the observation when drilling down into a child view, and restart +/// observation when popping back to the view: +/// +/// ```swift +/// .task { +/// try? await $reminders.load(Reminder.all).task +/// } +/// ``` +public struct FetchSubscription: Sendable { + let cancellable = LockIsolated?>(nil) + let onCancel: @Sendable () -> Void + + init(sharedReader: SharedReader) { + onCancel = { sharedReader.projectedValue = SharedReader(value: sharedReader.wrappedValue) } + } + + /// An async handle to the given fetch observation. + /// + /// This handle will suspend until the current task is cancelled, at which point it will terminate + /// the observation of the associated ``FetchAll``, ``FetchOne``, or ``Fetch``. + public var task: Void { + get async throws { + let task = Task { + try await withTaskCancellationHandler { + try await Task.never() + } onCancel: { + onCancel() + } + } + cancellable.withValue { $0 = task } + try await task.cancellableValue + } + } + + /// Cancels the database observation of the associated ``FetchAll``, ``FetchOne``, or ``Fetch``. + public func cancel() { + cancellable.value?.cancel() + } +} diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift b/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift new file mode 100644 index 00000000..025a2cba --- /dev/null +++ b/Sources/SQLiteData/StructuredQueries+GRDB/Table+GRDB.swift @@ -0,0 +1,63 @@ +import StructuredQueriesCore + +extension StructuredQueriesCore.Table { + /// Returns an array of all values fetched from the database. + /// + /// - Parameter db: A database connection. + /// - Returns: An array of all values decoded from the database. + @inlinable + public static func fetchAll(_ db: Database) throws -> [QueryOutput] { + try all.fetchAll(db) + } + + /// Returns a single value fetched from the database. + /// + /// - Parameter db: A database connection. + /// - Returns: A single value decoded from the database. + @inlinable + public static func fetchOne(_ db: Database) throws -> QueryOutput? { + try all.fetchOne(db) + } + + /// Returns the number of rows fetched by the query. + /// + /// - Parameter db: A database connection. + /// - Returns: The number of rows fetched by the query. + @inlinable + public static func fetchCount(_ db: Database) throws -> Int { + try all.fetchCount(db) + } + + /// Returns a cursor to all values fetched from the database. + /// + /// - Parameter db: A database connection. + /// - Returns: A cursor to all values decoded from the database. + @inlinable + public static func fetchCursor(_ db: Database) throws -> QueryCursor { + try all.fetchCursor(db) + } +} + +// NB: Swift 6.2.3 and 6.3-dev guard Select.find(_:) in swift-structured-queries due to compiler crashes. +// This extension depends on that method, so it must also be guarded. +// Tracking: https://github.com/doozMen/sqlite-data/issues/2 +#if !compiler(>=6.2.3) + extension StructuredQueriesCore.PrimaryKeyedTable { + /// Returns a single value fetched from the database for a given primary key. + /// + /// - Parameters + /// - db: A database connection. + /// - primaryKey: A primary key identifying a table row. + /// - Returns: A single value decoded from the database. + @inlinable + public static func find( + _ db: Database, + key primaryKey: some QueryExpression + ) throws -> QueryOutput { + guard let record = try Self.all.find(primaryKey).fetchOne(db) else { + throw NotFound() + } + return record + } + } +#endif