projection.scala

package ru.circumflex
package orm

import java.sql.ResultSet

Projections

In relational algebra a projection is a function which describes a subset of columns returned from an SQL query. In Circumflex ORM instances of the Projection trait are used to process ResultSet and determine the result type of SQL queries.

We distinguish between atomic and composite projections: the former ones span across only one column of ResultSet, the latter ones contain a list of internal projections and therefore span across multiple columns.

Like with relation nodes, special alias this is expanded into query-unique alias to prevent collisions when aliases are not assigned explicitly.

Circumflex ORM supports querying arbitrary expressions which your database understands, you only need to explicitly specify an expected type.

trait Projection[T] extends SQLable {

  def read(rs: ResultSet): Option[T]

  def sqlAliases: Seq[String]

  protected var _alias: String = "this"
  def alias = _alias
  def AS(alias: String): this.type = {
    this._alias = alias
    this
  }

  override def toString = toSql
}

object Projection {
  implicit def toOrder(p: Projection[_]): Order =
    new Order(p.alias, Nil)
}

trait AtomicProjection[T] extends Projection[T] {
  def expression: String
  def sqlAliases = List(alias)
}

trait CompositeProjection[T] extends Projection[T] {
  def subProjections: Seq[Projection[_]]
  def sqlAliases = subProjections.flatMap(_.sqlAliases)

  override def equals(obj: Any) = obj match {
    case p: CompositeProjection[_] =>
      this.subProjections.toList == p.subProjections.toList
    case _ => false
  }

  private var _hash = 0;
  override def hashCode: Int = {
    if (_hash == 0)
      for (p <- subProjections)
        _hash = 31 * _hash + p.hashCode
    _hash
  }

  def toSql = subProjections.map(_.toSql).mkString(", ")
}

class ExpressionProjection[T](val expression: String)
    extends AtomicProjection[T] {

  def toSql = ormConf.dialect.alias(expression, alias)

  def read(rs: ResultSet): Option[T] = {
    val o = rs.getObject(alias)
    if (rs.wasNull) None
    else Some(o.asInstanceOf[T])
  }

  override def equals(obj: Any) = obj match {
    case p: ExpressionProjection[_] =>
      p.expression == this.expression
    case _ => false
  }

  override def hashCode = expression.hashCode
}

class FieldProjection[T, R <: Record[_, R]](
        val node: RelationNode[_, R],
        val field: Field[T, R])
    extends AtomicProjection[T] {

  def expression = ormConf.dialect.qualifyColumn(field, node.alias)

  def toSql = ormConf.dialect.alias(expression, alias)

  def read(rs: ResultSet) = field.read(rs, alias)

  override def equals(obj: Any) = obj match {
    case p: FieldProjection[_, _] =>
      p.node == this.node && p.field.name == this.field.name
    case _ => false
  }

  override def hashCode = node.hashCode * 31 + field.name.hashCode
}

class RecordProjection[PK, R <: Record[PK, R]](val node: RelationNode[PK, R])
    extends CompositeProjection[R] {

  protected val _fieldProjections: Seq[FieldProjection[_, R]] = node
      .relation.fields.map(f => new FieldProjection(node, f))

  def subProjections = _fieldProjections

  protected def _readCell[T](rs: ResultSet, vh: ValueHolder[T, R]): Option[T] = vh match {
    case f: Field[T, R] => _fieldProjections.find(_.field == f)
        .flatMap(_.asInstanceOf[Projection[T]].read(rs))
    case a: Association[T, R, _] => _readCell(rs, a.field)
    case p: FieldComposition2[Any, Any, R] => (_readCell(rs, p._1), _readCell(rs, p._2)) match {
      case (Some(v1), Some(v2)) => Some((v1, v2).asInstanceOf[T])
      case _ => None
    }
  }

  def read(rs: ResultSet): Option[R] = _readCell(rs, node.relation.PRIMARY_KEY).flatMap(id =>
    tx.cache.cacheRecord(id, node.relation, Some(readRecord(rs))))

  protected def readRecord(rs: ResultSet): R = {
    val record: R = node.relation.recordClass.newInstance
    _fieldProjections.foreach { p =>
      node.relation.getField(record, p.field.asInstanceOf[Field[Any, R]]).set(p.read(rs))
    }
    record
  }

  override def equals(obj: Any) = obj match {
    case p: RecordProjection[_, _] => this.node == p.node
    case _ => false
  }

  override def hashCode = node.hashCode
}

class UntypedTupleProjection(val subProjections: Projection[_]*)
    extends CompositeProjection[Array[Option[Any]]] {
  def read(rs: ResultSet): Option[Array[Option[Any]]] = Some(subProjections.map(_.read(rs)).toArray)
}

class PairProjection[T1, T2] (_1: Projection[T1], _2: Projection[T2])
    extends CompositeProjection[(Option[T1], Option[T2])] {
  def subProjections = List[Projection[_]](_1, _2)
  def read(rs: ResultSet): Option[(Option[T1], Option[T2])] =
    Some((_1.read(rs), _2.read(rs)))
}

class AliasMapProjection(val subProjections: Seq[Projection[_]])
    extends CompositeProjection[Map[String, Any]] {
  def read(rs: ResultSet): Option[Map[String, Any]] = {
    val pairs = subProjections.flatMap { p =>
      p.read(rs).map(v => p.alias -> v).asInstanceOf[Option[(String, Any)]]
    }
    Some(Map[String, Any](pairs: _*))
  }
}