private[scalding] def getColumnFormats[T]()

in scalding-db/src/main/scala/com/twitter/scalding/db/macros/impl/ColumnDefinitionProviderImpl.scala [49:213]


  private[scalding] def getColumnFormats[T](
      c: Context
  )(implicit T: c.WeakTypeTag[T]): List[ColumnFormat[c.type]] = {
    import c.universe._

    if (!IsCaseClassImpl.isCaseClassType(c)(T.tpe))
      c.abort(
        c.enclosingPosition,
        s"""We cannot enforce ${T.tpe} is a case class, either it is not a case class or this macro call is possibly enclosed in a class.
        This will mean the macro is operating on a non-resolved type."""
      )

    // Field To JDBCColumn
    @tailrec
    def matchField(
        accessorTree: List[MethodSymbol],
        oTpe: Type,
        fieldName: FieldName,
        defaultValOpt: Option[c.Expr[String]],
        annotationInfo: List[(Type, Option[Int])],
        nullable: Boolean
    ): scala.util.Try[List[ColumnFormat[c.type]]] =
      oTpe match {
        // String handling
        case tpe if tpe =:= typeOf[String] =>
          StringTypeHandler(c)(accessorTree, fieldName, defaultValOpt, annotationInfo, nullable)
        case tpe if tpe =:= typeOf[Array[Byte]] =>
          BlobTypeHandler(c)(accessorTree, fieldName, defaultValOpt, annotationInfo, nullable)
        case tpe if tpe =:= typeOf[Byte] =>
          NumericTypeHandler(c)(accessorTree, fieldName, defaultValOpt, annotationInfo, nullable, "TINYINT")
        case tpe if tpe =:= typeOf[Short] =>
          NumericTypeHandler(c)(accessorTree, fieldName, defaultValOpt, annotationInfo, nullable, "SMALLINT")
        case tpe if tpe =:= typeOf[Int] =>
          NumericTypeHandler(c)(accessorTree, fieldName, defaultValOpt, annotationInfo, nullable, "INT")
        case tpe if tpe =:= typeOf[Long] =>
          NumericTypeHandler(c)(accessorTree, fieldName, defaultValOpt, annotationInfo, nullable, "BIGINT")
        case tpe if tpe =:= typeOf[Double] =>
          NumericTypeHandler(c)(accessorTree, fieldName, defaultValOpt, annotationInfo, nullable, "DOUBLE")
        case tpe if tpe =:= typeOf[Boolean] =>
          NumericTypeHandler(c)(accessorTree, fieldName, defaultValOpt, annotationInfo, nullable, "BOOLEAN")
        case tpe if tpe =:= typeOf[java.util.Date] =>
          DateTypeHandler(c)(accessorTree, fieldName, defaultValOpt, annotationInfo, nullable)
        case tpe if tpe.erasure =:= typeOf[Option[Any]] && nullable == true =>
          Failure(
            new Exception(
              s"Case class ${T.tpe} has field $fieldName which contains a nested option. This is not supported by this macro."
            )
          )

        case tpe if tpe.erasure =:= typeOf[Option[Any]] && nullable == false =>
          if (defaultValOpt.isDefined)
            Failure(
              new Exception(
                s"Case class ${T.tpe} has field $fieldName: ${oTpe.toString}, with a default value. Options cannot have default values"
              )
            )
          else {
            matchField(
              accessorTree,
              tpe.asInstanceOf[TypeRefApi].args.head,
              fieldName,
              None,
              annotationInfo,
              true
            )
          }
        case tpe if IsCaseClassImpl.isCaseClassType(c)(tpe) => expandMethod(accessorTree, tpe)

        // default
        case _ =>
          Failure(
            new Exception(
              s"Case class ${T.tpe} has field $fieldName: ${oTpe.toString}, which is not supported for talking to JDBC"
            )
          )
      }

    def expandMethod(
        outerAccessorTree: List[MethodSymbol],
        outerTpe: Type
    ): scala.util.Try[List[ColumnFormat[c.type]]] = {
      val defaultArgs = getDefaultArgs(c)(outerTpe)

      // Intializes the type info
      outerTpe.declarations.foreach(_.typeSignature)

      // We have to build this up front as if the case class definition moves to another file
      // the annotation moves from the value onto the getter method?
      val annotationData: Map[String, List[(Type, List[Tree])]] = outerTpe.declarations
        .map { m =>
          val mappedAnnotations = m.annotations.map(t => (t.tpe, t.scalaArgs))
          m.name.toString.trim -> mappedAnnotations
        }
        .groupBy(_._1)
        .map { case (k, l) =>
          (k, l.map(_._2).reduce(_ ++ _))
        }
        .filter { case (_, v) =>
          v.nonEmpty
        }

      outerTpe.declarations
        .collect { case m: MethodSymbol if m.isCaseAccessor => m }
        .map { m =>
          val fieldName = m.name.toString.trim
          val defaultVal = defaultArgs.get(fieldName)

          val annotationInfo: List[(Type, Option[Int])] = annotationData
            .getOrElse(m.name.toString.trim, Nil)
            .collect {
              case (tpe, List(Literal(Constant(siz: Int))))
                  if tpe =:= typeOf[com.twitter.scalding.db.macros.size] =>
                (tpe, Some(siz))
              case (tpe, _) if tpe =:= typeOf[com.twitter.scalding.db.macros.size] =>
                c.abort(
                  c.enclosingPosition,
                  "Hit a size macro where we couldn't parse the value. Probably not a literal constant. Only literal constants are supported."
                )
              case (tpe, _) if tpe <:< typeOf[com.twitter.scalding.db.macros.ScaldingDBAnnotation] =>
                (tpe, None)
            }

          matchField(
            outerAccessorTree :+ m,
            m.returnType,
            FieldName(fieldName),
            defaultVal,
            annotationInfo,
            false
          )
        }
        .toList
        // This algorithm returns the error from the first exception we run into.
        .foldLeft(scala.util.Try[List[ColumnFormat[c.type]]](Nil)) { case (pTry, nxt) =>
          (pTry, nxt) match {
            case (Success(l), Success(r)) => Success(l ::: r)
            case (f @ Failure(_), _)      => f
            case (_, f @ Failure(_))      => f
          }
        }
    }

    val formats = expandMethod(Nil, T.tpe) match {
      case Success(s) => s
      case Failure(e) => (c.abort(c.enclosingPosition, e.getMessage))
    }

    val duplicateFields = formats
      .map(_.fieldName)
      .groupBy(identity)
      .filter(_._2.size > 1)
      .keys

    if (duplicateFields.nonEmpty) {
      c.abort(
        c.enclosingPosition,
        s"""
        Duplicate field names found: ${duplicateFields.mkString(",")}.
        Please check your nested case classes.
        """
      )
    } else {
      formats
    }
  }