CoreDataSwift aims for behavioral parity with Apple Core Data — the
AppleCoreDataTests target runs the same scenarios against the real framework
to keep us honest. The differences below are on purpose, mostly because
this implementation runs long-lived server processes against Postgres rather
than app processes against SQLite. Anything not listed here that behaves
differently is a bug.
-
setPrimitiveValueis tracked. The change appears inchangedValues()and is saved. Apple's primitive writes bypass change tracking entirely (verified intestSetPrimitiveValueIsNotTracked_DEVIATION). Rationale: values written fromawakeFromInsert/awakeFromFetchmust survive refaults and reach the store; the untracked variant silently lost them. WhatsetPrimitiveValuestill skips versussetValue: the will/didChange observers and inverse-relationship maintenance. -
refresh(mergeChanges: false)on an unsaved inserted object keeps its pending values (including model defaults applied at creation). There is no committed state to reload, so discarding would leave a hollow object. Apple treats this as an edge case with data loss.
-
Validation errors are Swift enums (
NSManagedObjectValidationError), notNSCocoaErrorDomaincodes. Multiple failures wrap in.multiple; every error carries the object URI and a forensic detail (value never set vs explicitly nulled). -
mandatoryValidationPolicy(.errordefault /.warning) andvalidatesOnSavehave no Apple equivalent — rollout levers for data sets that predate validation. -
DBDefaultValue/DBDefaultFunction/DBType = autoincuserInfo keys exempt properties from the mandatory check: the database (DEFAULT expressions), the server framework (request-scoped functions like:appId), or a server-generated autoincrement (DBType = autoinc, the only supportedDBTypefor now) fills them. Apple has no such concept. -
Parent/child contexts are unsupported —
save()on a child context throwsparentContextsUnsupportedinstead of propagating (Apple) or silently dropping changes (the old behavior here).
-
mainQueueConcurrencyTypeuses a private serial queue, notDispatchQueue.main. On a server the main thread may never drain the main queue, and bindingperformAndWaitto it would hang forever. -
performblocks are not wrapped in an autoreleasepool or aprocessPendingChangescall (delete propagation runs eagerly atdelete()time instead). -
retainsRegisteredObjectsdefaults totrue(Apple:false). Existing consumers rely on the context keeping fetched objects alive. Long-lived contexts can opt into the weak registry. -
existingObject(with:)never throws for an unknown ID — it returns a hollow object faulted on first access. Apple throws when the object does not exist in the store.
-
Sort nil ordering follows Postgres: ascending puts NULLs last, descending puts them first (
ASC NULLS LAST/DESC NULLS FIRST), so in-memory sorts agree with the production database'sORDER BY. Apple's in-memoryNSSortDescriptorraises on nil; its SQLite store sorts nulls first in both directions. -
Incremental-store fetches keep the store's row order and never re-sort in memory (the DB collation is authoritative); unsaved pending objects are appended after the store rows.
-
Fetch flags the in-memory path does not implement (
fetchBatchSize,propertiesToFetch,propertiesToGroupBy,havingPredicate,returnsDistinctResults) log a one-shot warning and are ignored. For incremental stores the request passes through to the store, which may honor them.
- Unmodeled keys:
value(forKey:)returns nil on Linux instead of trapping like Apple KVC (there is noNSObjectKVC to fall back to).