in scio-google-cloud-platform/src/main/scala/com/spotify/scio/bigquery/types/ConverterProvider.scala [338:437]
private def toTableRowInternal(c: blackbox.Context)(tpe: c.Type): c.Tree = {
import c.universe._
// =======================================================================
// Converter helpers
// =======================================================================
def cast(tree: Tree, tpe: Type): Tree = {
val provider: OverrideTypeProvider =
OverrideTypeProviderFinder.getProvider
tpe match {
case t if provider.shouldOverrideType(c)(t) => q"$tree.toString"
case t if t =:= typeOf[Boolean] => tree
case t if t =:= typeOf[Int] => tree
case t if t =:= typeOf[Long] => q"$tree.toString" // json doesn't support long
case t if t =:= typeOf[Float] => q"$tree.toDouble" // json doesn't support float
case t if t =:= typeOf[Double] => tree
case t if t =:= typeOf[String] => tree
case t if t =:= typeOf[BigDecimal] =>
q"_root_.com.spotify.scio.bigquery.Numeric($tree).toString"
case t if t =:= typeOf[ByteString] =>
q"_root_.com.google.common.io.BaseEncoding.base64().encode($tree.toByteArray)"
case t if t =:= typeOf[Array[Byte]] =>
q"_root_.com.google.common.io.BaseEncoding.base64().encode($tree)"
case t if t =:= typeOf[Instant] =>
q"_root_.com.spotify.scio.bigquery.Timestamp($tree)"
case t if t =:= typeOf[LocalDate] =>
q"_root_.com.spotify.scio.bigquery.Date($tree)"
case t if t =:= typeOf[LocalTime] =>
q"_root_.com.spotify.scio.bigquery.Time($tree)"
case t if t =:= typeOf[LocalDateTime] =>
q"_root_.com.spotify.scio.bigquery.DateTime($tree)"
// different than nested record match below, even though those are case classes
case t if t =:= typeOf[Geography] =>
q"$tree.wkt"
case t if t =:= typeOf[Json] =>
// for TableRow/json, use parsed JSON to prevent escaping
q"_root_.com.spotify.scio.bigquery.types.Json.parse($tree)"
case t if t =:= typeOf[BigNumeric] =>
// for TableRow/json, use string to avoid precision loss (like numeric)
q"$tree.wkt.toString"
case t if isCaseClass(c)(t) => // nested records
val fn = TermName("r" + t.typeSymbol.name)
q"""{
val $fn = $tree
${constructor(t, fn)}
}
"""
case _ => c.abort(c.enclosingPosition, s"Unsupported type: $tpe")
}
}
def option(tree: Tree, tpe: Type): Tree =
q"if ($tree.isDefined) ${cast(q"$tree.get", tpe)} else null"
def list(tree: Tree, tpe: Type): Tree =
q"asJava($tree.map(x => ${cast(q"x", tpe)}))"
def field(symbol: Symbol, fn: TermName): (String, Tree) = {
val name = symbol.name.toString
val tpe = symbol.asMethod.returnType
val tree = q"$fn.${TermName(name)}"
if (tpe.erasure =:= typeOf[Option[_]].erasure) {
(name, option(tree, tpe.typeArgs.head))
} else if (tpe.erasure =:= typeOf[List[_]].erasure) {
(name, list(tree, tpe.typeArgs.head))
} else {
(name, cast(tree, tpe))
}
}
def constructor(tpe: Type, fn: TermName): Tree = {
val sets = tpe.erasure match {
case t if isCaseClass(c)(t) => getFields(c)(t).map(s => field(s, fn))
case _ => c.abort(c.enclosingPosition, s"Unsupported type: $tpe")
}
val header = q"val result = new ${p(c, GModel)}.TableRow()"
val body = sets.map { case (name, value) =>
q"if (${p(c, SBQ)}.types.ConverterUtil.notNull($value)) result.set($name, $value)"
}
val footer = q"result"
q"{$header; ..$body; $footer}"
}
// =======================================================================
// Entry point
// =======================================================================
val tn = TermName("r")
q"""(r: $tpe) => {
import _root_.scala.jdk.javaapi.CollectionConverters._
${constructor(tpe, tn)}
}
"""
}