KeyPath -> NSPredicate, securely

Hello All,

I'd like to propose a (small?) improvement to Foundation's NSPredicate that would unlock a lot of benefits.

Swift 4 has KeyPath support which provides type safe and expressive description of properties (lvals). I think that these feature fits perfectly within the domain of NSPredicate. I would like the ability to express the following:

let predicate = NSPredicate(format: "%@ == %@", argumentArray: [ \Person.name, "Fred"])

This arrangement keeps the sensitive "guts" of KeyPath internal to Swift/Foundation while unlocking some very powerful new features. For instance, with the above capability one could easily write an NSManagedObject extension to do the following:

let minglesGroup = Person.find(in: moc, where: \Person.age > 30 && \.friends.count < 2)

NSPredicate implied by this construction is a text-based predicate (not block based) so it can run on CoreData with a SQLite store at full speed. This kind of querying is much safer and easier to maintain than stringly-typed predicates because it is compile checked.

What do you think?

1 Like

I think pitches belong to Evolution category. //cc @forum_admins move post?

Agreed, I moved this to the Pitches category.

2 Likes

Hello Swift Community,

I think that syntatically checked Core Data predicates is very important in long perspective. But i would like to pay attention to some current problems. I think that Swift Interpolation "\()" and Swift 4 KeyPath features compatiblity support is also vey important.

We can make:

NSPredicate(format: "\(#keyPath(Object.integerProperty)) = \(object.integerProperty)")

integerProperty has Int64 type and we have implicity %K format in this code.

But we can not make:

NSPredicate(format: "\(\Object.integerProprty) = \(object.integerProperty)")

because of error: «Terminating app due to uncaught exception «NSInvalidArgumentException', reason: 'Unable to parse the format string "Swift.ReferenceWritableKeyPath<TestApp.Object, Swift.Int64>».
I understand why we have such error but this prevent KeyPath usage in places where it is really necessary.

Thanks.

The issue here may be that #keyPath makes sure that all parts of the key path are @objc accessible. I.e.:

class Object {
	var integerProperty: Int64 = 0
}

#keyPath(Object.integerProperty) // error: argument of '#keyPath' refers to non-'@objc' property 'integerProperty'

Hence using Swift keypaths in this context is IMHO quite dangerous as it would allow non-@objc properties to be referrenced in the predicates (and not just them) resulting in runtime crashes which is exactly what you're trying to avoid...

In this case Object is NSManagedObject subclass (property has NSManaged mark) thats why property is @objc accessible. I also tried NSNumber type and result was the same.
I think that %K is simply can not accept KeyPath types. May be it requires some changes in Core Data predicate format processing, may be some conversation options is needed in Swift to make %K usage available with KeyPath types.
In any case i would like to support this lynch.sft topic with syntatically checked predicates. And I think it can help to improve work with Swift colllection native types (Array, Dictionary, Set) also (now we use closures for arrays and it is really very powerfull, but we have lack of such powerfull Core Data features as embedded functions and subqueries in predicates).
May be this problems can help to understand how we should improve this Swift KeyPath direction to help Swift and Core Data evolution.

That is true - #keyPath produces a string - an ObjC key path is a string, with each path component separated by a dot. While KeyPath is a Swift struct. These are two very different concepts.

While in your case the \Object.integerProtperty is @objc (or annotated with @NSManaged), there is no guarantee that it will be. For example:

struct Foo {
    var integerProperty: Int64 = 0
}

Then \Foo.integerProperty is also a valid KeyPath, but passing it to a NSPredicate doesn't make sense - yet there can't be any compile-time check for this, unlike with #keyPath. With ObjC, you need the "stringified keypath" with all of the path components accessible via ObjC runtime - which is what #keyPath guarantees, but KeyPath does not. It is true that every #keyPath can be converted to KeyPath, but it doesn't work the other way around.

Please keep in mind that NSPredicate is not used just in Core Data, where properties are usually @NSManaged. On macOS, it is very common to use NSPredicate with NSArrayController and other UI stuff, where you need to make sure that the properties are explicitely marked as @objc as otherwise you crash during runtime. For which the #keyPath is great as it checks accessibility of the properties.

1 Like

What is the gain compared to a closure?

let minglesGroup = Person.find(in: moc, where: { $0.age > 30 && $0.friends.count < 2 })

Performace in case of CoreData. If you have thousands of entries, CoreData needs to fetch all Person entities, all being fault at first - then the closure is called on each of them, but as their all faults, for each of them, CoreData will need to open the underlying database and fetch the properties. NSPredicate (unless block-based) is translated into an SQL query.

2 Likes

Yes, I agree with charlieMonroe, it is performance. Now in Core Data for predicates we have high performance, subqueries and expression functions but lack code safety. In case of closures we have code safety but lack performace and some syntactic sugar (for example, optimized expression functions - Apple Developer Documentation).

1 Like

Not just a SQL query - NSPredicate parses an entire syntax tree which you can inspect and transform to whatever you like (with help from NSExpression). It's very cool.

You could build a bunch of extensions on KeyPath which could produce an internal syntax tree, so the compiler basically does the parsing for you. Something like this (VirtualKeyPath · GitHub), but instead of just keeping a closure in the VirtualKeyPath, you'd build up an internal NSExpression tree.

I think something like that is worth exploring, because theoretically you could just write:

let myPredicate: NSPredicate = (\Person.age < 30) && (\Person.friends.count < 2)
1 Like

@charlieMonroe is correct that any @objc reachable property could be a KeyPath and that an NSPredicate constructed in that way is not guaranteed to be valid in the context of the backing store. But this is a general principle of NSPredicate. The documentation is very clear that stores are not required to implement of NSPredicate's features, and they are free to implement features which are not expressible via NSPredicate.

The win of this proposal is not that all such constructed predicates are guaranteed to run on whatever store they are intended for, but that the most-often-encountered maintenance problems are alleviated via compile time checking. Ex: If Person.friends is refactored to Person.connections, then a text-based NSPredicate is very easy to overlook and would fail at runtime. In contrast a KeyPath based NSPredicate would either be included in the refactor, or would fail to compile.

I believe #keyPath has this benefit as well

This is true, but keypath does not permit the type verification of the operands. Consider:

let car = Car.find(in: moc, where: \.make = "Mitsubishi") // no problem
let predicate = NSPredicate(format: "%K == %@", argumentArray: [#keyPath(Person.favoriteAnimal), car])
// the above predicate will fail at run time.

let predicate2 = \Person.favoriteAnimal == car // implies some trivial sugar to convert KeyPaths to NSPredicat
// fails at compile time.

Of course :) I was just saying that the key path name compile check isn't something we would gain with this.

But this is cool. I don't know how hard this key paths DSL would be, but it would be nice.
Also it's not too hard to constrain the utilities with KeyPath<From, To> where From: NSObject

The issue here is that the NSObject constraint isn't enough - you don't need to use NSObject subclass as long as the accessors are @objc. What would be required here is to differentiate path segments by the type of their accessors. I.e.:

class Foo {
    var bar: Int = 0
    @objc var objcBar: Int =1
}

Now \Foo.bar could be KeyPath<Foo, Int> while \Foo.objcBar could be ObjCKeyPath<Foo, Int>.

It would require this kind of differentiations of keypaths based on the access. It would then require the following logic:

  • ObjCKeyPath + ObjCKeyPath = ObjCKeyPath
  • KeyPath + ObjCKeyPath = KeyPath

And creating a predicate from keypath would require you to use the ObjCKeyPath subtype.

Can you really have @objc properties in @non-objc types? I suppose it has to do with SwiftObject being an NSObject...

SwiftObject is not inherited from NSObject. There's a discussion about adding this inheritance, but it would be quite a breaking change.

All Swift classes are represented in the ObjC runtime using mangled names - you can get your "pure Swift" class using objc_getClassList. Hence every Swift class can have an ObjC dispatch table. Example:

class Foo {
    @objc var bar: Int = 0
    @objc func myFunc() { ... }
}

There is nothing preventing you from invoking bar or myFunc from ObjC runtime (objc_msgSend) - and you will succeed.

Also, with Swift 4, the @objc attribute is not implied even when inheriting from NSObject, so the inheritance has absolutely no affect on this:

class NSFoo: NSObject {
    var bar: Int = 0
}

You can't access bar from ObjC, and sending -bar message to the object using objc_msgSend will result in a crash (selector not recognized).

2 Likes

A few common thoughts. I think it is not only necessary to improve already existing special KeyPath syntax to make it available in Core Data. Now we have Swift Server APIs Project (https://swift.org/server-apis/). May be it is time to begin such preparation steps in collections/databases direction and KeyPath improvements will be only part of future Swift 10 databases support? lynch.sft proposal can be a good start for it but in this case it will be only first step of big problem. It can be premature in context of Swift 5 but I use Swift and Core Data actively and lack of safety is big problem. It is good discussion in any case.

My dream language would allow me to express powerful predicates and sort orderings in a very natural manner. Both predicates and sort orderings would have enough meta data to turn them into a database query and run into a SQL database or even something like MongoDB.

let orderings = [\Person.age.asc, \Person.friends.count.asc]
let predicate  = Predicate<Person>( \age > 30 && \friends.count < 2 )


let people = moc.select(
    from: Person, 
    where: predicate, 
    orderBy: orderings)

You get the idea. Something like that.