Hi,
Overview
- I am finding
#Predicateto be a bit tricky when used with Swift Data - It compiles fine but crashes at runtime
- I know the fix for the problem just wondering if such pitfalls can be avoided at compile time
Exception
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'can't use NULL on left hand side'
terminating due to uncaught exception of type NSException
CoreData: error: SQLCore dispatchRequest: exception handling request: <NSSQLFetchRequestContext: 0x11209b000> , can't use NULL on left hand side with userInfo of (null)
Questions
- Could anything be done to improve the safety to avoid such issues at runtime?
- Could I write the code better (better than the fix below) to avoid this?
My thoughts
- Fix is possible however it wasn't obvious to me that there was a problem with my original code
- Would be nice to prevent them at compile time if possible.
- Currently got to be really careful to avoid such crashes.
Code
import Foundation
import SwiftData
@Model
class Car {
var name: String
var modelRawValue: String?
init(name: String, modelRawValue: String?) {
self.name = name
self.modelRawValue = modelRawValue
}
}
enum CarModel: String, CaseIterable {
case modelA
case modelB
}
func makePredicate(filterModels: [CarModel]?) -> Predicate<Car> {
let filterRawValues = filterModels?.map { $0.rawValue }
let predicate = #Predicate<Car> { car in
if let filterRawValues {
if let carModelRawValue = car.modelRawValue {
filterRawValues.contains(carModelRawValue)
} else {
false
}
} else {
true
}
}
return predicate
}
func fetch(context: ModelContext) throws {
let predicate = makePredicate(filterModels: nil)
let fetchDescriptor = FetchDescriptor(predicate: predicate)
do {
let cars = try context.fetch(fetchDescriptor)
print(cars.count)
} catch {
print("Error: \(error)")
throw error
}
}
Fix
func makePredicate(filterModels: [CarModel]?) -> Predicate<Car> {
// Checking nil condition even before creating the predicate fixes the issue
guard let filterModels else { return .true }
let filterRawValues = filterModels.map { $0.rawValue }
let predicate = #Predicate<Car> { car in
if let carModelRawValue = car.modelRawValue {
filterRawValues.contains(carModelRawValue)
} else {
false
}
}
return predicate
}
I'd say that the fix you did is exactly right.
Basically, when you write #Predicate<Car> with a Swift closure, the macro converts the closure into a tree of PredicateExpressions, which is then converted to an NSPrediccate in Core Data, and filterRawValues.contains(...) becomes an expression of the predicate.
In runtime, the system doesn't really run the Swift code in the closure to perform dynamic branch elimination on captured variables. Instead, it passes the value of filterRawValues to the NSPrediate. When the value is nil, Core Data throws the error because it doesn't allow NULL on the left-hand side of an expression.
Moving if let filterRawValues outside of the #predicate macro avoids running the macro when filterRawValues is nil, hence no error.
I'd go further to make it a rule: Inside the closure of #Predicate, use if let only for unwrapping optional persistent properties of the model; move external parameter branching in Swift outside of the macro.
Best,
——
Ziqiao Chen
Worldwide Developer Relations.