Hi! I'm running into some confusing behavior when attempting to delete all instance of one model type from a ModelContext. My problem is specifically using the delete(model:where:includeSubclasses:)^1 function (and passing in a model type). I seem to be running into situations where this function fails silently without throwing an error (no models are deleted).
I am seeing this same behavior from Xcode_15.4.0 and Xcode_16_beta_4.
I start with a model:
@Model final public class Item {
  var timestamp: Date
  
  public init(timestamp: Date = .now) {
    self.timestamp = timestamp
  }
}
Here is an example of a Store class that wraps a ModelContext:
final public class Store {
  public let modelContext: ModelContext
  
  public init(modelContainer: SwiftData.ModelContainer) {
    self.modelContext = ModelContext(modelContainer)
  }
}
extension Store {
  private convenience init(
    schema: Schema,
    configuration: ModelConfiguration
  ) throws {
    let container = try ModelContainer(
      for: schema,
      configurations: configuration
    )
    self.init(modelContainer: container)
  }
}
extension Store {
  public convenience init(url: URL) throws {
    let schema = Schema(Self.models)
    let configuration = ModelConfiguration(url: url)
    try self.init(
      schema: schema,
      configuration: configuration
    )
  }
}
extension Store {
  public convenience init(isStoredInMemoryOnly: Bool = false) throws {
    let schema = Schema(Self.models)
    let configuration = ModelConfiguration(isStoredInMemoryOnly: isStoredInMemoryOnly)
    try self.init(
      schema: schema,
      configuration: configuration
    )
  }
}
extension Store {
  public func fetch<T>(_ type: T.Type) throws -> Array<T> where T : PersistentModel {
    try self.modelContext.fetch(
      FetchDescriptor<T>()
    )
  }
}
extension Store {
  public func fetchCount<T>(_ type: T.Type) throws -> Int where T : PersistentModel {
    try self.modelContext.fetchCount(
      FetchDescriptor<T>()
    )
  }
}
extension Store {
  public func insert<T>(_ model: T) where T : PersistentModel {
    self.modelContext.insert(model)
  }
}
extension Store {
  public func delete<T>(model: T.Type) throws where T : PersistentModel {
    try self.modelContext.delete(model: model)
  }
}
extension Store {
  public func deleteWithIteration<T>(model: T.Type) throws where T : PersistentModel {
    for model in try self.fetch(model) {
      self.modelContext.delete(model)
    }
  }
}
extension Store {
  private static var models: Array<any PersistentModel.Type> {
    [Item.self]
  }
}
That should be pretty simple… I can use this Store to read and write Item instances to a ModelContext.
Here is an example of an executable that shows off the unexpected behavior:
func main() async throws {
  do {
    let store = try Store(isStoredInMemoryOnly: true)
    store.insert(Item())
    print(try store.fetchCount(Item.self) == 1)
    try store.delete(model: Item.self)
    print(try store.fetchCount(Item.self) == 0)
  }
  do {
    let store = try Store(isStoredInMemoryOnly: true)
    store.insert(Item())
    print(try store.fetchCount(Item.self) == 1)
    try store.deleteWithIteration(model: Item.self)
    print(try store.fetchCount(Item.self) == 0)
  }
  do {
    let store = try StoreActor(isStoredInMemoryOnly: true)
    await store.insert(Item())
    print(try await store.fetchCount(Item.self) == 1)
    try await store.delete(model: Item.self)
    print(try await store.fetchCount(Item.self) == 0)
  }
  do {
    let store = try StoreActor(isStoredInMemoryOnly: true)
    await store.insert(Item())
    print(try await store.fetchCount(Item.self) == 1)
    try await store.deleteWithIteration(model: Item.self)
    print(try await store.fetchCount(Item.self) == 0)
  }
}
try await main()
My first step is to set up an executable with an info.plist to support SwiftData.^2
My expectation is all these print statements should be true. What actually happens is that the calls to delete(model:where:includeSubclasses:) seem to not be deleting any models (and also seem to not be throwing errors).
I also have the option to test this behavior with XCTest. I see the same unexpected behavior:
import XCTest
final class StoreXCTests : XCTestCase {
  func testDelete() throws {
    let store = try Store(isStoredInMemoryOnly: true)
    store.insert(Item())
    XCTAssert(try store.fetchCount(Item.self) == 1)
    try store.delete(model: Item.self)
    XCTAssert(try store.fetchCount(Item.self) == 0)
  }
  
  func testDeleteWithIteration() throws {
    let store = try Store(isStoredInMemoryOnly: true)
    store.insert(Item())
    XCTAssert(try store.fetchCount(Item.self) == 1)
    try store.deleteWithIteration(model: Item.self)
    XCTAssert(try store.fetchCount(Item.self) == 0)
  }
}
Those tests fail… implying that the delete(model:where:includeSubclasses:) is not actually deleting any models.
FWIW… I see the same behavior (from command-line and XCTest) when my Store conforms to ModelActor.^3 ^4
This does not seem to be the behavior I am seeing from using the delete(model:where:includeSubclasses:) in a SwiftUI app.^5 Calling the delete(model:where:includeSubclasses:) function from SwiftUI does delete all the model instances.
The SwiftUI app uses a ModelContext directly (without a Store type). I can trying writing unit tests directly against ModelContext and I see the same behavior as before (no model instances are being deleted).^6
Any ideas about that? Is this a known issue with SwiftData that is being tracked? Is the delete(model:where:includeSubclasses:) known to be "flaky" when called from outside SwiftUI? Is there anything about the way these ModelContext instance are being created that we think is leading to this unexpected behavior?