oyvindberg / typo

Typed Postgresql integration for Scala. Hopes to avoid typos
https://oyvindberg.github.io/typo/
MIT License
99 stars 9 forks source link

Derived Scala code fails compilation: Missing 'Ordering' instance for derived String Enumeration #54

Closed kolemannix closed 11 months ago

kolemannix commented 11 months ago

First off, let me say how excited I am to test drive this project! We've got hundreds of tables and over 600 calls to ctx.run, and a mixture of Doobie and Quill throughout the codebase. Our compile times are abysmal, and I'm so excited, looking at the very efficient hand-drived typeclass instances for Doobie, for the compile-time benefits this project may bring us!

I have a String Enumeration named Permission which is used part of a primary key in another table:

// Permission.scala
sealed abstract class Permission(val value: String)

object Permission {
  def apply(str: String): Either[String, Permission] =
    ByName.get(str).toRight(s"'$str' does not match any of the following legal values: $Names")
  def force(str: String): Permission =
    apply(str) match {
      case Left(msg) => sys.error(msg)
      case Right(value) => value
    }
  case object invite extends Permission("invite")
  case object admin extends Permission("admin")
  case object owner extends Permission("owner")
  case object support extends Permission("support")
  case object explore extends Permission("explore")
  case object write extends Permission("write")
  val All: List[Permission] = List(invite, admin, owner, support, explore, write)
  val Names: String = All.map(_.value).mkString(", ")
  val ByName: Map[String, Permission] = All.map(x => (x.value, x)).toMap

  implicit lazy val arrayGet: Get[Array[Permission]] = pattern.sql.generated.StringArrayMeta.get.map(_.map(force))
  implicit lazy val arrayPut: Put[Array[Permission]] = pattern.sql.generated.StringArrayMeta.put.contramap(_.map(_.value))
  implicit lazy val get: Get[Permission] = Meta.StringMeta.get.temap(Permission.apply)
  implicit lazy val put: Put[Permission] = Meta.StringMeta.put.contramap(_.value)
  implicit lazy val read: Read[Permission] = Read.fromGet(get)
  implicit lazy val write: Write[Permission] = Write.fromPut(put)
}
...
// UserPermissionId.scala
case class UserPermissionId(userId: AppUserId, permission: Permission)
object UserPermissionId {
  implicit lazy val ordering: Ordering[UserPermissionId] = Ordering.by(x => (x.userId, x.permission))
}

The Ordering[UserPermissionId] fails to compile because Permission has no Ordering.

Additionally (I can file a separate issue if desired), I have a name collision on the local write inside the Permission object. write is one of the Permissions, as well as the name of the derived doobie Write instance. I suppose I could get around this with custom naming? Have not tried yet.

oyvindberg commented 11 months ago

Hey, thanks for the report!

The ordering thing is an oversight, will be fixed with #55 .

oyvindberg commented 11 months ago

As for the name collision, there are many ways to fix it. the easiest is to choose a less-likely name for the typeclass instances.

I think I'll let it cook for a while and maybe do something more structured to avoid naming conflicts.

For now it's rather easy to work around if you customize naming. I updated the section now with this exact example

oyvindberg commented 11 months ago

First off, let me say how excited I am to test drive this project! We've got hundreds of tables and over 600 calls to ctx.run, and a mixture of Doobie and Quill throughout the codebase. Our compile times are abysmal, and I'm so excited, looking at the very efficient hand-drived typeclass instances for Doobie, for the compile-time benefits this project may bring us!

Happy to hear it!

I do find it interesting that you're most enthusiastic about the compile-time improvement instead of the prospect of not having to write all that code in the first place 😄

oyvindberg commented 11 months ago

0.4.1 on the way to maven central

kolemannix commented 11 months ago

I do find it interesting that you're most enthusiastic about the compile-time improvement instead of the prospect of not having to write all that code in the first place

You write it once; you wait for it to compile thousands of times :)

But anyway, our current setup is that we use Flyway to manage the actual schema, writing the schema by hand. We then manually maintain a "Row" case class for each table 🙃 . We then write an insert/get roundtrip test using Doobie's checking functionality against a local DB to ensure we can roundtrip our Row case classes, doobie also reports any mismatch of the types. Of course I'm excited just to generate the case class from the Schema using Typo, for sure!

Then we're using Quill for what you call "1. CRUD Operations" and "2. Simple Reads" in your docs. We write Doobie queries for 3. Complex Reads and 4. Dynamic Queries. The quill queries are where the vast majority of the compile times come from; we call ctx.run over 500 times; that's a lot of macro invocations that can also trigger macro invocations!

I am, of course, also excited to try your "SQL files" solution to "3. Complex Reads", but my team's most acute painpoint is compile times!