Skip to content
Merged
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
6 changes: 0 additions & 6 deletions Sources/CoreModel/Predicate/Evaluate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion Tests/CoreModelTests/BatchInsertTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Tests/CoreModelTests/CoreDataModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Tests/CoreModelTests/CoreDataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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, *)
Expand Down
49 changes: 49 additions & 0 deletions Tests/CoreModelTests/FoundationPredicateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,56 @@ import Testing
let greaterThanOrEqual = try FetchRequest.Predicate(#Predicate<PersonModel> { $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<PersonModel> { $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<PersonModel> { $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<PersonModel> { $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<PersonModel> { $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<EventObject> { $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 {
Expand Down
36 changes: 36 additions & 0 deletions Tests/CoreModelTests/KeyPathTraversalTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Tests/CoreModelTests/PersistentStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading