Which records will be downloaded when CKFetchRecordChangesOperation will called with nil token? - ios

Will CKFetchRecordChangesOperation retrieve all records from container?
I hoped it will download all my records, since I added many to different record types, but I get no one. I've initialised it with nil token.
reference says: A CKFetchRecordChangesOperation object reports on the changed and deleted records in the specified record zone. Use this type of operation object to optimize fetch operations for locally managed sets of records. Specifically, use it when you maintain a local cache of your record data and need to synchronize that cache periodically with the server.
I even runned CKFetchRecordChangesOperation from different device to exclude the case only changes performed on other devices is returned.

CKFetchRecordChangesOperation has nothing to do with CKSubscription. Instead it will just return all the changes to all recordTypes.
The documentation says this about the change token:
The change token from a previous fetch operation. This is the token
passed to your fetchRecordChangesCompletionBlock handler during a
previous fetch operation. Use this token to limit the returned data to
only those changes that have occurred since you last made the same
fetch request. If you specify nil for this parameter, the operation
object fetches all records and their contents.
So you will just get all the records.

Related

Do not fetch deletion changes using CkFetchRecordZoneChangesOperation

I use it to fetch all records from cloudkit database. I pass nil token to indicate that I need ALL changes. Later I fetch latest changes with apropriate token.
But first time I get thousends of changes related to deletion. It takes some time... Is there a way to indicate whether I need deletion changes or not? Or can I somehow that type of change mark as received or read and to not get it anymore?
first time I get thousends of changes related to deletion.
I understand you are mentioning the scenario when you are passing nil value to the server change token in CKFetchRecordZoneChangesOptions.
Is there a way to indicate whether I need deletion changes or not? Or can I somehow that type of change mark as received or read and to not get it anymore?
You get the IDs of the deleted records in the block: "setRecordWithIDWasDeletedBlock"
#property (nonatomic, copy, nullable) void (^recordWithIDWasDeletedBlock)(CKRecordID *recordID, NSString *recordType);
Please try by not setting that block property in your operation instance. If the block is NOT set, you won't receive information of the deleted records. So, do not set the block property for the first time (that is, when the change token is nil).
Later I fetch latest changes with apropriate token.
You can set the above mentioned block only if the change token is non-nil!

Prevent observer returning local "invalid" version of data after saving before save is eventually denied with `write` rule

I have an observer on a path and when I write "invalid" that data to that path (i.e. data that will fail due to a .write rule) the observer is firing first with the data (locally cached perhaps?), and then again with the initial version of the data once the write has failed.
How can I prevent this observer from returning this "invalid"/local version? I would expect observers to only be given values that have been saved to the database i.e. passed all rules.
Disabling FIRDatabase.database().persistenceEnabled doesn't affect this behavior.

CKFetchRecordChangesOperation what is the returned clientChangeToken?

I am working on an app with CloudKit enabled and a local CoreData cache which syncs with iCloud using CloudKit. I am using the CKFetchRecordChangesOperation class to fetch new, changed and deleted records. The returned CKServerChangeToken is used for every consecutive fetch. CKModifyRecordsOperation is used to add and delete records.
If I add a record using CKModifyRecordsOperation the operation will return a new CKRecord. And if I then perform a new fetch using CKFetchRecordChangesOperation the newly added record is fetched again which is unnecessary. Does anyone know how to prevent this behaviour?
I know there is a property called clientChangeTokenData of type NSData on the CKModifyRecordsOperation however the clientChangeToken returned from CKFetchRecordChangesOperation is always nil for some reason.
Besides the previousServerChangeToken there is no other mechanism for controlling what should be returned by the CKFetchRecordChangesOperation. It will just return the changes. This will include the changes that you have made yourself.
If that's a problem, then you could try using subscriptions instead. these will by default exclude the changes that are made by yourself.

Any (or best) way to get records from CloudKit that aren't on the device?

I have the following predicate:
let predicate = NSPredicate(format: "NOT (recordID in %#)", recordIDs)
-- recordIDs is an array of CKRecordID objects corresponding to the CKRecords on the device
...that produces a runtime error about the predicate. If I change the predicate format string to something else, the query runs fine. I have the "Query" checkbox checked for all the metadata for this record type in CloudKit.
According to CKQuery documentation:
Key names used in predicates correspond to fields in the currently evaluated record. Key names may include the names of the record’s metadata properties such as "creationDate” or any data fields you added to the record.
According to CKRecord documentation, these are the available metadata for querying:
recordID, recordType, creationDate, creatorUserRecordID, modificationDate, lastModifiedUserRecordID, recordChangeTag
You can use the creation date:
NSPredicate(format: "creationDate > %#", dateLastFetched)
After you pull the records down to the device and save them, save the dateLastFetched and use it for subsequent fetches.
Edit: Be sure to enable the creationDate query index on the CloudKit dashboard (it is not enabled by default like many other indexes)
This is an old question, so I'm not sure if it existed at the time, but the correct way to do this now is to use Apple's built-in server change token support.
Making a giant query including all existing record ID's on the device is going to be slow, and picking a date is going to be imprecise.
The right way to do this is to use CKFetchRecordZoneChangesOperation and pass in a CKServerChangeToken using the operation's configurationsByRecordZoneID property.
The first time you call, pass a nil change token. As records are received, CloudKit will call recordZoneChangeTokensUpdatedBlock with the latest change token. Persist the latest token so the next time you need records from the server, you can just pass in your most recent token and get only what's changed since then.
Enable the meta data index by clicking here:

Optimistic locking support in NSIncrementalStore subclass

I am implementing a custom NSIncrementalStore subclass which uses a relational database for persistent storage. One of the things that I still struggle with is the support for optimistic locking.
(feel free to skip this lengthy description right to my question below)
I analyzed how Core Data's SQLite incremental store approaches this problem by examining SQL logs produced by it and came up with following conclusions:
Each entity table in the database has a Z_OPT column which indicates the number of times a particular instance of this entity (row) has been modified, starting from 1 (initial insertion).
Each time a managed object is modified, Z_OPT value in its corresponding database row is incremented.
The store maintains cache (referred to as row cache in Core Data docs) of NSIncrementalStoreNode instances, each having a version property equal to Z_OPT value returned by previous SELECT or UPDATE SQL query on managed object's row.
When a managed object is returned from NSManagedObjectContext (e.g. by executing NSFetchRequest on it), MOC creates snapshot of this object which contains this version number.
When the object is modified or deleted, Core Data makes sure that it has not been modified or deleted outside the context by comparing versions of cached row and object snapshot. All of this happen when -save: is called on the context that the object belongs to. If the versions are different then a merge conflict is detected and handled based on set merging policy.
When MOC is being saved, the -newValuesForObjectWithID:withContext:error: method is called for each modified/deleted object which in turn returns NSIncrementalStoreNode with version number. This version is then compared to snapshot's version and if they are different, the save fails with appropriate merge conflicts (at least with default merge policy).
This simple use case works properly with my store since -newValuesForObjectWithID:withContext:error: checks the row cache first which is enough if the object was concurrently modified in other context using the same store instance. If this is the case, then the cache contains updated row with higher version number which is enough to detect a conflict.
But how can I detect than the underlying database has been modified outside my store, possibly by other application or other store instance using the same database file? I know this is an unfrequent edge case but Core Data handles it properly and I would prefer to do the same.
Core Data's store uses SQL queries like these to update/delete object's row:
UPDATE ZFOO SET Z_OPT=Y, (...) WHERE (...) AND Z_OPT=X
DELETE FROM ZFOO WHERE (...) AND Z_OPT=X
where:
X - version number last known to the store (from cache)
Y - new version number
If such a query fails (no rows affected) the row is updated in store's cache and its version compared against the one previously cached.
My question is: how can a custom NSIncrementalStore inform Core Data that optimistic locking failure has occurred for some updated/deleted/locked objects? It is only the store that is able to tell that when it handles NSSaveChangesRequest passed to it its -executeRequest:withContext:error: method.
If the underlying database does not change under the store, then conflicts are detected since Core Data calls -newValuesForObjectWithID:withContext:error: on each modified/deleted/locked object prior to executing save changes request on the store. I was not able to find any way for NSIncrementalStore to inform Core Data that an optimistic locking failure has occurred after it started to handle the save request. Is there some undocumented way to do that? Core Data seems to throw some exception in that case which is then magically translated into failed save request with NSError listing all the conflicts. I am only able to mimic that partly by returning nil from -executeRequest:withContext:error: and creating the error message by myself. I think there must be a way to use the standard Core Data conflict handling mechanism in this scenario as well.
I realize that this is not an answer to you question, but I will try and give you my point of view on CoreData and correlation to Databases:
(1st level cache)
NSPesistentStoreCoordinator + NSPersistentStore == A single connection to the database
(2nd level cache)
NSManagedObjectContext == cache over the connection holding changes
So, to my understanding your issue is that you have multiple connections to your store, each making changes, but you have no central version control over your records.
Your store will receive a -executeRequest:withContext:error: with NSSaveRequestType
You will then be responsible to verify that the record versions match, if you find a conflict in the connection level (level 1) you report version mismatch between the context (level 2) and the coordinator.
you need to report version missmatch between your connection (level 1) and your store.
To be able to do this your store must report changes on it across all connections to it (ConnectionManager), or it might offer hooks to changes performed on it.
I'm no SQLite expert, but the SQLite API does have something to offer in that area:
update hook
commit hook
changes
total changes
(I have no experience in setting these kind of hooks, but if CoreData use them it will not show in the debug logs)
you can report these errors by setting the error pointer (NSError**) and setting its internal data to match the one that CoreData coordinator is setting (create merge conflict and set the information in them as needed)
Note that optimistic locking failure will only occur during -executeRequest:withContext:error:
(unless you have a rogue connection to the store, one that is not tracked by the manager.
To support this behaviour your manager might need to verify each record as it is committed for a save [huge performance cost] , or use some hooks into the changes recently made to records
)
To handle multiple connections to your store you might need to have a shared cache of NSIncrementalStoreNode, keyed by the store url:
static #{
url1 : actualCacheMapping1,
url2 : actualCacheMapping2,
...
}
each connection save to the store will be verified agains the store url actual cache.
Hope this make some sense for you.
My question is: how can a custom NSIncrementalStore inform Core Data that optimistic locking failure has occurred for some updated/deleted/locked objects? It is only the store that is able to tell that when it handles NSSaveChangesRequest passed to it its -executeRequest:withContext:error: method.
In an NSIncrementalStore, NSIncrementalStoreNodes represent the store snapshots. The version property of the node is the optimistic locking primitive. The persistent store is responsible for detecting optimistic locking failures in at the store level, while the managed object context can detect them higher up. An optimistic locking failure at the store level might happen if the system the store is talking to was changed by something else, and there is a conflict between that system's state and that representation of state in the persistent store. For example, if the store was communicating with a web service and the web service data was changed by another user, etc.
If an optimistic locking failure is detected in your store implementation during a save, your store is responsible for creating NSMergeConflict objects describing it. These will be propagated up by the NSPersistentStoreCoordinator.
[[NSMergeConflict alloc] initWithSource:managedObject newVersion:newVersion oldVersion:oldVersion cachedSnapshot:inMemorySnapshot persistedSnapshot:storedSnapshot];
Snapshot dictionaries should include all modelled attribute property names as keys along with their values. This does not include relationships. For some stores, using the values from the reference objects or NSIncrementalStoreNodes may suffice as long as they only include the modelled attribute property name as keys (and those are easy to get from the entity description).
Once these objects have been created, create an NSError in the NSCocoaErrorDomain with the code NSPersistentStoreSaveConflictsError. The userInfo object should contain the key NSPersistentStoreSaveConflictsErrorKey which should contain an array of the NSMergeConflict objects. Return that from the save request, and the NSPersistentStoreCoordinator will be responsible for finding resolution. Rememeber, you should not generate merge conflicts for conflicts between the state of objects in the NSManagedObjectContext and your store, only for conflicts between whatever in-memory or cached state in your store and where ever the data is kept or persisted (like a web service, or database, etc.)

Resources