common.scala

package ru.circumflex.orm

import ru.circumflex.core._

SQLable & Expression

Every object capable of rendering itself into an SQL statement should extend the SQLable trait.

trait SQLable {
  def toSql: String
}

Parameterized expressions

The Expression trait provides basic functionality for dealing with SQL expressions with JDBC-style parameters.

trait Expression extends SQLable {

  def parameters: Seq[Any]

  def toInlineSql: String = parameters.foldLeft(toSql)((sql, p) =>
    sql.replaceFirst("\\?", dialect.escapeParameter(p)
        .replaceAll("\\\\", "\\\\\\\\")
        .replaceAll("\\$", "\\\\\\$")))

  override def equals(that: Any) = that match {
    case e: Expression =>
      e.toSql == this.toSql && e.parameters.toList == this.parameters.toList
    case _ => false
  }

  override def hashCode = 0

  override def toString = toSql
}

Schema Object

Every database object which could be created or dropped should implement the SchemaObject trait.

trait SchemaObject {
  def sqlCreate: String

  def sqlDrop: String

  def objectName: String

  override def hashCode = objectName.toLowerCase.hashCode

  override def equals(obj: Any) = obj match {
    case so: SchemaObject => so.objectName.equalsIgnoreCase(this.objectName)
    case _ => false
  }

  override def toString = objectName
}

Value holders

Value holder is an atomic data-carrier unit of a record. It carries methods for identifying and manipulating data fields inside persistent records.

trait ValueHolder[T, R <: Record[_, R]] extends Equals with Wrapper[Option[T]] {

  def name: String
  def record: R
  def item = value

Setters

Setters provide a handy mechanism for preprocessing values before setting them. They are functions T => T which are applied one-by-one each time you set new non-null value. You can add a setter by invoking the addSetter method:

val pkg = "package".TEXT.NOT_NULL
    .addSetter(_.trim)
    .addSetter(_.toLowerCase)
    .addSetter(_.replaceAll("/","."))

pkg := "  ru/circumflex/ORM  "  // "ru.circumflex.orm" will be assigned
  protected var _setters: Seq[T => T] = Nil
  def setters: Seq[T => T] = _setters
  def addSetter(f: T => T): this.type = {
    _setters ++= List(f)
    return this
  }

Accessing & Setting Values

Values are stored internally as Option[T]. None stands both for uninitialized and null values. Following examples show how field values can be accessed or set:

val id = "id" BIGINT

// accessing
id.value    // Option[Long]
id.get      // Option[Long]
id()        // Long or exception
getOrElse(default: Long)  // Long

// setting
id.set(Some(1l))
id.setNull
id := 1l

The null_? method indicates whether the underlying value is null or not.

  protected var _value: Option[T] = None

  // Accessing

  def value: Option[T] = _value
  def get: Option[T] = value
  def apply(): T = value.get
  def getOrElse(default: T): T = value.getOrElse(default)

  def null_?(): Boolean = value == None

  // Setting

  def set(v: Option[T]): this.type = {
    // process value with setters
    _value = v.map { v =>
      setters.foldLeft(v) { (v, f) => f(v) }
    }
    return this
  }
  def set(v: T): this.type = set(any2option(v))
  def setNull: this.type = set(None)
  def :=(v: T): Unit = set(v)

Methods from Option

Since ValueHolder is just a wrapper around Option, we provide some methods to work with your values in functional style (they delegate to their equivalents in Option).

  def map[B](f: T => B): Option[B] =
    value.map(f)
  def flatMap[B](f: T => Option[B]): Option[B] =
    value.flatMap(f)
  def orElse[B >: T](alternative: => Option[B]): Option[B] =
    value.orElse(alternative)

Equality & Others

Two fields are considered equal if they belong to the same type of records and share the same name.

The hashCode calculation is consistent with equals definition.

The canEqual method indicates whether the two fields belong to the same type of records.

Finally, toString returns the qualified name of relation which it belongs to followed by a dot and the field name.

  override def equals(that: Any): Boolean = that match {
    case that: ValueHolder[_, _] => this.canEqual(that) &&
        this.name == that.name
    case _ => false
  }
  override lazy val hashCode: Int =  record.relation.qualifiedName.hashCode * 31 +
      name.hashCode
  def canEqual(that: Any): Boolean = that match {
    case that: ValueHolder[_, _] => this.record.canEqual(that.record)
    case _ => false
  }
  override def toString: String = record.relation.qualifiedName + "." + name

The placeholder method returns an expression which is used to mark a parameter inside JDBC PreparedStatement (usually ? works, but custom data-type may require some special treatment).

  def placeholder = dialect.placeholder

Composing predicates

ValueHolder provides very basic functionality for predicates composition:

  • aliasedName returns the name of this holder qualified with node alias (in appropriate context);
  • EQ creates an equality predicate (i.e. column = value or column = column);
  • NE creates an inequality predicate (i.e. column <> value or column <> column).
  • IS_NULL and IS_NOT_NULL creates (not-)nullability predicates (i.e. column IS NULL or column IS NOT NULL).

More specific predicates can be acquired from subclasses.

  def aliasedName = aliasStack.pop match {
    case Some(alias: String) => alias + "." + name
    case _ => name
  }

  def EQ(value: T): Predicate =
    new SimpleExpression(dialect.EQ(aliasedName, placeholder), List(value))
  def EQ(col: ColumnExpression[_, _]): Predicate =
    new SimpleExpression(dialect.EQ(aliasedName, col.toSql), Nil)
  def NE(value: T): Predicate =
    new SimpleExpression(dialect.NE(aliasedName, placeholder), List(value))
  def NE(col: ColumnExpression[_, _]): Predicate =
    new SimpleExpression(dialect.NE(aliasedName, col.toSql), Nil)
  def IS_NULL: Predicate =
    new SimpleExpression(dialect.IS_NULL(aliasedName), Nil)
  def IS_NOT_NULL: Predicate =
    new SimpleExpression(dialect.IS_NOT_NULL(aliasedName), Nil)

}

class ColumnExpression[T, R <: Record[_, R]](column: ValueHolder[T, R])
    extends Expression {
  def parameters = Nil
  val toSql = column.aliasedName
}