<commit_before>//
//  Relationship.swift
//  ZeeQL
//
//  Created by <NAME> on 18/02/2017.
//  Copyright © 2017 ZeeZide GmbH. All rights reserved.
//


/**
 * A relationship connects two `Entity`'s using `Join`'s. It is one-way,
 * so you need to have separate `Relationship` objects for each direction.
 */
public protocol Relationship : Property, ExpressionEvaluation,
                               SmartDescription
{

  var  name              : String          { get }
  var  entity            : Entity          { get }
  var  destinationEntity : Entity?         { get }

  var  isToMany          : Bool            { get }
  var  minCount          : Int?            { get }
  var  maxCount          : Int?            { get }

  var  joins             : [ Join ]        { get }
  var  joinSemantic      : Join.Semantic   { get }
  
  func connectRelationships(in model : Model, entity: Entity)
  func disconnectRelationships()
  var  isPattern         : Bool            { get }
  
  var  updateRule        : ConstraintRule? { get }
  var  deleteRule        : ConstraintRule? { get }
  var  ownsDestination   : Bool            { get }
  
  var constraintName     : String?         { get }
  
  var isMandatory        : Bool            { get }
}

/**
 * Specifies what happens with the target if the source object of a
 * relationship gets deleted.
 */
public enum ConstraintRule {
  case nullify      // SET NULL
  case cascade
  case deny         // RESTRICT
  case noAction     // NO ACTION
  case applyDefault // SET DEFAULT
  
  public var sqlString : String { // TBD: name of property
    switch self {
      case .nullify:      return "SET NULL"
      case .cascade:      return "CASCADE"
      case .deny:         return "RESTRICT"
      case .noAction:     return "NO ACTION"
      case .applyDefault: return "SET DEFAULT"
    }
  }
}

fileprivate var log : ZeeQLLogger { return globalZeeQLLogger }

public extension Relationship { // default imp
  
  var joinSemantic   : Join.Semantic   { return .innerJoin }
  var deleteRule     : ConstraintRule? { return nil        }
  var updateRule     : ConstraintRule? { return nil        }
  var constraintName : String?         { return nil        }

  var minCount       : Int? { return isToMany ? nil : (isMandatory ? 0 : 1) }
  var maxCount       : Int? { return isToMany ? nil : 1 }
  
  var isMandatory : Bool { return isMandatoryDefaultImplementation }

  internal var isMandatoryDefaultImplementation : Bool {
    if isToMany { return (minCount ?? 0) > 0 }
    
    // 1:1: figure out whether the relationship has one required join
    for join in joins {
      if let source = join.source {
        if let nullable = source.allowsNull, !nullable { return true }
      }
      else if let sourceName = join.sourceName {
        if let source = entity[attribute: sourceName] {
          if let nullable = source.allowsNull, !nullable { return true }
        }
        else {
          log.error("testing isMandatory on relship w/o proper joins:",
                    self, join)
        }
      }
      else {
        log.error("testing isMandatory on relship w/o proper joins:", self)
        assert(join.sourceName != nil, "missing source in join")
      }
    }
    return false
  }
  
  // MARK: - ExpressionEvaluation
  
  func valueFor(object: Any?) -> Any? {
    return KeyValueCoding.value(forKeyPath: name, inObject: object)
  }

  func connectRelationships(in model : Model, entity: Entity) {}
  func disconnectRelationships() {}

  var  ownsDestination : Bool {
    guard let deleteRule = deleteRule else { return false }
    
    // TBD: I think this is right, if a delete cascades, we own the target
    if case .cascade = deleteRule { return true }
    return false
  }
}

public extension Relationship { // extra methods

  /**
   * Returns true if this is a flattened relationship. A flattened relationship
   * is a shortcut for a longer path. For example to retrieve the addresses of
   * the companies assigned to a person you could use:
   *
   *   employments.company.addresses
   *
   * Or you could create a 'flattened' relationship 'companyAddresses' which
   * contains this path. The path of the real relationship can be retrieved
   * using the relationshipPath() method.
   */
  var isFlattened : Bool { return relationshipPath != nil }
  
  var isCompound  : Bool { return joins.count > 1 }

  
  /**
   * Returns the Relationship objects for each component of the
   * relationshipPath() of a flattened Relationship. Eg:
   *
   *     employments.company.addresses
   *
   * could return three Relationship objects:
   *
   *     'employments', source = Persons,     dest = Employments
   *     'company',     source = Employments, dest = Companies
   *     'addresses',   source = Companies,   dest = Addresses
   * 
   * The method returns nil if this is not a flattened relationship.
   */
  var componentRelationships : [ Relationship ]? {
    guard let relationshipPath = relationshipPath else { return nil }
    
    var relentity : Entity? = entity

    var relships = [ Relationship ]()
    let path = relationshipPath.components(separatedBy: ".")
    relships.reserveCapacity(path.count)

    for p in path {
      guard let rel = relentity?[relationship: p] else {
        return nil // TODO: log
      }
      
      assert(!rel.isFlattened, "Not implemented")
        // TODO: pathes containing flattened relships
      
      relentity = rel.destinationEntity
    }
    
    return relships
  }

  /**
   * Makes the Relationship check whether any of its joins reference the
   * given property.
   * A property is an Attribute or Relationship object.
   */
  func references(property: Property) -> Bool {
    for join in joins {
      if join.references(property: property) { return true }
    }
    
    if relationshipPath != nil {
      if let props = componentRelationships {
        for r in props {
          if r === property { return true }
          // TBD: do we need to call referencesProperty on the relationships?
        }
      }
    }
    
    return false
  }
  
  
  /**
   * Checks whether the Relationship got resolved (whether the Entity of
   * the destination entity was looked up).
   */
  var isConnected : Bool {
    if destinationEntity == nil { return false }
    for join in joins {
      if !join.isConnected { return false }
    }
    return true
  }
  
  
  /**
   * *Locates* an inverse relationship in the destination entity.
   *
   * Note: this does *not* create one.
   * 
   * Example: n:1
   *   person  ( person_id, company_id )
   *   company ( company_id )
   * Person:
   *   toCompany [toOne] ( SRC.company_id = TAR.company_id )
   * Company:
   *   toPerson [toMany] ( SRC.company_id = TAR.company_id )
   */
  var inverseRelationship : Relationship? {
    // TBD: implement me
    // TBD: consider N:M relationships
    // find a relationship in the target which joins the same columns
    guard !joins.isEmpty else { return nil }
    guard joins.count < 2 else {
      assert(joins.count < 2, "not supported: inv-relship with multiple joins")
      return nil
    }
    
    guard let dest = destinationEntity else { return nil }
    
    for rel in dest.relationships {
      guard entity === rel.destinationEntity else { continue }
        // other entity, does not point back
      
      guard rel.joins.count == joins.count else { continue }
        // join array sizes do not match
      
      // TBD: we only support one join for now ...
      let myJoin = self.joins[0]
      let enemy  = rel.joins[0]
      
      // TBD: equality might not be correct since joins can be directed
      //      (relevant for LEFT/RIGHT joins I guess)
      if myJoin == enemy || myJoin.isReciprocalTo(join: enemy) {
        return rel
      }
    }
    
    return nil
  }
  
}

public extension Relationship {

  func appendToDescription(_ ms: inout String) {
    if isPattern { ms += " pattern" }
    
    ms += " \(name)"
    ms += isToMany ? "[1:n]" : "[1:1]"<commit_msg>Do not check for equality when looking up inverse<commit_after>//
//  Relationship.swift
//  ZeeQL
//
//  Created by <NAME> on 18/02/2017.
//  Copyright © 2017 ZeeZide GmbH. All rights reserved.
//


/**
 * A relationship connects two `Entity`'s using `Join`'s. It is one-way,
 * so you need to have separate `Relationship` objects for each direction.
 */
public protocol Relationship : Property, ExpressionEvaluation,
                               SmartDescription
{

  var  name              : String          { get }
  var  entity            : Entity          { get }
  var  destinationEntity : Entity?         { get }

  var  isToMany          : Bool            { get }
  var  minCount          : Int?            { get }
  var  maxCount          : Int?            { get }

  var  joins             : [ Join ]        { get }
  var  joinSemantic      : Join.Semantic   { get }
  
  func connectRelationships(in model : Model, entity: Entity)
  func disconnectRelationships()
  var  isPattern         : Bool            { get }
  
  var  updateRule        : ConstraintRule? { get }
  var  deleteRule        : ConstraintRule? { get }
  var  ownsDestination   : Bool            { get }
  
  var constraintName     : String?         { get }
  
  var isMandatory        : Bool            { get }
}

/**
 * Specifies what happens with the target if the source object of a
 * relationship gets deleted.
 */
public enum ConstraintRule {
  case nullify      // SET NULL
  case cascade
  case deny         // RESTRICT
  case noAction     // NO ACTION
  case applyDefault // SET DEFAULT
  
  public var sqlString : String { // TBD: name of property
    switch self {
      case .nullify:      return "SET NULL"
      case .cascade:      return "CASCADE"
      case .deny:         return "RESTRICT"
      case .noAction:     return "NO ACTION"
      case .applyDefault: return "SET DEFAULT"
    }
  }
}

fileprivate var log : ZeeQLLogger { return globalZeeQLLogger }

public extension Relationship { // default imp
  
  var joinSemantic   : Join.Semantic   { return .innerJoin }
  var deleteRule     : ConstraintRule? { return nil        }
  var updateRule     : ConstraintRule? { return nil        }
  var constraintName : String?         { return nil        }

  var minCount       : Int? { return isToMany ? nil : (isMandatory ? 0 : 1) }
  var maxCount       : Int? { return isToMany ? nil : 1 }
  
  var isMandatory : Bool { return isMandatoryDefaultImplementation }

  internal var isMandatoryDefaultImplementation : Bool {
    if isToMany { return (minCount ?? 0) > 0 }
    
    // 1:1: figure out whether the relationship has one required join
    for join in joins {
      if let source = join.source {
        if let nullable = source.allowsNull, !nullable { return true }
      }
      else if let sourceName = join.sourceName {
        if let source = entity[attribute: sourceName] {
          if let nullable = source.allowsNull, !nullable { return true }
        }
        else {
          log.error("testing isMandatory on relship w/o proper joins:",
                    self, join)
        }
      }
      else {
        log.error("testing isMandatory on relship w/o proper joins:", self)
        assert(join.sourceName != nil, "missing source in join")
      }
    }
    return false
  }
  
  // MARK: - ExpressionEvaluation
  
  func valueFor(object: Any?) -> Any? {
    return KeyValueCoding.value(forKeyPath: name, inObject: object)
  }

  func connectRelationships(in model : Model, entity: Entity) {}
  func disconnectRelationships() {}

  var  ownsDestination : Bool {
    guard let deleteRule = deleteRule else { return false }
    
    // TBD: I think this is right, if a delete cascades, we own the target
    if case .cascade = deleteRule { return true }
    return false
  }
}

public extension Relationship { // extra methods

  /**
   * Returns true if this is a flattened relationship. A flattened relationship
   * is a shortcut for a longer path. For example to retrieve the addresses of
   * the companies assigned to a person you could use:
   *
   *   employments.company.addresses
   *
   * Or you could create a 'flattened' relationship 'companyAddresses' which
   * contains this path. The path of the real relationship can be retrieved
   * using the relationshipPath() method.
   */
  var isFlattened : Bool { return relationshipPath != nil }
  
  var isCompound  : Bool { return joins.count > 1 }

  
  /**
   * Returns the Relationship objects for each component of the
   * relationshipPath() of a flattened Relationship. Eg:
   *
   *     employments.company.addresses
   *
   * could return three Relationship objects:
   *
   *     'employments', source = Persons,     dest = Employments
   *     'company',     source = Employments, dest = Companies
   *     'addresses',   source = Companies,   dest = Addresses
   * 
   * The method returns nil if this is not a flattened relationship.
   */
  var componentRelationships : [ Relationship ]? {
    guard let relationshipPath = relationshipPath else { return nil }
    
    var relentity : Entity? = entity

    var relships = [ Relationship ]()
    let path = relationshipPath.components(separatedBy: ".")
    relships.reserveCapacity(path.count)

    for p in path {
      guard let rel = relentity?[relationship: p] else {
        return nil // TODO: log
      }
      
      assert(!rel.isFlattened, "Not implemented")
        // TODO: pathes containing flattened relships
      
      relentity = rel.destinationEntity
    }
    
    return relships
  }

  /**
   * Makes the Relationship check whether any of its joins reference the
   * given property.
   * A property is an Attribute or Relationship object.
   */
  func references(property: Property) -> Bool {
    for join in joins {
      if join.references(property: property) { return true }
    }
    
    if relationshipPath != nil {
      if let props = componentRelationships {
        for r in props {
          if r === property { return true }
          // TBD: do we need to call referencesProperty on the relationships?
        }
      }
    }
    
    return false
  }
  
  
  /**
   * Checks whether the Relationship got resolved (whether the Entity of
   * the destination entity was looked up).
   */
  var isConnected : Bool {
    if destinationEntity == nil { return false }
    for join in joins {
      if !join.isConnected { return false }
    }
    return true
  }
  
  
  /**
   * *Locates* an inverse relationship in the destination entity.
   *
   * Note: this does *not* create one.
   * 
   * Example: n:1
   *   person  ( person_id, company_id )
   *   company ( company_id )
   * Person:
   *   toCompany [toOne] ( SRC.company_id = TAR.company_id )
   * Company:
   *   toPerson [toMany] ( SRC.company_id = TAR.company_id )
   */
  var inverseRelationship : Relationship? {
    // TBD: implement me
    // TBD: consider N:M relationships
    // find a relationship in the target which joins the same columns
    guard !joins.isEmpty else { return nil }
    guard joins.count < 2 else {
      assert(joins.count < 2, "not supported: inv-relship with multiple joins")
      return nil
    }
    
    guard let dest = destinationEntity else { return nil }
    
    for rel in dest.relationships {
      guard entity === rel.destinationEntity else { continue }
        // other entity, does not point back
      
      guard rel.joins.count == joins.count else { continue }
        // join array sizes do not match
      
      // TBD: we only support one join for now ...
      let myJoin = self.joins[0]
      let enemy  = rel.joins[0]
      
      #if false // 2019-09-05: yeah, that definitely feels wrong, it is equal
                // quite often (source.person_id = target.person_id), but that
                // is just a special case of the reciprocal
                // (source.person_id = target.manager_id)
        // TBD: equality might not be correct since joins can be directed
        //      (relevant for LEFT/RIGHT joins I guess)
        if myJoin == enemy || myJoin.isReciprocalTo(join: enemy) {
          return rel
        }
      #else
        if myJoin.isReciprocalTo(join: enemy) {
          return rel
        }
      #endif
    }
    
    return nil
  }
  
}

public extension Relationship {

  func appendToDescription(_ ms: inout String) {
    if isPattern { ms += " pattern" }
    
    ms += " \(name)"
    ms += isToMany ? "[1:n]" : "[1:1]"
{'fraction_non_alphanumeric': 0.0846139828706797, 'fraction_numerical': 0.003887505314948673, 'mean_word_length': 2.0970654627539504, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3615781', 'n_tokens_mistral': 4816, 'n_tokens_neox': 4578, 'n_words': 1613}