Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PgArraySupport: Type Parameter Missing in Generated Code #547

Closed
enzeart opened this issue Mar 22, 2022 · 6 comments
Closed

PgArraySupport: Type Parameter Missing in Generated Code #547

enzeart opened this issue Mar 22, 2022 · 6 comments

Comments

@enzeart
Copy link

enzeart commented Mar 22, 2022

Hello,

When using PgArraySupport in my custom profile, array types in the database result in code being generated that has scala.collection.immutable.Seq types with missing type parameters. This causes the following error during compilation: trait Seq takes type parameters. Am I doing something wrong, or is this a bug? My profile is shown below:

package com.example.db_profile
import com.github.tminglei.slickpg._

trait CustomPostgresProfile
    extends ExPostgresProfile
    with PgArraySupport
    with PgDate2Support
    with PgRangeSupport
    with PgHStoreSupport
    with PgSearchSupport
    with PgNetSupport
    with PgLTreeSupport {

  // Add back `capabilities.insertOrUpdate` to enable native `upsert` support; for postgres 9.5+
  override protected def computeCapabilities: Set[slick.basic.Capability] =
    super.computeCapabilities + slick.jdbc.JdbcCapabilities.insertOrUpdate

  override val api: API = MyAPI

  object MyAPI
      extends API
      with ArrayImplicits
      with DateTimeImplicits
      with NetImplicits
      with LTreeImplicits
      with RangeImplicits
      with HStoreImplicits
      with SearchImplicits
      with SearchAssistants

}

object CustomPostgresProfile extends CustomPostgresProfile
@tminglei
Copy link
Owner

@enzeart try

package com.example.db_profile
import com.github.tminglei.slickpg._

trait CustomPostgresProfile
    extends ExPostgresProfile
    with PgArraySupport
    with PgDate2Support
    with PgRangeSupport
    with PgHStoreSupport
    with PgSearchSupport
    with PgNetSupport
    with PgLTreeSupport {

  // Add back `capabilities.insertOrUpdate` to enable native `upsert` support; for postgres 9.5+
  override protected def computeCapabilities: Set[slick.basic.Capability] =
    super.computeCapabilities + slick.jdbc.JdbcCapabilities.insertOrUpdate

  override val api: API = new MyAPI {}

  trait MyAPI
      extends super.API
      with ArrayImplicits
      with DateTimeImplicits
      with NetImplicits
      with LTreeImplicits
      with RangeImplicits
      with HStoreImplicits
      with SearchImplicits
      with SearchAssistants

}

object CustomPostgresProfile extends CustomPostgresProfile

p.s. change object MyAPI to trait MyAPI.

@enzeart
Copy link
Author

enzeart commented Mar 30, 2022

@tminglei this didn't work for me

Here's my updated custom profile definition from a test project:

package du.mungus.play_grpc_test_5.db_profile
import com.github.tminglei.slickpg._
import slick.jdbc.{GetResult, PositionedResult}

import java.time.{Duration, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, OffsetTime}
import java.util.UUID
import scala.reflect.classTag

trait CustomPostgresProfile
    extends ExPostgresProfile
    with PgArraySupport
    with PgDate2Support
    with PgRangeSupport
    with PgHStoreSupport
    with PgSearchSupport
    with PgNetSupport
    with PgLTreeSupport {

  // Add back `capabilities.insertOrUpdate` to enable native `upsert` support; for postgres 9.5+
  override protected def computeCapabilities: Set[slick.basic.Capability] =
    super.computeCapabilities + slick.jdbc.JdbcCapabilities.insertOrUpdate

  override val api: API = new CustomAPI {}

  trait CustomAPI
      extends super.API
      with ArrayImplicits
      with DateTimeImplicits
      with NetImplicits
      with LTreeImplicits
      with RangeImplicits
      with HStoreImplicits
      with SearchImplicits
      with SearchAssistants {

    implicit object GetUuid extends GetResult[UUID] {
      override def apply(p: PositionedResult): UUID = UUID.fromString(p.nextString())
    }
  }

}

object CustomPostgresProfile extends CustomPostgresProfile {

  bindPgDateTypesToScala(
    classTag[LocalDate],
    classTag[LocalTime],
    classTag[LocalDateTime],
    classTag[OffsetTime],
    classTag[OffsetDateTime],
    classTag[Duration]
  )
}

Here's the table definition:

CREATE TABLE IF NOT EXISTS test (
    data        TEXT[] NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

Here's the code generated for the table:

package du.mungus.play_grpc_test_5.db
// AUTO-GENERATED Slick data model for table Test
trait TestTable {

  self:Tables  =>

  import profile.api._
  import slick.model.ForeignKeyAction
  // NOTE: GetResult mappers for plain SQL are only generated for tables where Slick knows how to map the types of all columns.
  import slick.jdbc.{GetResult => GR}
  /** Entity class storing rows of table Test
   *  @param data Database column data SqlType(_text)
   *  @param createdAt Database column created_at SqlType(timestamptz) */
  case class TestRow(data: scala.collection.immutable.Seq, createdAt: java.time.OffsetDateTime)
  /** GetResult implicit for fetching TestRow objects using plain SQL queries */
  implicit def GetResultTestRow(implicit e0: GR[scala.collection.immutable.Seq], e1: GR[java.time.OffsetDateTime]): GR[TestRow] = GR{
    prs => import prs._
    TestRow.tupled((<<[scala.collection.immutable.Seq], <<[java.time.OffsetDateTime]))
  }
  /** Table description of table test. Objects of this class serve as prototypes for rows in queries. */
  class Test(_tableTag: Tag) extends profile.api.Table[TestRow](_tableTag, "test") {
    def * = (data, createdAt) <> (TestRow.tupled, TestRow.unapply)
    /** Maps whole row to an option. Useful for outer joins. */
    def ? = ((Rep.Some(data), Rep.Some(createdAt))).shaped.<>({r=>import r._; _1.map(_=> TestRow.tupled((_1.get, _2.get)))}, (_:Any) =>  throw new Exception("Inserting into ? projection not supported."))

    /** Database column data SqlType(_text) */
    val data: Rep[scala.collection.immutable.Seq] = column[scala.collection.immutable.Seq]("data")
    /** Database column created_at SqlType(timestamptz) */
    val createdAt: Rep[java.time.OffsetDateTime] = column[java.time.OffsetDateTime]("created_at")
  }
  /** Collection-like TableQuery object for table Test */
  lazy val Test = new TableQuery(tag => new Test(tag))
}

The type parameters are still missing from the generated Seq fields for me.

@tminglei
Copy link
Owner

tminglei commented Apr 8, 2022

@enzeart did you customize your CodeGenerator like this?

@zhangchuanben
Copy link

Same issue with me 552 ^^

@enzeart
Copy link
Author

enzeart commented Jul 13, 2022

I got things working by doing the following steps.

I created a custom SourceCodeGenerator class. You have to take special care to override the packageCode and packageContainerCode methods to ensure your custom profile is used for the profile fields of the generated Tables trait. One of these methods is used for single-file code generation and the other is used for multi-file code generation. This brings all of the PgArraySupport implicits into scope for the generated code. The mappings from the database array types to the scala collections types are also configured here.

import slick.codegen.SourceCodeGenerator
import slick.model.Model
import slick.sql.SqlProfile.ColumnOption

class CustomSourceCodeGenerator(model: Model) extends SourceCodeGenerator(model) {

  override def Table: slick.model.Table => TableDef = new Table(_) { table =>

    override def Column: slick.model.Column => ColumnDef = new Column(_) { column =>

      override def rawType: String = {
        model.options
          .find(_.isInstanceOf[ColumnOption.SqlType])
          .flatMap { tpe =>
            tpe.asInstanceOf[ColumnOption.SqlType].typeName match {
              case "_text" | "text[]" | "_varchar" | "varchar[]" => Option("List[String]")
              case "_int8" | "int8[]"                            => Option("List[Long]")
              case "_int4" | "int4[]"                            => Option("List[Int]")
              case "_int2" | "int2[]"                            => Option("List[Short]")
              case _                                             => None
            }
          }
          .getOrElse {
            model.tpe match {
              case _ => super.rawType
            }
          }
      }

    }
  }

  override def packageCode(profile: String, pkg: String, container: String, parentType: Option[String]): String = {
    s"""
       |package $pkg
       |// AUTO-GENERATED Slick data model
       |/** Stand-alone Slick data model for immediate use */
       |object $container extends {
       |  val profile = $profile
       |} with $container
       |
       |/** Slick data model trait for extension, choice of backend or usage in the cake pattern. (Make sure to initialize this late.) */
       |trait $container${parentType.map(t => s" extends $t").getOrElse("")} {
       |  val profile: $profile
       |  import profile.api._
       |  ${indent(code)}
       |}
       |""".stripMargin
  }

  protected def handleQuotedNamed(tableName: String): String = {
    if (tableName.endsWith("`")) s"${tableName.init}Table`" else s"${tableName}Table"
  }

  override def packageContainerCode(profile: String, pkg: String, container: String): String = {
    val mixinCode =
      codePerTable.keys.map(tableName => s"${handleQuotedNamed(tableName)}").mkString("extends ", " with ", "")

    s"""
       |package $pkg
       |// AUTO-GENERATED Slick data model
       |/** Stand-alone Slick data model for immediate use */
       |object $container extends {
       |  val profile = $profile
       |} with $container
       |
       |/** Slick data model trait for extension, choice of backend or usage in the cake pattern. (Make sure to initialize this late.)
       |    Each generated XXXXTable trait is mixed in this trait hence allowing access to all the TableQuery lazy vals.
       |  */
       |trait $container${parentType.map(t => s" extends $t").getOrElse("")} $mixinCode {
       |  val profile: $profile
       |  import profile.api._
       |  ${indent(codeForContainer)}
       |
       |}
       |""".stripMargin
  }
}

I changed the type of the overridden api field from API to CustomAPI.type so its implicits don't get hidden by the more generic interface.

import com.github.tminglei.slickpg._
import slick.jdbc.{GetResult, PositionedResult}

import java.time.{Duration, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, OffsetTime}
import java.util.UUID
import scala.reflect.classTag

trait CustomPostgresProfile
    extends ExPostgresProfile
    with PgArraySupport
    with PgDate2Support
    with PgRangeSupport
    with PgHStoreSupport
    with PgSearchSupport
    with PgNetSupport
    with PgLTreeSupport {

  // Add back `capabilities.insertOrUpdate` to enable native `upsert` support; for postgres 9.5+
  override protected def computeCapabilities: Set[slick.basic.Capability] =
    super.computeCapabilities + slick.jdbc.JdbcCapabilities.insertOrUpdate

  override val api: CustomAPI.type = CustomAPI

  object CustomAPI
      extends super.API
      with ArrayImplicits
      with DateTimeImplicits
      with NetImplicits
      with LTreeImplicits
      with RangeImplicits
      with HStoreImplicits
      with SearchImplicits
      with SearchAssistants {

    implicit object GetUuid extends GetResult[UUID] {
      override def apply(p: PositionedResult): UUID = UUID.fromString(p.nextString())
    }
  }

}

object CustomPostgresProfile extends CustomPostgresProfile {

  bindPgDateTypesToScala(
    classTag[LocalDate],
    classTag[LocalTime],
    classTag[LocalDateTime],
    classTag[OffsetTime],
    classTag[OffsetDateTime],
    classTag[Duration]
  )
}

I then use the default SourceCodeGenerator entrypoint to drive the codegen process, but I pass CustomSourceCodeGenerator as the value for the codeGeneratorClass argument.

@tminglei is this a sound approach?

@tminglei
Copy link
Owner

@enzeart sounds good. 👍 You did what I would do when met similar scenes.

@enzeart enzeart closed this as completed Jul 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants