Skip to content
224 changes: 0 additions & 224 deletions Sources/CoreDataModel/FunctionEvaluation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,228 +75,4 @@ internal extension FetchRequest.Predicate.Expression {
}
}

// MARK: - In-memory evaluation

internal extension FetchRequest.Predicate {

/// Evaluate this predicate against a fetched object in memory, calling registered
/// functions for any `.function` expression.
func evaluate(
with data: ModelData,
functions: [String: DatabaseFunction]
) -> Bool {
switch self {
case let .value(value):
return value
case let .compound(compound):
switch compound {
case let .and(subpredicates):
return subpredicates.allSatisfy { $0.evaluate(with: data, functions: functions) }
case let .or(subpredicates):
return subpredicates.contains { $0.evaluate(with: data, functions: functions) }
case let .not(subpredicate):
return subpredicate.evaluate(with: data, functions: functions) == false
}
case let .comparison(comparison):
return comparison.evaluate(with: data, functions: functions)
}
}
}

internal extension FetchRequest.Predicate.Comparison {

func evaluate(
with data: ModelData,
functions: [String: DatabaseFunction]
) -> Bool {
let lhs = left.evaluate(with: data, functions: functions)
let rhs = right.evaluate(with: data, functions: functions)
return type.evaluate(lhs, rhs, options: options)
}
}

internal extension FetchRequest.Predicate.Expression {

/// Resolve this expression to a value for a fetched object.
func evaluate(
with data: ModelData,
functions: [String: DatabaseFunction]
) -> AttributeValue? {
switch self {
case let .attribute(value):
return value
case let .keyPath(keyPath):
return data.attributes[PropertyKey(rawValue: keyPath.rawValue)]
case let .function(function):
guard let registered = functions[function.name] else {
return nil
}
let arguments = function.arguments.map { $0.evaluate(with: data, functions: functions) }
return registered.evaluate(arguments)
case .relationship:
// relationships aren't compared by the in-memory function path
return nil
}
}
}

// MARK: - Operator evaluation

private extension FetchRequest.Predicate.Comparison.Operator {

func evaluate(
_ lhs: AttributeValue?,
_ rhs: AttributeValue?,
options: Set<FetchRequest.Predicate.Comparison.Option>
) -> Bool {
let caseInsensitive = options.contains(.caseInsensitive)
switch self {
case .equalTo:
return AttributeValue.areEqual(lhs, rhs, caseInsensitive: caseInsensitive)
case .notEqualTo:
return AttributeValue.areEqual(lhs, rhs, caseInsensitive: caseInsensitive) == false
case .lessThan:
return (AttributeValue.order(lhs, rhs)).map { $0 < 0 } ?? false
case .lessThanOrEqualTo:
return (AttributeValue.order(lhs, rhs)).map { $0 <= 0 } ?? false
case .greaterThan:
return (AttributeValue.order(lhs, rhs)).map { $0 > 0 } ?? false
case .greaterThanOrEqualTo:
return (AttributeValue.order(lhs, rhs)).map { $0 >= 0 } ?? false
case .beginsWith:
return AttributeValue.stringCompare(lhs, rhs, caseInsensitive: caseInsensitive) { $0.hasPrefix($1) }
case .endsWith:
return AttributeValue.stringCompare(lhs, rhs, caseInsensitive: caseInsensitive) { $0.hasSuffix($1) }
case .contains:
return AttributeValue.stringCompare(lhs, rhs, caseInsensitive: caseInsensitive) { $0.contains($1) }
case .like, .matches:
return AttributeValue.stringCompare(lhs, rhs, caseInsensitive: caseInsensitive) { subject, pattern in
subject.range(of: like(pattern: pattern, matches: self == .matches), options: .regularExpression) != nil
}
case .in, .between:
// right-hand collections aren't represented as a single AttributeValue
return false
}
}

/// Convert a Cocoa-style `LIKE` pattern (`*`, `?`) or a full regular expression into
/// an anchored regular expression string.
private func like(pattern: String, matches: Bool) -> String {
guard matches == false else {
return pattern // already a regular expression
}
let escaped = NSRegularExpression.escapedPattern(for: pattern)
.replacingOccurrences(of: "\\*", with: ".*")
.replacingOccurrences(of: "\\?", with: ".")
return "^" + escaped + "$"
}
}

private extension AttributeValue {

/// A numeric representation for comparable value types, for ordering comparisons.
var comparableDouble: Double? {
switch self {
case let .bool(value): return value ? 1 : 0
case let .int16(value): return Double(value)
case let .int32(value): return Double(value)
case let .int64(value): return Double(value)
case let .float(value): return Double(value)
case let .double(value): return value
case let .decimal(value): return NSDecimalNumber(decimal: value).doubleValue
case let .date(value): return value.timeIntervalSinceReferenceDate
default: return nil
}
}

var stringValue: String? {
if case let .string(value) = self { return value }
return nil
}

static func areEqual(_ lhs: AttributeValue?, _ rhs: AttributeValue?, caseInsensitive: Bool) -> Bool {
switch (lhs, rhs) {
case (.none, .none), (.some(.null), .none), (.none, .some(.null)), (.some(.null), .some(.null)):
return true
case let (.some(left), .some(right)):
if caseInsensitive, let l = left.stringValue, let r = right.stringValue {
return l.caseInsensitiveCompare(r) == .orderedSame
}
return left == right
default:
return false
}
}

/// Ordering of two values: negative if `lhs < rhs`, zero if equal, positive if greater;
/// `nil` if the values aren't order-comparable.
static func order(_ lhs: AttributeValue?, _ rhs: AttributeValue?) -> Int? {
guard let lhs, let rhs else { return nil }
if let l = lhs.comparableDouble, let r = rhs.comparableDouble {
if l < r { return -1 }
if l > r { return 1 }
return 0
}
if let l = lhs.stringValue, let r = rhs.stringValue {
switch l.compare(r) {
case .orderedAscending: return -1
case .orderedSame: return 0
case .orderedDescending: return 1
}
}
return nil
}

static func stringCompare(
_ lhs: AttributeValue?,
_ rhs: AttributeValue?,
caseInsensitive: Bool,
_ compare: (String, String) -> Bool
) -> Bool {
guard var subject = lhs?.stringValue, var pattern = rhs?.stringValue else {
return false
}
if caseInsensitive {
subject = subject.lowercased()
pattern = pattern.lowercased()
}
return compare(subject, pattern)
}
}

// MARK: - In-memory sorting

internal extension Array where Element == ModelData {

/// Sort in memory by the given descriptors, resolving function terms with the
/// registered functions. Property terms fall back to attribute ordering.
func sortedInMemory(
by sortDescriptors: [FetchRequest.SortDescriptor],
functions: [String: DatabaseFunction]
) -> [ModelData] {
guard sortDescriptors.isEmpty == false else { return self }
return sorted { first, second in
for descriptor in sortDescriptors {
let lhs: AttributeValue?
let rhs: AttributeValue?
switch descriptor.term {
case let .property(property):
lhs = first.attributes[property]
rhs = second.attributes[property]
case let .function(function):
let expression = FetchRequest.Predicate.Expression.function(function)
lhs = expression.evaluate(with: first, functions: functions)
rhs = expression.evaluate(with: second, functions: functions)
}
guard let comparison = AttributeValue.order(lhs, rhs), comparison != 0 else {
continue
}
return descriptor.ascending ? comparison < 0 : comparison > 0
}
// stable tiebreaker on id, matching the native fetch's trailing id sort
return first.id.rawValue < second.id.rawValue
}
}
}

#endif
4 changes: 3 additions & 1 deletion Sources/CoreDataModel/NSManagedObjectContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ extension NSManagedObjectContext: ModelStorage {
if let predicate = fetchRequest.predicate {
results = results.filter { predicate.evaluate(with: $0, functions: functions) }
}
results = results.sortedInMemory(by: fetchRequest.sortDescriptors, functions: functions)
if fetchRequest.sortDescriptors.isEmpty == false {
results = results.sorted(by: fetchRequest.sortDescriptors, functions: functions)
}
if fetchRequest.fetchOffset > 0 {
results = Array(results.dropFirst(fetchRequest.fetchOffset))
}
Expand Down
84 changes: 84 additions & 0 deletions Sources/CoreModel/FetchRequestEvaluation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// FetchRequestEvaluation.swift
// CoreModel
//
// Created by Alsey Coleman Miller on 7/21/26.
// Copyright © 2026 PureSwift. All rights reserved.
//

public extension FetchRequest {

/// Execute this fetch request against an in-memory collection of objects.
///
/// Filters by entity and predicate, sorts by the sort descriptors
/// (with a stable identifier tiebreaker), then applies the fetch offset and limit.
///
/// - Parameters:
/// - objects: The objects to evaluate the fetch request against.
/// - functions: Custom functions (keyed by name) that `.function` expressions can invoke.
/// - Returns: The objects matching this fetch request, in sorted order.
func evaluate(
_ objects: [ModelData],
functions: [String: DatabaseFunction] = [:]
) -> [ModelData] {
var results = objects.filter { $0.entity == entity }
if let predicate {
results = results.filter { predicate.evaluate(with: $0, functions: functions) }
}
results = results.sorted(by: sortDescriptors, functions: functions)
if fetchOffset > 0 {
results = Array(results.dropFirst(fetchOffset))
}
if fetchLimit > 0 {
results = Array(results.prefix(fetchLimit))
}
return results
}
}

public extension Array where Element == ModelData {

/// Filter in memory by the given predicate.
///
/// - Parameters:
/// - predicate: The predicate each object must satisfy.
/// - functions: Custom functions (keyed by name) that `.function` expressions can invoke.
func filtered(
by predicate: FetchRequest.Predicate,
functions: [String: DatabaseFunction] = [:]
) -> [ModelData] {
filter { predicate.evaluate(with: $0, functions: functions) }
}

/// Sort in memory by the given descriptors, resolving function terms with the
/// registered functions. Property terms fall back to attribute ordering.
///
/// Ties (and an empty descriptor list) are broken by ordering on the
/// object identifier, so results are deterministic.
func sorted(
by sortDescriptors: [FetchRequest.SortDescriptor],
functions: [String: DatabaseFunction] = [:]
) -> [ModelData] {
sorted { first, second in
for descriptor in sortDescriptors {
let lhs: AttributeValue?
let rhs: AttributeValue?
switch descriptor.term {
case let .property(property):
lhs = first.attributes[property]
rhs = second.attributes[property]
case let .function(function):
let expression = FetchRequest.Predicate.Expression.function(function)
lhs = expression.evaluate(with: first, functions: functions)?.attributeValue
rhs = expression.evaluate(with: second, functions: functions)?.attributeValue
}
guard let comparison = AttributeValue.order(lhs, rhs), comparison != 0 else {
continue
}
return descriptor.ascending ? comparison < 0 : comparison > 0
}
// stable tiebreaker on the object identifier
return first.id.rawValue < second.id.rawValue
}
}
}
Loading
Loading