diff --git a/Sources/CoreModel/Predicate/Evaluate.swift b/Sources/CoreModel/Predicate/Evaluate.swift index 6c8f3e2..939562b 100644 --- a/Sources/CoreModel/Predicate/Evaluate.swift +++ b/Sources/CoreModel/Predicate/Evaluate.swift @@ -88,12 +88,6 @@ internal extension PredicateValue { return value } - /// The values traversed through a to-many relationship, if any. - var aggregateValues: [PredicateValue]? { - guard case let .aggregate(values) = self else { return nil } - return values - } - /// An object identifier this value can represent, for relationship comparisons. var objectIDValue: ObjectID? { switch self { diff --git a/Tests/CoreModelTests/BatchInsertTests.swift b/Tests/CoreModelTests/BatchInsertTests.swift index e0dd420..e89e262 100644 --- a/Tests/CoreModelTests/BatchInsertTests.swift +++ b/Tests/CoreModelTests/BatchInsertTests.swift @@ -13,7 +13,7 @@ import Testing @testable import CoreModel @testable import CoreDataModel -@Suite +@Suite(.serialized) struct BatchInsertTests { /// Synthetic catalog payload: many events sharing a small set of people through diff --git a/Tests/CoreModelTests/CoreDataModelTests.swift b/Tests/CoreModelTests/CoreDataModelTests.swift index 338bd5a..b4ea1cb 100644 --- a/Tests/CoreModelTests/CoreDataModelTests.swift +++ b/Tests/CoreModelTests/CoreDataModelTests.swift @@ -18,7 +18,7 @@ import Testing // suite exercises (`ManagedObjectViewContext`, `NSPersistentContainer.syncLoadPersistentStores()`, // etc.) need macOS 12/iOS 15/watchOS 8/tvOS 15, below this package's deployment target, so // each test guards its body with a runtime `if #available` instead. -@Suite struct CoreDataModelTests { +@Suite(.serialized) struct CoreDataModelTests { static func makeContext() throws -> NSManagedObjectContext { let model = Model(entities: Person.self, Event.self) diff --git a/Tests/CoreModelTests/CoreDataTests.swift b/Tests/CoreModelTests/CoreDataTests.swift index 0c25690..e05c656 100644 --- a/Tests/CoreModelTests/CoreDataTests.swift +++ b/Tests/CoreModelTests/CoreDataTests.swift @@ -13,7 +13,7 @@ import Testing @testable import CoreModel @testable import CoreDataModel -@Suite +@Suite(.serialized) struct CoreDataTests { @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) diff --git a/Tests/CoreModelTests/FoundationPredicateTests.swift b/Tests/CoreModelTests/FoundationPredicateTests.swift index deeacba..180059c 100644 --- a/Tests/CoreModelTests/FoundationPredicateTests.swift +++ b/Tests/CoreModelTests/FoundationPredicateTests.swift @@ -67,7 +67,56 @@ import Testing let greaterThanOrEqual = try FetchRequest.Predicate(#Predicate { $0.age >= 20 }) #expect(greaterThanOrEqual == .comparison(.init(left: .keyPath("age"), right: .attribute(.int64(20)), type: .greaterThanOrEqualTo))) #expect(Self.people.filtered(by: greaterThanOrEqual).map(\.id.rawValue) == ["Alice", "Alina"]) + + let lessThanOrEqual = try FetchRequest.Predicate(#Predicate { $0.age <= 20 }) + #expect(lessThanOrEqual == .comparison(.init(left: .keyPath("age"), right: .attribute(.int64(20)), type: .lessThanOrEqualTo))) + #expect(Self.people.filtered(by: lessThanOrEqual).map(\.id.rawValue) == ["Bob", "Alina"]) + + let greaterThan = try FetchRequest.Predicate(#Predicate { $0.age > 20 }) + #expect(greaterThan == .comparison(.init(left: .keyPath("age"), right: .attribute(.int64(20)), type: .greaterThan))) + #expect(Self.people.filtered(by: greaterThan).map(\.id.rawValue) == ["Alice"]) + } + + @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) + @Test func sequenceContains() throws { + + // an element tested against a collection property + let predicate = try FetchRequest.Predicate(#Predicate { $0.scores.contains(10) }) + #expect(predicate == .comparison(.init( + left: .keyPath("scores"), + right: .attribute(.int64(10)), + type: .contains + ))) + } + + #if canImport(Darwin) + @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) + @Test func localizedStandardContains() throws { + + // maps to a case- and diacritic-insensitive CONTAINS + let predicate = try FetchRequest.Predicate(#Predicate { $0.name.localizedStandardContains("ali") }) + #expect(predicate == .comparison(.init( + left: .keyPath("name"), + right: .attribute(.string("ali")), + type: .contains, + options: [.caseInsensitive, .diacriticInsensitive] + ))) + // the case-insensitive option applies when evaluating in memory + #expect(Self.people.filtered(by: predicate).map(\.id.rawValue) == ["Alice", "Alina"]) + } + + @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) + @Test func objectKeyPath() throws { + + // an @objc root resolves its key paths through Key-Value Coding + let predicate = try FetchRequest.Predicate(#Predicate { $0.name == "Event 1" }) + #expect(predicate == .comparison(.init( + left: .keyPath("name"), + right: .attribute(.string("Event 1")), + type: .equalTo + ))) } + #endif @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) @Test func compound() throws { diff --git a/Tests/CoreModelTests/KeyPathTraversalTests.swift b/Tests/CoreModelTests/KeyPathTraversalTests.swift index 6c774c0..1573578 100644 --- a/Tests/CoreModelTests/KeyPathTraversalTests.swift +++ b/Tests/CoreModelTests/KeyPathTraversalTests.swift @@ -101,6 +101,42 @@ import Testing #expect(request.evaluate(Fixture.all).map(\.id.rawValue) == ["alice"]) } + @Test func nullRelationship() { + + // a null relationship has nothing to traverse into + let dave = ModelData( + entity: "Person", + id: ObjectID(rawValue: "dave"), + attributes: ["name": .string("Dave")], + relationships: ["events": .null, "favorite": .null] + ) + let objects = Fixture.all + [dave] + let any = FetchRequest(entity: "Person", predicate: "events.name".compare(.any, .equalTo, [], .attribute(.string("WWDC")))) + #expect(any.evaluate(objects).map(\.id.rawValue) == ["alice"]) + let toOne = FetchRequest(entity: "Person", predicate: FetchRequest.Predicate.comparison(.init( + left: .keyPath("favorite.name"), + right: .attribute(.string("WWDC")), + type: .equalTo + ))) + #expect(toOne.evaluate(objects).map(\.id.rawValue) == ["alice"]) + } + + @Test func modifierOnNonCollection() { + + // an ALL/ANY modifier on a plain attribute falls back to a direct comparison + let predicate = "name".compare(.any, .equalTo, [], .attribute(.string("Alice"))) + let request = FetchRequest(entity: "Person", predicate: predicate) + #expect(request.evaluate(Fixture.all).map(\.id.rawValue) == ["alice"]) + } + + @Test func missingRelationshipProperty() { + + // a key path whose leading key isn't a relationship doesn't resolve + let predicate = "name.length".compare(.any, .equalTo, [], .attribute(.int64(5))) + let request = FetchRequest(entity: "Person", predicate: predicate) + #expect(request.evaluate(Fixture.all).isEmpty) + } + @Test func unresolvedRelatedObjects() { // without the related objects, a traversing key path can't resolve diff --git a/Tests/CoreModelTests/PersistentStorageTests.swift b/Tests/CoreModelTests/PersistentStorageTests.swift index 7d84903..687001c 100644 --- a/Tests/CoreModelTests/PersistentStorageTests.swift +++ b/Tests/CoreModelTests/PersistentStorageTests.swift @@ -111,7 +111,7 @@ struct AllTypes: Equatable, Hashable, Codable, Identifiable { // - Note: `@Test`/`@Suite` can't be combined with a declaration-level `@available` — see // CoreDataModelTests.swift for the same note. Each test guards its body with a runtime // `if #available` instead. -@Suite struct PersistentStorageTests { +@Suite(.serialized) struct PersistentStorageTests { static func makeStorage(model: Model = Model(entities: Person.self, Event.self, AllTypes.self)) -> PersistentContainerStorage { let description = NSPersistentStoreDescription()