sirthias / borer

Efficient CBOR and JSON (de)serialization in Scala
https://sirthias.github.io/borer/
Mozilla Public License 2.0
224 stars 14 forks source link

deriveAllCodecs fails for no apparent reason #630

Closed guntiso closed 1 year ago

guntiso commented 1 year ago

Does not compile because Could not find implicit Encoder[CompilerAst.SQLDefBase] etc etc. But SQLDefBase is Exp.

sealed trait Exp

sealed trait DMLExp extends Exp {
  def table: Ident
  def alias: String
  def cols: List[Col]
  def filter: Arr
  def vals: Exp
  def returning: Option[Cols]
  def db: Option[String]
}
sealed trait Const extends Exp {
  def value: Any
}
case class IntConst(value: Int) extends Const
case class StringConst(value: String) extends Const
case class BigDecimalConst(value: BigDecimal) extends Const
case class BooleanConst(value: Boolean) extends Const

case class Sql(sql: String) extends Exp
case class Ident(ident: List[String]) extends Exp
case class Variable(variable: String, members: List[String] = Nil, opt: Boolean) extends Exp
case class Id(name: String) extends Exp
case class IdRef(name: String) extends Exp
case class Res(rNr: Int, col: Exp) extends Exp
case class Cast(exp: Exp, typ: String) extends Exp
case class UnOp(operation: String, operand: Exp) extends Exp
case class ChildQuery(query: Exp, db: Option[String]) extends Exp
case class Fun(
  name: String,
  parameters: List[Exp],
  distinct: Boolean,
  aggregateOrder: Option[Ord],
  aggregateWhere: Option[Exp]
) extends Exp
case class TableColDef(name: String, typ: Option[String])
case class FunAsTable(
  fun: Fun, cols: Option[List[TableColDef]], withOrdinality: Boolean) extends Exp
case class In(lop: Exp, rop: List[Exp], not: Boolean) extends Exp
case class BinOp(op: String, lop: Exp, rop: Exp) extends Exp
case class TerOp(lop: Exp, op1: String, mop: Exp, op2: String, rop: Exp) extends Exp
case class Join(default: Boolean, expr: Exp = null, noJoin: Boolean) extends Exp
case class Obj(obj: Exp, alias: String = null, join: Join = null, outerJoin: String = null,
  nullable: Boolean = false) extends Exp
case class Col(col: Exp, alias: String = null) extends Exp
case class Cols(distinct: Boolean, cols: List[Col]) extends Exp
case class Grp(cols: List[Exp], having: Exp = null) extends Exp
case class OrdCol(nullsFirst: Exp = null, exp: Exp, nullsLast: Exp = null) extends Exp
case class Ord(cols: List[OrdCol]) extends Exp
case class Query(
  tables: List[Obj], filter: Filters, cols: Cols = null,
  group: Grp = null, order: Ord = null, offset: Exp = null, limit: Exp = null) extends Exp
case class WithTable(name: String, cols: List[String], recursive: Boolean, table: Exp) extends Exp
case class With(tables: List[WithTable], query: Exp) extends Exp
case class Values(values: List[Arr]) extends Exp
case class Insert(
  table: Ident = null, alias: String = null, cols: List[Col],
  vals: Exp = null, returning: Option[Cols],db: Option[String]) extends DMLExp {
  override def filter = null
}
case class Update(
  table: Ident = null, alias: String = null, filter: Arr = null, cols: List[Col],
  vals: Exp = null, returning: Option[Cols], db: Option[String]) extends DMLExp
case class ValuesFromSelect(select: Query) extends Exp
case class Delete(
  table: Ident = null, alias: String = null, filter: Arr,
  using: Exp = null, returning: Option[Cols], db: Option[String]) extends DMLExp {
  override def cols = null
  override def vals = using
}
case class Arr(elements: List[Exp]) extends Exp
case class Filters(filters: List[Arr]) extends Exp
case object All extends Exp
case class IdentAll(ident: Ident) extends Exp
sealed trait Null extends Exp
case object Null extends Null
case object NullUpdate extends Null
case class Braces(expr: Exp) extends Exp

object CompilerAst {
  sealed trait CompilerExp extends Exp

  case class ExprType(name: String = null)
  sealed trait TypedExp extends CompilerExp {
    def exp: Exp
    def typ: ExprType
  }

  case class TableDef(name: String, exp: Obj) extends CompilerExp
  case class TableObj(obj: Exp) extends CompilerExp
  case class TableAlias(obj: Exp) extends CompilerExp
  case class ColDef(name: String = null, col: Exp, typ: ExprType) extends TypedExp {
    def exp: ColDef = this
  }
  case class ChildDef(exp: Exp, db: Option[String]) extends TypedExp {
    val typ: ExprType = ExprType(this.getClass.getName)
  }
  case class FunDef(name: String, exp: Fun, typ: ExprType, procedure: Fun) extends TypedExp
  case class FunAsTableDef(
    exp: FunDef, cols: Option[List[TableColDef]], withOrdinality: Boolean) extends CompilerExp
  case class RecursiveDef(exp: Exp) extends TypedExp {
    val typ: ExprType = ExprType(this.getClass.getName)
  }
  case class PrimitiveExp(exp: Exp) extends CompilerExp
  case class PrimitiveDef(exp: Exp, typ: ExprType) extends TypedExp
  sealed trait RowDefBase extends TypedExp {
    def cols: List[ColDef]
    val typ: ExprType = ExprType(this.getClass.getName)
  }
  sealed trait SQLDefBase extends RowDefBase {
    def tables: List[TableDef]
  }
  sealed trait DMLDefBase extends SQLDefBase {
    def db: Option[String]
  }
  sealed trait SelectDefBase extends SQLDefBase

  case class SelectDef(cols: List[ColDef], tables: List[TableDef],exp: Query) extends SelectDefBase
  case class BinSelectDef(
    leftOperand: SelectDefBase, rightOperand: SelectDefBase, exp: BinOp) extends SelectDefBase {
    def cols = Nil
    def tables = Nil
  }
  case class BracesSelectDef(exp: SelectDefBase) extends SelectDefBase {
    def cols = Nil
    def tables = Nil
  }
  case class WithTableDef(
    cols: List[ColDef],
    tables: List[TableDef],
    recursive: Boolean,
    exp: SQLDefBase
  ) extends SelectDefBase
  case class ValuesFromSelectDef(exp: SelectDefBase) extends SelectDefBase {
    def cols = Nil
    def tables = Nil
  }
  case class FunSelectDef(
    cols: List[ColDef], tables: List[TableDef], exp: FunDef) extends SelectDefBase
  case class InsertDef(cols: List[ColDef], tables: List[TableDef], exp: Insert) extends DMLDefBase {
    def db = None
  }
  case class UpdateDef(cols: List[ColDef], tables: List[TableDef], exp: Update) extends DMLDefBase {
    def db = None
  }
  case class DeleteDef(tables: List[TableDef], exp: Delete) extends DMLDefBase {
    def cols = Nil
    def db = None
  }
  case class ReturningDMLDef(
    cols: List[ColDef], tables: List[TableDef], exp: DMLDefBase) extends SelectDefBase
  sealed trait WithQuery extends SQLDefBase {
    def exp: SQLDefBase
    def withTables: List[WithTableDef]
  }
  sealed trait WithSelectBase extends SelectDefBase with WithQuery {
    def exp: SelectDefBase
  }
  sealed trait WithDMLQuery extends DMLDefBase with WithQuery {
    def exp: DMLDefBase
    override def db: Option[String] = None
  }
  case class WithSelectDef(
    exp: SelectDefBase, withTables: List[WithTableDef]) extends WithSelectBase {
    def cols = exp.cols
    def tables = exp.tables
  }
  case class WithDMLDef(exp: DMLDefBase, withTables: List[WithTableDef]) extends WithDMLQuery {
    def cols = exp.cols
    def tables = exp.tables
  }
  case class ArrayDef(cols: List[ColDef]) extends RowDefBase {
    def exp = this
  }
}

object AstSerializer {
  def main(args: Array[String]): Unit = {
    import io.bullet.borer._
    import io.bullet.borer.derivation.MapBasedCodecs._
    import io.bullet.borer.Codec
    import CompilerAst._
    implicit lazy val exprTypeCodec:      Codec[ExprType]       = deriveCodec    [ExprType]
    implicit lazy val tableColDefCodec:   Codec[TableColDef]    = deriveCodec    [TableColDef]
    // implicit lazy val joinCodec:          Codec[Join]           = deriveCodec    [Join]
    // implicit lazy val objCodec:           Codec[Obj]            = deriveCodec    [Obj]
    // implicit lazy val tableDefCodec:      Codec[TableDef]       = deriveCodec    [TableDef]
    // implicit lazy val identCodec:         Codec[Ident]          = deriveCodec    [Ident]
    // implicit lazy val arrCodec:           Codec[Arr]            = deriveCodec    [Arr]
    // implicit lazy val colCodec:           Codec[Col]            = deriveCodec    [Col]
    // implicit lazy val colsCodec:          Codec[Cols]           = deriveCodec    [Cols]
    // implicit lazy val deleteCodec:        Codec[Delete]         = deriveCodec    [Delete]
    // implicit lazy val colDefCodec:        Codec[ColDef]         = deriveCodec    [ColDef]
    // implicit lazy val insertCodec:        Codec[Insert]         = deriveCodec    [Insert]
    // implicit lazy val updateCodec:        Codec[Update]         = deriveCodec    [Update]
    // implicit lazy val withTableDefCodec:  Codec[WithTableDef]   = deriveCodec    [WithTableDef]
    // implicit lazy val dmlDefBaseCodec:    Codec[DMLDefBase]     = deriveAllCodecs[DMLDefBase]
    // implicit lazy val ordColCodec:        Codec[OrdCol]         = deriveCodec    [OrdCol]
    // implicit lazy val ordCodec:           Codec[Ord]            = deriveCodec    [Ord]
    // implicit lazy val funCodec:           Codec[Fun]            = deriveCodec    [Fun]
    // implicit lazy val funDefCodec:        Codec[FunDef]         = deriveCodec    [FunDef]
    // implicit lazy val filtersCodec:       Codec[Filters]        = deriveCodec    [Filters]
    // implicit lazy val grpCodec:           Codec[Grp]            = deriveCodec    [Grp]
    // implicit lazy val queryCodec:         Codec[Query]          = deriveCodec    [Query]
    // implicit lazy val binOpCodec:         Codec[BinOp]          = deriveCodec    [BinOp]
    // implicit lazy val selectDefBaseCodec: Codec[SelectDefBase]  = deriveAllCodecs[SelectDefBase]
    // implicit lazy val sqlDefBaseCodec:    Codec[SQLDefBase]     = deriveAllCodecs[SQLDefBase]
    implicit lazy val expCodec:           Codec[Exp]            = deriveAllCodecs[Exp]

    val ast: Exp = InsertDef(List(
      ColDef("DUMMY",Obj(Ident(List("DUMMY")),null,null,null,false),ExprType("java.lang.Integer"))),
        List(TableDef("DUMMY",Obj(TableObj(Ident(List("DUMMY"))),null,null,null,false))),
        Insert(null,null,List(),Values(List(Arr(List(BigDecimalConst(0))))),None,None))
    val bytes = Cbor.encode(ast).toByteArray
    val decoded = Cbor.decode(bytes).to[Exp].value
    if (ast == decoded)
      println("OK")
    else
      println("ERROR")
  }
}
sirthias commented 1 year ago

Thank you for reporting! This is a nice large test case for the deriveAll macros. I'll look into it and report back tomorrow.

sirthias commented 1 year ago

Your example is excellent, since it contains a lot of important example cases for how complicated the dependencies between the sub nodes of an ADT can be. Based on this example I was able to significantly improve the logic which drives the deriveAll macros. Would it be ok if I included a stripped down version of your ADT in the tests for borer? I.e. would you be ok with donating your ADT to the project as a test example?

sirthias commented 1 year ago

I've significantly improved the deriveAll logic, which should now be able to properly detect and automatically resolve all cycling dependencies within a complex ADT such as yours.

The ADT in your example can now receive its Codec with a simple:

  given Codec[Exp] = {
    import MapBasedCodecs._
    given Codec[ExprType]    = deriveCodec
    given Codec[TableColDef] = deriveCodec
    deriveAllCodecs[Exp]
  }

Note that ExprType and TableColDef still need their own line because they aren't part of the ADT.

I've included your ADT as a test case, because it demonstrates a lot of previously untested phenomena, such as dependencies and recursion into abstract sub types. Please let me know if you would like me to take it out again, maybe due to some IP-related issue!

Thanks again for reporting!

guntiso commented 1 year ago

Would it be ok if I included a stripped down version of your ADT in the tests for borer? I.e. would you be ok with donating your ADT to the project as a test example?

Ok. Thanks for the fix!