Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
314 changes: 314 additions & 0 deletions Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions Sources/CoreDataRepository/CoreDataRepository+Fetch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,31 @@ extension CoreDataRepository {
}
}

/// Fetch items from the store with a ``NSFetchRequest`` and receive updates as the store changes.
///
/// This endpoint allows separate fetch requests for fetching and change tracking. There are times where CoreData
/// will not recognize changes with a specific predicate. The fix, is to use a simplified predicate for change
/// tracking and the full predicate for fetching.
@inlinable
public func fetchSubscription<Model: FetchableUnmanagedModel>(
request: NSFetchRequest<Model.ManagedModel>,
changeTrackingRequest: NSFetchRequest<Model.ManagedModel>,
of _: Model.Type
) -> AsyncStream<Result<[Model], CoreDataError>> {
AsyncStream { continuation in
let subscription = FetchSubscription(
fetchRequest: request,
fetchResultControllerRequest: changeTrackingRequest,
context: context.childContext(),
continuation: continuation
)
continuation.onTermination = { _ in
subscription.cancel()
}
subscription.manualFetch()
}
}

/// Fetch items from the store with a ``NSFetchRequest`` and receive updates as the store changes.
@inlinable
public func fetchThrowingSubscription<Model: FetchableUnmanagedModel>(
Expand All @@ -58,6 +83,31 @@ extension CoreDataRepository {
}
}

/// Fetch items from the store with a ``NSFetchRequest`` and receive updates as the store changes.
///
/// This endpoint allows separate fetch requests for fetching and change tracking. There are times where CoreData
/// will not recognize changes with a specific predicate. The fix, is to use a simplified predicate for change
/// tracking and the full predicate for fetching.
@inlinable
public func fetchThrowingSubscription<Model: FetchableUnmanagedModel>(
request: NSFetchRequest<Model.ManagedModel>,
changeTrackingRequest: NSFetchRequest<Model.ManagedModel>,
of _: Model.Type
) -> AsyncThrowingStream<[Model], Error> {
AsyncThrowingStream { continuation in
let subscription = FetchThrowingSubscription(
fetchRequest: request,
fetchResultControllerRequest: changeTrackingRequest,
context: context.childContext(),
continuation: continuation
)
continuation.onTermination = { _ in
subscription.cancel()
}
subscription.manualFetch()
}
}

/// Fetch items from the store with a ``NSFetchRequest`` and transform the results.
@inlinable
public func fetch<Managed: NSManagedObject, Output>(
Expand Down
58 changes: 50 additions & 8 deletions Sources/CoreDataRepository/Internal/AggregateSubscription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ final class AggregateSubscription<Value: Numeric & Sendable>: Subscription<Value
}

@usableFromInline
// swiftlint:disable:next function_body_length
convenience init(
function: CoreDataRepository.AggregateFunction,
context: NSManagedObjectContext,
Expand All @@ -52,39 +51,77 @@ final class AggregateSubscription<Value: Numeric & Sendable>: Subscription<Value
) {
let request: NSFetchRequest<NSDictionary>
do {
request = try NSFetchRequest<NSDictionary>.request(
request = try NSFetchRequest.request(
function: function,
predicate: predicate,
entityDesc: entityDesc,
attributeDesc: attributeDesc,
groupBy: groupBy
)
} catch let error as CoreDataError {
} catch {
self.init(
fetchRequest: NSFetchRequest(),
fetchResultControllerRequest: NSFetchRequest(),
context: context,
continuation: continuation
)
self.fail(error)
fail(error)
return
} catch let error as CocoaError {
}
guard entityDesc == attributeDesc.entity else {
self.init(
fetchRequest: NSFetchRequest(),
fetchResultControllerRequest: NSFetchRequest(),
context: context,
continuation: continuation
)
self.fail(.cocoa(error))
guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else {
fail(.propertyDoesNotMatchEntity(description: nil))
return
}
guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName
else {
fail(.propertyDoesNotMatchEntity(description: entityName))
return
}
fail(
.propertyDoesNotMatchEntity(
description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)"
)
)
return
}
self.init(request: request, context: context, continuation: continuation)
}

@usableFromInline
convenience init(
function: CoreDataRepository.AggregateFunction,
context: NSManagedObjectContext,
predicate: NSPredicate,
changeTrackingRequest: NSFetchRequest<NSManagedObject>,
entityDesc: NSEntityDescription,
attributeDesc: NSAttributeDescription,
groupBy: NSAttributeDescription? = nil,
continuation: AsyncStream<Result<Value, CoreDataError>>.Continuation
) {
let request: NSFetchRequest<NSDictionary>
do {
request = try NSFetchRequest.request(
function: function,
predicate: predicate,
entityDesc: entityDesc,
attributeDesc: attributeDesc,
groupBy: groupBy
)
} catch {
self.init(
fetchRequest: NSFetchRequest(),
fetchResultControllerRequest: NSFetchRequest(),
context: context,
continuation: continuation
)
fail(.unknown(error as NSError))
fail(error)
return
}
guard entityDesc == attributeDesc.entity else {
Expand All @@ -110,6 +147,11 @@ final class AggregateSubscription<Value: Numeric & Sendable>: Subscription<Value
)
return
}
self.init(request: request, context: context, continuation: continuation)
self.init(
fetchRequest: request,
fetchResultControllerRequest: changeTrackingRequest,
context: context,
continuation: continuation
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,51 +44,88 @@ final class AggregateThrowingSubscription<Value: Numeric & Sendable>: ThrowingSu
}

@usableFromInline
// swiftlint:disable:next function_body_length
convenience init(
function: CoreDataRepository.AggregateFunction,
context: NSManagedObjectContext,
predicate: NSPredicate,
entityDesc: NSEntityDescription,
attributeDesc: NSAttributeDescription,
groupBy: NSAttributeDescription? = nil,
continuation: AsyncThrowingStream<Value, Error>.Continuation
continuation: AsyncThrowingStream<Value, any Error>.Continuation
) {
let request: NSFetchRequest<NSDictionary>
do {
request = try NSFetchRequest<NSDictionary>.request(
request = try NSFetchRequest.request(
function: function,
predicate: predicate,
entityDesc: entityDesc,
attributeDesc: attributeDesc,
groupBy: groupBy
)
} catch let error as CoreDataError {
} catch {
self.init(
fetchRequest: NSFetchRequest(),
fetchResultControllerRequest: NSFetchRequest(),
context: context,
continuation: continuation
)
self.fail(error)
fail(error)
return
} catch let error as CocoaError {
}
guard entityDesc == attributeDesc.entity else {
self.init(
fetchRequest: NSFetchRequest(),
fetchResultControllerRequest: NSFetchRequest(),
context: context,
continuation: continuation
)
self.fail(.cocoa(error))
guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else {
fail(.propertyDoesNotMatchEntity(description: nil))
return
}
guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName
else {
fail(.propertyDoesNotMatchEntity(description: entityName))
return
}
fail(
.propertyDoesNotMatchEntity(
description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)"
)
)
return
}
self.init(request: request, context: context, continuation: continuation)
}

@usableFromInline
convenience init(
function: CoreDataRepository.AggregateFunction,
context: NSManagedObjectContext,
predicate: NSPredicate,
changeTrackingRequest: NSFetchRequest<NSManagedObject>,
entityDesc: NSEntityDescription,
attributeDesc: NSAttributeDescription,
groupBy: NSAttributeDescription? = nil,
continuation: AsyncThrowingStream<Value, any Error>.Continuation
) {
let request: NSFetchRequest<NSDictionary>
do {
request = try NSFetchRequest.request(
function: function,
predicate: predicate,
entityDesc: entityDesc,
attributeDesc: attributeDesc,
groupBy: groupBy
)
} catch {
self.init(
fetchRequest: NSFetchRequest(),
fetchResultControllerRequest: NSFetchRequest(),
context: context,
continuation: continuation
)
fail(.unknown(error as NSError))
fail(error)
return
}
guard entityDesc == attributeDesc.entity else {
Expand All @@ -114,6 +151,11 @@ final class AggregateThrowingSubscription<Value: Numeric & Sendable>: ThrowingSu
)
return
}
self.init(request: request, context: context, continuation: continuation)
self.init(
fetchRequest: request,
fetchResultControllerRequest: changeTrackingRequest,
context: context,
continuation: continuation
)
}
}
43 changes: 27 additions & 16 deletions Sources/CoreDataRepository/Internal/CountSubscription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ final class CountSubscription<Value: Numeric & Sendable>: Subscription<Value, NS
{
@usableFromInline
override func fetch() {
frc.managedObjectContext.perform { [weak self, frc] in
frc.managedObjectContext.perform { [weak self, frc, request] in
if (frc.fetchedObjects ?? []).isEmpty {
self?.start()
}
do {
let count = try frc.managedObjectContext.count(for: frc.fetchRequest)
let count = try frc.managedObjectContext.count(for: request)
self?.send(Value(exactly: count) ?? Value.zero)
} catch let error as CocoaError {
self?.fail(.cocoa(error))
Expand All @@ -38,38 +38,49 @@ final class CountSubscription<Value: Numeric & Sendable>: Subscription<Value, NS
) {
let request: NSFetchRequest<NSDictionary>
do {
request = try NSFetchRequest<NSDictionary>.countRequest(
request = try NSFetchRequest.countRequest(
predicate: predicate,
entityDesc: entityDesc
)
} catch let error as CoreDataError {
self.init(
fetchRequest: NSFetchRequest(),
fetchResultControllerRequest: NSFetchRequest(),
context: context,
continuation: continuation
)
self.fail(error)
return
} catch let error as CocoaError {
} catch {
self.init(
fetchRequest: NSFetchRequest(),
fetchResultControllerRequest: NSFetchRequest(),
context: context,
continuation: continuation
)
self.fail(.cocoa(error))
fail(error)
return
}
self.init(request: request, context: context, continuation: continuation)
}

@usableFromInline
convenience init(
context: NSManagedObjectContext,
predicate: NSPredicate,
changeTrackingRequest: NSFetchRequest<NSManagedObject>,
entityDesc: NSEntityDescription,
continuation: AsyncStream<Result<Value, CoreDataError>>.Continuation
) {
let request: NSFetchRequest<NSDictionary>
do {
request = try NSFetchRequest.countRequest(predicate: predicate, entityDesc: entityDesc)
} catch {
self.init(
fetchRequest: NSFetchRequest(),
fetchResultControllerRequest: NSFetchRequest(),
context: context,
continuation: continuation
)
fail(.unknown(error as NSError))
fail(error)
return
}
self.init(request: request, context: context, continuation: continuation)
self.init(
fetchRequest: request,
fetchResultControllerRequest: changeTrackingRequest,
context: context,
continuation: continuation
)
}
}
Loading