Skip to content

Commit 65503f5

Browse files
authored
Merge pull request #35 from PureSwift/feature/predicate
Support the predicate expressions CoreData can represent
2 parents ea30b8b + 5b11022 commit 65503f5

12 files changed

Lines changed: 1206 additions & 88 deletions

‎Sources/CoreDataModel/FunctionEvaluation.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ internal extension FetchRequest.Predicate.Expression {
7171
return true
7272
case .attribute, .relationship, .keyPath:
7373
return false
74+
case let .arithmetic(arithmetic):
75+
return arithmetic.left.containsFunction || arithmetic.right.containsFunction
7476
}
7577
}
7678
}

‎Sources/CoreDataModel/NSPredicate.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ internal extension FetchRequest.Predicate.Expression {
127127
case let .attribute(value): return NSExpression(forConstantValue: value.toFoundation())
128128
case let .relationship(value): return NSExpression(forConstantValue: value.toFoundation())
129129
case let .function(value): return NSExpression(forFunction: value.name, arguments: value.arguments.map { $0.toFoundation() })
130+
case let .arithmetic(value): return NSExpression(forFunction: value.function.rawValue, arguments: [value.left.toFoundation(), value.right.toFoundation()])
130131
}
131132
}
132133
}

‎Sources/CoreModel/FetchRequestEvaluation.swift‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public extension FetchRequest {
1313
/// Filters by entity and predicate, sorts by the sort descriptors
1414
/// (with a stable identifier tiebreaker), then applies the fetch offset and limit.
1515
///
16+
/// Objects of other entities may be included; they're filtered out of the results but
17+
/// remain available for key paths that traverse a relationship (e.g. `events.name`).
18+
///
1619
/// - Parameters:
1720
/// - objects: The objects to evaluate the fetch request against.
1821
/// - functions: Custom functions (keyed by name) that `.function` expressions can invoke.
@@ -23,7 +26,8 @@ public extension FetchRequest {
2326
) -> [ModelData] {
2427
var results = objects.filter { $0.entity == entity }
2528
if let predicate {
26-
results = results.filter { predicate.evaluate(with: $0, functions: functions) }
29+
let index = objects.index()
30+
results = results.filter { predicate.evaluate(with: $0, functions: functions, objects: index) }
2731
}
2832
results = results.sorted(by: sortDescriptors, functions: functions)
2933
if fetchOffset > 0 {
@@ -47,7 +51,20 @@ public extension Array where Element == ModelData {
4751
by predicate: FetchRequest.Predicate,
4852
functions: [String: DatabaseFunction] = [:]
4953
) -> [ModelData] {
50-
filter { predicate.evaluate(with: $0, functions: functions) }
54+
let index = self.index()
55+
return filter { predicate.evaluate(with: $0, functions: functions, objects: index) }
56+
}
57+
58+
/// Index these objects by identifier, for resolving key paths that traverse a relationship.
59+
///
60+
/// - Note: Built by hand rather than with `Dictionary.init(_:uniquingKeysWith:)`,
61+
/// which relies on dynamic casting and is unavailable under Embedded Swift.
62+
internal func index() -> [ObjectID: ModelData] {
63+
var index = [ObjectID: ModelData](minimumCapacity: count)
64+
for object in self where index[object.id] == nil {
65+
index[object.id] = object
66+
}
67+
return index
5168
}
5269

5370
/// Sort in memory by the given descriptors, resolving function terms with the

‎Sources/CoreModel/InMemoryStorage.swift‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ internal final class InMemoryStorage {
6666
try validate(fetchRequest.entity)
6767
let values = (state.objects[fetchRequest.entity].map { Array($0.values) } ?? [])
6868
.map { normalized(entity: fetchRequest.entity, $0, objects: state.objects) }
69-
return fetchRequest.evaluate(values, functions: state.functions)
69+
// objects of other entities are filtered out of the results, but let key paths
70+
// that traverse a relationship (e.g. `events.name`) resolve their related objects
71+
let related = state.objects
72+
.filter { $0.key != fetchRequest.entity }
73+
.flatMap { $0.value.values }
74+
return fetchRequest.evaluate(values + related, functions: state.functions)
7075
}
7176
}
7277

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// ArithmeticExpression.swift
3+
// CoreModel
4+
//
5+
// Created by Alsey Coleman Miller on 8/16/26.
6+
// Copyright © 2026 PureSwift. All rights reserved.
7+
//
8+
9+
public extension FetchRequest.Predicate {
10+
11+
/// An arithmetic operation on two expressions (e.g. `age + 1`).
12+
struct ArithmeticExpression: Equatable, Hashable, Sendable {
13+
14+
/// The arithmetic function to apply.
15+
public var function: Function
16+
17+
/// The left operand.
18+
public var left: Expression
19+
20+
/// The right operand.
21+
public var right: Expression
22+
23+
public init(function: Function, left: Expression, right: Expression) {
24+
self.function = function
25+
self.left = left
26+
self.right = right
27+
}
28+
}
29+
}
30+
31+
// MARK: - Supporting Types
32+
33+
public extension FetchRequest.Predicate.ArithmeticExpression {
34+
35+
/// Arithmetic function.
36+
///
37+
/// Raw values match the corresponding `NSExpression` function names,
38+
/// with operands passed in `(left, right)` order.
39+
enum Function: String, Sendable, CaseIterable {
40+
41+
/// Addition (`left + right`).
42+
case add = "add:to:"
43+
44+
/// Subtraction (`left - right`).
45+
case subtract = "from:subtract:"
46+
47+
/// Multiplication (`left * right`).
48+
case multiply = "multiply:by:"
49+
50+
/// Division (`left / right`).
51+
///
52+
/// Integer operands divide truncating, the way Swift's `/` and
53+
/// `NSExpression`'s `divide:by:` both do; floating-point operands
54+
/// produce a floating-point value.
55+
case divide = "divide:by:"
56+
57+
/// Remainder (`left % right`), integers only.
58+
case modulus = "modulus:by:"
59+
}
60+
}
61+
62+
public extension FetchRequest.Predicate.ArithmeticExpression.Function {
63+
64+
/// The operator symbol (e.g. `+`).
65+
var symbol: String {
66+
switch self {
67+
case .add: return "+"
68+
case .subtract: return "-"
69+
case .multiply: return "*"
70+
case .divide: return "/"
71+
case .modulus: return "%"
72+
}
73+
}
74+
}
75+
76+
// MARK: - CustomStringConvertible
77+
78+
extension FetchRequest.Predicate.ArithmeticExpression: CustomStringConvertible {
79+
80+
public var description: String {
81+
"(" + left.description + " " + function.symbol + " " + right.description + ")"
82+
}
83+
}
84+
85+
// MARK: - Codable
86+
87+
#if !hasFeature(Embedded)
88+
extension FetchRequest.Predicate.ArithmeticExpression: Codable {}
89+
extension FetchRequest.Predicate.ArithmeticExpression.Function: Codable {}
90+
#endif

0 commit comments

Comments
 (0)