SwiftData crashes when saving certain types of enum cases

Originator:darren.mo
Number:rdar://FB13363786 Date Originated:2023-11-11
Status:Closed Resolved:iOS 17.5
Product:SwiftData Product Version:iOS 17.2 Beta 2
Classification:Crash Reproducible:Always
 
I have a model class that has a persistent property for a `Codable` enum. If the enum type is written a certain way, the app will crash because SwiftData raises the following Objective-C exception: `-[__NSDictionaryM UTF8String]: unrecognized selector sent to instance 0x6000010f69a0`. The full stack trace is attached.

I have attached a minimal sample project reproducing the issue. `Item.swift` and `ContentView.swift` are annotated with comments explaining the significance of certain pieces of code. Below is a summary of the conditions that are needed to trigger the SwiftData bug:
- There needs to be a nested enum.
- Within the nested enum, there needs to be a case without an associated value.
- The nested enum must have more than one case.
- A second insert of the same type of item is needed to trigger the SwiftData bug.

---

I also spent a few hours reverse-engineering SwiftData/Core Data. I believe I have found the bug and I believe the bug can be resolved by less than a day of work on your part. The bug is in the `NSSQLBindVariable` class.

`NSSQLBindVariable` is part of the code that deals with SQLite prepared statements. It represents the value that is bound to a given parameter in the SQL statement template. It has an initializer `initWithValue:sqlType:propertyDescription:allowCoercion:` that takes in the parameter value of the first SQL row that is being inserted. It also has a setter method `setValue:` that updates the parameter value for subsequent rows. The bug is in the setter method.

The initializer looks similar to the following:
```
- (instancetype)initWithValue:(id)value sqlType:(int)sqlType … {
  self = [super init];
  if (self) {
    if (value == [NSNull null]) {
      value = nil;
    }
    if (sqlType == 6 && ![value isNSString__]) {
      value = [value description];
    }
    _value = value;
  }
  return self;
}
```

As shown above, the initializer potentially performs a couple transformations to the `value` parameter before storing it:
1. It replaces `NSNull` with `nil`.
2. If the value is supposed to be a SQLite text value and the value is not already an `NSString`, then it calls `[value description]` to get an `NSString`.

The setter method, on the other hand, does none of those transformations before storing its `value` parameter. This is the bug.

Let’s see how the bug manifests and results in the crash. SwiftData adds a SQLite table column for each case (or nested case) of a `Codable` enum. For nested enum cases without associated values, the encoded value is an empty dictionary that, for some reason, SwiftData wants to store as a SQLite text value. The `NSSQLBindVariable` initializer is able to handle this situation because it calls the dictionary’s `description` method to get an `NSString`. The `NSSQLBindVariable` setter method’s lack of similar logic causes it to propagate the dictionary, which eventually results in some code trying to call `UTF8String` on the dictionary, resulting in the crash.

An obvious fix would be to refactor the value transformation logic from the `NSSQLBindVariable` initializer into a separate method to be called by both the initializer and the setter method.

There may also be another bug that is worth following up on: why is SwiftData trying to store the empty dictionary as a SQLite text value?

Comments

Apple

Hello,

Thank you for filing the feedback report. We made changes to our system. Please verify this issue with iOS 17.5 beta and update your bug report with your results by logging into https://feedbackassistant.apple.com/ or by using the Feedback Assistant app.

iOS 17.5 beta (Build: 21F5048f) https://developer.apple.com/download/ Posted Date: April 2, 2024

If resolved, you can close your feedback by choosing the Close Feedback menu item in the Actions pop-up in Feedback Assistant.

If the issue persists, please attach a new sysdiagnose captured in the latest build and attach it to the bug report. Thank you.

iOS sysdiagnose Instructions: https://developer.apple.com/services-account/download?path=/iOS/iOS_Logs/sysdiagnose_Logging_Instructions.pdf


For a complete list of logging instructions visit: https://developer.apple.com/bug-reporting/profiles-and-logs/

We appreciate your feedback and looking forward to hear from you.


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!