Does CloudKit guarantee CKRecord.Reference is always valid?

I'm considering using CloudKit in my app (it doesn't use Core Data) and have read as many materials as I can find. I haven't fully grasped it yet and have a basic question on CKRecord.Reference. Does CloudKit guarantee CKRecord.Reference value is always valid? By valid I mean the target CkRecord pointed by the CKRecord.Reference exists in the database.

Let's consider an example. Suppose there are two tables: Account and Transaction:

Account Table:

  AccountNumber Currency Rate
  ------------- -------- ----
  a1            USD      0.03

Transaction Table:

  TransactionNumber AccountNumber Amount
  ----------------- ------------- ------
  t1                a1            20

Now suppose user does the following:

  1. User first deletes account a1 and its associated transactions t1 on device A. The device saves the change to cloud.
  2. Then user adds a new transaction t2 to account a1 on device B, before the device receives the change made in step 1 from cloud. Since a1 hasn't been deleted on device B, the operation should succeed locally. The device tries to save the change to cloud too.

My questions:

Q1) Will device B be able to save the change in step 2 to cloud?

I hope it would fail, because otherwise it would lead to inconsistent data. But I find the following in CKModifyRecordsOperation doc (emphasis mine), which implies CloudKit allows invalid reference:

During a save operation, CloudKit requires that the target record of the parent reference, if set, exists in the database or is part of the same operation; all other reference fields are exempt from this requirement.

(BTW, I think the fact that, when using CloudKit, Core Data requires all relations must be optional also indicates that CloudKit can't guarantee relation is always valid, though I think that is mainly an issue on client side caused by data transfer size. The above example, however, is different in that it's an issue on cloud side - the data on cloud is inconsistent).

I also find the following in the document. However, I don't think it helps in the above example, because IIUC CloudKit can only detect conflict when the changes on the same record but the changes in step 1 and step 2 are on different records.

Because records can change between the time you fetch them and the time you save them, the save policy determines whether new changes overwrite existing changes. By default, the operation reports an error when there’s a newer version on the server.

If the above understanding is correct, however, I don't understand why the same document has the following requirement, which implies CloudKit doesn't allow invalid reference:

When creating two new records that have a reference between them, use the same operation to save both records at the same time.

Q2) Suppose CloudKit allows invalid reference on cloud side (that is, device B successfully saves the change in step 2 to cloud) , I wonder what's the best practice to deal with it?

I think the issue is different from the optional relation requirement in Core Data when using CloudKit, because in that case the data is consistent on cloud side and eventually the client will receive complete data. In the above example, however, the data on cloud is inconsistent so the client has to remedy it somehow (although client has little information helping it).

One approach I think of is to avoid the issue in the first place. My idea is to maintain a counter in the database and requires client to increase the counter (it's not Lamport clock. BTW, is it possible to use Lamport clock in this case?) when making any change. This should help CloudKit to detect conflict (though I can't think out a good strategy on how client should deal with it. A simple one is perhaps to prompt user to select one copy). However, this approach effectively uses cloud as a centralized server, which I suspect isn't the typical way how people use CloudKit, and it requires clients to maintain local counter value in various situations. I wonder what's the typical approach? Am I missing something?

Thanks for any help.

Answered by DTS Engineer in 810806022

When saving a record that contains a CKReference field, CloudKit verifies that the target record exists in the same record zone. If it doesn't, CloudKit reports an error and does not proceed the save.

In the case where you create two new records that have a reference between them, you use the same operation to save both records, and CloudKit creates them at the same time.

CloudKit does allow a CKReference field to be nil.

In the case you described, where a1 is not a new record, I'd expect that CKModifyRecordsOperation returns you an error. Did you not get that?

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accepted Answer

When saving a record that contains a CKReference field, CloudKit verifies that the target record exists in the same record zone. If it doesn't, CloudKit reports an error and does not proceed the save.

In the case where you create two new records that have a reference between them, you use the same operation to save both records, and CloudKit creates them at the same time.

CloudKit does allow a CKReference field to be nil.

In the case you described, where a1 is not a new record, I'd expect that CKModifyRecordsOperation returns you an error. Did you not get that?

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

When saving a record that contains a CKReference field, CloudKit verifies that the target record exists in the same record zone. If it doesn't, CloudKit reports an error and does not proceed the save.

Hi Ziqiao, Thanks for your explanation. It has been very helpful. I'm going to create a FB to request adding information like this to CloudKit document.

Your answer has covered the scenario in my original question. If you don't mind, I have a further quesiton. Can I assume that CloudKit makes sure that CKRecord.Reference is always valid in general? Let's consider a different scencairo, for example.

Setup: suppose we have account a1 in the database, and there are no transactions associated with it yet.

  1. User first adds a transaction t1 on device A. The device saves the change to cloud.
  2. User then deletes account a1 on device B before the device receives the change in step 1 from cloud. Since there isn't t1 on local data store, the operation should succeed locally. Then device B tries to save the change on cloud.

My question: what will happen when device B saves the change in step 2 to cloud?

A) If t1's reference to a1 is created with CKReferenceActionDeleteSelf, I think CloudKit will delete t1 automatically. (A note to myself: will device B receives CKDatabaseNotification and sees t1 when doing CKFetchDatabaseChangesOperation?)

B) If t1's reference to a1 is created with CKReferenceActionNone, CloudKit will do nothing and the data on cloud becomes inconsistent. Does this mean it's up to app developer to set up the reference with proper action and otherwise CloudKit doesn't guarantee reference validity?

[quoted] In the case you described, where a1 is not a new record, I'd expect that CKModifyRecordsOperation returns you an error. Did you not get that? [/quoted]

I'm evaluating if it's feasible to add CloudKit support (specifically, CKSyncEngine) to my app. I'm still in the early phase of trying to understand how it works. So I haven't tried any code yet (partially beause it involves timing and it's hard to get definite conclusion based on experiment results).

Thanks Again.

Does CloudKit guarantee CKRecord.Reference is always valid?
 
 
Q