iosdev

A very annoying Core Data NSPredicate crash

A bug in Apple’s code that appears only in 10.3 and only on device

This started with a very strange crash report:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: 'keypath (null).orderitems.order not found in entity <NSSQLEntity ProductItem id=20>'

The stack trace lead to this particular code:

let predicate = NSPredicate(format: "%K.%K.%K == %d AND ANY %K.%K.%K == %@",
		ProductItem.Relationships.coloredProduct,
		ColoredProduct.Relationships.product,
		Product.Attributes.seasonCode,
		seasonCode,
		ProductItem.Relationships.orderitems,
		OrderItem.Relationships.order,
		Order.Attributes.orderCode,
		orderCode)

Now, of course, that crash description makes no sense. Core Data model is fine, it obviously had ProductItem entity and proper relationship to OrderItem entity. This code works great in iOS 11 on both the device and simulator. It even worked in iOS 10.3.1 Simulator in Xcode. But still – I had crash reports from 4 different iPad devices running 10.3.1. This was not an obvious error in the code, it was something really devious.

I knew from experience that Core Data’s NSPredicate to SQLite transformation engine can be finicky at times, so I rewrote the format into this:

let predicate = NSPredicate(format: "%K.%K.%K == %d AND SUBQUERY(%K, $oi, $oi.%K.%K == %@).@count > 0",
		ProductItem.Relationships.coloredProduct,
		ColoredProduct.Relationships.product,
		Product.Attributes.seasonCode,
		seasonCode,
		ProductItem.Relationships.orderitems,
		OrderItem.Relationships.order,
		Order.Attributes.orderCode,
		orderCode)

This yielded no changes, still crashes on the same devices. Which were in a different country thus no way to attach LLDB to them.

I had 0 iPads with 10.3 but then luckily remembered an old iPhone 5 my kids use for drone control. This was also 32bit CPU like the one in iPad 4th gen and it was also running only 10.3.

And voila, it crashes on that device as well. The only suspicious thing was the predicate itself. When printing it out in the console, this is what is shows:

coloredProduct.product.seasonCode = 82 AND
SUBQUERY(, $oi, $oi.orderItems.order == "orderCode")@.count > 0

Do you see it? SUBQUERY(, $oi... should be SUBQUERY(orderItems, $oi.... I tried with the original form using ANY and this gives me the original crash report description:

coloredProduct.product.seasonCode = 82 AND
ANY (null).orderItems.order == "orderCode"

So in this particular case and only on 10.3 (again, it’s fine on iOS 11) all the values get shifted by one placeholder.

The solution is to use NSCompoundPredicate form:

var predicates: [NSPredicate] = []
predicates.append(
	NSPredicate(format: "%K.%K.%K == %d",
				ProductItem.Relationships.coloredProduct,
				ColoredProduct.Relationships.product,
				Product.Attributes.seasonCode,
				order.seasonCode)
)
predicates.append(
	NSPredicate(format: "SUBQUERY(%K, $oi, $oi.%K.%K == %@).@count > 0",
				ProductItem.Relationships.orderitems,
				OrderItem.Relationships.order,
				Order.Attributes.orderCode,
				orderCode)
)
let predicate = NSCompoundPredicate.init(andPredicateWithSubpredicates: predicates)

This works fine on both 10.3 and 11.x devices.

Chug this one under the “Core Data mysterious ways”.