-
-
Save NoahKamara/d8660881b2ef8d6be18b8e26ed349bb7 to your computer and use it in GitHub Desktop.
import Foundation | |
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) | |
/// Allows you to use an existing Predicate as a ``StandardPredicateExpression`` | |
struct VariableWrappingExpression<T>: StandardPredicateExpression { | |
let predicate: Predicate<T> | |
let variable: PredicateExpressions.Variable<T> | |
func evaluate(_ bindings: PredicateBindings) throws -> Bool { | |
// resolve the variable | |
let value = try variable.evaluate(bindings) | |
// create bindings for the expression of the predicate | |
let innerBindings = bindings.binding(predicate.variable, to: value) | |
return try predicate.expression.evaluate(innerBindings) | |
} | |
} | |
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) | |
extension Predicate { | |
typealias Expression = any StandardPredicateExpression<Bool> | |
/// Returns the result of combining the predicates using the given closure. | |
/// | |
/// - Parameters: | |
/// - predicates: an array of predicates to combine | |
/// - nextPartialResult: A closure that combines an accumulating expression and | |
/// an expression of the sequence into a new accumulating value, to be used | |
/// in the next call of the `nextPartialResult` closure or returned to | |
/// the caller. | |
/// - Returns: The final accumulated expression. If the sequence has no elements, | |
/// the result is `initialResult`. | |
static func combining<T>( | |
_ predicates: [Predicate<T>], | |
nextPartialResult: (Expression, Expression) -> Expression | |
) -> Predicate<T> { | |
return Predicate<T>({ variable in | |
let expressions = predicates.map({ | |
VariableWrappingExpression<T>(predicate: $0, variable: variable) | |
}) | |
guard let first = expressions.first else { | |
return PredicateExpressions.Value(true) | |
} | |
let closure: (any StandardPredicateExpression<Bool>, any StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> = { | |
nextPartialResult($0,$1) | |
} | |
return expressions.dropFirst().reduce(first, closure) | |
}) | |
} | |
} | |
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) | |
public extension Array { | |
/// Joins multiple predicates with an ``PredicateExpressions.Conjunction`` | |
/// - Returns: A predicate evaluating to true if **all** sub-predicates evaluate to true | |
func conjunction<T>() -> Predicate<T> where Element == Predicate<T> { | |
func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> { | |
PredicateExpressions.Conjunction(lhs: lhs, rhs: rhs) | |
} | |
return Predicate<T>.combining(self, nextPartialResult: { | |
buildConjunction(lhs: $0, rhs: $1) | |
}) | |
} | |
/// Joins multiple predicates with an ``PredicateExpressions.Disjunction`` | |
/// - Returns: A predicate evaluating to true if **any** sub-predicate evaluates to true | |
func disjunction<T>() -> Predicate<T> where Element == Predicate<T> { | |
func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> { | |
PredicateExpressions.Disjunction(lhs: lhs, rhs: rhs) | |
} | |
return Predicate<T>.combining(self, nextPartialResult: { | |
buildConjunction(lhs: $0, rhs: $1) | |
}) | |
} | |
} |
@NoahKamara Hello, thank you for your work.
I tried to merge the following two predicates into one. Both p1 and p2 can retrieve data normally. However, after merging, I encounter a runtime error: “Query encountered an error: SwiftData.SwiftDataError(_error: SwiftData.SwiftDataError._Error.unsupportedPredicate)”
What could be the reason?
let now = Date.now
let p1 = #Predicate<Item>{ $0.timestamp >= now }
let p2 = #Predicate<Item>{ $0.name == "abc"}
let a = [p1,p2]
let p3 = a.conjunction()
@Query(filter:p3) private var items: [Item]
@NoahKamara , I found the reason. There is no issue with your code; the composed predicate can correctly evaluate normal Swift types. However, after converting the predicate into a VariableWrappingExpression, SwiftData fails to correctly translate the transformed predicate into SQL statements.
Ah thanks for adding this info. Seems like there simply isn't a way to do this yet.
I might try later to implement a tranforming function similar to what apple outlines here which would transform the expression and build a new one that does not require custom expressions
@fatbobman
I've started work on a library that with a protocol for traversing a predicate and replacing a variable with another.
I've implemented the protocol for Equal and a few other expressions and added tests showing that this works with SwiftData
This solves the unsupportedPredicate issue because there are no custom predicates.
Implementation for all expressions should be fairly easy. It is essentially a recursive call on all child predicates that either ends in a predicate that does not support conversion, a leaf (returning a copy of self), or a variable, in which case it returns the replacement variable.
I plan on further building this proof of concept into a full library
@NoahKamara I'm really glad to hear that, and I wish you success soon! I've just published an article discussing the challenges currently faced when composing complex predicates for SwiftData. If your library is completed, I would be very happy to recommend it to everyone.
https://fatbobman.com/en/posts/how-to-dynamically-construct-complex-predicates-for-swiftdata/
@fatbobman just an fyi: the initial loading time for your website is ~25 secs I'm guessing this is due to my location (Germany)
@fatbobman I have added all the Expressions that are mention in the overview page of Predicate
https://github.com/NoahKamara/CompoundPredicate
@NoahKamara I had considered this implementation approach before, but it felt like there was too much work to be done, so I gave up. Bravo, you've done an impressive job. I will update the article later and recommend your achievements to more users.
@fatbobman thanks for the mention :) It was actually way less work than i thought. basically just call the initializer of each predicate with the properties of the self
instance. since all of them must be expressions as well i can just recursively call my implementation of replacing(variable, replacement)
and only need to add proper logic for the Variable expression
Explanation:
TLDR: This Gist implements two methods
conjunction()
anddisjunction()
onArray<Predicate<T>>
for combining predicatesThis is the explanation, copied from this SO question. i recommend reading the thread it for further context
the reason for the error and subsequent crash is that the
PredicateExpressions.Variable
is used to resolve the Predicate input.This is how Predicate Variable reolving works internally:
The Predicate you create looks something like this (when expanded):
The closure takes parameters of
PredicateExpressions.Variable<Input>
which you need to pass as an argument to your expression$0
This variable will be unique for every predicate you created, meaning when you combine them using just the
predicate.expression
property, each expression has a distinct Variable leading to aunresolved Variable
error.I created a custom
StandardPredicateExpression
that takes a predicate and a variable and will do the following in it's evaluate method:Extending the excellent work by [@orgtre][1] to create a solution that takes an array of predicates and a closure for combining them