Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
9552324
#11 Add `composite` case to `AttributeType`
colemancda Aug 16, 2026
2571e7c
#11 Add `composite` case to `AttributeValue`
colemancda Aug 16, 2026
57eba6d
#11 Add composite convenience API to `Attribute`
colemancda Aug 16, 2026
89c5418
#11 Add property and key path resolution to `EntityDescription`
colemancda Aug 16, 2026
927e2e4
#11 Add `CompositeAttributeEncodable`
colemancda Aug 16, 2026
f009144
#11 Add `CompositeAttributeDecodable`
colemancda Aug 16, 2026
83a3c6b
#11 Add `CompositeAttributeCodable`
colemancda Aug 16, 2026
e648f2d
#11 Add composite attribute predicate description
colemancda Aug 16, 2026
fd4e32c
#11 Evaluate key paths into composite attributes
colemancda Aug 16, 2026
ba2dbd7
#11 Sort by composite attribute key paths
colemancda Aug 16, 2026
f68afb4
#11 Support composite attribute key paths in `SortTerm`
colemancda Aug 16, 2026
ab577ab
#11 Normalize composite attribute values
colemancda Aug 16, 2026
07decb6
#11 Declare `@CompositeAttribute` macro
colemancda Aug 16, 2026
c3ec5d4
#11 Add `CompositeAttributeMacro`
colemancda Aug 16, 2026
f7fb139
#11 Generate composite attributes in `EntityMacro`
colemancda Aug 16, 2026
bc0fbf2
#11 Add `invalidCompositeAttribute` macro error
colemancda Aug 16, 2026
c352640
#11 Register `CompositeAttributeMacro`
colemancda Aug 16, 2026
ca8c687
#11 Add `CoreDataModelError`
colemancda Aug 16, 2026
4e876bc
#11 Convert composite attribute types to CoreData
colemancda Aug 16, 2026
2b9ec1f
#11 Build `NSCompositeAttributeDescription`
colemancda Aug 16, 2026
fd67a58
#11 Add composite attributes to `NSEntityDescription`
colemancda Aug 16, 2026
92c2885
#11 Make `NSManagedObjectModel.init(model:)` throwing
colemancda Aug 16, 2026
f4db6f6
#11 Make `PersistentContainerStorage.init` throwing
colemancda Aug 16, 2026
2a558e1
#11 Marshal composite attribute values to and from CoreData
colemancda Aug 16, 2026
7713370
#11 Convert composite attribute values to `NSDictionary`
colemancda Aug 16, 2026
1517ee4
#11 Convert test model to composite attributes
colemancda Aug 16, 2026
34decc6
#11 Update expected attribute types for composites
colemancda Aug 16, 2026
05070c1
#11 Update CoreData tests for composite attributes
colemancda Aug 16, 2026
f6ccd49
#11 Update batch insert tests for composite attributes
colemancda Aug 16, 2026
99ac90d
#11 Update CoreData model tests for throwing initializers
colemancda Aug 16, 2026
6914ff8
#11 Update persistent storage tests for throwing initializers
colemancda Aug 16, 2026
ea7c912
#11 Add tests for `@CompositeAttribute` macro
colemancda Aug 16, 2026
479d328
#11 Add composite attribute coding and schema tests
colemancda Aug 16, 2026
ee1bffd
#11 Add composite attribute predicate and sort tests
colemancda Aug 16, 2026
37007a2
#11 Add composite attribute in-memory store tests
colemancda Aug 16, 2026
3f9fafd
#11 Add composite attribute CoreData tests
colemancda Aug 16, 2026
c9b2068
#11 Add nested composite attribute test model
colemancda Aug 16, 2026
97718e9
#11 Use shared nested composite type in tests
colemancda Aug 16, 2026
3612dee
#11 Add nested composite attribute store tests
colemancda Aug 16, 2026
02826d6
#11 Add tests for entity description property lookup
colemancda Aug 16, 2026
a49b78f
#11 Add tests for CoreDataModelError and unsupported attribute descri…
colemancda Aug 16, 2026
4376009
Merge remote-tracking branch 'origin/master' into feature/composite-a…
colemancda Aug 16, 2026
ecb7e47
#11 Add tests for relationship key paths into composite attributes
colemancda Aug 16, 2026
f78f025
Merge remote-tracking branch 'origin/master' into feature/composite-a…
colemancda Aug 16, 2026
156d0a6
#11 Serialize CompositeCoreDataTests to avoid concurrent CoreData stacks
colemancda Aug 16, 2026
e1be582
#11 Serialize CompositeNestedTests to avoid concurrent CoreData stacks
colemancda Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Sources/CoreDataModel/CoreDataModelError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// CoreDataModelError.swift
// CoreDataModel
//
// Created by Alsey Coleman Miller on 8/16/26.
//

#if canImport(CoreData)
import Foundation
import CoreModel

/// An error building a CoreData model from a CoreModel ``Model``.
public enum CoreDataModelError: Error {

/// The model declares a composite attribute, which requires
/// macOS 14, iOS 17, tvOS 17, or watchOS 10 or later.
case compositeAttributesUnavailable(EntityName, PropertyKey)

/// A composite attribute declared no elements.
///
/// CoreData rejects a composite type with an empty element list.
case emptyCompositeAttribute(PropertyKey)

/// The stored value for a composite attribute did not match its element descriptions.
case invalidCompositeValue(PropertyKey)
}

// MARK: - CustomNSError

extension CoreDataModelError: CustomNSError {

public static var errorDomain: String { "org.pureswift.CoreDataModel.CoreDataModelError" }

public var errorCode: Int {
switch self {
case .compositeAttributesUnavailable: return 1
case .emptyCompositeAttribute: return 2
case .invalidCompositeValue: return 3
}
}

public var errorUserInfo: [String: Any] {
[NSLocalizedDescriptionKey: description]
}
}

// MARK: - CustomStringConvertible

extension CoreDataModelError: CustomStringConvertible {

public var description: String {
switch self {
case let .compositeAttributesUnavailable(entity, key):
return "Composite attribute \(entity).\(key) requires macOS 14, iOS 17, tvOS 17, or watchOS 10 or later."
case let .emptyCompositeAttribute(key):
return "Composite attribute \(key) must declare at least one element."
case let .invalidCompositeValue(key):
return "Invalid value for composite attribute \(key)."
}
}
}

#endif
44 changes: 42 additions & 2 deletions Sources/CoreDataModel/NSAttributeDescription.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// NSAttributeDescription.swift
//
//
//
// Created by Alsey Coleman Miller on 8/17/23.
//
Expand All @@ -11,16 +11,56 @@ import CoreData
import CoreModel

public extension NSAttributeDescription {


/// - Warning: A composite attribute needs an `NSCompositeAttributeDescription`,
/// which this initializer cannot return. Use ``make(attribute:isOptional:)`` instead.
convenience init(
attribute: Attribute,
isOptional: Bool = true
) {
self.init()
self.name = attribute.id.rawValue
self.isOptional = isOptional
assert(attribute.type.isComposite == false, "Use NSAttributeDescription.make(attribute:) for composite attributes")
self.attributeType = .init(attributeType: attribute.type)
}
}

internal extension NSAttributeDescription {

/// Build the CoreData property description for a CoreModel attribute.
///
/// A composite attribute produces an `NSCompositeAttributeDescription`, which is a
/// different class and so cannot come from a `convenience init` on this one.
///
/// - Note: CoreData's two other constraints on elements hold by construction here:
/// an element can never be a relationship (``Attribute`` has no relationship type),
/// and the element tree is a finite value, so it cannot be recursive.
static func make(attribute: Attribute, isOptional: Bool = true) throws -> NSAttributeDescription {
guard case let .composite(elements) = attribute.type else {
return NSAttributeDescription(attribute: attribute, isOptional: isOptional)
}
guard elements.isEmpty == false else {
throw CoreDataModelError.emptyCompositeAttribute(attribute.id)
}
guard #available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) else {
throw CoreDataModelError.compositeAttributesUnavailable("", attribute.id)
}
let description = NSCompositeAttributeDescription()
description.name = attribute.id.rawValue
description.isOptional = isOptional
description.attributeType = .composite
// - Note: Elements are always optional. CoreData raises a "missing mandatory
// data" validation fault on save when a non-optional element is absent, and
// `momc` does not diagnose it at build time.
var children = [NSAttributeDescription]()
children.reserveCapacity(elements.count)
for element in elements {
children.append(try make(attribute: element, isOptional: true))
}
description.elements = children
return description
}
}

#endif
49 changes: 47 additions & 2 deletions Sources/CoreDataModel/NSAttributeType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,22 @@ import Foundation
import CoreData
import CoreModel

internal extension NSAttributeType {

/// `NSCompositeAttributeType` (2100).
///
/// - Note: Spelled by raw value so that referencing it doesn't depend on the
/// availability annotations of the imported enum case. The
/// `NSCompositeAttributeDescription` *class* still requires macOS 14 / iOS 17.
static var composite: NSAttributeType { NSAttributeType(rawValue: 2100)! }
}

public extension NSAttributeType {

init(attributeType: AttributeType) {
switch attributeType {
case .composite:
self = .composite
case .bool:
self = .booleanAttributeType
case .int16:
Expand Down Expand Up @@ -43,7 +55,40 @@ public extension NSAttributeType {
}

public extension AttributeType {


/// Reconstruct the CoreModel attribute type from a CoreData attribute description,
/// including composite attributes and their (possibly nested) elements.
///
/// Prefer this over ``init(attributeType:)``, which cannot represent composites:
/// the elements live on the description, not on the `NSAttributeType`.
init?(attribute: NSAttributeDescription) {
guard attribute.attributeType == .composite else {
guard let type = AttributeType(attributeType: attribute.attributeType) else {
return nil
}
self = type
return
}
guard #available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) else {
return nil
}
guard let composite = attribute as? NSCompositeAttributeDescription else {
return nil
}
var elements = [Attribute]()
elements.reserveCapacity(composite.elements.count)
for element in composite.elements {
// recursion handles nested composites
guard let type = AttributeType(attribute: element) else {
return nil
}
elements.append(Attribute(id: PropertyKey(rawValue: element.name), type: type))
}
self = .composite(elements)
}

/// - Note: Returns `nil` for `.compositeAttributeType`, whose elements cannot be
/// recovered from the type alone. Use ``init(attribute:)`` to round-trip composites.
init?(attributeType: NSAttributeType) {
switch attributeType {
case .undefinedAttributeType:
Expand Down
12 changes: 10 additions & 2 deletions Sources/CoreDataModel/NSEntityDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import CoreModel

internal extension NSEntityDescription {

convenience init(entity: EntityDescription) {
convenience init(entity: EntityDescription) throws {
self.init()
self.name = entity.id.rawValue
// add id attribute
Expand All @@ -27,7 +27,15 @@ internal extension NSEntityDescription {
var properties = [NSPropertyDescription]()
properties.reserveCapacity(entity.attributes.count + entity.relationships.count + 1)
properties.append(id)
properties += entity.attributes.map { NSAttributeDescription(attribute: $0) }
for attribute in entity.attributes {
do {
properties.append(try NSAttributeDescription.make(attribute: attribute))
}
catch CoreDataModelError.compositeAttributesUnavailable(_, let key) {
// re-throw with the entity name attached
throw CoreDataModelError.compositeAttributesUnavailable(entity.id, key)
}
}
properties += entity.relationships.map { NSRelationshipDescription(relationship: $0) }
self.properties = properties
self.uniquenessConstraints = [[NSManagedObject.BuiltInProperty.id.rawValue as NSString]]
Expand Down
Loading
Loading