Skip to content

Commit

Permalink
Feature/column names (#863)
Browse files Browse the repository at this point in the history
* Optimised recursive macro route.

* Removing things no longer needed.

* Improving error message.

* Fixing compilation errors.

* Simplifying local setup
  • Loading branch information
alexflav23 authored Nov 10, 2018
1 parent f86a1c5 commit e7a68e3
Show file tree
Hide file tree
Showing 9 changed files with 72 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,71 +15,34 @@
*/
package com.outworkers.phantom.builder.primitives

import java.net.InetAddress
import java.nio.{BufferUnderflowException, ByteBuffer}
import java.util.{Date, UUID}
import java.nio.BufferUnderflowException

import com.datastax.driver.core.exceptions.InvalidTypeException
import com.outworkers.phantom.builder.query.prepared.ListValue
import com.outworkers.phantom.macros.toolbelt.BlackboxToolbelt
import org.joda.time.DateTime

import scala.collection.concurrent.TrieMap
import scala.reflect.macros.blackbox

@macrocompat.bundle
class PrimitiveMacro(override val c: blackbox.Context) extends BlackboxToolbelt {
import c.universe._

def printType(tpe: Type): String = showCode(tq"${tpe.dealias}")

val rowByNameType = tq"_root_.com.datastax.driver.core.GettableByNameData"
val rowByIndexType = tq"_root_.com.outworkers.phantom.IndexedRow"
val protocolVersion = tq"_root_.com.datastax.driver.core.ProtocolVersion"
private[this] val protocolVersion = tq"_root_.com.datastax.driver.core.ProtocolVersion"
private[this] val versionTerm = q"version"

val boolType = tq"_root_.scala.Boolean"
val strType: Tree = tq"_root_.java.lang.String"
val intType: Tree = tq"_root_.scala.Int"
val byteType: Tree = tq"_root_.scala.Byte"
val doubleType: Tree = tq"_root_.scala.Double"
val shortType: Tree = tq"_root_.scala.Short"
val uuidType: Tree = tq"_root_.java.util.UUID"
val longType: Tree = tq"_root_.scala.Long"
val floatType: Tree = tq"_root_.scala.Float"
val dateType: Tree = tq"_root_.java.util.Date"
val tupleValue: Tree = tq"_root_.com.datastax.driver.core.TupleValue"
val localDate: Tree = tq"_root_.com.datastax.driver.core.LocalDate"
val dateTimeType: Tree = tq"_root_.org.joda.time.DateTime"
val localJodaDate: Tree = tq"_root_.org.joda.time.LocalDate"
val bigDecimalType: Tree = tq"_root_.scala.math.BigDecimal"
val inetType: Tree = tq"_root_.java.net.InetAddress"
val bigIntType = tq"_root_.scala.math.BigInt"
val bufferType = tq"_root_.java.nio.ByteBuffer"
val bufferCompanion = q"_root_.java.nio.ByteBuffer"

private[this] val listValueType: Type = typeOf[ListValue[_]]
private[this] val boolType = typeOf[scala.Boolean]
private[this] val strType = typeOf[java.lang.String]
private[this] val bufferType = typeOf[_root_.java.nio.ByteBuffer]
private[this] val bufferCompanion = q"_root_.java.nio.ByteBuffer"

private[this] val bufferException = typeOf[BufferUnderflowException]
private[this] val invalidTypeException = typeOf[InvalidTypeException]

val codecUtils = q"_root_.com.datastax.driver.core.CodecUtils"
val builder = q"_root_.com.outworkers.phantom.builder"
val cql = q"_root_.com.outworkers.phantom.builder.query.engine.CQLQuery"
val syntax = q"_root_.com.outworkers.phantom.builder.syntax.CQLSyntax"
private[this] val codecUtils = q"_root_.com.datastax.driver.core.CodecUtils"
private[this] val builder = q"_root_.com.outworkers.phantom.builder"

val prefix = q"_root_.com.outworkers.phantom.builder.primitives"

def tryT(x: Tree): Tree = tq"scala.util.Try[$x]"
def tryT(x: Type): Tree = tq"scala.util.Try[$x]"
private[this] val prefix = q"_root_.com.outworkers.phantom.builder.primitives"

def typed[A : c.WeakTypeTag]: Symbol = weakTypeOf[A].typeSymbol

/**
* Adds a caching layer for subsequent requests to materialise the same primitive type.
* This adds a simplistic caching layer that computes primitives based on types.
*/
val treeCache: TrieMap[Symbol, Tree] = TrieMap.empty[Symbol, Tree]

def isTuple(tpe: Type): Boolean = {
tpe.typeSymbol.fullName startsWith "scala.Tuple"
}
Expand All @@ -88,28 +51,12 @@ class PrimitiveMacro(override val c: blackbox.Context) extends BlackboxToolbelt
tpe.typeSymbol.fullName startsWith "scala.Option"
}

def printType(tpe: Type): String = showCode(tq"${tpe.dealias}")

object Symbols {
val intSymbol: Symbol = typed[Int]
val byteSymbol: Symbol = typed[Byte]
val stringSymbol: Symbol = typed[String]
val boolSymbol: Symbol = typed[Boolean]
val shortSymbol: Symbol = typed[Short]
val longSymbol: Symbol = typed[Long]
val doubleSymbol: Symbol = typed[Double]
val floatSymbol: Symbol = typed[Float]
val dateSymbol: Symbol = typed[Date]
val listSymbol: Symbol = typed[scala.collection.immutable.List[_]]
val setSymbol: Symbol = typed[scala.collection.immutable.Set[_]]
val mapSymbol: Symbol = typed[scala.collection.immutable.Map[_, _]]
val dateTimeSymbol: Symbol = typed[DateTime]
val localDateSymbol: Symbol = typed[com.datastax.driver.core.LocalDate]
val uuidSymbol: Symbol = typed[UUID]
val jodaLocalDateSymbol: Symbol = typed[org.joda.time.LocalDate]
val timestampSymbol: Symbol = typed[java.sql.Timestamp]
val inetSymbol: Symbol = typed[InetAddress]
val bigInt: Symbol = typed[BigInt]
val bigDecimal: Symbol = typed[BigDecimal]
val buffer: Symbol = typed[ByteBuffer]
val enumValue: Symbol = typed[Enumeration#Value]
val enum: Symbol = typed[Enumeration]
}
Expand Down Expand Up @@ -212,7 +159,7 @@ class PrimitiveMacro(override val c: blackbox.Context) extends BlackboxToolbelt
val extractorTerms = indexedFields.map { case (_, i) => fqTerm(i) }
val fieldExtractor = q"for (..$deserializedFields) yield new $tpe(..$extractorTerms)"

val tree = q"""new $prefix.Primitive[$tpe] {
q"""new $prefix.Primitive[$tpe] {
override def dataType: $strType = {
$builder.QueryBuilder.Collections
.tupleType(..${fields.map(_.cassandraType)})
Expand Down Expand Up @@ -255,10 +202,6 @@ class PrimitiveMacro(override val c: blackbox.Context) extends BlackboxToolbelt

override def shouldFreeze: $boolType = true
}"""

if (showTrees) c.echo(c.enclosingPosition, showCode(tree))

tree
}

def mapPrimitive(tpe: Type): Tree = {
Expand All @@ -275,8 +218,6 @@ class PrimitiveMacro(override val c: blackbox.Context) extends BlackboxToolbelt
}
}



def optionPrimitive(tpe: Type): Tree = {
tpe.typeArgs match {
case head :: Nil => q"$prefix.Primitives.option[$head]"
Expand Down Expand Up @@ -331,10 +272,17 @@ class PrimitiveMacro(override val c: blackbox.Context) extends BlackboxToolbelt
case Symbols.listSymbol => listPrimitive(wkType)
case Symbols.setSymbol => setPrimitive(wkType)
case Symbols.mapSymbol => mapPrimitive(wkType)
case _ => c.abort(c.enclosingPosition, s"Cannot find primitive implementation for $tpe")
case _ => c.abort(
c.enclosingPosition,
s"""
Cannot derive or find primitive implementation for $tpe.
|Please create a Primitive manually using Primitive.iso or make sure
|the implicit Primitve for $tpe is imported in the right scope.
""".stripMargin
)
}

tree
evalTree(tree)
}

def materializer[T : c.WeakTypeTag]: Tree = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
*/
package com.outworkers.phantom.column

import com.outworkers.phantom.Row
import com.outworkers.phantom.CassandraTable
import com.outworkers.phantom.{ CassandraTable, Row }
import com.outworkers.phantom.builder.QueryBuilder
import com.outworkers.phantom.builder.primitives.Primitive

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,6 @@ trait RootMacro extends HListHelpers with WhiteboxToolbelt {
case None => lm
}
}

/**
* Every entry in this ordered map is a traversable of type [[M]].
* That means every key holds a sequence of elements.
* This function will remove the element [[elem]] from that sequence
* for the provided key.
*/
def -(key: K, elem: V): ListMap[K, M[V]] = remove(key, elem)
}

case class TableDescriptor(
Expand Down Expand Up @@ -434,7 +426,7 @@ trait RootMacro extends HListHelpers with WhiteboxToolbelt {
root.typeSignature.typeParams match {
// We use the special API to see what type was passed through to AbstractColumn[_]
// with special thanks to https://github.com/joroKr21 for helping me not rip
// the remainder of my hair off while uncovering this marvelous macro API method.
// off the remainder of my already receding hairline.
case head :: Nil => Column.Field(
member.asModule.name.toTermName,
head.asType.toType.asSeenFrom(memberType, colSymbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ import scala.annotation.implicitNotFound
import scala.collection.immutable.ListMap
import scala.reflect.macros.whitebox

@implicitNotFound(msg = "Table ${T} is missing a PartitionKey column.")
@implicitNotFound(
msg =
"""Table ${T} is most likely missing a PartitionKey column.|
Also check that the fields in your table match types inside ${R}.
""".stripMargin
)
trait TableHelper[T <: CassandraTable[T, R], R] extends Serializable {

type Repr <: HList
Expand Down Expand Up @@ -114,9 +119,7 @@ class TableHelperMacro(override val c: whitebox.Context) extends WhiteboxToolbel
.map(_.typeSymbol.typeSignatureIn(table).typeSymbol.name.toTermName)
.map(name => q"$tableTerm.$name")

if (partitionKeys.isEmpty) {
error(s"Table $tableName needs to have at least one partition key")
}
c.inferImplicitValue(typeOf[NamingStrategy], silent = true)

val primaries = filterColumns[PrimaryKey](columns)
.map(_.typeSymbol.typeSignatureIn(table).typeSymbol.name.toTermName)
Expand Down Expand Up @@ -170,68 +173,6 @@ class TableHelperMacro(override val c: whitebox.Context) extends WhiteboxToolbel
TermName(term.decodedName.toString.trim.toLowerCase)
}

def hardMatch(
columnFields: ListMap[Type, Seq[TermName]],
unprocessed: List[Record.Field],
descriptor: TableDescriptor
): TableDescriptor = {
unprocessed match {
case recField :: tail =>
columnFields.find { case (tpe, seq) => predicate(recField, tpe) } map { case (_, seq) => seq } match {
// It's possible that after all easy matches have been exhausted, no column fields are left to match
// with remaining record fields for the given type.
case None =>
val un = Unmatched(recField, s"Table doesn't contain a column of type ${printType(recField.tpe)}")
hardMatch(columnFields, tail, descriptor withoutMatch un)

// We once again repeat the case, because we may under some circumstances find a single remaining
// column term to match with after all direct and easy matches have happened for a given type.
case Some(Seq(h)) =>
hardMatch(
columnFields - recField.tpe,
tail,
descriptor withMatch MatchedField(recField, Column.Field(h, recField.tpe))
)

case Some(seq) => seq.find(recField.name ==) match {
// In theory this case should not re-occur because we only add elements
// to the unprocessed if no direct term name matches were found.
case Some(matchingName) =>
info(s"Found matching column term name for ${recField.debugString} in unprocessed queue.")
val m = MatchedField(recField, Column.Field(matchingName, recField.tpe))
hardMatch(columnFields - (recField.tpe, matchingName), tail, descriptor withMatch m)

// The real case we are attempting to handle here is when we have no clue which one
// of multiple record strings matches which column field.
case None =>
// we now attempt to match a few variations of the term name.
// and check if the column members contain some possible variations.
val possibilities = variations(recField.name)

seq.find(colTerm => possibilities.exists(lowercased(colTerm) ==)) match {
case Some(matchingName) =>
val m = MatchedField(recField, Column.Field(matchingName, recField.tpe))
hardMatch(columnFields - (recField.tpe, matchingName), tail, descriptor withMatch m)

case None =>
// This is still our worst case scenario, where no variation of a term name
// was found and we still have multiple potential matches for a record field.
// Under such circumstances we use the first available column term name
// with respect to the write order.
val firstName = seq.headOption.getOrElse(
abort("Found empty term sequence which should never happen!!!")
)

val m = MatchedField(recField, Column.Field(firstName, recField.tpe))
hardMatch(columnFields - (recField.tpe, firstName), tail, descriptor withMatch m)
}
}
}

case Nil => descriptor
}
}

/**
* This works by recursively parsing a list of fields extracted here as record members.
* The algorithm will take every field from the record and:
Expand Down Expand Up @@ -268,7 +209,7 @@ class TableHelperMacro(override val c: whitebox.Context) extends WhiteboxToolbel
): TableDescriptor = {
recordFields match { case recField :: tail =>

columnFields.find { case (tpe, seq) => predicate(recField, tpe) } map { case (_, seq) => seq } match {
columnFields.find { case (tpe, _) => predicate(recField, tpe) } map { case (_, seq) => seq } match {
case None =>
val un = Unmatched(recField, s"Table doesn't contain a column of type ${printType(recField.tpe)}")
extractorRec(columnFields, tail, descriptor withoutMatch un, unprocessed)
Expand All @@ -287,25 +228,40 @@ class TableHelperMacro(override val c: whitebox.Context) extends WhiteboxToolbel
val m = MatchedField(recField, Column.Field(matchingName, recField.tpe))

extractorRec(
columnFields remove (recField.tpe, matchingName),
columnFields remove(recField.tpe, matchingName),
tail,
descriptor withMatch m,
unprocessed
)

case None =>
extractorRec(
columnFields,
tail,
descriptor,
unprocessed :+ recField
)
}
// we now attempt to match a few variations of the term name.
// and check if the column members contain some possible variations.
val possibilities = variations(recField.name)

seq.find(colTerm => possibilities.exists(lowercased(colTerm) ==)) match {
case Some(matchingName) =>
val m = MatchedField(recField, Column.Field(matchingName, recField.tpe))
extractorRec(columnFields remove(recField.tpe, matchingName), tail, descriptor withMatch m)

case None =>
// This is still our worst case scenario, where no variation of a term name
// was found and we still have multiple potential matches for a record field.
// Under such circumstances we use the first available column term name
// with respect to the write order.
val firstName = seq.headOption.getOrElse(
abort("Found empty term sequence which should never happen!!!")
)

val m = MatchedField(recField, Column.Field(firstName, recField.tpe))
extractorRec(columnFields remove(recField.tpe, firstName), tail, descriptor withMatch m)
}
}
}

// return a descriptor where the sequence of unmatched table columns
// is the original list minus all the elements missing
case Nil => hardMatch(columnFields, unprocessed, descriptor)
case Nil => descriptor
}
}

Expand Down Expand Up @@ -464,7 +420,7 @@ class TableHelperMacro(override val c: whitebox.Context) extends WhiteboxToolbel

val accessors = columns.map(_.asTerm.name).map(tm => q"table.instance.${tm.toTermName}").distinct
// Validate that column names at compile time.
//columns.map(col => validateColumnName(col.asTerm.name))
// columns.map(col => validateColumnName(col.asTerm.name))

val clsName = TypeName(c.freshName("anon$"))
val storeTpe = descriptor.hListStoreType.getOrElse(nothingTpe)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ private[phantom] trait BlackboxToolbelt {
}
}

def evalTree(tree: => Tree): Tree = {
if (showTrees) {
c.echo(c.enclosingPosition, showCode(tree))
}
tree
}

def info(msg: String, force: Boolean = false): Unit = {
if (showLogs) {
c.info(c.enclosingPosition, msg, force)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,6 @@ trait HListHelpers {
}
}

unfold(tpe, List()).reverse
unfold(tpe, Nil).reverse
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ abstract class IndexedCollectionsTable extends Table[IndexedCollectionsTable, Te
object mapIntToText extends MapColumn[Int, String] with Index with Keys

object mapIntToInt extends MapColumn[Int, Int]

}


Expand Down
Loading

0 comments on commit e7a68e3

Please sign in to comment.