iosswiftgenericsnspredicateswiftdata

Predicates with generics


I’m trying to use Predicate with generics but I keep getting the following compilation error :

Cannot convert value of type 'PredicateExpressions.Equal<PredicateExpressions.ConditionalCast<PredicateExpressions.KeyPath<PredicateExpressions.Variable<E.EntityType>, E.EntityType.PrimaryKey?>, Key>, PredicateExpressions.Value<Optional>>' to closure result type 'any StandardPredicateExpression'

I have the following :

public protocol Entity {
  associatedtype DomainModel
  associatedtype PrimaryKey: Equatable

  /// The entity's domain model
  var domainModel: DomainModel { get }
  /// The entity's primary key
  var primaryKey: PrimaryKey { get }
}

public protocol DomainEntity {
  associatedtype EntityType: PersistentModel & Entity

  var entity: EntityType? { get }
}

@ModelActor
actor DataSource<E: DomainEntity> {
  typealias Model = E.EntityType.DomainModel

  func get<Key: Equatable>(by key: Key) throws -> Model? {
    let predicate: Predicate<E.EntityType> = #Predicate {
      ($0.primaryKey as? Key) == key // I’m having the error here
    }
    let fetchDescriptor = FetchDescriptor<E.EntityType>(predicate: predicate)

    guard let entity: E.EntityType = try modelContext.fetch(fetchDescriptor).first else {
      return nil
    }

    guard let model = entity.domainModel as? Model else {
      throw DatabaseError.conversionFailed
    }
    return model
  }
}

I get that the error is kind of explaining that it can’t figure out the underlying types, but as both properties are Equatable, it shouldn’t be an issue.

Does anyone have an idea of how I could do such a comparison using predicates? Or is there another way?


Solution

  • #Predicate requires you to write a StandardPredicateExpression, which inherits from Codable.

    For the predicate to be a StandardPredicateExpression, constant values such as the parameter key should also be Codable. Otherwise it would be no more than a regular PredicateExpression.

    Try expanding the #Predicate macro and see how the parameter key becomes PredicateExpressions.build_Arg(key). This creates a PredicateExpressions.Value<Key>, which only conforms to StandardPredicateExpression when Key is Codable.

    The generic type parameter Key should be constrained to Codable as well:

    func get<Key: Equatable & Codable>(by key: Key) throws -> Model?
    

    That said, wouldn't it make more sense to just use E.EntityType.PrimaryKey, instead of a separate type parameter? You don't need the as? Key cast at all.

    func get(by key: E.EntityType.PrimaryKey) throws -> Model? {
        let predicate: Predicate<E.EntityType> = #Predicate {
            $0.primaryKey == key
        }
        ...
    }
    

    This means that PrimaryKey should be constrained to Codable too:

    associatedtype PrimaryKey: Equatable, Codable