Video #351: Tour of SQLiteData: Previews
Episode: Video #351 Date: Jan 19, 2026 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep351-tour-of-sqlitedata-previews

Description
We’ve extended the tour with a few bonus episodes that show how SQLiteData integrates with Xcode previews and tests! No need to painstakingly mock your persistence layer: previews actually hit the database, and the library automatically supplies a mock CloudKit sync engine so you can easily preview how iCloud sharing looks in your UI.
Video
Cloudflare Stream video ID: 1920dd9949494cf5f7054b6e3efc0d98 Local file: video_351_tour-of-sqlitedata-previews.mp4 *(download with --video 351)*
References
- Discussions
- SQLiteData
- StructuredQueries
- 0351-sqlite-data-tour-pt5
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
Last week we concluded a 4-part tour of our powerful SQLiteData library. In that series we created a “Scorekeeper” app that allows one to create games, in each game create players, assign images to those players, change the score for each player, and sort the players by their scores. Stephen
— 0:23
This moderately complex app allowed us to explore most of the core aspects of persistence in the real world and it helped to see how SQLiteData can really shine. With SQLiteData we were able to easily persist and query for our data using type-safe and schema-safe tools. We even stored assets directly in SQLite. Further, all of the data in the database seamlessly synchronized to each device of the user with basically no additional work. And we showed how with just a few lines of code we could even make it possible to share records with other iCloud users for collaboration. Brandon
— 0:55
And all of that was really cool, but once it was all over we realized that are two really important topics that we did not cover, and they both very closely related. We would like to take a few more episodes to show how one can get the most out of Xcode previews when using SQLiteData and then we want to show how you can still write unit tests on your app’s logic. Stephen
— 1:17
We are going to start with Xcode previews. Previews are an amazing tool to help you rapidly iterate on the design and behavior of your views, but when you start integrating complex things like synchronization and record sharing into your features it is surprisingly easy to essentially break your previews. After all, in previews there is no access to iCloud servers or any of the CloudKit infrastructure, and so if we implemented that functionality into SQLiteData naively we might make it so that your app is no longer previewable. Brandon
— 1:43
Well, luckily for us we took a lot of our time perfecting these tools to work well with previews. We even built an in-memory replica of CloudKit’s cloud database API specifically so that the sync engine could continue running during previews and tests, which can give us a lot of confidence that things will behave correctly in production if they behave correctly in previews and tests. And we even recently made improvements to these tools that we will get to show off in this episode.
— 2:14
So, let’s get started with Xcode previews! Xcode previews
— 2:20
I have the Scorekeeper Xcode project open right now, and this is the exact state of the app we built during our tour of SQLiteData. It is even pinned to a slightly older version of SQLiteData, which is 1.4.3, and Swift Dependencies, because that is what we used during those episodes. Since then we have released new versions with improvements to how the sync engine and dependencies work in previews and tests, and we will update to the newest in just a moment.
— 2:46
But before doing that, let’s just try running the preview for the games list feature: Fatal Error in ScorekeeperApp.swift Scorekeeper crashed due to fatalError in ScorekeeperApp.swift at line 12. ‘try!’ expression unexpectedly raised an error: SQLite error 1: no such table: games - while executing CREATE TEMPORARY TRIGGER IF NOT EXISTS "sqlitedata_icloud_after_insert_on_games" AFTER INSERT ON "games" FOR EACH ROW BEGIN WITH "rootShares" AS (SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") IS (NULL)) AND (("sqlitedata_icloud_metadata"."recordType") IS (NULL)) UNION ALL SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" FROM "sqlitedata_icloud_metadata" JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName") IS ("rootShares"."parentRecordName")) SELECT RAISE(ABORT, 'co.pointfree.SQLiteData.CloudKit.write-permission-error') FROM "rootShares" WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'games', coalesce("sqlitedata_icloud_currentZoneName"(), 'co.pointfree.SQLiteData.defaultZone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; END
— 2:56
Well, sadly we get a crash in the preview.
— 3:08
This crash actually has nothing to do with SQLiteData, and is instead a known quirk of Xcode previews. While trying to run the preview for this one screen, Xcode is secretly running the entry point of the app in the background. That means its even trying to prepare a database and start up a sync engine.
— 3:38
This is something we fixed recently, along with a bunch of other improvements, so let’s update SQLiteData to its newest version, which is 1.5, and Swift Dependencies, which is 1.10.1…
— 4:06
And finally our preview is now working. We can add and delete games, update scores, and do basically anything.
— 4:39
But remember, a huge piece of functionality we added at the end of our tour was the ability for our users to share a game with other iCloud users so that they can collaborate on the game. We of course have no hope of testing all of that functionality in a preview since it involves having multiple devices running at the same time.
— 5:10
However , there was a critical bit of functionality that we added for iCloud sharing that should be possible to view in previews. And that is when a game is shared, we showed a little networking icon next to the game title. We chose something quite simple just to get the job done last episode, but what if we wanted to take a bit more time to style shared games? Are we really going to be relegated to running the app in the simulator and having a shared game with another iCloud user? That seems like a pain.
— 6:01
Well, luckily for us the SyncEngine was built in a way that uses a custom, in-memory version of CloudKit that we built so that these kinds of interactions can be tested in situations where one does not have access to iCloud servers, such as previews and tests. Our “mock” CloudKit can even emulate sharing of iCloud records, in the sense that records can be marked as shared in the sync metadata, and we even went as far as emulating the cloud sharing view!
— 6:50
To see this, let’s drill down to a game and tap the share icon. A sheet pops up that roughly looks like the cloud sharing view we would see when running in the simulator. However, this is a fake version of that view provided by SQLiteData that is only used in Xcode previews.
— 7:21
The act of tapping on the share icon has recorded that the game we are viewing is shared, and if we dismiss the sheet, and go back to the list of games, we will even see that the network icon has appeared next to the game. This is because a share has been recorded in our mock CloudKit, and that share was sent to the sync engine, which stored the data in its local SyncMetadata , and that instantly made the icon appear. We can even emulate stopping the sharing of this record.
— 7:50
It may not seem like it, but the fact that we were able to interact with iCloud sharing in a preview and see our views react to records being shared is kind of amazing. None of CloudKit’s APIs work in previews, whether that been modifying or fetching records from iCloud, let alone something as complex as iCloud sharing working.
— 8:15
But the reason we are able to see basic interactions with CloudKit working in previews is because we have meticulously abstracted away the interfaces for core CloudKit APIs, such as CKSyncEngine , CKContainer and CKDatabase , so that SQLiteData’s tools interact with those interfaces rather than the concrete APIs. And further, we created our own versions of all of those tools that work in-memory and emulate how the real tools behave.
— 8:48
This allows us to to run the sync engine in sandboxed environments, such as previews and tests. And while you technically do not need to be aware of these underpinnings to use SQLiteData successfully, we would be remiss if we didn’t expose our viewers to these concepts at least a little.
— 9:07
So let’s dive into the underbelly of SQLiteData to see how these things work.
— 9:12
If we look at SQLiteData’s SyncEngine we will see that it doesn’t interact directly with any of CloudKit’s APIs, such as CKSyncEngine or CKContainer . Instead it refers to protocol existentials that have abstracted away those interfaces, such as the SyncEngineProtocol : let defaultSyncEngines: @Sendable (any DatabaseReader, SyncEngine) -> ( private: any SyncEngineProtocol, shared: any SyncEngineProtocol )
— 9:32
And this protocol exposes the minimal interface that we need in our tools: @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) package protocol SyncEngineProtocol< Database, State >: AnyObject, Sendable { associatedtype State: CKSyncEngineStateProtocol associatedtype Database: CloudDatabase var database: Database { get } var state: State { get } func cancelOperations() async func fetchChanges( _ options: CKSyncEngine.FetchChangesOptions ) async throws func recordZoneChangeBatch( pendingChanges: [CKSyncEngine.PendingRecordZoneChange], recordProvider: @Sendable (CKRecord.ID) async -> CKRecord? ) async -> CKSyncEngine.RecordZoneChangeBatch? func sendChanges( _ options: CKSyncEngine.SendChangesOptions ) async throws }
— 9:41
Things like the underlying cloud database the sync engine interacts with, as well as endpoints for fetching changes, sending changes, and computing the next batch of local records that can be sent to the database.
— 9:55
There is also a protocol to abstract away CKDatabase , which is CloudKit’s interface to modifying and fetching records from iCloud, and it’s called CloudDatabase : package protocol CloudDatabase: AnyObject, Hashable, Sendable { var databaseScope: CKDatabase.Scope { get } func record( for recordID: CKRecord.ID ) async throws -> CKRecord @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) func records( for ids: [CKRecord.ID], desiredKeys: [CKRecord.FieldKey]? ) async throws -> [ CKRecord.ID: Result<CKRecord, any Error> ] @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) func modifyRecords( saving recordsToSave: [CKRecord], deleting recordIDsToDelete: [CKRecord.ID], savePolicy: CKModifyRecordsOperation.RecordSavePolicy, atomically: Bool ) async throws -> ( saveResults: [CKRecord.ID: Result<CKRecord, any Error>], deleteResults: [CKRecord.ID: Result<Void, any Error>] ) @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) func modifyRecordZones( saving recordZonesToSave: [CKRecordZone], deleting recordZoneIDsToDelete: [CKRecordZone.ID] ) async throws -> ( saveResults: [ CKRecordZone.ID: Result<CKRecordZone, any Error> ], deleteResults: [ CKRecordZone.ID: Result<Void, any Error> ] ) }
— 10:02
It provides us the basics for fetching and modifying records in the database.
— 10:17
And there’s even a protocol to abstract CKContainer , which is CloudKit’s interface to a pair of private and shared cloud databases, and facilitates sharing records and accepting shares: @available(iOS 15, tvOS 15, macOS 12, watchOS 8, *) package protocol CloudContainer<Database>: AnyObject, Equatable, Hashable, Sendable { associatedtype Database: CloudDatabase func accountStatus() async throws -> CKAccountStatus var containerIdentifier: String? { get } var rawValue: CKContainer { get } var privateCloudDatabase: Database { get } func accept( _ metadata: ShareMetadata ) async throws -> CKShare static func createContainer( identifier containerIdentifier: String ) -> Self var sharedCloudDatabase: Database { get } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) func shareMetadata( for share: CKShare, shouldFetchRootRecord: Bool ) async throws -> ShareMetadata }
— 10:36
All of these protocols are used in SQLiteData’s SyncEngine instead of the concrete types. That means we are able to create mock versions of these concepts and plug them into the sync engine, and as long as the mocks behave somewhat realistically we will have the ability to run the sync engine in isolation.
— 10:56
And it’s in the creation of these mocks that the vast majority of our work was been done. We have taken many, many hours to read CloudKit’s documentation to understand all of its subtle nuances, and we have directly invoked CloudKit’s APIs in a variety of scenarios to confirm the documentation is correct, and find new, undocumented behavior in iCloud.
— 11:21
We codified all of that information into a rather beefy MockCloudDatabase that holds all of its data in memory so that it’s easy to interact with and discard when necessary: @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) package final class MockCloudDatabase: CloudDatabase { package let state = LockIsolated(State()) package let databaseScope: CKDatabase.Scope let _container = IsolatedWeakVar<MockCloudContainer>() let dataManager = Dependency(\.dataManager) package struct State { private var lastRecordChangeTag = 0 package var storage: [CKRecordZone.ID: Zone] = [:] var assets: [AssetID: Data] = [:] var deletedRecords: [(CKRecord.ID, CKRecord.RecordType)] = [] mutating func nextRecordChangeTag() -> Int { lastRecordChangeTag += 1 return lastRecordChangeTag } } struct AssetID: Hashable { let recordID: CKRecord.ID let key: String } package struct Zone { package var zone: CKRecordZone package var records: [CKRecord.ID: CKRecord] = [:] } … }
— 11:39
It holds onto a dictionary mapping zone IDs to zones, which is in turn a dictionary mapping record IDs to records. The mock database also handles assets which allows us to write tests that SQLiteData properly moves large binary blobs to temporary locations on disk and correctly tells the sync engine where to grab those files to upload. This database even handles record change tags so that we can write tests on conflict resolution just as it would work when hitting iCloud’s live servers.
— 11:59
In fact, we can even see how this works by seeing where the serverRecordChanged CKError is thrown: case ( .some(let existingRecord), .some(let recordToSaveChangeTag) ): // We are trying to save a record with a change tag that // also already exists in the DB. If the tags match, we // can save the record. Otherwise, we notify the sync // engine that the server record has changed since it was // last synced. if existingRecord._recordChangeTag == recordToSaveChangeTag { precondition(existingRecord._recordChangeTag != nil) saveRecordToDatabase() } else { saveResults[recordToSave.recordID] = .failure( CKError( .serverRecordChanged, userInfo: [ CKRecordChangedErrorServerRecordKey: existingRecord.copy() as Any, CKRecordChangedErrorClientRecordKey: recordToSave.copy(), ] ) ) } break
— 12:09
Here we check if the existing record in the database has the same recordChangeTag as the record being saved, and if it does we save the record. And if it does not we emit an error, and even attach copies of the server and client records so that conflict resolution on the client can be carried out.
— 12:46
Our mocks are so complex that they require a test suite of their own! We can go to MockCloudDatabaseTests.swift to see a whole bunch of tests for various scenarios: func fetchRecordInUnknownZone() func fetchUnknownRecord() func saveTransaction_ChildBeforeParent() func saveTransaction_ChildNoParent() func saveInUnknownZone() func deleteTransaction_ParentBeforeChild() func deleteUnknownRecord() func deleteRecordInUnknownZone() func deleteTransaction_DeleteParentButNotChild() func deleteUnknownZone() func accountTemporarilyAvailable() func noAccount() func accountNotDetermined() func restrictedAccount() func saveShareWithoutRootRecord() func saveShareAndRootThenSaveShareAlone() func saveRecordThatWasPreviouslyDeleted() func saveSharedRecordWithoutParent() func deletingShareOwnedByCurrentUserDeletesShareAndDoesNotDeleteAssociatedData() func deletingShareNotOwnedByCurrentUserDeletesOnlyShareAndNotAssociatedRecords() func batchRequestFailed() func limitExceeded_modifyRecords() func records_limitExceeded()
— 13:02
There are some really nuanced tests in here, such as the test that what happens when the owner of a share deletes the share versus when just a participant deletes it, as well as tests for how atomic transactions work.
— 13:18
And you may have heard somewhere that testing mocks is bad, but that is referring to something different from this. When people say that they mean that if you so fully mock out large swaths of functionality in your features you run the risk of literally just testing the code inside your mocks rather than the logic in your feature.
— 13:37
But in this situation right here, the system we are mocking is so complex, and we have tried our hardest to replicate it as faithfully as possible, that the mocked object itself warrants its own test suite. Without this we could subtly break its functionality, such as the nuanced recordChangeTag logic, and then all of our tests against the SQLiteData SyncEngine would be interacting with a mirage of CloudKit that was incorrect, allowing us to implement features that would not actually work when targeting the real CloudKit APIs.
— 14:10
And the really cool thing about all of these mocks, is that the SyncEngine will automatically use them under the hood whenever it is initialized in a non-live context: guard context == .live else { let privateDatabase = MockCloudDatabase( databaseScope: .private ) let sharedDatabase = MockCloudDatabase( databaseScope: .shared ) let container = MockCloudContainer( containerIdentifier: containerIdentifier ?? "iCloud.co.pointfree.SQLiteData.Tests", privateCloudDatabase: privateDatabase, sharedCloudDatabase: sharedDatabase ) privateDatabase.set(container: container) sharedDatabase.set(container: container) try self.init( container: container, defaultZone: defaultZone, defaultSyncEngines: { _, syncEngine in ( private: MockSyncEngine( database: privateDatabase, parentSyncEngine: syncEngine, state: MockSyncEngineState() ), shared: MockSyncEngine( database: sharedDatabase, parentSyncEngine: syncEngine, state: MockSyncEngineState() ) ) }, userDatabase: userDatabase, logger: logger, delegate: delegate, tables: allTables, privateTables: allPrivateTables ) try setUpSyncEngine() if startImmediately { _ = try start() } return } This means anytime you create a SyncEngine in tests or previews you will get a version of the sync engine that does not hit iCloud directly, and instead interacts with a fully in-memory version of CloudKit. Better iteration in previews
— 15:09
There is no way to sugar coat this: we simply could not have built SQLiteData without all of this testing infrastructure in place. The library is so complex we could not possibly keep every little edge case in our minds at once, and this is the kind of library we plan on maintaining for many years to come, and so we always need a reference for how the sync engine is intended to work. Stephen
— 15:30
Each time we experienced behavior that we did not expect in the sync engine, or when one of our community members would report a bug, we would first write a test against the mocks to see if there was some behavior of CloudKit that we had not yet captured. After that we would write a test against the SyncEngine to see if we could reproduce the incorrect behavior there. And so far, every single bug report has been capable of being distilled with this process, and that has enabled us to find and fix bugs quickly, and deploy releases in a matter of days or sometimes hours. Brandon
— 16:00
And the really amazing part is that even though we were primarily driven to doing all of this work to mock our interactions with CloudKit, a wonderful side benefit is that it instantly unlocks better Xcode previews. We’ve already seen that the basics of sharing a record works in previews. We can drill down to a game, share it, and then go back to the games list to see that the view has updated to show the little networking icon next to the game. Stephen
— 16:26
But it is a bit of a bummer that each time we make a small cosmetic change to our view we have to repeat that process, over and over: drill down, share game, pop back, see result, make a change, drill down, share game, pop back, see result.
— 16:39
Well, amazingly, there is a better way, and it’s just a matter of invoking sync engine APIs directly in the preview just as you would in your feature code.
— 16:47
Let’s take a look.
— 16:50
Just to show the pain again, let’s run the preview, drill down to a game, share it, dismiss the share sheet, and then pop back to see the little networking icon. And say I decided that the network icon isn’t really clear enough to users that that game is shared. I want to update it to instead show a “Share” label under the title of the game: VStack { Text(row.game.title) .font(.headline) if row.isShared { Text("Shared") } }
— 17:23
Well, to see this change I have no choice but to drill down to the game, share it again, dismiss the sheet, pop back, and now I can see the result. And oops, I forgot to align the VStack , so I have to make that change: VStack(alignment: .leading) { … }
— 17:39
…and again drill down to the game, share, dismiss sheet, pop back, and verify. Looks better, but it would probably be better to de-emphasize the “Shared” label and make it gray: Text("Shared") .foregroundStyle(.gray)
— 17:57
…and once again drill down, share, dismiss, pop, verify.
— 18:03
This is of course a pain, and there is a better way. But before showing the better way, I want to first make a comment on an alternative way that kind gets the job done, but in our opinion is not worth the trouble.
— 18:20
A pattern that we see a lot in the SwiftUI community is to extract out little pieces of logic-less views and preview those directly. That way you can just feed whatever data you want directly to the view to preview it. So, in our app, that might mean to extract out the row view into its own dedicated SwiftUI View conformance that holds onto simple data: struct GameRowView: View { let title: String let isShared: Bool let playerCount: Int var body: some View { HStack { VStack(alignment: .leading) { Text(title) .font(.headline) if isShared { Text("Shared") .foregroundStyle(.gray) } } Spacer() Text("\(playerCount)") Image(systemName: "person.2.fill") .foregroundStyle(.gray) } } }
— 18:44
And then we can preview this row directly to see what it looks like: #Preview { List { GameRowView( title: "Weekly poker night", isShared: true, playerCount: 10 ) } }
— 18:55
We personally do not think this is the way to go. And it’s not that we think it isn’t a good idea to break large views into smaller ones, or that it’s a bad idea to pass simple data to views. All of that is fine, and you should absolutely be doing that.
— 19:07
The part we have a problem with is this right here: #Preview { List { GameRowView( title: "Weekly poker night", isShared: true, playerCount: 10 ) } }
— 19:09
We are bypassing all of the logic and behavior of our feature in order to just forcibly cram whatever data we want into this view. That means higher up in the view hierarchy we could have serious bugs that we are not notified of because we aren’t exercising any of that behavior in the view.
— 19:25
So, if you want to factorize your GamesView into this separate GameRowView , then go right ahead! But we are still going to strive to make the preview for our GamesView usable without resorting to previewing this inert view in isolation.
— 19:41
To make one of the games showing in the preview shared we will invoke the actual APIs in the preview that we would use in our feature code. In particular, the share(record:) method defined on the SyncEngine .
— 19:53
That means we need access to the sync engine in the preview, and it may seem weird but we can declare directly in the #Preview macro: #Preview { @Dependency(\.defaultSyncEngine) var syncEngine … }
— 20:01
We can even mark it as @Previewable so that the #Preview macro hoists it up to be stored properties on the SwiftUI view it creates under the hood: #Preview { @Previewable @Dependency(\.defaultSyncEngine) var syncEngine … }
— 20:11
Then we can add a .task view modifier to the NavigationStack in the preview so that we can invoke the share(record:) method: NavigationStack { GamesView() } .task { syncEngine.share( record: <#PrimaryKeyedTable#>, configure: <#(CKShare) -> Void#> ) }
— 20:20
But to do this we need an actual record to share. So, let’s use a database connection: @Previewable @Dependency(\.defaultDatabase) var database
— 20:30
…to fetch any game from the database: let game = try! await database.read { db in try Game.fetchOne(db)! }
— 20:49
And now we can share that game: _ = try! await syncEngine.share(record: game) { _ in }
— 21:09
This unfortunately leads to a crash due to the share(record:) method throwing an error, but luckily for us this error is very descriptive: ‘try!’ expression unexpectedly raised an error: SQLiteData.SyncEngine.(unknown context at $340b6f420).SharingError(recordTableName: Optional("games"), recordPrimaryKey: Optional("00000000-0000-0000-0000-000000000001"), reason: SQLiteData.SyncEngine.(unknown context at $340b6f420).SharingError.Reason.recordMetadataNotFound, debugDescription: "No sync metadata found for record. Has the record been saved to the database and synchronized to iCloud? You can invoke \'SyncEngine.syncChanges()\' to force synchronization.")
— 21:36
In order to share a record it must first be sync’d to CloudKit, and that even includes when the mock version of CloudKit is used in previews. The mock version of CloudKit automatically synchronizes records on a 1 second timer, but we don’t want to wait around for that process to kick in. Instead, we can just tell the sync engine to send its changes immediately: try! await syncEngine.sendChanges() And with that done we can see in the preview that the first game is already shared. We didn’t have to do anything extra in the preview, and with every single change to our view will update the preview, and that game will already be shared.
— 21:56
This sendChanges method is the exact same method we have used in past episodes when we showed how to force the sync engine to immediately synchronize any pending changes. This was useful for implementing a pull-to-refresh interaction, but it’s also useful right here in previews.
— 22:22
For example, suppose I want to combine our designs by showing the network icon and “Shared” label: Text("\(Image(systemName: "network")) Shared")
— 22:47
If you didn’t know that SwiftUI images can be interpolated into text views, well now you do. And we get instant feedback in the preview that this actually works. No need to actually click around in the preview to re-share the game. Improving the games list
— 23:00
We’ve now seen that we can “share” records over iCloud directly in a preview, making it possible to see how our UI changes when certain games are shared. And I say “share” in quotes because of course the data is not actually being shared with some external system. Instead, there is a whole world of mocked CloudKit services running inside the sync engine that emulate how CloudKit behaves when running on devices. Thanks to those mocks we get to test nearly ever facet of our feature in the preview without resorting to patterns like breaking down our view into little inert units just to get previews working. Brandon
— 23:32
And now that it’s so easy to preview our data when iCloud sharing is involved, let’s perform a more complex refactor to our games list. Instead of just showing a simple “Shared” label below the title of the games that are shared, let’s fully separate all private games from the shared games. This will make it much clearer which games are shared and which are not. And along the way we will share a few tips to make working with previews even nicer.
— 24:01
Let’s get started.
— 24:04
What if instead of querying for games as just one single array of rows, we split the query into two: one that fetches private games and the other fetches shared games: // @FetchAll var rows: [Row] @FetchAll var privateRows: [Row] @FetchAll var sharedRows: [Row]
— 24:23
Then in the body of the view we can create two sections for the private and shared rows: List { if !privateRows.isEmpty { Section { } header: { Text("Private games") } } if !sharedRows.isEmpty { Section { } header: { Text("Shared games") } } }
— 24:55
But we of course don’t want to just copy-and-paste a ForEach for the rows into each of these sections. This is a great use for factoring out a tiny view with the essentials of the ForEach so that we can use it in two places: struct GamesSection: View { @Dependency(\.defaultDatabase) var database let rows: [GamesView.Row] var body: some View { ForEach(rows, id: \.game.id) { row in NavigationLink { GameView(game: row.game) } label: { GameRowView( title: row.game.title, isShared: row.isShared, playerCount: row.playerCount ) } } .onDelete { offsets in withErrorReporting { try database.write { db in try Game.find(offsets.map { rows[$0].game.id }) .delete() .execute(db) } } } } }
— 25:43
And now we can render the GamesSection twice in the list: List { if !privateRows.isEmpty { Section { GamesSection(rows: privateRows) } header: { Text("Private games") } } if !sharedRows.isEmpty { Section { GamesSection(rows: sharedRows) } header: { Text("Shared games") } } }
— 25:55
And finally we need to update the task view modifier that is responsible for loading the query into $privateRows and $sharedRows . One added complication here is that we are currently awaiting the .task property because that allows us to tie the lifecycle of the database subscription to the lifecycle of the view. That means when we navigate away from the view we will stop observing changes to the database, which is great for performance.
— 26:36
But now that we are separating things into two @FetchAll s we will need to grab onto those tasks rather than awaiting them, and then await them once the queries have been loaded: .task { await withErrorReporting { let privateTask = try await $privateRows.load( Game .group(by: \.id) .leftJoin(Player.all) { $0.id.eq($1.gameID) } .order { $1.count().desc() } .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($2.id) } .where { !$2.isShared.ifnull(false) } .select { Row.Columns( game: $0, isShared: $2.isShared.ifnull(false), playerCount: $1.count() ) }, animation: .default ) let sharedTask = try await $sharedRows.load( Game .group(by: \.id) .leftJoin(Player.all) { $0.id.eq($1.gameID) } .order { $1.count().desc() } .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($2.id) } .where { $2.isShared.ifnull(false) } .select { Row.Columns( game: $0, isShared: $2.isShared.ifnull(false), playerCount: $1.count() ) }, animation: .default ) try await privateTask.task try await sharedTask.task } }
— 28:25
And there is of course a lot of repetition in this code, but it can be made a lot more succinct. The two queries we are constructing are identical except for the one single where clause. So we can extract out the base of the query into a variable so that it can be reused: .task { await withErrorReporting { let baseQuery = Game .group(by: \.id) .leftJoin(Player.all) { $0.id.eq($1.gameID) } .order { $1.count().desc() } .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($2.id) } .select { Row.Columns( game: $0, isShared: $2.isShared.ifnull(false), playerCount: $1.count() ) } let privateTask = try await $privateRows.load( baseQuery .where { !$2.isShared.ifnull(false) }, animation: .default ) let sharedTask = try await $sharedRows.load( baseQuery .where { $2.isShared.ifnull(false) }, animation: .default ) try await privateTask.task try await sharedTask.task } }
— 29:37
That is much nicer. And if we run the preview we will see everything works. The preview starts with one game already shared. We can drill down to that game and *un-*share it, and then pop back to the root to see that now no games are shared. And of course we can drill down into some other game, share it, and then pop back to see that that game moved down to the shared games.
— 30:10
This is pretty amazing, but there are 2 quick things we can do to improve our experience as developers using previews. First, if we open the logs for the preview we will see that there is a ton of stuff printed that is related to the sync engine. To see just how bad this can be, let’s clear the logs, and then add a new game. The mere act of doing that has spewed out all of this into our logs: BEGIN IMMEDIATE TRANSACTION INSERT INTO "games" ("id", "title") VALUES (NULL, 'Asdfasdf') -- TRIGGER sqlitedata_icloud_after_insert_on_games -- WITH "rootShares" AS (SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") IS (NULL)) AND (("sqlitedata_icloud_metadata"."recordType") IS (NULL)) UNION ALL SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" FROM "sqlitedata_icloud_metadata" JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName") IS ("rootShares"."parentRecordName")) SELECT RAISE(ABORT, 'co.pointfree.SQLiteData.CloudKit.write-permission-error') FROM "rootShares" WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))) -- INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'games', coalesce("sqlitedata_icloud_currentZoneName"(), 'co.pointfree.SQLiteData.defaultZone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING -- TRIGGER sqlitedata_icloud_after_insert_on_sqlitedata_icloud_metadata -- SELECT RAISE(ABORT, 'co.pointfree.SQLiteData.CloudKit.invalid-record-name-error') WHERE NOT (((substr("new"."recordName", 1, 1)) <> ('_')) AND ((octet_length("new"."recordName")) <= (255))) AND ((octet_length("new"."recordName")) = (length("new"."recordName"))) -- SELECT "sqlitedata_icloud_didUpdate"("new"."recordName", "new"."zoneName", "new"."ownerName", "new"."zoneName", "new"."ownerName", NULL) COMMIT TRANSACTION PRAGMA query_only = 1 BEGIN DEFERRED TRANSACTION SELECT "games"."id" AS "id", "games"."title" AS "title", ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) AS "isShared", count("players"."id") AS "playerCount" FROM "games" LEFT JOIN "players" ON ("games"."id") = ("players"."gameID") LEFT JOIN "sqlitedata_icloud_metadata" ON ("games"."id", 'games') = ("sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType") WHERE NOT (ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0)) GROUP BY "games"."id" ORDER BY count("players"."id") DESC COMMIT TRANSACTION PRAGMA query_only = 0 PRAGMA query_only = 1 BEGIN DEFERRED TRANSACTION SELECT "games"."id" AS "id", "games"."title" AS "title", ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) AS "isShared", count("players"."id") AS "playerCount" FROM "games" LEFT JOIN "players" ON ("games"."id") = ("players"."gameID") LEFT JOIN "sqlitedata_icloud_metadata" ON ("games"."id", 'games') = ("sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType") WHERE ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) GROUP BY "games"."id" ORDER BY count("players"."id") DESC COMMIT TRANSACTION PRAGMA query_only = 0 BEGIN DEFERRED TRANSACTION PRAGMA schema_version SELECT "games"."id", "games"."title" FROM "games" WHERE "games"."id" = '8d062021-bec3-449c-a16d-7e0b2844701b' LIMIT 1 COMMIT TRANSACTION BEGIN IMMEDIATE TRANSACTION SELECT "sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType", "sqlitedata_icloud_metadata"."zoneName", "sqlitedata_icloud_metadata"."ownerName", "sqlitedata_icloud_metadata"."recordName", "sqlitedata_icloud_metadata"."parentRecordPrimaryKey", "sqlitedata_icloud_metadata"."parentRecordType", "sqlitedata_icloud_metadata"."parentRecordName", "sqlitedata_icloud_metadata"."lastKnownServerRecord", "sqlitedata_icloud_metadata"."_lastKnownServerRecordAllFields", "sqlitedata_icloud_metadata"."share", "sqlitedata_icloud_metadata"."_isDeleted", "sqlitedata_icloud_metadata"."hasLastKnownServerRecord", "sqlitedata_icloud_metadata"."isShared", "sqlitedata_icloud_metadata"."userModificationTime" FROM "sqlitedata_icloud_metadata" WHERE ((("sqlitedata_icloud_metadata"."recordName") = ('8d062021-bec3-449c-a16d-7e0b2844701b:games')) AND (("sqlitedata_icloud_metadata"."zoneName") = ('co.pointfree.SQLiteData.defaultZone'))) AND (("sqlitedata_icloud_metadata"."ownerName") = ('__defaultOwner__')) LIMIT 1 UPDATE "sqlitedata_icloud_metadata" SET "zoneName" = 'co.pointfree.SQLiteData.defaultZone', "ownerName" = '__defaultOwner__', "lastKnownServerRecord" = x'62706c6973743030d40102030405060762582476657273696f6e592461726368697665725424746f7058246f626a6563747312000186a05f100f4e534b657965644172636869766572df102d08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353635383535383535353538383535353846353535353535353535353835353535383535383538355d5e3538355f1016546f6d6273746f6e65645075626c69634b65794944735f101948617355706461746564506172656e745265666572656e63655f1013436861696e50726f74656374696f6e446174615d4b6e6f776e546f5365727665725f1011446973706c61796564486f73746e616d655942617365546f6b656e5f101057616e7473436861696e5043534b65795b5265636f72644374696d655a526f7574696e674b65795f101250726f74656374696f6e44617461457461675e45787069726174696f6e446174655f10194d6572676561626c6556616c756544656c74615265636f72645f101a4e65656473526f6c6c416e64436f756e7465725369676e4b65795f102650726576696f757350726f74656374696f6e446174614574616746726f6d556e6974546573745f1012436f6e666c6963744c6f73657245746167735f101a50726576696f757350726f74656374696f6e44617461457461675f10144861735570646174656445787069726174696f6e5a5265636f7264547970655f101343726561746f72557365725265636f726449445f100f506172656e745265666572656e636559536861726545746167585043534b657949445c5a6f6e656973684b657949445f10204d757461626c65456e637279707465645075626c696353686172696e674b657954455461675f101650726576696f757353686172655265666572656e63655f10104d6f64696669656442794465766963655e50726f74656374696f6e446174615f10115573654c696768747765696768745043535e53686172655265666572656e63655f10115570646174656445787069726174696f6e5355524c5f1016436861696e506172656e745075626c69634b6579494457457870697265645f10184c6173744d6f646966696564557365725265636f726449445b5265636f72644d74696d655f101557616e74735075626c696353686172696e674b65795f10165a6f6e6550726f74656374696f6e4461746145746167595761734361636865645f100f436861696e507269766174654b65795a5065726d697373696f6e585265636f726449445c416c6c5043534b65794944735f10184861735570646174656453686172655265666572656e63655f101750726576696f7573506172656e745265666572656e636580000980000880008000088000800080008000080880008000800008800180008000800080008000800080008000800080000880008000800080000880008000088000088000100080028000088000a96364656c6d7677787f55246e756c6c5567616d6573d3666768696a6b5624636c6173735a5265636f72644e616d65565a6f6e6549448008800380045f102a38643036323032312d626563332d343439632d613136642d3765306232383434373031623a67616d6573d56e6f7071665d357374755f1010646174616261736553636f70654b65795f1011616e6f6e796d6f7573434b557365724944596f776e65724e616d65585a6f6e654e616d6580008006800580075f1023636f2e706f696e74667265652e53514c697465446174612e64656661756c745a6f6e655f10105f5f64656661756c744f776e65725f5fd2797a7b7c5a24636c6173736e616d655824636c61737365735e434b5265636f72645a6f6e654944a27d7e5e434b5265636f72645a6f6e654944584e534f626a656374d2797a80815a434b5265636f72644944a2827e5a434b5265636f7264494400080011001a0024002900320037004900a600bf00db00f100ff0113011d0130013c0147015c016b018701a401cd01e201ff02160221023702490253025c0269028c029102aa02bd02cc02e002ef03030307032003280343034f03670380038a039c03a703b003bd03d803f203f403f503f703f803fa03fc03fd03ff040104030405040604070409040b040d040e04100412041404160418041a041c041e042004220424042504270429042b042d042e043004320433043504360438043a043c043e043f0441044b04510457045e0465047004770479047b047d04aa04b504c804dc04e604ef04f104f304f504f7051d05300535054005490558055b056a05730578058305860000000000000201000000000000008300000000000000000000000000000591', "_lastKnownServerRecordAllFields" = x'62706c6973743030d400010002000300040005000600070068582476657273696f6e592461726368697665725424746f7058246f626a6563747312000186a05f100f4e534b657965644172636869766572df103000080009000a000b000c000d000e000f0010001100120013001400150016001700180019001a001b001c001d001e001f0020002100220023002400250026002700280029002a002b002c002d002e002f00300031003200330034003500360037003800390038003b00380038003b0038003800380038003b003b003800380038003b00490038003800380038003800380038003800380038003b0038003800380038003b00380038003b005d005e00380038003b0038006300640038003b00385f1016546f6d6273746f6e65645075626c69634b65794944735f101948617355706461746564506172656e745265666572656e63655f1013436861696e50726f74656374696f6e446174615d4b6e6f776e546f5365727665725f1011446973706c61796564486f73746e616d655942617365546f6b656e5f101057616e7473436861696e5043534b65795b5265636f72644374696d655a526f7574696e674b65795f101250726f74656374696f6e44617461457461675e45787069726174696f6e446174655f10194d6572676561626c6556616c756544656c74615265636f72645f101a4e65656473526f6c6c416e64436f756e7465725369676e4b65795f102650726576696f757350726f74656374696f6e446174614574616746726f6d556e6974546573745f1012436f6e666c6963744c6f73657245746167735f101a50726576696f757350726f74656374696f6e44617461457461675f10144861735570646174656445787069726174696f6e5a5265636f7264547970655f101343726561746f72557365725265636f726449445f100f506172656e745265666572656e636559536861726545746167585043534b657949445c5a6f6e656973684b657949445f10204d757461626c65456e637279707465645075626c696353686172696e674b657954455461675f101650726576696f757353686172655265666572656e63655f10104d6f64696669656442794465766963655e50726f74656374696f6e446174615f10115573654c696768747765696768745043535e53686172655265666572656e63655f10115570646174656445787069726174696f6e5355524c5f1016436861696e506172656e745075626c69634b6579494457457870697265645f10184c6173744d6f646966696564557365725265636f726449445b5265636f72644d74696d655f101557616e74735075626c696353686172696e674b65795f1013456e6372797074656456616c756553746f72655a56616c756553746f72655c506c7567696e4669656c64735f10165a6f6e6550726f74656374696f6e4461746145746167595761734361636865645f100f436861696e507269766174654b65795a5065726d697373696f6e585265636f726449445c416c6c5043534b65794944735f10184861735570646174656453686172655265666572656e63655f101750726576696f7573506172656e745265666572656e636580000980000880008000088000800080008000080880008000800008800180008000800080008000800080008000800080000880008000800080000880008000088010800980008000088000100080028000088000af10280069006a006b00720073007c007d007e0085008900910097009c00a000a300a800ac00b100bf00c000c100c200c300c400c800c900ce00d100d200d700da00db00de00df00e200e300f100f300f600fe55246e756c6c5567616d6573d3006c006d006e006f007000715624636c6173735a5265636f72644e616d65565a6f6e6549448008800380045f102a38643036323032312d626563332d343439632d613136642d3765306232383434373031623a67616d6573d50074007500760077006c006300380079007a007b5f1010646174616261736553636f70654b65795f1011616e6f6e796d6f7573434b557365724944596f776e65724e616d65585a6f6e654e616d6580008006800580075f1023636f2e706f696e74667265652e53514c697465446174612e64656661756c745a6f6e655f10105f5f64656661756c744f776e65725f5fd2007f0080008100825a24636c6173736e616d655824636c61737365735e434b5265636f72645a6f6e654944a2008300845e434b5265636f72645a6f6e654944584e534f626a656374d2007f0080008600875a434b5265636f72644944a2008800845a434b5265636f72644944d4008a008b008c006c008d008e008f00905b4368616e6765644b6579735e4f726967696e616c56616c7565735c5265636f726456616c756573800d800c800a800fd300920093006c009400950096574e532e6b6579735a4e532e6f626a65637473a0a0800bd2007f0080009800995f10134e534d757461626c6544696374696f6e617279a3009a009b00845f10134e534d757461626c6544696374696f6e6172795c4e5344696374696f6e617279d300920093006c009d009e0096a0a0800bd20093006c00a100a2a0800ed2007f008000a400a55c4e534d757461626c65536574a300a600a700845c4e534d757461626c65536574554e53536574d2007f008000a900aa5f1012434b5265636f726456616c756553746f7265a200ab00845f1012434b5265636f726456616c756553746f7265d4008a008b008c006c00ad00ae00af00b08026802380118027d300920093006c00b200b80096a500b300b400b500b600b780128013'/*+1250 bytes*/, "userModificationTime" = 1768332077865461000 WHERE ((("sqlitedata_icloud_metadata"."recordName") = ('8d062021-bec3-449c-a16d-7e0b2844701b:games')) AND (("sqlitedata_icloud_metadata"."zoneName") = ('co.pointfree.SQLiteData.defaultZone'))) AND (("sqlitedata_icloud_metadata"."ownerName") = ('__defaultOwner__')) -- TRIGGER sqlitedata_icloud_after_update_on_sqlitedata_icloud_metadata -- TRIGGER sqlitedata_icloud_after_zone_update_on_sqlitedata_icloud_metadata COMMIT TRANSACTION BEGIN IMMEDIATE TRANSACTION SELECT "sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType", "sqlitedata_icloud_metadata"."zoneName", "sqlitedata_icloud_metadata"."ownerName", "sqlitedata_icloud_metadata"."recordName", "sqlitedata_icloud_metadata"."parentRecordPrimaryKey", "sqlitedata_icloud_metadata"."parentRecordType", "sqlitedata_icloud_metadata"."parentRecordName", "sqlitedata_icloud_metadata"."lastKnownServerRecord", "sqlitedata_icloud_metadata"."_lastKnownServerRecordAllFields", "sqlitedata_icloud_metadata"."share", "sqlitedata_icloud_metadata"."_isDeleted", "sqlitedata_icloud_metadata"."hasLastKnownServerRecord", "sqlitedata_icloud_metadata"."isShared", "sqlitedata_icloud_metadata"."userModificationTime" FROM "sqlitedata_icloud_metadata" WHERE ((("sqlitedata_icloud_metadata"."recordName") = ('8d062021-bec3-449c-a16d-7e0b2844701b:games')) AND (("sqlitedata_icloud_metadata"."zoneName") = ('co.pointfree.SQLiteData.defaultZone'))) AND (("sqlitedata_icloud_metadata"."ownerName") = ('__defaultOwner__')) LIMIT 1 UPDATE "sqlitedata_icloud_metadata" SET "zoneName" = 'co.pointfree.SQLiteData.defaultZone', "ownerName" = '__defaultOwner__', "lastKnownServerRecord" = x'62706c6973743030d40102030405060762582476657273696f6e592461726368697665725424746f7058246f626a6563747312000186a05f100f4e534b657965644172636869766572df102d08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353635383535383535353538383535353846353535353535353535353835353535383535383538355d5e3538355f1016546f6d6273746f6e65645075626c69634b65794944735f101948617355706461746564506172656e745265666572656e63655f1013436861696e50726f74656374696f6e446174615d4b6e6f776e546f5365727665725f1011446973706c61796564486f73746e616d655942617365546f6b656e5f101057616e7473436861696e5043534b65795b5265636f72644374696d655a526f7574696e674b65795f101250726f74656374696f6e44617461457461675e45787069726174696f6e446174655f10194d6572676561626c6556616c756544656c74615265636f72645f101a4e65656473526f6c6c416e64436f756e7465725369676e4b65795f102650726576696f757350726f74656374696f6e446174614574616746726f6d556e6974546573745f1012436f6e666c6963744c6f73657245746167735f101a50726576696f757350726f74656374696f6e44617461457461675f10144861735570646174656445787069726174696f6e5a5265636f7264547970655f101343726561746f72557365725265636f726449445f100f506172656e745265666572656e636559536861726545746167585043534b657949445c5a6f6e656973684b657949445f10204d757461626c65456e637279707465645075626c696353686172696e674b657954455461675f101650726576696f757353686172655265666572656e63655f10104d6f64696669656442794465766963655e50726f74656374696f6e446174615f10115573654c696768747765696768745043535e53686172655265666572656e63655f10115570646174656445787069726174696f6e5355524c5f1016436861696e506172656e745075626c69634b6579494457457870697265645f10184c6173744d6f646966696564557365725265636f726449445b5265636f72644d74696d655f101557616e74735075626c696353686172696e674b65795f10165a6f6e6550726f74656374696f6e4461746145746167595761734361636865645f100f436861696e507269766174654b65795a5065726d697373696f6e585265636f726449445c416c6c5043534b65794944735f10184861735570646174656453686172655265666572656e63655f101750726576696f7573506172656e745265666572656e636580000980000880008000088000800080008000080880008000800008800180008000800080008000800080008000800080000880008000800080000880008000088000088000100080028000088000a96364656c6d7677787f55246e756c6c5567616d6573d3666768696a6b5624636c6173735a5265636f72644e616d65565a6f6e6549448008800380045f102a38643036323032312d626563332d343439632d613136642d3765306232383434373031623a67616d6573d56e6f7071665d357374755f1010646174616261736553636f70654b65795f1011616e6f6e796d6f7573434b557365724944596f776e65724e616d65585a6f6e654e616d6580008006800580075f1023636f2e706f696e74667265652e53514c697465446174612e64656661756c745a6f6e655f10105f5f64656661756c744f776e65725f5fd2797a7b7c5a24636c6173736e616d655824636c61737365735e434b5265636f72645a6f6e654944a27d7e5e434b5265636f72645a6f6e654944584e534f626a656374d2797a80815a434b5265636f72644944a2827e5a434b5265636f7264494400080011001a0024002900320037004900a600bf00db00f100ff0113011d0130013c0147015c016b018701a401cd01e201ff02160221023702490253025c0269028c029102aa02bd02cc02e002ef03030307032003280343034f03670380038a039c03a703b003bd03d803f203f403f503f703f803fa03fc03fd03ff040104030405040604070409040b040d040e04100412041404160418041a041c041e042004220424042504270429042b042d042e043004320433043504360438043a043c043e043f0441044b04510457045e0465047004770479047b047d04aa04b504c804dc04e604ef04f104f304f504f7051d05300535054005490558055b056a05730578058305860000000000000201000000000000008300000000000000000000000000000591', "_lastKnownServerRecordAllFields" = x'62706c6973743030d400010002000300040005000600070068582476657273696f6e592461726368697665725424746f7058246f626a6563747312000186a05f100f4e534b657965644172636869766572df103000080009000a000b000c000d000e000f0010001100120013001400150016001700180019001a001b001c001d001e001f0020002100220023002400250026002700280029002a002b002c002d002e002f00300031003200330034003500360037003800390038003b00380038003b0038003800380038003b003b003800380038003b00490038003800380038003800380038003800380038003b0038003800380038003b00380038003b005d005e00380038003b0038006300640038003b00385f1016546f6d6273746f6e65645075626c69634b65794944735f101948617355706461746564506172656e745265666572656e63655f1013436861696e50726f74656374696f6e446174615d4b6e6f776e546f5365727665725f1011446973706c61796564486f73746e616d655942617365546f6b656e5f101057616e7473436861696e5043534b65795b5265636f72644374696d655a526f7574696e674b65795f101250726f74656374696f6e44617461457461675e45787069726174696f6e446174655f10194d6572676561626c6556616c756544656c74615265636f72645f101a4e65656473526f6c6c416e64436f756e7465725369676e4b65795f102650726576696f757350726f74656374696f6e446174614574616746726f6d556e6974546573745f1012436f6e666c6963744c6f73657245746167735f101a50726576696f757350726f74656374696f6e44617461457461675f10144861735570646174656445787069726174696f6e5a5265636f7264547970655f101343726561746f72557365725265636f726449445f100f506172656e745265666572656e636559536861726545746167585043534b657949445c5a6f6e656973684b657949445f10204d757461626c65456e637279707465645075626c696353686172696e674b657954455461675f101650726576696f757353686172655265666572656e63655f10104d6f64696669656442794465766963655e50726f74656374696f6e446174615f10115573654c696768747765696768745043535e53686172655265666572656e63655f10115570646174656445787069726174696f6e5355524c5f1016436861696e506172656e745075626c69634b6579494457457870697265645f10184c6173744d6f646966696564557365725265636f726449445b5265636f72644d74696d655f101557616e74735075626c696353686172696e674b65795f1013456e6372797074656456616c756553746f72655a56616c756553746f72655c506c7567696e4669656c64735f10165a6f6e6550726f74656374696f6e4461746145746167595761734361636865645f100f436861696e507269766174654b65795a5065726d697373696f6e585265636f726449445c416c6c5043534b65794944735f10184861735570646174656453686172655265666572656e63655f101750726576696f7573506172656e745265666572656e636580000980000880008000088000800080008000080880008000800008800180008000800080008000800080008000800080000880008000800080000880008000088014800980008000088000100080028000088000af102a0069006a006b00720073007c007d007e0085008900910099009a009b00a000a600a800ab00af00b400b800bd00cb00cc00cd00ce00cf00d000d400d500da00dd00de00e300e600e700ea00eb00ee00ef00fd010555246e756c6c5567616d6573d3006c006d006e006f007000715624636c6173735a5265636f72644e616d65565a6f6e6549448008800380045f102a38643036323032312d626563332d343439632d613136642d3765306232383434373031623a67616d6573d50074007500760077006c006300380079007a007b5f1010646174616261736553636f70654b65795f1011616e6f6e796d6f7573434b557365724944596f776e65724e616d65585a6f6e654e616d6580008006800580075f1023636f2e706f696e74667265652e53514c697465446174612e64656661756c745a6f6e655f10105f5f64656661756c744f776e65725f5fd2007f0080008100825a24636c6173736e616d655824636c61737365735e434b5265636f72645a6f6e654944a2008300845e434b5265636f72645a6f6e654944584e534f626a656374d2007f0080008600875a434b5265636f72644944a2008800845a434b5265636f72644944d4008a008b008c006c008d008e008f00905b4368616e6765644b6579735e4f726967696e616c56616c7565735c5265636f726456616c7565738011800e800a8013d300920093006c009400960098574e532e6b6579735a4e532e6f626a65637473a10095800ba10097800c800d5f10105f7265636f72644368616e67655461671009d2007f0080009c009d5f10134e534d757461626c6544696374696f6e617279a3009e009f00845f10134e534d757461626c6544696374696f6e6172795c4e5344696374696f6e617279d300920093006c00a100a30098a10095800ba100a4800f800dd1006c00a78010d2007f008000a900aa564e534e756c6ca200a90084d20093006c00ac00aea10095800b8012d2007f008000b000b15c4e534d757461626c65536574a300b200b300845c4e534d757461626c65536574554e53536574d2007f008000b500b65f1012434b5265636f726456616c756553746f7265a200b700845f'/*+1309 bytes*/, "userModificationTime" = 1768332077865461000 WHERE ((("sqlitedata_icloud_metadata"."recordName") = ('8d062021-bec3-449c-a16d-7e0b2844701b:games')) AND (("sqlitedata_icloud_metadata"."zoneName") = ('co.pointfree.SQLiteData.defaultZone'))) AND (("sqlitedata_icloud_metadata"."ownerName") = ('__defaultOwner__')) -- TRIGGER sqlitedata_icloud_after_update_on_sqlitedata_icloud_metadata -- TRIGGER sqlitedata_icloud_after_zone_update_on_sqlitedata_icloud_metadata COMMIT TRANSACTION BEGIN IMMEDIATE TRANSACTION COMMIT TRANSACTION BEGIN IMMEDIATE TRANSACTION SELECT "sqlitedata_icloud_unsyncedRecordIDs"."recordName", "sqlitedata_icloud_unsyncedRecordIDs"."zoneName", "sqlitedata_icloud_unsyncedRecordIDs"."ownerName" FROM "sqlitedata_icloud_unsyncedRecordIDs" COMMIT TRANSACTION BEGIN IMMEDIATE TRANSACTION INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType", "lastKnownServerRecord", "_lastKnownServerRecordAllFields", "share", "_isDeleted", "userModificationTime") VALUES ('8d062021-bec3-449c-a16d-7e0b2844701b', 'games', 'co.pointfree.SQLiteData.defaultZone', '__defaultOwner__', NULL, NULL, x'62706c6973743030d40102030405060762582476657273696f6e592461726368697665725424746f7058246f626a6563747312000186a05f100f4e534b657965644172636869766572df102d08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353635383535383535353538383535353846353535353535353535353835353535383535383538355d5e3538355f1016546f6d6273746f6e65645075626c69634b65794944735f101948617355706461746564506172656e745265666572656e63655f1013436861696e50726f74656374696f6e446174615d4b6e6f776e546f5365727665725f1011446973706c61796564486f73746e616d655942617365546f6b656e5f101057616e7473436861696e5043534b65795b5265636f72644374696d655a526f7574696e674b65795f101250726f74656374696f6e44617461457461675e45787069726174696f6e446174655f10194d6572676561626c6556616c756544656c74615265636f72645f101a4e65656473526f6c6c416e64436f756e7465725369676e4b65795f102650726576696f757350726f74656374696f6e446174614574616746726f6d556e6974546573745f1012436f6e666c6963744c6f73657245746167735f101a50726576696f757350726f74656374696f6e44617461457461675f10144861735570646174656445787069726174696f6e5a5265636f7264547970655f101343726561746f72557365725265636f726449445f100f506172656e745265666572656e636559536861726545746167585043534b657949445c5a6f6e656973684b657949445f10204d757461626c65456e637279707465645075626c696353686172696e674b657954455461675f101650726576696f757353686172655265666572656e63655f10104d6f64696669656442794465766963655e50726f74656374696f6e446174615f10115573654c696768747765696768745043535e53686172655265666572656e63655f10115570646174656445787069726174696f6e5355524c5f1016436861696e506172656e745075626c69634b6579494457457870697265645f10184c6173744d6f646966696564557365725265636f726449445b5265636f72644d74696d655f101557616e74735075626c696353686172696e674b65795f10165a6f6e6550726f74656374696f6e4461746145746167595761734361636865645f100f436861696e507269766174654b65795a5065726d697373696f6e585265636f726449445c416c6c5043534b65794944735f10184861735570646174656453686172655265666572656e63655f101750726576696f7573506172656e745265666572656e636580000980000880008000088000800080008000080880008000800008800180008000800080008000800080008000800080000880008000800080000880008000088000088000100080028000088000a96364656c6d7677787f55246e756c6c5567616d6573d3666768696a6b5624636c6173735a5265636f72644e616d65565a6f6e6549448008800380045f102a38643036323032312d626563332d343439632d613136642d3765306232383434373031623a67616d6573d56e6f7071665d357374755f1010646174616261736553636f70654b65795f1011616e6f6e796d6f7573434b557365724944596f776e65724e616d65585a6f6e654e616d6580008006800580075f1023636f2e706f696e74667265652e53514c697465446174612e64656661756c745a6f6e655f10105f5f64656661756c744f776e65725f5fd2797a7b7c5a24636c6173736e616d655824636c61737365735e434b5265636f72645a6f6e654944a27d7e5e434b5265636f72645a6f6e654944584e534f626a656374d2797a80815a434b5265636f72644944a2827e5a434b5265636f7264494400080011001a0024002900320037004900a600bf00db00f100ff0113011d0130013c0147015c016b018701a401cd01e201ff02160221023702490253025c0269028c029102aa02bd02cc02e002ef03030307032003280343034f03670380038a039c03a703b003bd03d803f203f403f503f703f803fa03fc03fd03ff040104030405040604070409040b040d040e04100412041404160418041a041c041e042004220424042504270429042b042d042e043004320433043504360438043a043c043e043f0441044b04510457045e0465047004770479047b047d04aa04b504c804dc04e604ef04f104f304f504f7051d05300535054005490558055b056a05730578058305860000000000000201000000000000008300000000000000000000000000000591', x'62706c6973743030d400010002000300040005000600070068582476657273696f6e592461726368697665725424746f7058246f626a6563747312000186a05f100f4e534b657965644172636869766572df103000080009000a000b000c000d000e000f0010001100120013001400150016001700180019001a001b001c001d001e001f0020002100220023002400250026002700280029002a002b002c002d002e002f00300031003200330034003500360037003800390038003b00380038003b0038003800380038003b003b003800380038003b00490038003800380038003800380038003800380038003b0038003800380038003b00380038003b005d005e00380038003b0038006300640038003b00385f1016546f6d6273746f6e65645075626c69634b65794944735f101948617355706461746564506172656e745265666572656e63655f1013436861696e50726f74656374696f6e446174615d4b6e6f776e546f5365727665725f1011446973706c61796564486f73746e616d655942617365546f6b656e5f101057616e7473436861696e5043534b65795b5265636f72644374696d655a526f7574696e674b65795f101250726f74656374696f6e44617461457461675e45787069726174696f6e446174655f10194d6572676561626c6556616c756544656c74615265636f72645f101a4e65656473526f6c6c416e64436f756e7465725369676e4b65795f102650726576696f757350726f74656374696f6e446174614574616746726f6d556e6974546573745f1012436f6e666c6963744c6f73657245746167735f101a50726576696f757350726f74656374696f6e44617461457461675f10144861735570646174656445787069726174696f6e5a5265636f7264547970655f101343726561746f72557365725265636f726449445f100f506172656e745265666572656e636559536861726545746167585043534b657949445c5a6f6e656973684b657949445f10204d757461626c65456e637279707465645075626c696353686172696e674b657954455461675f101650726576696f757353686172655265666572656e63655f10104d6f64696669656442794465766963655e50726f74656374696f6e446174615f10115573654c696768747765696768745043535e53686172655265666572656e63655f10115570646174656445787069726174696f6e5355524c5f1016436861696e506172656e745075626c69634b6579494457457870697265645f10184c6173744d6f646966696564557365725265636f726449445b5265636f72644d74696d655f101557616e74735075626c696353686172696e674b65795f1013456e6372797074656456616c756553746f72655a56616c756553746f72655c506c7567696e4669656c64735f10165a6f6e6550726f74656374696f6e4461746145746167595761734361636865645f100f436861696e507269766174654b65795a5065726d697373696f6e585265636f726449445c416c6c5043534b65794944735f10184861735570646174656453686172655265666572656e63655f101750726576696f7573506172656e745265666572656e636580000980000880008000088000800080008000080880008000800008800180008000800080008000800080008000800080000880008000800080000880008000088014800980008000088000100080028000088000af102a0069006a006b00720073007c007d007e0085008900910099009a009b00a000a600a800ab00af00b400b800bd00cb00cc00cd00ce00cf00d000d400d500da00dd00de00e300e600e700ea00eb00ee00ef00fd010555246e756c6c5567616d6573d3006c006d006e006f007000715624636c6173735a5265636f72644e616d65565a6f6e6549448008800380045f102a38643036323032312d626563332d343439632d613136642d3765306232383434373031623a67616d6573d50074007500760077006c006300380079007a007b5f1010646174616261736553636f70654b65795f1011616e6f6e796d6f7573434b557365724944596f776e65724e616d65585a6f6e654e616d6580008006800580075f1023636f2e706f696e74667265652e53514c697465446174612e64656661756c745a6f6e655f10105f5f64656661756c744f776e65725f5fd2007f0080008100825a24636c6173736e616d655824636c61737365735e434b5265636f72645a6f6e654944a2008300845e434b5265636f72645a6f6e654944584e534f626a656374d2007f0080008600875a434b5265636f72644944a2008800845a434b5265636f72644944d4008a008b008c006c008d008e008f00905b4368616e6765644b6579735e4f726967696e616c56616c7565735c5265636f726456616c7565738011800e800a8013d300920093006c009400960098574e532e6b6579735a4e532e6f626a65637473a10095800ba10097800c800d5f10105f7265636f72644368616e67655461671009d2007f0080009c009d5f10134e534d757461626c6544696374696f6e617279a3009e009f00845f10134e534d757461626c6544696374696f6e6172795c4e5344696374696f6e617279d300920093006c00a100a30098a10095800ba100a4800f800dd1006c00a78010d2007f008000a900aa564e534e756c6ca200a90084d20093006c00ac00aea10095800b8012d2007f008000b000b15c4e534d757461626c65536574a300b200b300845c4e534d757461626c65536574554e53536574d2007f008000b500b65f1012434b5265636f726456616c756553746f7265a200b700845f'/*+1309 bytes*/, NULL, 0, 1768332077865461000) ON CONFLICT ("recordPrimaryKey", "recordType") DO UPDATE SET "zoneName" = 'co.pointfree.SQLiteData.defaultZone', "ownerName" = '__defaultOwner__' -- TRIGGER sqlitedata_icloud_after_update_on_sqlitedata_icloud_metadata -- TRIGGER sqlitedata_icloud_after_zone_update_on_sqlitedata_icloud_metadata SELECT "sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType", "sqlitedata_icloud_metadata"."zoneName", "sqlitedata_icloud_metadata"."ownerName", "sqlitedata_icloud_metadata"."recordName", "sqlitedata_icloud_metadata"."parentRecordPrimaryKey", "sqlitedata_icloud_metadata"."parentRecordType", "sqlitedata_icloud_metadata"."parentRecordName", "sqlitedata_icloud_metadata"."lastKnownServerRecord", "sqlitedata_icloud_metadata"."_lastKnownServerRecordAllFields", "sqlitedata_icloud_metadata"."share", "sqlitedata_icloud_metadata"."_isDeleted", "sqlitedata_icloud_metadata"."hasLastKnownServerRecord", "sqlitedata_icloud_metadata"."isShared", "sqlitedata_icloud_metadata"."userModificationTime" FROM "sqlitedata_icloud_metadata" WHERE ((("sqlitedata_icloud_metadata"."recordName") = ('8d062021-bec3-449c-a16d-7e0b2844701b:games')) AND (("sqlitedata_icloud_metadata"."zoneName") = ('co.pointfree.SQLiteData.defaultZone'))) AND (("sqlitedata_icloud_metadata"."ownerName") = ('__defaultOwner__')) LIMIT 1 SELECT "games"."id", "games"."title" FROM "games" WHERE ("games"."id") IN (('8d062021-bec3-449c-a16d-7e0b2844701b')) LIMIT 1 INSERT INTO "games" ("id", "title") VALUES ('8d062021-bec3-449c-a16d-7e0b2844701b', 'Asdfasdf') ON CONFLICT("id") DO UPDATE SET "title" = "excluded"."title" -- TRIGGER sqlitedata_icloud_after_update_on_games -- WITH "rootShares" AS (SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") IS (NULL)) AND (("sqlitedata_icloud_metadata"."recordType") IS (NULL)) UNION ALL SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" FROM "sqlitedata_icloud_metadata" JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName") IS ("rootShares"."parentRecordName")) SELECT RAISE(ABORT, 'co.pointfree.SQLiteData.CloudKit.write-permission-error') FROM "rootShares" WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))) -- INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'games', coalesce("sqlitedata_icloud_currentZoneName"(), 'co.pointfree.SQLiteData.defaultZone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING -- UPDATE "sqlitedata_icloud_metadata" SET "zoneName" = coalesce("sqlitedata_icloud_currentZoneName"(), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce("sqlitedata_icloud_currentOwnerName"(), "sqlitedata_icloud_metadata"."ownerName"), "parentRecordPrimaryKey" = NULL, "parentRecordType" = NULL, "userModificationTime" = "sqlitedata_icloud_currentTime"() WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('games')) -- TRIGGER sqlitedata_icloud_after_update_on_sqlitedata_icloud_metadata -- TRIGGER sqlitedata_icloud_after_zone_update_on_sqlitedata_icloud_metadata DELETE FROM "sqlitedata_icloud_unsyncedRecordIDs" WHERE ((("sqlitedata_icloud_unsyncedRecordIDs"."recordName") = ('8d062021-bec3-449c-a16d-7e0b2844701b:games')) AND (("sqlitedata_icloud_unsyncedRecordIDs"."zoneName") = ('co.pointfree.SQLiteData.defaultZone'))) AND (("sqlitedata_icloud_unsyncedRecordIDs"."ownerName") = ('__defaultOwner__')) UPDATE "sqlitedata_icloud_metadata" SET "zoneName" = 'co.pointfree.SQLiteData.defaultZone', "ownerName" = '__defaultOwner__', "lastKnownServerRecord" = x'62706c6973743030d40102030405060762582476657273696f6e592461726368697665725424746f7058246f626a6563747312000186a05f100f4e534b657965644172636869766572df102d08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353635383535383535353538383535353846353535353535353535353835353535383535383538355d5e3538355f1016546f6d6273746f6e65645075626c69634b65794944735f101948617355706461746564506172656e745265666572656e63655f1013436861696e50726f74656374696f6e446174615d4b6e6f776e546f5365727665725f1011446973706c61796564486f73746e616d655942617365546f6b656e5f101057616e7473436861696e5043534b65795b5265636f72644374696d655a526f7574696e674b65795f101250726f74656374696f6e44617461457461675e45787069726174696f6e446174655f10194d6572676561626c6556616c756544656c74615265636f72645f101a4e65656473526f6c6c416e64436f756e7465725369676e4b65795f102650726576696f757350726f74656374696f6e446174614574616746726f6d556e6974546573745f1012436f6e666c6963744c6f73657245746167735f101a50726576696f757350726f74656374696f6e44617461457461675f10144861735570646174656445787069726174696f6e5a5265636f7264547970655f101343726561746f72557365725265636f726449445f100f506172656e745265666572656e636559536861726545746167585043534b657949445c5a6f6e656973684b657949445f10204d757461626c65456e637279707465645075626c696353686172696e674b657954455461675f101650726576696f757353686172655265666572656e63655f10104d6f64696669656442794465766963655e50726f74656374696f6e446174615f10115573654c696768747765696768745043535e53686172655265666572656e63655f10115570646174656445787069726174696f6e5355524c5f1016436861696e506172656e745075626c69634b6579494457457870697265645f10184c6173744d6f646966696564557365725265636f726449445b5265636f72644d74696d655f101557616e74735075626c696353686172696e674b65795f10165a6f6e6550726f74656374696f6e4461746145746167595761734361636865645f100f436861696e507269766174654b65795a5065726d697373696f6e585265636f726449445c416c6c5043534b65794944735f10184861735570646174656453686172655265666572656e63655f101750726576696f7573506172656e745265666572656e636580000980000880008000088000800080008000080880008000800008800180008000800080008000800080008000800080000880008000800080000880008000088000088000100080028000088000a96364656c6d7677787f55246e756c6c5567616d6573d3666768696a6b5624636c6173735a5265636f72644e616d65565a6f6e6549448008800380045f102a38643036323032312d626563332d343439632d613136642d3765306232383434373031623a67616d6573d56e6f7071665d357374755f1010646174616261736553636f70654b65795f1011616e6f6e796d6f7573434b557365724944596f776e65724e616d65585a6f6e654e616d6580008006800580075f1023636f2e706f696e74667265652e53514c697465446174612e64656661756c745a6f6e655f10105f5f64656661756c744f776e65725f5fd2797a7b7c5a24636c6173736e616d655824636c61737365735e434b5265636f72645a6f6e654944a27d7e5e434b5265636f72645a6f6e654944584e534f626a656374d2797a80815a434b5265636f72644944a2827e5a434b5265636f7264494400080011001a0024002900320037004900a600bf00db00f100ff0113011d0130013c0147015c016b018701a401cd01e201ff02160221023702490253025c0269028c029102aa02bd02cc02e002ef03030307032003280343034f03670380038a039c03a703b003bd03d803f203f403f503f703f803fa03fc03fd03ff040104030405040604070409040b040d040e04100412041404160418041a041c041e042004220424042504270429042b042d042e043004320433043504360438043a043c043e043f0441044b04510457045e0465047004770479047b047d04aa04b504c804dc04e604ef04f104f304f504f7051d05300535054005490558055b056a05730578058305860000000000000201000000000000008300000000000000000000000000000591', "_lastKnownServerRecordAllFields" = x'62706c6973743030d400010002000300040005000600070068582476657273696f6e592461726368697665725424746f7058246f626a6563747312000186a05f100f4e534b657965644172636869766572df103000080009000a000b000c000d000e000f0010001100120013001400150016001700180019001a001b001c001d001e001f0020002100220023002400250026002700280029002a002b002c002d002e002f00300031003200330034003500360037003800390038003b00380038003b0038003800380038003b003b003800380038003b00490038003800380038003800380038003800380038003b0038003800380038003b00380038003b005d005e00380038003b0038006300640038003b00385f1016546f6d6273746f6e65645075626c69634b65794944735f101948617355706461746564506172656e745265666572656e63655f1013436861696e50726f74656374696f6e446174615d4b6e6f776e546f5365727665725f1011446973706c61796564486f73746e616d655942617365546f6b656e5f101057616e7473436861696e5043534b65795b5265636f72644374696d655a526f7574696e674b65795f101250726f74656374696f6e44617461457461675e45787069726174696f6e446174655f10194d6572676561626c6556616c756544656c74615265636f72645f101a4e65656473526f6c6c416e64436f756e7465725369676e4b65795f102650726576696f757350726f74656374696f6e446174614574616746726f6d556e6974546573745f1012436f6e666c6963744c6f73657245746167735f101a50726576696f757350726f74656374696f6e44617461457461675f10144861735570646174656445787069726174696f6e5a5265636f7264547970655f101343726561746f72557365725265636f726449445f100f506172656e745265666572656e636559536861726545746167585043534b657949445c5a6f6e656973684b657949445f10204d757461626c65456e637279707465645075626c696353686172696e674b657954455461675f101650726576696f757353686172655265666572656e63655f10104d6f64696669656442794465766963655e50726f74656374696f6e446174615f10115573654c696768747765696768745043535e53686172655265666572656e63655f10115570646174656445787069726174696f6e5355524c5f1016436861696e506172656e745075626c69634b6579494457457870697265645f10184c6173744d6f646966696564557365725265636f726449445b5265636f72644d74696d655f101557616e74735075626c696353686172696e674b65795f1013456e6372797074656456616c756553746f72655a56616c756553746f72655c506c7567696e4669656c64735f10165a6f6e6550726f74656374696f6e4461746145746167595761734361636865645f100f436861696e507269766174654b65795a5065726d697373696f6e585265636f726449445c416c6c5043534b65794944735f10184861735570646174656453686172655265666572656e63655f101750726576696f7573506172656e745265666572656e636580000980000880008000088000800080008000080880008000800008800180008000800080008000800080008000800080000880008000800080000880008000088014800980008000088000100080028000088000af102a0069006a006b00720073007c007d007e0085008900910099009a009b00a000a600a800ab00af00b400b800bd00cb00cc00cd00ce00cf00d000d400d500da00dd00de00e300e600e700ea00eb00ee00ef00fd010555246e756c6c5567616d6573d3006c006d006e006f007000715624636c6173735a5265636f72644e616d65565a6f6e6549448008800380045f102a38643036323032312d626563332d343439632d613136642d3765306232383434373031623a67616d6573d50074007500760077006c006300380079007a007b5f1010646174616261736553636f70654b65795f1011616e6f6e796d6f7573434b557365724944596f776e65724e616d65585a6f6e654e616d6580008006800580075f1023636f2e706f696e74667265652e53514c697465446174612e64656661756c745a6f6e655f10105f5f64656661756c744f776e65725f5fd2007f0080008100825a24636c6173736e616d655824636c61737365735e434b5265636f72645a6f6e654944a2008300845e434b5265636f72645a6f6e654944584e534f626a656374d2007f0080008600875a434b5265636f72644944a2008800845a434b5265636f72644944d4008a008b008c006c008d008e008f00905b4368616e6765644b6579735e4f726967696e616c56616c7565735c5265636f726456616c7565738011800e800a8013d300920093006c009400960098574e532e6b6579735a4e532e6f626a65637473a10095800ba10097800c800d5f10105f7265636f72644368616e67655461671009d2007f0080009c009d5f10134e534d757461626c6544696374696f6e617279a3009e009f00845f10134e534d757461626c6544696374696f6e6172795c4e5344696374696f6e617279d300920093006c00a100a30098a10095800ba100a4800f800dd1006c00a78010d2007f008000a900aa564e534e756c6ca200a90084d20093006c00ac00aea10095800b8012d2007f008000b000b15c4e534d757461626c65536574a300b200b300845c4e534d757461626c65536574554e53536574d2007f008000b500b65f1012434b5265636f726456616c756553746f7265a200b700845f'/*+1309 bytes*/, "userModificationTime" = 1768332077865461000 WHERE ((("sqlitedata_icloud_metadata"."recordName") = ('8d062021-bec3-449c-a16d-7e0b2844701b:games')) AND (("sqlitedata_icloud_metadata"."zoneName") = ('co.pointfree.SQLiteData.defaultZone'))) AND (("sqlitedata_icloud_metadata"."ownerName") = ('__defaultOwner__')) -- TRIGGER sqlitedata_icloud_after_update_on_sqlitedata_icloud_metadata -- TRIGGER sqlitedata_icloud_after_zone_update_on_sqlitedata_icloud_metadata COMMIT TRANSACTION PRAGMA query_only = 1 BEGIN DEFERRED TRANSACTION SELECT "games"."id" AS "id", "games"."title" AS "title", ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) AS "isShared", count("players"."id") AS "playerCount" FROM "games" LEFT JOIN "players" ON ("games"."id") = ("players"."gameID") LEFT JOIN "sqlitedata_icloud_metadata" ON ("games"."id", 'games') = ("sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType") WHERE NOT (ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0)) GROUP BY "games"."id" ORDER BY count("players"."id") DESC COMMIT TRANSACTION PRAGMA query_only = 0 PRAGMA query_only = 1 BEGIN DEFERRED TRANSACTION SELECT "games"."id" AS "id", "games"."title" AS "title", ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) AS "isShared", count("players"."id") AS "playerCount" FROM "games" LEFT JOIN "players" ON ("games"."id") = ("players"."gameID") LEFT JOIN "sqlitedata_icloud_metadata" ON ("games"."id", 'games') = ("sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType") WHERE ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) GROUP BY "games"."id" ORDER BY count("players"."id") DESC COMMIT TRANSACTION PRAGMA query_only = 0
— 31:08
That is a real pain and is going to make tracking down our own queries much more difficult. Well, there is a very simple way to omit these logs from being printed. db.trace { guard !SyncEngine.isSynchronizing else { return } print($0.extendedDescription) }
— 32:02
With that done our logs have been reduced quite a bit: BEGIN IMMEDIATE TRANSACTION INSERT INTO "games" ("id", "title") VALUES (NULL, 'Asdfasd') -- TRIGGER sqlitedata_icloud_after_insert_on_games -- WITH "rootShares" AS (SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") IS (NULL)) AND (("sqlitedata_icloud_metadata"."recordType") IS (NULL)) UNION ALL SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" FROM "sqlitedata_icloud_metadata" JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName") IS ("rootShares"."parentRecordName")) SELECT RAISE(ABORT, 'co.pointfree.SQLiteData.CloudKit.write-permission-error') FROM "rootShares" WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))) -- INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'games', coalesce("sqlitedata_icloud_currentZoneName"(), 'co.pointfree.SQLiteData.defaultZone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING -- TRIGGER sqlitedata_icloud_after_insert_on_sqlitedata_icloud_metadata -- SELECT RAISE(ABORT, 'co.pointfree.SQLiteData.CloudKit.invalid-record-name-error') WHERE NOT (((substr("new"."recordName", 1, 1)) <> ('_')) AND ((octet_length("new"."recordName")) <= (255))) AND ((octet_length("new"."recordName")) = (length("new"."recordName"))) -- SELECT "sqlitedata_icloud_didUpdate"("new"."recordName", "new"."zoneName", "new"."ownerName", "new"."zoneName", "new"."ownerName", NULL) COMMIT TRANSACTION PRAGMA query_only = 1 BEGIN DEFERRED TRANSACTION SELECT "games"."id" AS "id", "games"."title" AS "title", ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) AS "isShared", count("players"."id") AS "playerCount" FROM "games" LEFT JOIN "players" ON ("games"."id") = ("players"."gameID") LEFT JOIN "sqlitedata_icloud_metadata" ON ("games"."id", 'games') = ("sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType") WHERE NOT (ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0)) GROUP BY "games"."id" ORDER BY count("players"."id") DESC COMMIT TRANSACTION PRAGMA query_only = 0 PRAGMA query_only = 1 BEGIN DEFERRED TRANSACTION SELECT "games"."id" AS "id", "games"."title" AS "title", ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) AS "isShared", count("players"."id") AS "playerCount" FROM "games" LEFT JOIN "players" ON ("games"."id") = ("players"."gameID") LEFT JOIN "sqlitedata_icloud_metadata" ON ("games"."id", 'games') = ("sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType") WHERE ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) GROUP BY "games"."id" ORDER BY count("players"."id") DESC COMMIT TRANSACTION PRAGMA query_only = 0 BEGIN DEFERRED TRANSACTION PRAGMA schema_version SELECT "games"."id", "games"."title" FROM "games" WHERE "games"."id" = 'b10c83be-76b4-4f3f-a578-e56aea93827b' LIMIT 1 COMMIT TRANSACTION BEGIN IMMEDIATE TRANSACTION COMMIT TRANSACTION BEGIN IMMEDIATE TRANSACTION COMMIT TRANSACTION BEGIN IMMEDIATE TRANSACTION COMMIT TRANSACTION BEGIN IMMEDIATE TRANSACTION COMMIT TRANSACTION BEGIN IMMEDIATE TRANSACTION COMMIT TRANSACTION PRAGMA query_only = 1 BEGIN DEFERRED TRANSACTION SELECT "games"."id" AS "id", "games"."title" AS "title", ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) AS "isShared", count("players"."id") AS "playerCount" FROM "games" LEFT JOIN "players" ON ("games"."id") = ("players"."gameID") LEFT JOIN "sqlitedata_icloud_metadata" ON ("games"."id", 'games') = ("sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType") WHERE NOT (ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0)) GROUP BY "games"."id" ORDER BY count("players"."id") DESC COMMIT TRANSACTION PRAGMA query_only = 0 PRAGMA query_only = 1 BEGIN DEFERRED TRANSACTION SELECT "games"."id" AS "id", "games"."title" AS "title", ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) AS "isShared", count("players"."id") AS "playerCount" FROM "games" LEFT JOIN "players" ON ("games"."id") = ("players"."gameID") LEFT JOIN "sqlitedata_icloud_metadata" ON ("games"."id", 'games') = ("sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType") WHERE ifnull((("sqlitedata_icloud_metadata"."isShared" = 1) AND ("sqlitedata_icloud_metadata"."share" OR 1)), 0) GROUP BY "games"."id" ORDER BY count("players"."id") DESC COMMIT TRANSACTION PRAGMA query_only = 0
— 32:07
There still is a little bit of unfortunate noise in these logs: INSERT INTO "games" ("id", "title") VALUES (NULL, 'Asdfasd') -- TRIGGER sqlitedata_icloud_after_insert_on_games -- WITH "rootShares" AS (SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") IS (NULL)) AND (("sqlitedata_icloud_metadata"."recordType") IS (NULL)) UNION ALL SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" FROM "sqlitedata_icloud_metadata" JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName") IS ("rootShares"."parentRecordName")) SELECT RAISE(ABORT, 'co.pointfree.SQLiteData.CloudKit.write-permission-error') FROM "rootShares" WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))) -- INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'games', coalesce("sqlitedata_icloud_currentZoneName"(), 'co.pointfree.SQLiteData.defaultZone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING -- TRIGGER sqlitedata_icloud_after_insert_on_sqlitedata_icloud_metadata -- SELECT RAISE(ABORT, 'co.pointfree.SQLiteData.CloudKit.invalid-record-name-error') WHERE NOT (((substr("new"."recordName", 1, 1)) <> ('_')) AND ((octet_length("new"."recordName")) <= (255))) AND ((octet_length("new"."recordName")) = (length("new"."recordName"))) -- SELECT "sqlitedata_icloud_didUpdate"("new"."recordName", "new"."zoneName", "new"."ownerName", "new"."zoneName", "new"."ownerName", NULL)
— 32:41
This log is showing the triggers that were executed when we inserted into the “games” table. We have a trigger on the “games” table that does an insert into the metadata table, and then a trigger on the metadata table that performs some validation logic. These triggers are set up by the sync engine, and it seems that SQLite automatically prints this out when triggers are invoked.
— 33:05
But luckily, queries like these are printed as SQL comments prefixed with -- , and while they can be helpful for debugging, they’re generally noise that we want to ignore, so we will filter them out from the trace, as well: db.trace { guard !SyncEngine.isSynchronizing, !$0.expandedDescription.hasPrefix("--") else { return } print($0.expandedDescription) }
— 34:09
So that removed a lot of other noise from the logs.
— 34:12
And the last bit of finesse we can apply has to do with toying with the settings of the Xcode preview. For example, suppose we wanted to check out our interface in dark mode. A moment after we tweak that setting we will see that the preview crashes: ‘try!’ expression unexpectedly raised an error: SQLite error 19: UNIQUE constraint failed: games.id - while executing INSERT INTO "games" ("id", "title") VALUES (?, ?), (NULL, ?), (NULL, ?)
— 34:31
A uniqueness constraint is failing on the “id” column of the “games” table when trying to perform an insert into the table.
— 34:42
This is happening because the preview is re-seeding the database while still running the same process, and because we hard coded the IDs of our games: Game.Draft(id: UUID(1), title: "Family gin rummy") …we are now trying to insert a second row with id UUID(1) .
— 35:19
The solution to this is quite easy. Just don’t force try seeding the database: try? $0.defaultDatabase.seed()
— 35:33
Unfortunately we get a different crash, this time a CancellationError is being thrown when we try! in the task view modifier. We can just wrap the whole thing in do - catch to work around this and simply use try instead: .task { do { … try … try … } catch {} }
— 36:09
It will be fine for the preview to fail to seed in case it is run a second time in the same process. Now we can tweak preview settings however we want, such as changing dark mode, orientation, or whatever else. Next time: Writing our first test
— 36:38
We have now shown that previews are not inhibited whatsoever by SQLiteData. All of our feature code works in previews, and SQLiteData has even gone above and beyond to make sure that even some advanced functionality works in previews, such as record sharing.
— 36:53
The same cannot be said of other persistence frameworks out there. Large 3rd party libraries such as Firebase are notorious for putting strain on Xcode’s ability to preview SwiftUI views, and many times people have no choice but to define those little inert views and extract them to a separate package just so that they can get some preview coverage on them. Stephen
— 37:16
Now let’s turn our attention to unit tests, which are closely related to previews. Unit tests require that we be able to run our app’s features in complete isolation. Isolation means that the code won’t be running on a simulator or actual device, and so the code paths we are testing can’t ever access the real life APIs that interact with the device. Isolation also means that the code paths we execute during tests should not reach out to the outside world to get data because we most likely will not be able to assert on what that data is in the test.
— 37:45
This can be surprisingly tricky to get right, but SQLiteData’s SyncEngine has most of the hard work taken care of for you. Each of your tests can use a sync engine that is fully isolated from not only the outside world, but also from every other test running in parallel. This means we can write tests much like we would normally without even thinking about the sync engine, and if we need to test something sync engine specific, like the iCloud sharing behavior, then we have that tool available to us via our mocks.
— 38:12
Let’s take a look at how this works…next time! References SQLiteData Brandon Williams & Stephen Celis A fast, lightweight replacement for SwiftData, powered by SQL. https://github.com/pointfreeco/sqlite-data StructuredQueries A library for building SQL in a safe, expressive, and composable manner. https://github.com/pointfreeco/swift-structured-queries Downloads Sample code 0351-sqlite-data-tour-pt5 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .