From 3ecd313f44ad0966e80f4847ba8fc7525151f4c4 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Fri, 28 Aug 2026 22:15:25 -0500 Subject: [PATCH 1/4] Prevent runtime crashes for invalid subscription requests runtime-crash-on-failure-to-build-count-fetch-request --- .../CoreDataRepository+Aggregate.swift | 486 +++++++++++------- .../Internal/AggregateSubscription.swift | 94 +--- .../AggregateThrowingSubscription.swift | 94 +--- .../Internal/BaseSubscription.swift | 8 + .../Internal/CountSubscription.swift | 41 +- .../Internal/CountThrowingSubscription.swift | 44 +- .../AggregateTests.swift | 295 +++++++++++ 7 files changed, 679 insertions(+), 383 deletions(-) diff --git a/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift b/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift index fa27ba5..3724d5d 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift @@ -73,15 +73,22 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncStream> { AsyncStream { continuation in - let subscription = AggregateSubscription( - function: .average, - context: context.childContext(), - predicate: predicate, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateSubscription + do throws(CoreDataError) { + subscription = try AggregateSubscription( + function: .average, + context: context.childContext(), + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.yield(.failure(error)) + continuation.finish() + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -104,16 +111,23 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncStream> { AsyncStream { continuation in - let subscription = AggregateSubscription( - function: .average, - context: context.childContext(), - predicate: predicate, - changeTrackingRequest: changeTrackingRequest, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateSubscription + do throws(CoreDataError) { + subscription = try AggregateSubscription( + function: .average, + context: context.childContext(), + predicate: predicate, + changeTrackingRequest: changeTrackingRequest, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.yield(.failure(error)) + continuation.finish() + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -131,15 +145,21 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - let subscription = AggregateThrowingSubscription( - function: .average, - context: context.childContext(), - predicate: predicate, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateThrowingSubscription + do { + subscription = try AggregateThrowingSubscription( + function: .average, + context: context.childContext(), + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.finish(throwing: error) + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -162,16 +182,22 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - let subscription = AggregateThrowingSubscription( - function: .average, - context: context.childContext(), - predicate: predicate, - changeTrackingRequest: changeTrackingRequest, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateThrowingSubscription + do { + subscription = try AggregateThrowingSubscription( + function: .average, + context: context.childContext(), + predicate: predicate, + changeTrackingRequest: changeTrackingRequest, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.finish(throwing: error) + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -210,12 +236,19 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncStream> { AsyncStream { continuation in - let subscription = CountSubscription( - context: context.childContext(), - predicate: predicate, - entityDesc: entityDesc, - continuation: continuation - ) + let subscription: CountSubscription + do throws(CoreDataError) { + subscription = try CountSubscription( + context: context.childContext(), + predicate: predicate, + entityDesc: entityDesc, + continuation: continuation + ) + } catch { + continuation.yield(.failure(error)) + continuation.finish() + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -236,13 +269,20 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncStream> { AsyncStream { continuation in - let subscription = CountSubscription( - context: context.childContext(), - predicate: predicate, - changeTrackingRequest: changeTrackingRequest, - entityDesc: entityDesc, - continuation: continuation - ) + let subscription: CountSubscription + do throws(CoreDataError) { + subscription = try CountSubscription( + context: context.childContext(), + predicate: predicate, + changeTrackingRequest: changeTrackingRequest, + entityDesc: entityDesc, + continuation: continuation + ) + } catch { + continuation.yield(.failure(error)) + continuation.finish() + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -258,12 +298,18 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - let subscription = CountThrowingSubscription( - context: context.childContext(), - predicate: predicate, - entityDesc: entityDesc, - continuation: continuation - ) + let subscription: CountThrowingSubscription + do { + subscription = try CountThrowingSubscription( + context: context.childContext(), + predicate: predicate, + entityDesc: entityDesc, + continuation: continuation + ) + } catch { + continuation.finish(throwing: error) + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -284,13 +330,19 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - let subscription = CountThrowingSubscription( - context: context.childContext(), - predicate: predicate, - changeTrackingRequest: changeTrackingRequest, - entityDesc: entityDesc, - continuation: continuation - ) + let subscription: CountThrowingSubscription + do { + subscription = try CountThrowingSubscription( + context: context.childContext(), + predicate: predicate, + changeTrackingRequest: changeTrackingRequest, + entityDesc: entityDesc, + continuation: continuation + ) + } catch { + continuation.finish(throwing: error) + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -330,15 +382,22 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncStream> { AsyncStream { continuation in - let subscription = AggregateSubscription( - function: .max, - context: context.childContext(), - predicate: predicate, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateSubscription + do throws(CoreDataError) { + subscription = try AggregateSubscription( + function: .max, + context: context.childContext(), + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.yield(.failure(error)) + continuation.finish() + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -362,16 +421,23 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncStream> { AsyncStream { continuation in - let subscription = AggregateSubscription( - function: .max, - context: context.childContext(), - predicate: predicate, - changeTrackingRequest: changeTrackingRequest, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateSubscription + do throws(CoreDataError) { + subscription = try AggregateSubscription( + function: .max, + context: context.childContext(), + predicate: predicate, + changeTrackingRequest: changeTrackingRequest, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.yield(.failure(error)) + continuation.finish() + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -390,15 +456,21 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - let subscription = AggregateThrowingSubscription( - function: .max, - context: context.childContext(), - predicate: predicate, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateThrowingSubscription + do { + subscription = try AggregateThrowingSubscription( + function: .max, + context: context.childContext(), + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.finish(throwing: error) + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -422,16 +494,22 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - let subscription = AggregateThrowingSubscription( - function: .max, - context: context.childContext(), - predicate: predicate, - changeTrackingRequest: changeTrackingRequest, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateThrowingSubscription + do { + subscription = try AggregateThrowingSubscription( + function: .max, + context: context.childContext(), + predicate: predicate, + changeTrackingRequest: changeTrackingRequest, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.finish(throwing: error) + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -471,15 +549,22 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncStream> { AsyncStream { continuation in - let subscription = AggregateSubscription( - function: .min, - context: context.childContext(), - predicate: predicate, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateSubscription + do throws(CoreDataError) { + subscription = try AggregateSubscription( + function: .min, + context: context.childContext(), + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.yield(.failure(error)) + continuation.finish() + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -503,16 +588,23 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncStream> { AsyncStream { continuation in - let subscription = AggregateSubscription( - function: .min, - context: context.childContext(), - predicate: predicate, - changeTrackingRequest: changeTrackingRequest, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateSubscription + do throws(CoreDataError) { + subscription = try AggregateSubscription( + function: .min, + context: context.childContext(), + predicate: predicate, + changeTrackingRequest: changeTrackingRequest, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.yield(.failure(error)) + continuation.finish() + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -531,15 +623,21 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - let subscription = AggregateThrowingSubscription( - function: .min, - context: context.childContext(), - predicate: predicate, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateThrowingSubscription + do { + subscription = try AggregateThrowingSubscription( + function: .min, + context: context.childContext(), + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.finish(throwing: error) + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -563,16 +661,22 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - let subscription = AggregateThrowingSubscription( - function: .min, - context: context.childContext(), - predicate: predicate, - changeTrackingRequest: changeTrackingRequest, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateThrowingSubscription + do { + subscription = try AggregateThrowingSubscription( + function: .min, + context: context.childContext(), + predicate: predicate, + changeTrackingRequest: changeTrackingRequest, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.finish(throwing: error) + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -611,15 +715,22 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncStream> { AsyncStream { continuation in - let subscription = AggregateSubscription( - function: .sum, - context: context.childContext(), - predicate: predicate, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateSubscription + do throws(CoreDataError) { + subscription = try AggregateSubscription( + function: .sum, + context: context.childContext(), + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.yield(.failure(error)) + continuation.finish() + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -642,16 +753,23 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncStream> { AsyncStream { continuation in - let subscription = AggregateSubscription( - function: .sum, - context: context.childContext(), - predicate: predicate, - changeTrackingRequest: changeTrackingRequest, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateSubscription + do throws(CoreDataError) { + subscription = try AggregateSubscription( + function: .sum, + context: context.childContext(), + predicate: predicate, + changeTrackingRequest: changeTrackingRequest, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.yield(.failure(error)) + continuation.finish() + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -669,15 +787,21 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - let subscription = AggregateThrowingSubscription( - function: .sum, - context: context.childContext(), - predicate: predicate, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateThrowingSubscription + do { + subscription = try AggregateThrowingSubscription( + function: .sum, + context: context.childContext(), + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.finish(throwing: error) + return + } continuation.onTermination = { _ in subscription.cancel() } @@ -700,16 +824,22 @@ extension CoreDataRepository { as _: Value.Type ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - let subscription = AggregateThrowingSubscription( - function: .sum, - context: context.childContext(), - predicate: predicate, - changeTrackingRequest: changeTrackingRequest, - entityDesc: entityDesc, - attributeDesc: attributeDesc, - groupBy: groupBy, - continuation: continuation - ) + let subscription: AggregateThrowingSubscription + do { + subscription = try AggregateThrowingSubscription( + function: .sum, + context: context.childContext(), + predicate: predicate, + changeTrackingRequest: changeTrackingRequest, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy, + continuation: continuation + ) + } catch { + continuation.finish(throwing: error) + return + } continuation.onTermination = { _ in subscription.cancel() } diff --git a/Sources/CoreDataRepository/Internal/AggregateSubscription.swift b/Sources/CoreDataRepository/Internal/AggregateSubscription.swift index 60ae717..79a38bb 100644 --- a/Sources/CoreDataRepository/Internal/AggregateSubscription.swift +++ b/Sources/CoreDataRepository/Internal/AggregateSubscription.swift @@ -48,48 +48,25 @@ final class AggregateSubscription: Subscription>.Continuation - ) { - let request: NSFetchRequest - 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(error) - return - } + ) throws(CoreDataError) { + let request: NSFetchRequest = try NSFetchRequest.request( + function: function, + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy + ) guard entityDesc == attributeDesc.entity else { - self.init( - fetchRequest: NSFetchRequest(), - fetchResultControllerRequest: NSFetchRequest(), - context: context, - continuation: continuation - ) guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else { - fail(.propertyDoesNotMatchEntity(description: nil)) - return + throw CoreDataError.propertyDoesNotMatchEntity(description: nil) } guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName else { - fail(.propertyDoesNotMatchEntity(description: entityName)) - return + throw CoreDataError.propertyDoesNotMatchEntity(description: entityName) } - fail( - .propertyDoesNotMatchEntity( - description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)" - ) + throw CoreDataError.propertyDoesNotMatchEntity( + description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)" ) - return } self.init(request: request, context: context, continuation: continuation) } @@ -104,48 +81,25 @@ final class AggregateSubscription: Subscription>.Continuation - ) { - let request: NSFetchRequest - 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(error) - return - } + ) throws(CoreDataError) { + let request: NSFetchRequest = try NSFetchRequest.request( + function: function, + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy + ) guard entityDesc == attributeDesc.entity else { - self.init( - fetchRequest: NSFetchRequest(), - fetchResultControllerRequest: NSFetchRequest(), - context: context, - continuation: continuation - ) guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else { - fail(.propertyDoesNotMatchEntity(description: nil)) - return + throw CoreDataError.propertyDoesNotMatchEntity(description: nil) } guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName else { - fail(.propertyDoesNotMatchEntity(description: entityName)) - return + throw CoreDataError.propertyDoesNotMatchEntity(description: entityName) } - fail( - .propertyDoesNotMatchEntity( - description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)" - ) + throw CoreDataError.propertyDoesNotMatchEntity( + description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)" ) - return } self.init( fetchRequest: request, diff --git a/Sources/CoreDataRepository/Internal/AggregateThrowingSubscription.swift b/Sources/CoreDataRepository/Internal/AggregateThrowingSubscription.swift index 41af4da..0987b6f 100644 --- a/Sources/CoreDataRepository/Internal/AggregateThrowingSubscription.swift +++ b/Sources/CoreDataRepository/Internal/AggregateThrowingSubscription.swift @@ -52,48 +52,25 @@ final class AggregateThrowingSubscription: ThrowingSu attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, continuation: AsyncThrowingStream.Continuation - ) { - let request: NSFetchRequest - 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(error) - return - } + ) throws(CoreDataError) { + let request: NSFetchRequest = try NSFetchRequest.request( + function: function, + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy + ) guard entityDesc == attributeDesc.entity else { - self.init( - fetchRequest: NSFetchRequest(), - fetchResultControllerRequest: NSFetchRequest(), - context: context, - continuation: continuation - ) guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else { - fail(.propertyDoesNotMatchEntity(description: nil)) - return + throw CoreDataError.propertyDoesNotMatchEntity(description: nil) } guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName else { - fail(.propertyDoesNotMatchEntity(description: entityName)) - return + throw CoreDataError.propertyDoesNotMatchEntity(description: entityName) } - fail( - .propertyDoesNotMatchEntity( - description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)" - ) + throw CoreDataError.propertyDoesNotMatchEntity( + description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)" ) - return } self.init(request: request, context: context, continuation: continuation) } @@ -108,48 +85,25 @@ final class AggregateThrowingSubscription: ThrowingSu attributeDesc: NSAttributeDescription, groupBy: NSAttributeDescription? = nil, continuation: AsyncThrowingStream.Continuation - ) { - let request: NSFetchRequest - 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(error) - return - } + ) throws(CoreDataError) { + let request: NSFetchRequest = try NSFetchRequest.request( + function: function, + predicate: predicate, + entityDesc: entityDesc, + attributeDesc: attributeDesc, + groupBy: groupBy + ) guard entityDesc == attributeDesc.entity else { - self.init( - fetchRequest: NSFetchRequest(), - fetchResultControllerRequest: NSFetchRequest(), - context: context, - continuation: continuation - ) guard let entityName = entityDesc.name ?? entityDesc.managedObjectClassName else { - fail(.propertyDoesNotMatchEntity(description: nil)) - return + throw CoreDataError.propertyDoesNotMatchEntity(description: nil) } guard let attributeEntityName = attributeDesc.entity.name ?? attributeDesc.entity.managedObjectClassName else { - fail(.propertyDoesNotMatchEntity(description: entityName)) - return + throw CoreDataError.propertyDoesNotMatchEntity(description: entityName) } - fail( - .propertyDoesNotMatchEntity( - description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)" - ) + throw CoreDataError.propertyDoesNotMatchEntity( + description: "\(entityName) != \(attributeDesc.name).\(attributeEntityName)" ) - return } self.init( fetchRequest: request, diff --git a/Sources/CoreDataRepository/Internal/BaseSubscription.swift b/Sources/CoreDataRepository/Internal/BaseSubscription.swift index 822d9f0..e9a3bd1 100644 --- a/Sources/CoreDataRepository/Internal/BaseSubscription.swift +++ b/Sources/CoreDataRepository/Internal/BaseSubscription.swift @@ -23,6 +23,14 @@ class BaseSubscription< fetchResultControllerRequest: NSFetchRequest, context: NSManagedObjectContext ) { + if fetchRequest.sortDescriptors == nil { + // if `sortDescriptors` is `nil`, an exception is raised and causes a runtime crash + fetchRequest.sortDescriptors = [] + } + if fetchResultControllerRequest.sortDescriptors == nil { + // if `sortDescriptors` is `nil`, an exception is raised and causes a runtime crash + fetchResultControllerRequest.sortDescriptors = [] + } request = fetchRequest frc = NSFetchedResultsController( fetchRequest: fetchResultControllerRequest, diff --git a/Sources/CoreDataRepository/Internal/CountSubscription.swift b/Sources/CoreDataRepository/Internal/CountSubscription.swift index 29e6ad3..ab7f680 100644 --- a/Sources/CoreDataRepository/Internal/CountSubscription.swift +++ b/Sources/CoreDataRepository/Internal/CountSubscription.swift @@ -35,23 +35,11 @@ final class CountSubscription: Subscription>.Continuation - ) { - let request: NSFetchRequest - do { - request = try NSFetchRequest.countRequest( - predicate: predicate, - entityDesc: entityDesc - ) - } catch { - self.init( - fetchRequest: NSFetchRequest(), - fetchResultControllerRequest: NSFetchRequest(), - context: context, - continuation: continuation - ) - fail(error) - return - } + ) throws(CoreDataError) { + let request: NSFetchRequest = try NSFetchRequest.countRequest( + predicate: predicate, + entityDesc: entityDesc + ) self.init(request: request, context: context, continuation: continuation) } @@ -62,20 +50,11 @@ final class CountSubscription: Subscription, entityDesc: NSEntityDescription, continuation: AsyncStream>.Continuation - ) { - let request: NSFetchRequest - do { - request = try NSFetchRequest.countRequest(predicate: predicate, entityDesc: entityDesc) - } catch { - self.init( - fetchRequest: NSFetchRequest(), - fetchResultControllerRequest: NSFetchRequest(), - context: context, - continuation: continuation - ) - fail(error) - return - } + ) throws(CoreDataError) { + let request: NSFetchRequest = try NSFetchRequest.countRequest( + predicate: predicate, + entityDesc: entityDesc + ) self.init( fetchRequest: request, fetchResultControllerRequest: changeTrackingRequest, diff --git a/Sources/CoreDataRepository/Internal/CountThrowingSubscription.swift b/Sources/CoreDataRepository/Internal/CountThrowingSubscription.swift index aeb1ff5..74e264f 100644 --- a/Sources/CoreDataRepository/Internal/CountThrowingSubscription.swift +++ b/Sources/CoreDataRepository/Internal/CountThrowingSubscription.swift @@ -39,23 +39,11 @@ final class CountThrowingSubscription: ThrowingSubscr predicate: NSPredicate, entityDesc: NSEntityDescription, continuation: AsyncThrowingStream.Continuation - ) { - let request: NSFetchRequest - do { - request = try NSFetchRequest.countRequest( - predicate: predicate, - entityDesc: entityDesc - ) - } catch { - self.init( - fetchRequest: NSFetchRequest(), - fetchResultControllerRequest: NSFetchRequest(), - context: context, - continuation: continuation - ) - fail(error) - return - } + ) throws(CoreDataError) { + let request: NSFetchRequest = try NSFetchRequest.countRequest( + predicate: predicate, + entityDesc: entityDesc + ) self.init(request: request, context: context, continuation: continuation) } @@ -66,23 +54,11 @@ final class CountThrowingSubscription: ThrowingSubscr changeTrackingRequest: NSFetchRequest, entityDesc: NSEntityDescription, continuation: AsyncThrowingStream.Continuation - ) { - let request: NSFetchRequest - do { - request = try NSFetchRequest.countRequest( - predicate: predicate, - entityDesc: entityDesc - ) - } catch { - self.init( - fetchRequest: NSFetchRequest(), - fetchResultControllerRequest: NSFetchRequest(), - context: context, - continuation: continuation - ) - fail(error) - return - } + ) throws(CoreDataError) { + let request: NSFetchRequest = try NSFetchRequest.countRequest( + predicate: predicate, + entityDesc: entityDesc + ) self.init( fetchRequest: request, fetchResultControllerRequest: changeTrackingRequest, diff --git a/Tests/CoreDataRepositoryTests/AggregateTests.swift b/Tests/CoreDataRepositoryTests/AggregateTests.swift index 89d0371..ec6d27a 100644 --- a/Tests/CoreDataRepositoryTests/AggregateTests.swift +++ b/Tests/CoreDataRepositoryTests/AggregateTests.swift @@ -312,6 +312,37 @@ extension CoreDataRepositoryTests { let finalCount = try await task.value expectNoDifference(finalCount, 2) } + + @Test + func countSubscriptionInvalidEntityDesc() async throws { + await #expect(throws: CoreDataError.atLeastOneAttributeDescRequired) { + let stream = repository + .countSubscription( + predicate: NSPredicate(value: true), + entityDesc: NSEntityDescription(), + as: Int.self + ) + for await result in stream { + _ = try result.get() + return + } + } + } + + @Test + func countThrowingSubscriptionInvalidEntityDesc() async throws { + await #expect(throws: CoreDataError.atLeastOneAttributeDescRequired) { + let stream = repository + .countThrowingSubscription( + predicate: NSPredicate(value: true), + entityDesc: NSEntityDescription(), + as: Int.self + ) + for try await _ in stream { + Issue.record("No values should be emitted") + } + } + } @Test(arguments: [false, true]) func sumSuccess(inTransaction: Bool) async throws { @@ -616,6 +647,72 @@ extension CoreDataRepositoryTests { let finalCount = try await task.value expectNoDifference(finalCount, 2) } + + @Test + func sumSubscriptionInvalidEntityDesc() async throws { + await #expect(throws: CoreDataError.noEntityNameFound) { + let stream = repository + .sumSubscription( + predicate: NSPredicate(value: true), + entityDesc: NSEntityDescription(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for await result in stream { + _ = try result.get() + return + } + } + } + + @Test + func sumSubscriptionInvalidAttributeDesc() async throws { + await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { + let stream = repository + .sumSubscription( + predicate: NSPredicate(value: true), + entityDesc: ManagedModel_UuidId.entity(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for await result in stream { + _ = try result.get() + return + } + } + } + + @Test + func sumThrowingSubscriptionInvalidEntityDesc() async throws { + await #expect(throws: CoreDataError.noEntityNameFound) { + let stream = repository + .sumThrowingSubscription( + predicate: NSPredicate(value: true), + entityDesc: NSEntityDescription(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for try await _ in stream { + Issue.record("No values should be emitted") + } + } + } + + @Test + func sumThrowingSubscriptionInvalidAttributeDesc() async throws { + await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { + let stream = repository + .sumThrowingSubscription( + predicate: NSPredicate(value: true), + entityDesc: ManagedModel_UuidId.entity(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for try await _ in stream { + Issue.record("No values should be emitted") + } + } + } @Test(arguments: [false, true]) func averageSuccess(inTransaction: Bool) async throws { @@ -928,6 +1025,72 @@ extension CoreDataRepositoryTests { let finalCount = try await task.value expectNoDifference(finalCount, 2) } + + @Test + func averageSubscriptionInvalidEntityDesc() async throws { + await #expect(throws: CoreDataError.noEntityNameFound) { + let stream = repository + .averageSubscription( + predicate: NSPredicate(value: true), + entityDesc: NSEntityDescription(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for await result in stream { + _ = try result.get() + return + } + } + } + + @Test + func averageSubscriptionInvalidAttributeDesc() async throws { + await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { + let stream = repository + .averageSubscription( + predicate: NSPredicate(value: true), + entityDesc: ManagedModel_UuidId.entity(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for await result in stream { + _ = try result.get() + return + } + } + } + + @Test + func averageThrowingSubscriptionInvalidEntityDesc() async throws { + await #expect(throws: CoreDataError.noEntityNameFound) { + let stream = repository + .averageThrowingSubscription( + predicate: NSPredicate(value: true), + entityDesc: NSEntityDescription(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for try await _ in stream { + Issue.record("No values should be emitted") + } + } + } + + @Test + func averageThrowingSubscriptionInvalidAttributeDesc() async throws { + await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { + let stream = repository + .averageThrowingSubscription( + predicate: NSPredicate(value: true), + entityDesc: ManagedModel_UuidId.entity(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for try await _ in stream { + Issue.record("No values should be emitted") + } + } + } @Test(arguments: [false, true]) func minSuccess(inTransaction: Bool) async throws { @@ -1240,6 +1403,72 @@ extension CoreDataRepositoryTests { let finalCount = try await task.value expectNoDifference(finalCount, 2) } + + @Test + func minSubscriptionInvalidEntityDesc() async throws { + await #expect(throws: CoreDataError.noEntityNameFound) { + let stream = repository + .minSubscription( + predicate: NSPredicate(value: true), + entityDesc: NSEntityDescription(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for await result in stream { + _ = try result.get() + return + } + } + } + + @Test + func minSubscriptionInvalidAttributeDesc() async throws { + await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { + let stream = repository + .minSubscription( + predicate: NSPredicate(value: true), + entityDesc: ManagedModel_UuidId.entity(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for await result in stream { + _ = try result.get() + return + } + } + } + + @Test + func minThrowingSubscriptionInvalidEntityDesc() async throws { + await #expect(throws: CoreDataError.noEntityNameFound) { + let stream = repository + .minThrowingSubscription( + predicate: NSPredicate(value: true), + entityDesc: NSEntityDescription(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for try await _ in stream { + Issue.record("No values should be emitted") + } + } + } + + @Test + func minThrowingSubscriptionInvalidAttributeDesc() async throws { + await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { + let stream = repository + .minThrowingSubscription( + predicate: NSPredicate(value: true), + entityDesc: ManagedModel_UuidId.entity(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for try await _ in stream { + Issue.record("No values should be emitted") + } + } + } @Test(arguments: [false, true]) func maxSuccess(inTransaction: Bool) async throws { @@ -1552,6 +1781,72 @@ extension CoreDataRepositoryTests { let finalCount = try await task.value expectNoDifference(finalCount, 2) } + + @Test + func maxSubscriptionInvalidEntityDesc() async throws { + await #expect(throws: CoreDataError.noEntityNameFound) { + let stream = repository + .maxSubscription( + predicate: NSPredicate(value: true), + entityDesc: NSEntityDescription(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for await result in stream { + _ = try result.get() + return + } + } + } + + @Test + func maxSubscriptionInvalidAttributeDesc() async throws { + await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { + let stream = repository + .maxSubscription( + predicate: NSPredicate(value: true), + entityDesc: ManagedModel_UuidId.entity(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for await result in stream { + _ = try result.get() + return + } + } + } + + @Test + func maxThrowingSubscriptionInvalidEntityDesc() async throws { + await #expect(throws: CoreDataError.noEntityNameFound) { + let stream = repository + .maxThrowingSubscription( + predicate: NSPredicate(value: true), + entityDesc: NSEntityDescription(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for try await _ in stream { + Issue.record("No values should be emitted") + } + } + } + + @Test + func maxThrowingSubscriptionInvalidAttributeDesc() async throws { + await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { + let stream = repository + .maxThrowingSubscription( + predicate: NSPredicate(value: true), + entityDesc: ManagedModel_UuidId.entity(), + attributeDesc: NSAttributeDescription(), + as: Int.self + ) + for try await _ in stream { + Issue.record("No values should be emitted") + } + } + } @Test(arguments: [false, true]) func countWithPredicate(inTransaction: Bool) async throws { From 3788d28cbf57a9dd03a0ace8001d25c7e816e2c2 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Fri, 28 Aug 2026 22:19:48 -0500 Subject: [PATCH 2/4] Fix erasure of CoreDataError to NSError in count endpoint runtime-crash-on-failure-to-build-count-fetch-request --- Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift b/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift index 3724d5d..fd8c9f7 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift @@ -220,6 +220,8 @@ extension CoreDataRepository { .countRequest(predicate: predicate, entityDesc: entityDesc) let count = try scratchPad.count(for: request) return Value(exactly: count) ?? Value.zero + } catch let error as CoreDataError { + throw error } catch let error as CocoaError { throw CoreDataError.cocoa(error) } catch { From c58deb014995bff18f1deae67c5401d8d62f48ae Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Fri, 28 Aug 2026 22:24:36 -0500 Subject: [PATCH 3/4] Format runtime-crash-on-failure-to-build-count-fetch-request --- .../AggregateTests.swift | 37 +++++++++---------- .../BatchRequestTests.swift | 1 - .../CoreDataTestSuiteTests.swift | 1 - .../CoreDataRepositoryTests/CreateTests.swift | 1 - .../Create_BatchTests.swift | 1 - .../CoreDataRepositoryTests/CustomTests.swift | 1 - .../CoreDataRepositoryTests/DeleteTests.swift | 1 - .../Delete_BatchTests.swift | 1 - .../CoreDataRepositoryTests/FetchTests.swift | 1 - Tests/CoreDataRepositoryTests/ReadTests.swift | 1 - .../Read_BatchTests.swift | 1 - .../TransactionTests.swift | 1 - .../CoreDataRepositoryTests/UpdateTests.swift | 1 - .../Update_BatchTests.swift | 1 - 14 files changed, 18 insertions(+), 32 deletions(-) diff --git a/Tests/CoreDataRepositoryTests/AggregateTests.swift b/Tests/CoreDataRepositoryTests/AggregateTests.swift index ec6d27a..7098faf 100644 --- a/Tests/CoreDataRepositoryTests/AggregateTests.swift +++ b/Tests/CoreDataRepositoryTests/AggregateTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct AggregateTests: CoreDataTestSuite, Sendable { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext @@ -312,7 +311,7 @@ extension CoreDataRepositoryTests { let finalCount = try await task.value expectNoDifference(finalCount, 2) } - + @Test func countSubscriptionInvalidEntityDesc() async throws { await #expect(throws: CoreDataError.atLeastOneAttributeDescRequired) { @@ -328,7 +327,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func countThrowingSubscriptionInvalidEntityDesc() async throws { await #expect(throws: CoreDataError.atLeastOneAttributeDescRequired) { @@ -647,7 +646,7 @@ extension CoreDataRepositoryTests { let finalCount = try await task.value expectNoDifference(finalCount, 2) } - + @Test func sumSubscriptionInvalidEntityDesc() async throws { await #expect(throws: CoreDataError.noEntityNameFound) { @@ -664,7 +663,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func sumSubscriptionInvalidAttributeDesc() async throws { await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { @@ -681,7 +680,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func sumThrowingSubscriptionInvalidEntityDesc() async throws { await #expect(throws: CoreDataError.noEntityNameFound) { @@ -697,7 +696,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func sumThrowingSubscriptionInvalidAttributeDesc() async throws { await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { @@ -1025,7 +1024,7 @@ extension CoreDataRepositoryTests { let finalCount = try await task.value expectNoDifference(finalCount, 2) } - + @Test func averageSubscriptionInvalidEntityDesc() async throws { await #expect(throws: CoreDataError.noEntityNameFound) { @@ -1042,7 +1041,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func averageSubscriptionInvalidAttributeDesc() async throws { await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { @@ -1059,7 +1058,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func averageThrowingSubscriptionInvalidEntityDesc() async throws { await #expect(throws: CoreDataError.noEntityNameFound) { @@ -1075,7 +1074,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func averageThrowingSubscriptionInvalidAttributeDesc() async throws { await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { @@ -1403,7 +1402,7 @@ extension CoreDataRepositoryTests { let finalCount = try await task.value expectNoDifference(finalCount, 2) } - + @Test func minSubscriptionInvalidEntityDesc() async throws { await #expect(throws: CoreDataError.noEntityNameFound) { @@ -1420,7 +1419,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func minSubscriptionInvalidAttributeDesc() async throws { await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { @@ -1437,7 +1436,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func minThrowingSubscriptionInvalidEntityDesc() async throws { await #expect(throws: CoreDataError.noEntityNameFound) { @@ -1453,7 +1452,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func minThrowingSubscriptionInvalidAttributeDesc() async throws { await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { @@ -1781,7 +1780,7 @@ extension CoreDataRepositoryTests { let finalCount = try await task.value expectNoDifference(finalCount, 2) } - + @Test func maxSubscriptionInvalidEntityDesc() async throws { await #expect(throws: CoreDataError.noEntityNameFound) { @@ -1798,7 +1797,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func maxSubscriptionInvalidAttributeDesc() async throws { await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { @@ -1815,7 +1814,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func maxThrowingSubscriptionInvalidEntityDesc() async throws { await #expect(throws: CoreDataError.noEntityNameFound) { @@ -1831,7 +1830,7 @@ extension CoreDataRepositoryTests { } } } - + @Test func maxThrowingSubscriptionInvalidAttributeDesc() async throws { await #expect(throws: CoreDataError.propertyDoesNotMatchEntity(description: "ManagedModel_UuidId")) { diff --git a/Tests/CoreDataRepositoryTests/BatchRequestTests.swift b/Tests/CoreDataRepositoryTests/BatchRequestTests.swift index 7da17ea..54545b2 100644 --- a/Tests/CoreDataRepositoryTests/BatchRequestTests.swift +++ b/Tests/CoreDataRepositoryTests/BatchRequestTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct BatchRequestTests: CoreDataTestSuite, Sendable { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/CoreDataTestSuiteTests.swift b/Tests/CoreDataRepositoryTests/CoreDataTestSuiteTests.swift index 1bd95ea..c5392ac 100644 --- a/Tests/CoreDataRepositoryTests/CoreDataTestSuiteTests.swift +++ b/Tests/CoreDataRepositoryTests/CoreDataTestSuiteTests.swift @@ -10,7 +10,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct CoreDataTestSuiteTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/CreateTests.swift b/Tests/CoreDataRepositoryTests/CreateTests.swift index 26561b2..65295f9 100644 --- a/Tests/CoreDataRepositoryTests/CreateTests.swift +++ b/Tests/CoreDataRepositoryTests/CreateTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct CreateTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/Create_BatchTests.swift b/Tests/CoreDataRepositoryTests/Create_BatchTests.swift index cbf1b13..506ba55 100644 --- a/Tests/CoreDataRepositoryTests/Create_BatchTests.swift +++ b/Tests/CoreDataRepositoryTests/Create_BatchTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct Create_BatchTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/CustomTests.swift b/Tests/CoreDataRepositoryTests/CustomTests.swift index 622f0e1..68984a3 100644 --- a/Tests/CoreDataRepositoryTests/CustomTests.swift +++ b/Tests/CoreDataRepositoryTests/CustomTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct CustomTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/DeleteTests.swift b/Tests/CoreDataRepositoryTests/DeleteTests.swift index 7914afc..fdb5d5a 100644 --- a/Tests/CoreDataRepositoryTests/DeleteTests.swift +++ b/Tests/CoreDataRepositoryTests/DeleteTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct DeleteTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/Delete_BatchTests.swift b/Tests/CoreDataRepositoryTests/Delete_BatchTests.swift index 191f796..c23c309 100644 --- a/Tests/CoreDataRepositoryTests/Delete_BatchTests.swift +++ b/Tests/CoreDataRepositoryTests/Delete_BatchTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct Delete_BatchTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/FetchTests.swift b/Tests/CoreDataRepositoryTests/FetchTests.swift index ebd3279..0e51abd 100644 --- a/Tests/CoreDataRepositoryTests/FetchTests.swift +++ b/Tests/CoreDataRepositoryTests/FetchTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct FetchRepositoryTests: CoreDataTestSuite, @unchecked Sendable { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/ReadTests.swift b/Tests/CoreDataRepositoryTests/ReadTests.swift index 6c632fb..cb6d769 100644 --- a/Tests/CoreDataRepositoryTests/ReadTests.swift +++ b/Tests/CoreDataRepositoryTests/ReadTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct ReadTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/Read_BatchTests.swift b/Tests/CoreDataRepositoryTests/Read_BatchTests.swift index 5186944..0b79f04 100644 --- a/Tests/CoreDataRepositoryTests/Read_BatchTests.swift +++ b/Tests/CoreDataRepositoryTests/Read_BatchTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct Read_BatchTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/TransactionTests.swift b/Tests/CoreDataRepositoryTests/TransactionTests.swift index 14f19e8..83b81f1 100644 --- a/Tests/CoreDataRepositoryTests/TransactionTests.swift +++ b/Tests/CoreDataRepositoryTests/TransactionTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct TransactionTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/UpdateTests.swift b/Tests/CoreDataRepositoryTests/UpdateTests.swift index 0a99796..5e41b2b 100644 --- a/Tests/CoreDataRepositoryTests/UpdateTests.swift +++ b/Tests/CoreDataRepositoryTests/UpdateTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct UpdateTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext diff --git a/Tests/CoreDataRepositoryTests/Update_BatchTests.swift b/Tests/CoreDataRepositoryTests/Update_BatchTests.swift index 62ad03e..5b582d6 100644 --- a/Tests/CoreDataRepositoryTests/Update_BatchTests.swift +++ b/Tests/CoreDataRepositoryTests/Update_BatchTests.swift @@ -11,7 +11,6 @@ import Internal import Testing extension CoreDataRepositoryTests { - @Suite struct Update_BatchTests: CoreDataTestSuite { let container: NSPersistentContainer let repositoryContext: NSManagedObjectContext From b4052a0efa742f26dc961fcd5dad6a77f16d28dd Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Mon, 31 Aug 2026 11:31:26 -0500 Subject: [PATCH 4/4] handle thrown CoreDataError in aggregate send function runtime-crash-on-failure-to-build-count-fetch-request --- Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift b/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift index fd8c9f7..b8e982c 100644 --- a/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift +++ b/Sources/CoreDataRepository/CoreDataRepository+Aggregate.swift @@ -897,6 +897,8 @@ extension CoreDataRepository { return try Self.aggregate(context: scratchPad, request: request) } catch let error as CocoaError { throw CoreDataError.cocoa(error) + } catch let error as CoreDataError { + throw error } catch { throw CoreDataError.unknown(error as NSError) }